diff --git a/Gopkg.lock b/Gopkg.lock index 9fe8c4f2..72d4d193 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,10 +1,10 @@ -memo = "bd95ed8c2b0aa32327ae55d88bff888b8198d238f7a71eee0f8663494664a0ac" +memo = "e59ec63c1c329674a0e5e4236131c787e5b81bab37529104fdc02ed8fdf29283" [[projects]] branch = "master" name = "github.com/Azure/azure-storage-go" packages = ["."] - revision = "4fe73b0b4f68bf8a7cad2920ef563fe4c40ac5c0" + revision = "32cfbe17a139c17f84be16bdf8f9c45c840a046b" [[projects]] name = "github.com/Azure/go-autorest" @@ -22,7 +22,7 @@ memo = "bd95ed8c2b0aa32327ae55d88bff888b8198d238f7a71eee0f8663494664a0ac" branch = "master" name = "github.com/Sirupsen/logrus" packages = ["."] - revision = "508f304878257fb578be3e863e3990ed9ec3aa2e" + revision = "acfabf31db8f45a9174f54a0d48ea4d15627af4d" [[projects]] name = "github.com/asaskevich/govalidator" @@ -50,8 +50,8 @@ memo = "bd95ed8c2b0aa32327ae55d88bff888b8198d238f7a71eee0f8663494664a0ac" [[projects]] name = "github.com/cheggaaa/pb" packages = ["."] - revision = "b6229822fa186496fcbf34111237e7a9693c6971" - version = "v1.0.13" + revision = "f6ccf2184de4dd34495277e38dc19b6e7fbe0ea2" + version = "v1.0.15" [[projects]] name = "github.com/dgrijalva/jwt-go" @@ -123,7 +123,7 @@ memo = "bd95ed8c2b0aa32327ae55d88bff888b8198d238f7a71eee0f8663494664a0ac" branch = "master" name = "github.com/knqyf263/go-deb-version" packages = ["."] - revision = "bec774d791d03b721a20bd3ca1fbdd566fd0f2b9" + revision = "9865fe14d09b1c729188ac810466dde90f897ee3" [[projects]] branch = "master" @@ -138,10 +138,10 @@ memo = "bd95ed8c2b0aa32327ae55d88bff888b8198d238f7a71eee0f8663494664a0ac" version = "v0.1.0" [[projects]] - branch = "master" + branch = "improve-db" name = "github.com/kotakanbe/goval-dictionary" packages = ["config","db","log","models"] - revision = "545199055508ae62a6d3bd34ef83034fbfc04d7f" + revision = "5f7aa97d45d565eaccc70c0c365e21624a9c6e3f" [[projects]] branch = "master" @@ -158,8 +158,8 @@ memo = "bd95ed8c2b0aa32327ae55d88bff888b8198d238f7a71eee0f8663494664a0ac" [[projects]] name = "github.com/labstack/gommon" packages = ["color","log"] - revision = "9cedb429ffbe71a32a3ae7c65fd109cb7ae07804" - version = "v0.2.0" + revision = "1121fd3e243c202482226a7afe4dcd07ffc4139a" + version = "v0.2.1" [[projects]] name = "github.com/mattn/go-colorable" @@ -249,22 +249,22 @@ memo = "bd95ed8c2b0aa32327ae55d88bff888b8198d238f7a71eee0f8663494664a0ac" branch = "master" name = "golang.org/x/crypto" packages = ["curve25519","ed25519","ed25519/internal/edwards25519","ssh","ssh/agent","ssh/terminal"] - revision = "04eae0b62feaaf659a0ce2c4e8dc70b6ae2fff67" + revision = "ab89591268e0c8b748cbe4047b00197516011af5" [[projects]] branch = "master" name = "golang.org/x/net" packages = ["context","idna","publicsuffix"] - revision = "feeb485667d1fdabe727840fe00adc22431bc86e" + revision = "84f0e6f92b10139f986b1756e149a7d9de270cdc" [[projects]] branch = "master" name = "golang.org/x/sys" packages = ["unix"] - revision = "9ccfe848b9db8435a24c424abbc07a921adf1df5" + revision = "1e99a4f9d247b28c670884b9a8d6801f39a47b77" [[projects]] branch = "master" name = "golang.org/x/text" packages = ["internal/gen","internal/triegen","internal/ucd","secure/bidirule","transform","unicode/bidi","unicode/cldr","unicode/norm","unicode/rangetable"] - revision = "470f45bf29f4147d6fbd7dfd0a02a848e49f5bf4" + revision = "19e51611da83d6be54ddafce4a4af510cb3e9ea4" diff --git a/Gopkg.toml b/Gopkg.toml index e5e7ee17..1a77d819 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -28,7 +28,7 @@ name = "github.com/kotakanbe/go-cve-dictionary" [[dependencies]] - branch = "master" + branch = "improve-db" name = "github.com/kotakanbe/goval-dictionary" [[dependencies]] diff --git a/commands/report.go b/commands/report.go index 96223beb..1fe6c93b 100644 --- a/commands/report.go +++ b/commands/report.go @@ -463,7 +463,7 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} var res models.ScanResults for _, r := range results { - res = append(res, r.FilterByCvssOver()) + res = append(res, r.FilterByCvssOver(c.Conf.CvssScoreOver)) // TODO Add sort function to ScanResults @@ -545,10 +545,14 @@ func fillCveInfoFromCveDB(r *models.ScanResult) error { func fillCveInfoFromOvalDB(r *models.ScanResult) error { var ovalClient oval.Client switch r.Family { - case "ubuntu", "debian": + case "debian": ovalClient = oval.NewDebian() - case "rhel", "centos": + case "ubuntu": + ovalClient = oval.NewUbuntu() + case "rhel": ovalClient = oval.NewRedhat() + case "centos": + ovalClient = oval.NewCentOS() case "amazon", "oraclelinux", "Raspbian", "FreeBSD": //TODO implement OracleLinux return nil diff --git a/models/models.go b/models/models.go index 4030475f..ab8bdfe4 100644 --- a/models/models.go +++ b/models/models.go @@ -22,7 +22,6 @@ import ( "strings" "time" - "github.com/future-architect/vuls/config" cvedict "github.com/kotakanbe/go-cve-dictionary/models" ) @@ -117,6 +116,8 @@ func (r ScanResult) ConvertNvdToModel(cveID string, nvd cvedict.Nvd) *CveContent Summary: nvd.Summary, Cvss2Score: nvd.Score, Cvss2Vector: vector, + Severity: "", // severity is not contained in NVD + SourceLink: "https://nvd.nist.gov/vuln/detail/" + cveID, Cpes: cpes, CweID: nvd.CweID, References: refs, @@ -132,10 +133,7 @@ func (r ScanResult) ConvertJvnToModel(cveID string, jvn cvedict.Jvn) *CveContent cpes = append(cpes, Cpe{CpeName: c.CpeName}) } - refs := []Reference{{ - Link: jvn.JvnLink, - Source: string(JVN), - }} + refs := []Reference{} for _, r := range jvn.References { refs = append(refs, Reference{ Link: r.Link, @@ -152,6 +150,7 @@ func (r ScanResult) ConvertJvnToModel(cveID string, jvn cvedict.Jvn) *CveContent Severity: jvn.Severity, Cvss2Score: jvn.Score, Cvss2Vector: vector, + SourceLink: jvn.JvnLink, Cpes: cpes, References: refs, Published: jvn.PublishedDate, @@ -160,15 +159,22 @@ func (r ScanResult) ConvertJvnToModel(cveID string, jvn cvedict.Jvn) *CveContent } // FilterByCvssOver is filter function. -func (r ScanResult) FilterByCvssOver() ScanResult { +func (r ScanResult) FilterByCvssOver(over float64) ScanResult { // TODO: Set correct default value - if config.Conf.CvssScoreOver == 0 { - config.Conf.CvssScoreOver = -1.1 + if over == 0 { + over = -1.1 } // TODO: Filter by ignore cves??? filtered := r.ScannedCves.Find(func(v VulnInfo) bool { - return config.Conf.CvssScoreOver <= v.CveContents.CvssV2Score() + values := v.CveContents.Cvss2Scores() + for _, v := range values { + score := v.Value.Score + if over <= score { + return true + } + } + return false }) copiedScanResult := r @@ -234,12 +240,12 @@ func (r ScanResult) FormatServerName() string { } // CveSummary summarize the number of CVEs group by CVSSv2 Severity -func (r ScanResult) CveSummary() string { +func (r ScanResult) CveSummary(ignoreUnscoreCves bool) string { var high, medium, low, unknown int for _, vInfo := range r.ScannedCves { - score := vInfo.CveContents.CvssV2Score() + score := vInfo.CveContents.MaxCvss2Score().Value.Score if score < 0.1 { - score = vInfo.CveContents.CvssV3Score() + score = vInfo.CveContents.MaxCvss3Score().Value.Score } switch { case 7.0 <= score: @@ -253,7 +259,7 @@ func (r ScanResult) CveSummary() string { } } - if config.Conf.IgnoreUnscoredCves { + if ignoreUnscoreCves { return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d)", high+medium+low, high, medium, low) } @@ -298,23 +304,25 @@ const ( FailedToFindVersionInChangelog = "FailedToFindVersionInChangelog" ) -// CpeNameMatch is a ranking how confident the CVE-ID was deteted correctly -var CpeNameMatch = Confidence{100, CpeNameMatchStr} +var ( + // CpeNameMatch is a ranking how confident the CVE-ID was deteted correctly + CpeNameMatch = Confidence{100, CpeNameMatchStr} -// YumUpdateSecurityMatch is a ranking how confident the CVE-ID was deteted correctly -var YumUpdateSecurityMatch = Confidence{100, YumUpdateSecurityMatchStr} + // YumUpdateSecurityMatch is a ranking how confident the CVE-ID was deteted correctly + YumUpdateSecurityMatch = Confidence{100, YumUpdateSecurityMatchStr} -// PkgAuditMatch is a ranking how confident the CVE-ID was deteted correctly -var PkgAuditMatch = Confidence{100, PkgAuditMatchStr} + // PkgAuditMatch is a ranking how confident the CVE-ID was deteted correctly + PkgAuditMatch = Confidence{100, PkgAuditMatchStr} -// OvalMatch is a ranking how confident the CVE-ID was deteted correctly -var OvalMatch = Confidence{100, OvalMatchStr} + // OvalMatch is a ranking how confident the CVE-ID was deteted correctly + OvalMatch = Confidence{100, OvalMatchStr} -// ChangelogExactMatch is a ranking how confident the CVE-ID was deteted correctly -var ChangelogExactMatch = Confidence{95, ChangelogExactMatchStr} + // ChangelogExactMatch is a ranking how confident the CVE-ID was deteted correctly + ChangelogExactMatch = Confidence{95, ChangelogExactMatchStr} -// ChangelogLenientMatch is a ranking how confident the CVE-ID was deteted correctly -var ChangelogLenientMatch = Confidence{50, ChangelogLenientMatchStr} + // ChangelogLenientMatch is a ranking how confident the CVE-ID was deteted correctly + ChangelogLenientMatch = Confidence{50, ChangelogLenientMatchStr} +) // VulnInfos is VulnInfo list, getter/setter, sortable methods. type VulnInfos map[string]VulnInfo @@ -340,22 +348,33 @@ type VulnInfo struct { CveContents CveContents } -// NilToEmpty set nil slice or map fields to empty to avoid null in JSON -func (v *VulnInfo) NilToEmpty() { - if v.CpeNames == nil { - v.CpeNames = []string{} - } - if v.DistroAdvisories == nil { - v.DistroAdvisories = []DistroAdvisory{} - } - if v.PackageNames == nil { - v.PackageNames = []string{} - } - if v.CveContents == nil { - v.CveContents = NewCveContents() - } +// Cvss2CalcURL returns CVSS v2 caluclator's URL +func (v VulnInfo) Cvss2CalcURL() string { + return "https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?name=" + v.CveID } +// Cvss3CalcURL returns CVSS v3 caluclator's URL +func (v VulnInfo) Cvss3CalcURL() string { + return "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?name=" + v.CveID +} + +// TODO +// NilToEmpty set nil slice or map fields to empty to avoid null in JSON +// func (v *VulnInfo) NilToEmpty() { +// if v.CpeNames == nil { +// v.CpeNames = []string{} +// } +// if v.DistroAdvisories == nil { +// v.DistroAdvisories = []DistroAdvisory{} +// } +// if v.PackageNames == nil { +// v.PackageNames = []string{} +// } +// if v.CveContents == nil { +// v.CveContents = NewCveContents() +// } +// } + // CveContentType is a source of CVE information type CveContentType string @@ -387,9 +406,6 @@ const ( // RedHat is RedHat RedHat CveContentType = "redhat" - // CentOS is CentOS - CentOS CveContentType = "centos" - // Debian is Debian Debian CveContentType = "debian" @@ -400,6 +416,29 @@ const ( Unknown CveContentType = "unknown" ) +// CveContentTypes has slide of CveContentType +type CveContentTypes []CveContentType + +// AllCveContetTypes has all of CveContentTypes +var AllCveContetTypes = CveContentTypes{NVD, JVN, RedHat, Debian, Ubuntu} + +// Except returns CveContentTypes except for given args +func (c CveContentTypes) Except(excepts ...CveContentType) (excepted CveContentTypes) { + for _, ctype := range c { + found := false + for _, except := range excepts { + if ctype == except { + found = true + break + } + } + if !found { + excepted = append(excepted, ctype) + } + } + return +} + // CveContents has CveContent type CveContents map[CveContentType]CveContent @@ -412,25 +451,339 @@ func NewCveContents(conts ...CveContent) CveContents { return m } -// CvssV2Score returns CVSS V2 Score -func (v CveContents) CvssV2Score() float64 { - //TODO - if cont, found := v[NVD]; found { - return cont.Cvss2Score - } else if cont, found := v[JVN]; found { - return cont.Cvss2Score - } else if cont, found := v[RedHat]; found { - return cont.Cvss2Score - } - return -1.1 +// CveContentStr has CveContentType and Value +type CveContentStr struct { + Type CveContentType + Value string } -// CvssV3Score returns CVSS V2 Score -func (v CveContents) CvssV3Score() float64 { - if cont, found := v[RedHat]; found { - return cont.Cvss3Score +// Except returns CveContents except given keys for enumeration +func (v CveContents) Except(exceptCtypes ...CveContentType) (values CveContents) { + for ctype, content := range v { + found := false + for _, exceptCtype := range exceptCtypes { + if ctype == exceptCtype { + found = true + break + } + } + if !found { + values[ctype] = content + } } - return -1.1 + return +} + +// CveContentCvss2 has CveContentType and Cvss2 +type CveContentCvss2 struct { + Type CveContentType + Value Cvss2 +} + +// Cvss2 has CVSS v2 +type Cvss2 struct { + Score float64 + Vector string + Severity string +} + +func cvss2ScoreToSeverity(score float64) string { + if 7.0 <= score { + return "HIGH" + } else if 4.0 <= score { + return "MEDIUM" + } + return "LOW" +} + +// Cvss2Scores returns CVSS V2 Scores +func (v CveContents) Cvss2Scores() (values []CveContentCvss2) { + order := []CveContentType{NVD, RedHat, JVN} + for _, ctype := range order { + if cont, found := v[ctype]; found && 0 < cont.Cvss2Score { + // https://nvd.nist.gov/vuln-metrics/cvss + sev := cont.Severity + if ctype == NVD { + sev = cvss2ScoreToSeverity(cont.Cvss2Score) + } + values = append(values, CveContentCvss2{ + Type: ctype, + Value: Cvss2{ + Score: cont.Cvss2Score, + Vector: cont.Cvss2Vector, + Severity: sev, + }, + }) + } + } + return +} + +// MaxCvss2Score returns Max CVSS V2 Score +func (v CveContents) MaxCvss2Score() CveContentCvss2 { + //TODO Severity Ubuntu, Debian... + order := []CveContentType{NVD, RedHat, JVN} + max := 0.0 + value := CveContentCvss2{ + Type: Unknown, + Value: Cvss2{}, + } + for _, ctype := range order { + if cont, found := v[ctype]; found && max < cont.Cvss2Score { + // https://nvd.nist.gov/vuln-metrics/cvss + sev := cont.Severity + if ctype == NVD { + sev = cvss2ScoreToSeverity(cont.Cvss2Score) + } + value = CveContentCvss2{ + Type: ctype, + Value: Cvss2{ + Score: cont.Cvss2Score, + Vector: cont.Cvss2Vector, + Severity: sev, + }, + } + max = cont.Cvss2Score + } + } + return value +} + +// CveContentCvss3 has CveContentType and Cvss3 +type CveContentCvss3 struct { + Type CveContentType + Value Cvss3 +} + +// Cvss3 has CVSS v3 +type Cvss3 struct { + Score float64 + Vector string + Severity string +} + +func cvss3ScoreToSeverity(score float64) string { + if 9.0 <= score { + return "CRITICAL" + } else if 7.0 <= score { + return "HIGH" + } else if 4.0 <= score { + return "MEDIUM" + } + return "LOW" +} + +// Cvss3Scores returns CVSS V3 Score +func (v CveContents) Cvss3Scores() (values []CveContentCvss3) { + //TODO Severity Ubuntu, Debian... + order := []CveContentType{RedHat} + for _, ctype := range order { + if cont, found := v[ctype]; found && 0 < cont.Cvss3Score { + // https://nvd.nist.gov/vuln-metrics/cvss + sev := cont.Severity + if ctype == NVD { + sev = cvss3ScoreToSeverity(cont.Cvss2Score) + } + values = append(values, CveContentCvss3{ + Type: ctype, + Value: Cvss3{ + Score: cont.Cvss3Score, + Vector: cont.Cvss3Vector, + Severity: sev, + }, + }) + } + } + return +} + +// MaxCvss3Score returns Max CVSS V3 Score +func (v CveContents) MaxCvss3Score() CveContentCvss3 { + //TODO Severity Ubuntu, Debian... + order := []CveContentType{RedHat} + max := 0.0 + value := CveContentCvss3{ + Type: Unknown, + Value: Cvss3{}, + } + for _, ctype := range order { + if cont, found := v[ctype]; found && max < cont.Cvss3Score { + // https://nvd.nist.gov/vuln-metrics/cvss + sev := cont.Severity + if ctype == NVD { + sev = cvss3ScoreToSeverity(cont.Cvss2Score) + } + value = CveContentCvss3{ + Type: ctype, + Value: Cvss3{ + Score: cont.Cvss3Score, + Vector: cont.Cvss3Vector, + Severity: sev, + }, + } + max = cont.Cvss3Score + } + } + return value +} + +// Titles returns tilte (TUI) +func (v CveContents) Titles(lang, myFamily string) (values []CveContentStr) { + if lang == "ja" { + if cont, found := v[JVN]; found && 0 < len(cont.Title) { + values = append(values, CveContentStr{JVN, cont.Title}) + } + } + + order := CveContentTypes{NVD, NewCveContentType(myFamily)} + order = append(order, AllCveContetTypes.Except(append(order, JVN)...)...) + for _, ctype := range order { + // Only JVN has meaningful title. so return first 100 char of summary + if cont, found := v[ctype]; found && 0 < len(cont.Summary) { + summary := strings.Replace(cont.Summary, "\n", " ", -1) + index := 75 + if len(summary) < index { + index = len(summary) + } + values = append(values, CveContentStr{ + Type: ctype, + Value: summary[0:index] + "...", + }) + } + } + + if len(values) == 0 { + values = []CveContentStr{{ + Type: Unknown, + Value: "-", + }} + } + return +} + +// Summaries returns summaries +func (v CveContents) Summaries(lang, myFamily string) (values []CveContentStr) { + if lang == "ja" { + if cont, found := v[JVN]; found && 0 < len(cont.Summary) { + summary := cont.Title + summary += "\n" + strings.Replace( + strings.Replace(cont.Summary, "\n", " ", -1), "\r", " ", -1) + values = append(values, CveContentStr{JVN, summary}) + } + } + + order := CveContentTypes{NVD, NewCveContentType(myFamily)} + order = append(order, AllCveContetTypes.Except(append(order, JVN)...)...) + for _, ctype := range order { + if cont, found := v[ctype]; found && 0 < len(cont.Summary) { + summary := strings.Replace(cont.Summary, "\n", " ", -1) + values = append(values, CveContentStr{ + Type: ctype, + Value: summary, + }) + } + } + + if len(values) == 0 { + values = []CveContentStr{{ + Type: Unknown, + Value: "-", + }} + } + return +} + +// SourceLinks returns link of source +func (v CveContents) SourceLinks(lang, myFamily string) (values []CveContentStr) { + if lang == "ja" { + if cont, found := v[JVN]; found && !cont.Empty() { + values = append(values, CveContentStr{JVN, cont.SourceLink}) + } + } + + order := CveContentTypes{NVD, NewCveContentType(myFamily)} + for _, ctype := range order { + if cont, found := v[ctype]; found { + values = append(values, CveContentStr{ctype, cont.SourceLink}) + } + } + return +} + +// Severities returns Severities +// func (v CveContents) Severities(myFamily string) (values []CveContentValue) { +// order := CveContentTypes{NVD, NewCveContentType(myFamily)} +// order = append(order, AllCveContetTypes.Except(append(order)...)...) + +// for _, ctype := range order { +// if cont, found := v[ctype]; found && 0 < len(cont.Severity) { +// values = append(values, CveContentValue{ +// Type: ctype, +// Value: cont.Severity, +// }) +// } +// } +// return +// } + +// CveContentCpes has CveContentType and Value +type CveContentCpes struct { + Type CveContentType + Value []Cpe +} + +// Cpes returns affected CPEs of this Vulnerability +func (v CveContents) Cpes(myFamily string) (values []CveContentCpes) { + order := CveContentTypes{NewCveContentType(myFamily)} + order = append(order, AllCveContetTypes.Except(append(order)...)...) + + for _, ctype := range order { + if cont, found := v[ctype]; found && 0 < len(cont.Cpes) { + values = append(values, CveContentCpes{ + Type: ctype, + Value: cont.Cpes, + }) + } + } + return +} + +// CveContentRefs has CveContentType and Cpes +type CveContentRefs struct { + Type CveContentType + Value []Reference +} + +// References returns References +func (v CveContents) References(myFamily string) (values []CveContentRefs) { + order := CveContentTypes{NewCveContentType(myFamily)} + order = append(order, AllCveContetTypes.Except(append(order)...)...) + + for _, ctype := range order { + if cont, found := v[ctype]; found && 0 < len(cont.References) { + values = append(values, CveContentRefs{ + Type: ctype, + Value: cont.References, + }) + } + } + return +} + +// CweIDs returns CweIDs +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, + }) + } + } + return } // CveContent has abstraction of various vulnerability information @@ -444,8 +797,9 @@ type CveContent struct { Cvss2Vector string Cvss3Score float64 Cvss3Vector string + SourceLink string Cpes []Cpe - References []Reference + References References CweID string Published time.Time LastModified time.Time @@ -461,11 +815,22 @@ type Cpe struct { CpeName string } +// References is a slice of Reference +type References []Reference + +// Find elements that matches the function passed in argument +func (r References) Find(f func(r Reference) bool) (refs []Reference) { + for _, rr := range r { + refs = append(refs, rr) + } + return +} + // Reference has a related link of the CVE type Reference struct { - RefID string Source string Link string + RefID string } // Packages is Map of Package @@ -504,6 +869,15 @@ func (ps Packages) Merge(other Packages) Packages { return merged } +// FormatVersionsFromTo returns updatable packages +func (ps Packages) FormatVersionsFromTo() string { + ss := []string{} + for _, pack := range ps { + ss = append(ss, pack.FormatVersionFromTo()) + } + return strings.Join(ss, "\n") +} + // FormatUpdatablePacksSummary returns a summary of updatable packages func (ps Packages) FormatUpdatablePacksSummary() string { nUpdatable := 0 @@ -527,15 +901,8 @@ type Package struct { NotFixedYet bool // Ubuntu OVAL Only } -// Changelog has contents of changelog and how to get it. -// Method: modesl.detectionMethodStr -type Changelog struct { - Contents string - Method string -} - -// FormatCurrentVer returns package name-version-release -func (p Package) FormatCurrentVer() string { +// FormatVer returns package name-version-release +func (p Package) FormatVer() string { str := p.Name if 0 < len(p.Version) { str = fmt.Sprintf("%s-%s", str, p.Version) @@ -558,6 +925,18 @@ func (p Package) FormatNewVer() string { return str } +// FormatVersionFromTo formats installed and new package version +func (p Package) FormatVersionFromTo() string { + return fmt.Sprintf("%s -> %s", p.FormatVer(), p.FormatNewVer()) +} + +// Changelog has contents of changelog and how to get it. +// Method: modesl.detectionMethodStr +type Changelog struct { + Contents string + Method string +} + // DistroAdvisory has Amazon Linux, RHEL, FreeBSD Security Advisory information. type DistroAdvisory struct { AdvisoryID string diff --git a/oval/debian.go b/oval/debian.go index ac108d9d..15d48ead 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -12,34 +12,23 @@ import ( ovalmodels "github.com/kotakanbe/goval-dictionary/models" ) -// Debian is the interface for Debian OVAL -type Debian struct{} +// DebianBase is the base struct of Debian and Ubuntu +type DebianBase struct{} -// NewDebian creates OVAL client for Debian -func NewDebian() Debian { - return Debian{} -} - -// FillCveInfoFromOvalDB returns scan result after updating CVE info by OVAL -func (o Debian) FillCveInfoFromOvalDB(r *models.ScanResult) error { +// fillCveInfoFromOvalDB returns scan result after updating CVE info by OVAL +func (o DebianBase) fillCveInfoFromOvalDB(r *models.ScanResult) error { ovalconf.Conf.DBType = config.Conf.OvalDBType ovalconf.Conf.DBPath = config.Conf.OvalDBPath util.Log.Infof("open oval-dictionary db (%s): %s", config.Conf.OvalDBType, config.Conf.OvalDBPath) - if err := db.OpenDB(); err != nil { - return fmt.Errorf("Failed to open OVAL DB. err: %s", err) + ovaldb, err := db.NewDB(r.Family) + if err != nil { + return err } - var d db.OvalDB - switch r.Family { - case "debian": - d = db.NewDebian() - case "ubuntu": - d = db.NewUbuntu() - } for _, pack := range r.Packages { - definitions, err := d.GetByPackName(r.Release, pack.Name) + definitions, err := ovaldb.GetByPackName(r.Release, pack.Name) if err != nil { return fmt.Errorf("Failed to get Debian OVAL info by package name: %v", err) } @@ -59,7 +48,7 @@ func (o Debian) FillCveInfoFromOvalDB(r *models.ScanResult) error { return nil } -func (o Debian) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Definition) { +func (o DebianBase) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Definition) { ovalContent := *o.convertToModel(definition) ovalContent.Type = models.NewCveContentType(r.Family) vinfo, ok := r.ScannedCves[definition.Debian.CveID] @@ -89,7 +78,7 @@ func (o Debian) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini r.ScannedCves[definition.Debian.CveID] = vinfo } -func (o Debian) convertToModel(def *ovalmodels.Definition) *models.CveContent { +func (o DebianBase) convertToModel(def *ovalmodels.Definition) *models.CveContent { var refs []models.Reference for _, r := range def.References { refs = append(refs, models.Reference{ @@ -98,6 +87,7 @@ func (o Debian) convertToModel(def *ovalmodels.Definition) *models.CveContent { RefID: r.RefID, }) } + return &models.CveContent{ CveID: def.Debian.CveID, Title: def.Title, @@ -106,3 +96,51 @@ func (o Debian) convertToModel(def *ovalmodels.Definition) *models.CveContent { References: refs, } } + +// Debian is the interface for Debian OVAL +type Debian struct { + DebianBase +} + +// NewDebian creates OVAL client for Debian +func NewDebian() *Debian { + return &Debian{} +} + +// FillCveInfoFromOvalDB returns scan result after updating CVE info by OVAL +func (o Debian) FillCveInfoFromOvalDB(r *models.ScanResult) error { + if err := o.fillCveInfoFromOvalDB(r); err != nil { + return err + } + for _, vuln := range r.ScannedCves { + if cont, ok := vuln.CveContents[models.Debian]; ok { + cont.SourceLink = "https://security-tracker.debian.org/tracker/" + cont.CveID + vuln.CveContents[models.Debian] = cont + } + } + return nil +} + +// Ubuntu is the interface for Debian OVAL +type Ubuntu struct { + DebianBase +} + +// NewUbuntu creates OVAL client for Debian +func NewUbuntu() *Ubuntu { + return &Ubuntu{} +} + +// FillCveInfoFromOvalDB returns scan result after updating CVE info by OVAL +func (o Ubuntu) FillCveInfoFromOvalDB(r *models.ScanResult) error { + if err := o.fillCveInfoFromOvalDB(r); err != nil { + return err + } + for _, vuln := range r.ScannedCves { + if cont, ok := vuln.CveContents[models.Ubuntu]; ok { + cont.SourceLink = "http://people.ubuntu.com/~ubuntu-security/cve/" + cont.CveID + vuln.CveContents[models.Ubuntu] = cont + } + } + return nil +} diff --git a/oval/redhat.go b/oval/redhat.go index 009324c9..edc4b3ee 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -14,27 +14,31 @@ import ( ovalmodels "github.com/kotakanbe/goval-dictionary/models" ) -// Redhat is the interface for Redhat OVAL -type Redhat struct{} +// RedHatBase is the base struct for RedHat and CentOS +type RedHatBase struct{} -// NewRedhat creates OVAL client for Redhat -func NewRedhat() Redhat { - return Redhat{} +// FillCveInfoFromOvalDB returns scan result after updating CVE info by OVAL +func (o RedHatBase) FillCveInfoFromOvalDB(r *models.ScanResult) error { + if err := o.fillCveInfoFromOvalDB(r); err != nil { + return err + } + for _, vuln := range r.ScannedCves { + if cont, ok := vuln.CveContents[models.RedHat]; ok { + cont.SourceLink = "https://access.redhat.com/security/cve/" + cont.CveID + } + } + return nil } // FillCveInfoFromOvalDB returns scan result after updating CVE info by OVAL -func (o Redhat) FillCveInfoFromOvalDB(r *models.ScanResult) error { +func (o RedHatBase) fillCveInfoFromOvalDB(r *models.ScanResult) error { ovalconf.Conf.DBType = config.Conf.OvalDBType ovalconf.Conf.DBPath = config.Conf.OvalDBPath util.Log.Infof("open oval-dictionary db (%s): %s", config.Conf.OvalDBType, config.Conf.OvalDBPath) - if err := db.OpenDB(); err != nil { - return fmt.Errorf("Failed to open OVAL DB. err: %s", err) - } - d := db.NewRedHat() - + defer d.Close() for _, pack := range r.Packages { definitions, err := d.GetByPackName(r.Release, pack.Name) if err != nil { @@ -56,7 +60,7 @@ func (o Redhat) FillCveInfoFromOvalDB(r *models.ScanResult) error { return nil } -func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Definition) { +func (o RedHatBase) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Definition) { for _, cve := range definition.Advisory.Cves { ovalContent := *o.convertToModel(cve.CveID, definition) vinfo, ok := r.ScannedCves[cve.CveID] @@ -87,7 +91,7 @@ func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini } } -func (o Redhat) convertToModel(cveID string, def *ovalmodels.Definition) *models.CveContent { +func (o RedHatBase) convertToModel(cveID string, def *ovalmodels.Definition) *models.CveContent { for _, cve := range def.Advisory.Cves { if cve.CveID != cveID { continue @@ -114,6 +118,7 @@ func (o Redhat) convertToModel(cveID string, def *ovalmodels.Definition) *models Cvss2Vector: vec2, Cvss3Score: score3, Cvss3Vector: vec3, + SourceLink: "https://access.redhat.com/security/cve/" + cve.CveID, References: refs, CweID: cve.Cwe, Published: def.Advisory.Issued, @@ -125,7 +130,7 @@ func (o Redhat) convertToModel(cveID string, def *ovalmodels.Definition) *models // ParseCvss2 divide CVSSv2 string into score and vector // 5/AV:N/AC:L/Au:N/C:N/I:N/A:P -func (o Redhat) parseCvss2(scoreVector string) (score float64, vector string) { +func (o RedHatBase) parseCvss2(scoreVector string) (score float64, vector string) { var err error ss := strings.Split(scoreVector, "/") if 1 < len(ss) { @@ -139,7 +144,7 @@ func (o Redhat) parseCvss2(scoreVector string) (score float64, vector string) { // ParseCvss3 divide CVSSv3 string into score and vector // 5.6/CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L -func (o Redhat) parseCvss3(scoreVector string) (score float64, vector string) { +func (o RedHatBase) parseCvss3(scoreVector string) (score float64, vector string) { var err error ss := strings.Split(scoreVector, "/CVSS:3.0/") if 1 < len(ss) { @@ -150,3 +155,23 @@ func (o Redhat) parseCvss3(scoreVector string) (score float64, vector string) { } return 0, "" } + +// RedHat is the interface for RedhatBase OVAL +type RedHat struct { + RedHatBase +} + +// NewRedhat creates OVAL client for Redhat +func NewRedhat() RedHat { + return RedHat{} +} + +// CentOS is the interface for CentOS OVAL +type CentOS struct { + RedHatBase +} + +// NewCentOS creates OVAL client for CentOS +func NewCentOS() CentOS { + return CentOS{} +} diff --git a/oval/redhat_test.go b/oval/redhat_test.go index c9b57dc8..c186e1d8 100644 --- a/oval/redhat_test.go +++ b/oval/redhat_test.go @@ -27,7 +27,7 @@ func TestParseCvss2(t *testing.T) { }, } for _, tt := range tests { - s, v := Redhat{}.parseCvss2(tt.in) + s, v := RedHatBase{}.parseCvss2(tt.in) if s != tt.out.score || v != tt.out.vector { t.Errorf("\nexpected: %f, %s\n actual: %f, %s", tt.out.score, tt.out.vector, s, v) @@ -60,7 +60,7 @@ func TestParseCvss3(t *testing.T) { }, } for _, tt := range tests { - s, v := Redhat{}.parseCvss3(tt.in) + s, v := RedHatBase{}.parseCvss3(tt.in) if s != tt.out.score || v != tt.out.vector { t.Errorf("\nexpected: %f, %s\n actual: %f, %s", tt.out.score, tt.out.vector, s, v) diff --git a/report/email.go b/report/email.go index 8dd3a941..5dc06ba9 100644 --- a/report/email.go +++ b/report/email.go @@ -50,7 +50,9 @@ func (w EMailWriter) Write(rs ...models.ScanResult) (err error) { conf.EMail.SubjectPrefix, r.ServerInfo()) } else { subject = fmt.Sprintf("%s%s %s", - conf.EMail.SubjectPrefix, r.ServerInfo(), r.CveSummary()) + conf.EMail.SubjectPrefix, + r.ServerInfo(), + r.CveSummary(config.Conf.IgnoreUnscoredCves)) } message = formatFullPlainText(r) if err := sender.Send(subject, message); err != nil { @@ -72,7 +74,7 @@ One Line Summary subject := fmt.Sprintf("%s %s", conf.EMail.SubjectPrefix, - totalResult.CveSummary(), + totalResult.CveSummary(config.Conf.IgnoreUnscoredCves), ) return sender.Send(subject, message) } diff --git a/report/slack.go b/report/slack.go index c1c6733c..166622ad 100644 --- a/report/slack.go +++ b/report/slack.go @@ -156,7 +156,10 @@ func msgText(r models.ScanResult) string { // notifyUsers = getNotifyUsers(config.Conf.Slack.NotifyUsers) // } serverInfo := fmt.Sprintf("*%s*", r.ServerInfo()) - return fmt.Sprintf("%s\n%s\n>%s", notifyUsers, serverInfo, r.CveSummary()) + return fmt.Sprintf("%s\n%s\n>%s", + notifyUsers, + serverInfo, + r.CveSummary(config.Conf.IgnoreUnscoredCves)) } func toSlackAttachments(scanResult models.ScanResult) (attaches []*attachment) { diff --git a/report/util.go b/report/util.go index f33e71e4..2752d8b2 100644 --- a/report/util.go +++ b/report/util.go @@ -25,10 +25,9 @@ import ( "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" "github.com/gosuri/uitable" - "github.com/k0kubun/pp" ) -const maxColWidth = 80 +const maxColWidth = 100 func formatScanSummary(rs ...models.ScanResult) string { table := uitable.New() @@ -65,7 +64,7 @@ func formatOneLineSummary(rs ...models.ScanResult) string { if len(r.Errors) == 0 { cols = []interface{}{ r.FormatServerName(), - r.CveSummary(), + r.CveSummary(config.Conf.IgnoreUnscoredCves), r.Packages.FormatUpdatablePacksSummary(), } } else { @@ -87,15 +86,14 @@ func formatShortPlainText(r models.ScanResult) string { vulns := r.ScannedCves if !config.Conf.IgnoreUnscoredCves { - //TODO Refactoring vulns = r.ScannedCves.Find(func(v models.VulnInfo) bool { - if 0 < v.CveContents.CvssV2Score() || 0 < v.CveContents.CvssV3Score() { + if 0 < v.CveContents.MaxCvss2Score().Value.Score || + 0 < v.CveContents.MaxCvss3Score().Value.Score { return true } return false }) } - pp.Println(vulns) var buf bytes.Buffer for i := 0; i < len(r.ServerInfo()); i++ { @@ -104,7 +102,7 @@ func formatShortPlainText(r models.ScanResult) string { header := fmt.Sprintf("%s\n%s\n%s\t%s\n\n", r.ServerInfo(), buf.String(), - r.CveSummary(), + r.CveSummary(config.Conf.IgnoreUnscoredCves), r.Packages.FormatUpdatablePacksSummary(), ) @@ -114,84 +112,91 @@ func formatShortPlainText(r models.ScanResult) string { header, r.Errors) } - //TODO - // if len(cves) == 0 { - // return fmt.Sprintf(` - // %s - // No CVE-IDs are found in updatable packages. - // %s - // `, header, r.Packages.FormatUpdatablePacksSummary()) - // } + if len(vulns) == 0 { + return fmt.Sprintf(` + %s + No CVE-IDs are found in updatable packages. + %s + `, header, r.Packages.FormatUpdatablePacksSummary()) + } - // for _, d := range cves { - // var packsVer string - // for _, p := range d.Packages { - // packsVer += fmt.Sprintf( - // "%s -> %s\n", p.FormatCurrentVer(), p.FormatNewVer()) - // } - // for _, n := range d.CpeNames { - // packsVer += n - // } + 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" + // } - // var scols []string - // switch { - // // case config.Conf.Lang == "ja" && - // //TODO - // // 0 < d.CveDetail.Jvn.CvssScore(): - // // summary := fmt.Sprintf("%s\n%s\n%s\n%sConfidence: %v", - // // d.CveDetail.Jvn.CveTitle(), - // // d.CveDetail.Jvn.Link(), - // // distroLinks(d, r.Family)[0].url, - // // packsVer, - // // d.VulnInfo.Confidence, - // // ) - // // scols = []string{ - // // d.CveDetail.CveID, - // // fmt.Sprintf("%-4.1f (%s)", - // // d.CveDetail.CvssScore(config.Conf.Lang), - // // d.CveDetail.Jvn.CvssSeverity(), - // // ), - // // summary, - // // } + 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, + }} + } - // case 0 < d.CvssV2Score(): - // var nvd *models.CveContent - // if cont, found := d.Get(models.NVD); found { - // nvd = cont - // } - // summary := fmt.Sprintf("%s\n%s/%s\n%s\n%sConfidence: %v", - // nvd.Summary, - // cveDetailsBaseURL, - // d.VulnInfo.CveID, - // distroLinks(d, r.Family)[0].url, - // packsVer, - // d.VulnInfo.Confidence, - // ) - // scols = []string{ - // d.VulnInfo.CveID, - // fmt.Sprintf("%-4.1f (%s)", - // d.CvssV2Score(), - // "TODO", - // ), - // summary, - // } - // default: - // summary := fmt.Sprintf("%s\n%sConfidence: %v", - // distroLinks(d, r.Family)[0].url, packsVer, d.VulnInfo.Confidence) - // scols = []string{ - // d.VulnInfo.CveID, - // "?", - // summary, - // } - // } + 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\n", vuln.Cvss2CalcURL()) - // cols := make([]interface{}, len(scols)) - // for i := range cols { - // cols[i] = scols[i] - // } - // stable.AddRow(cols...) - // stable.AddRow("") - // } + 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) + } + 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) + } + + rightCol := fmt.Sprintf(`%s +%s +--- +%s +%sConfidence: %v`, + maxCvss, + summaries[0].Value, + links[0].Value, + cvsses, + // packsVer, + vuln.Confidence, + ) + + leftCol := fmt.Sprintf("%s", vuln.CveID) + scols := []string{leftCol, rightCol} + cols := make([]interface{}, len(scols)) + for i := range cols { + cols[i] = scols[i] + } + stable.AddRow(cols...) + stable.AddRow("") + } return fmt.Sprintf("%s\n%s\n", header, stable) } @@ -205,7 +210,7 @@ func formatFullPlainText(r models.ScanResult) string { header := fmt.Sprintf("%s\n%s\n%s\t%s\n", r.ServerInfo(), buf.String(), - r.CveSummary(), + r.CveSummary(config.Conf.IgnoreUnscoredCves), r.Packages.FormatUpdatablePacksSummary(), ) @@ -481,7 +486,7 @@ func addPackages(table *uitable.Table, packs []models.Package) *uitable.Table { title = "Package" } ver := fmt.Sprintf( - "%s -> %s", p.FormatCurrentVer(), p.FormatNewVer()) + "%s -> %s", p.FormatVer(), p.FormatNewVer()) table.AddRow(title, ver) } return table @@ -522,7 +527,7 @@ func formatOneChangelog(p models.Package) string { } packVer := fmt.Sprintf("%s -> %s", - p.FormatCurrentVer(), p.FormatNewVer()) + p.FormatVer(), p.FormatNewVer()) var delim bytes.Buffer for i := 0; i < len(packVer); i++ { delim.WriteString("-") diff --git a/scan/debian.go b/scan/debian.go index ddcef4cd..28498cb8 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -673,6 +673,7 @@ func (o *debian) parseChangelog(changelog, name, ver string, confidence models.C pack := o.Packages[name] pack.Changelog = clog + // TODO Mutex o.Packages[name] = pack cves := []DetectedCveID{} diff --git a/scan/redhat.go b/scan/redhat.go index 013f64a7..ef1c0cd9 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -326,6 +326,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er o.log.Debugf("%s", pp.Sprintf("%v", packages)) // set candidate version info + //TODO Mutex?? o.Packages.MergeNewVersion(packages) // Collect CVE-IDs in changelog @@ -355,6 +356,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er Contents: *clog, Method: models.ChangelogExactMatchStr, } + //TODO Mutex o.Packages[p.Name] = p break }