From 10a27042b5451e026a7d4358170dccd047dd2b5f Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Sat, 22 Apr 2017 17:02:35 +0900 Subject: [PATCH 001/113] Support Debian --- commands/report.go | 27 ++++++++++- commands/scan.go | 34 ++++++++------ commands/util.go | 18 ++++++++ config/config.go | 10 +++-- models/models.go | 94 +++++++++++++++++++++++++++++++------- oval/debian.go | 109 +++++++++++++++++++++++++++++++++++++++++++++ oval/oval.go | 24 ++++++++++ oval/redhat.go | 11 +++++ 8 files changed, 295 insertions(+), 32 deletions(-) create mode 100644 oval/debian.go create mode 100644 oval/oval.go create mode 100644 oval/redhat.go diff --git a/commands/report.go b/commands/report.go index b1797742..163d5c6e 100644 --- a/commands/report.go +++ b/commands/report.go @@ -50,6 +50,9 @@ type ReportCmd struct { cvedbpath string cvedbURL string + ovaldbtype string + ovaldbpath string + toSlack bool toEMail bool toLocalFile bool @@ -162,6 +165,19 @@ func (p *ReportCmd) SetFlags(f *flag.FlagSet) { defaultCveDBPath, "/path/to/sqlite3 (For get cve detail from cve.sqlite3)") + f.StringVar( + &p.ovaldbtype, + "ovaldb-type", + "sqlite3", + "DB type for fetching OVAL dictionary (sqlite3 or mysql)") + + defaultOvalDBPath := filepath.Join(wd, "oval.sqlite3") + f.StringVar( + &p.ovaldbpath, + "ovaldb-path", + defaultOvalDBPath, + "/path/to/sqlite3 (For get oval detail from oval.sqlite3)") + f.StringVar( &p.cvedbURL, "cvedb-url", @@ -276,6 +292,8 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} c.Conf.CveDBType = p.cvedbtype c.Conf.CveDBPath = p.cvedbpath c.Conf.CveDBURL = p.cvedbURL + c.Conf.OvalDBType = p.ovaldbtype + c.Conf.OvalDBPath = p.ovaldbpath c.Conf.CvssScoreOver = p.cvssScoreOver c.Conf.IgnoreUnscoredCves = p.ignoreUnscoredCves c.Conf.HTTPProxy = p.httpProxy @@ -399,11 +417,18 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} } } - filled, err := fillCveInfoFromCveDB(r) + filled, err := fillCveInfoFromOvalDB(r) + if err != nil { + util.Log.Errorf("Failed to fill OVAL information: %s", err) + return subcommands.ExitFailure + } + + filled, err = fillCveInfoFromCveDB(*filled) if err != nil { util.Log.Errorf("Failed to fill CVE information: %s", err) return subcommands.ExitFailure } + filled.Lang = c.Conf.Lang if err := overwriteJSONFile(dir, *filled); err != nil { util.Log.Errorf("Failed to write JSON: %s", err) diff --git a/commands/scan.go b/commands/scan.go index 74128cb0..5359be60 100644 --- a/commands/scan.go +++ b/commands/scan.go @@ -35,19 +35,20 @@ import ( // ScanCmd is Subcommand of host discovery mode type ScanCmd struct { - debug bool - configPath string - resultsDir string - logDir string - cacheDBPath string - httpProxy string - askKeyPassword bool - containersOnly bool - skipBroken bool - sshNative bool - pipe bool - timeoutSec int - scanTimeoutSec int + debug bool + configPath string + resultsDir string + logDir string + cacheDBPath string + httpProxy string + askKeyPassword bool + containersOnly bool + packageListOnly bool + skipBroken bool + sshNative bool + pipe bool + timeoutSec int + scanTimeoutSec int } // Name return subcommand name @@ -132,6 +133,12 @@ func (p *ScanCmd) SetFlags(f *flag.FlagSet) { "Ask ssh privatekey password before scanning", ) + f.BoolVar( + &p.packageListOnly, + "package-list-only", + false, + "List all packages without scan") + f.BoolVar( &p.pipe, "pipe", @@ -223,6 +230,7 @@ func (p *ScanCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) c.Conf.SSHNative = p.sshNative c.Conf.HTTPProxy = p.httpProxy c.Conf.ContainersOnly = p.containersOnly + c.Conf.PackageListOnly = p.packageListOnly c.Conf.SkipBroken = p.skipBroken util.Log.Info("Validating config...") diff --git a/commands/util.go b/commands/util.go index 73a44ad3..d34a197a 100644 --- a/commands/util.go +++ b/commands/util.go @@ -31,6 +31,7 @@ import ( c "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/cveapi" "github.com/future-architect/vuls/models" + "github.com/future-architect/vuls/oval" "github.com/future-architect/vuls/report" "github.com/future-architect/vuls/util" ) @@ -180,6 +181,23 @@ func fillCveInfoFromCveDB(r models.ScanResult) (*models.ScanResult, error) { return r.FillCveDetail() } +func fillCveInfoFromOvalDB(r models.ScanResult) (*models.ScanResult, error) { + var ovalClient oval.OvalClient + switch r.Family { + case "ubuntu", "debian": + ovalClient = oval.NewDebian() + fmt.Println("hello") + case "redhat": + // TODO: RedHat + // ovalClient = oval.NewRedhat() + } + result, err := ovalClient.FillCveInfoFromOvalDB(r) + if err != nil { + return nil, err + } + return result, nil +} + func loadPreviousScanHistory(current models.ScanHistory) (previous models.ScanHistory, err error) { var dirs jsonDirs if dirs, err = lsValidJSONDirs(); err != nil { diff --git a/config/config.go b/config/config.go index ff42bfd4..db813314 100644 --- a/config/config.go +++ b/config/config.go @@ -44,9 +44,10 @@ type Config struct { CvssScoreOver float64 IgnoreUnscoredCves bool - SSHNative bool - ContainersOnly bool - SkipBroken bool + SSHNative bool + ContainersOnly bool + PackageListOnly bool + SkipBroken bool HTTPProxy string `valid:"url"` LogDir string @@ -57,6 +58,9 @@ type Config struct { CveDBURL string CacheDBPath string + OvalDBType string + OvalDBPath string + FormatXML bool FormatJSON bool FormatOneEMail bool diff --git a/models/models.go b/models/models.go index ddc94a3d..7b772f61 100644 --- a/models/models.go +++ b/models/models.go @@ -25,6 +25,7 @@ import ( "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/cveapi" cve "github.com/kotakanbe/go-cve-dictionary/models" + goval "github.com/kotakanbe/goval-dictionary/models" ) // ScanHistory is the history of Scanning. @@ -67,9 +68,9 @@ type ScanResult struct { // Scanned Vulns via SSH + CPE Vulns ScannedCves []VulnInfo - KnownCves []CveInfo - UnknownCves []CveInfo - IgnoredCves []CveInfo + KnownCves CveInfos + UnknownCves CveInfos + IgnoredCves CveInfos Packages PackageInfoList @@ -92,7 +93,7 @@ func (r ScanResult) FillCveDetail() (*ScanResult, error) { return nil, err } - known, unknown, ignored := CveInfos{}, CveInfos{}, CveInfos{} + r.IgnoredCves = CveInfos{} for _, d := range ds { cinfo := CveInfo{ CveDetail: d, @@ -104,7 +105,7 @@ func (r ScanResult) FillCveDetail() (*ScanResult, error) { found := false for _, icve := range config.Conf.Servers[r.ServerName].IgnoreCves { if icve == d.CveID { - ignored = append(ignored, cinfo) + r.IgnoredCves.Insert(cinfo) found = true break } @@ -113,29 +114,45 @@ func (r ScanResult) FillCveDetail() (*ScanResult, error) { continue } + // Update known if KnownCves already have cinfo + if c, ok := r.KnownCves.Get(cinfo.CveID); ok { + c.CveDetail = d + r.KnownCves.Update(c) + continue + } + + // Update unknown if UnknownCves already have cinfo + if c, ok := r.UnknownCves.Get(cinfo.CveID); ok { + c.CveDetail = d + r.UnknownCves.Update(c) + continue + } + // unknown if d.CvssScore(config.Conf.Lang) <= 0 { - unknown = append(unknown, cinfo) + r.UnknownCves.Insert(cinfo) continue } // known - known = append(known, cinfo) + r.KnownCves.Insert(cinfo) } - sort.Sort(known) - sort.Sort(unknown) - sort.Sort(ignored) - r.KnownCves = known - r.UnknownCves = unknown - r.IgnoredCves = ignored + sort.Sort(r.KnownCves) + sort.Sort(r.UnknownCves) + sort.Sort(r.IgnoredCves) return &r, nil } // FilterByCvssOver is filter function. func (r ScanResult) FilterByCvssOver() ScanResult { cveInfos := []CveInfo{} + // TODO: Set correct default value + if config.Conf.CvssScoreOver == 0 { + config.Conf.CvssScoreOver = -1.1 + } + for _, cveInfo := range r.KnownCves { - if config.Conf.CvssScoreOver < cveInfo.CveDetail.CvssScore(config.Conf.Lang) { + if config.Conf.CvssScoreOver <= cveInfo.CveDetail.CvssScore(config.Conf.Lang) { cveInfos = append(cveInfos, cveInfo) } } @@ -260,6 +277,9 @@ const ( // PkgAuditMatchStr is a String representation of PkgAuditMatch PkgAuditMatchStr = "PkgAuditMatch" + // OvalMatchStr is a String representation of OvalMatch + OvalMatchStr = "OvalMatch" + // ChangelogExactMatchStr is a String representation of ChangelogExactMatch ChangelogExactMatchStr = "ChangelogExactMatch" @@ -282,6 +302,9 @@ var YumUpdateSecurityMatch = Confidence{100, YumUpdateSecurityMatchStr} // PkgAuditMatch is a ranking how confident the CVE-ID was deteted correctly var PkgAuditMatch = Confidence{100, PkgAuditMatchStr} +// OvalMatch is a ranking how confident the CVE-ID was deteted correctly +var OvalMatch = Confidence{100, OvalMatchStr} + // ChangelogExactMatch is a ranking how confident the CVE-ID was deteted correctly var ChangelogExactMatch = Confidence{95, ChangelogExactMatchStr} @@ -368,9 +391,50 @@ func (c CveInfos) Less(i, j int) bool { return c[j].CveDetail.CvssScore(lang) < c[i].CveDetail.CvssScore(lang) } +func (c CveInfos) Get(cveID string) (CveInfo, bool) { + for _, cve := range c { + if cve.VulnInfo.CveID == cveID { + return cve, true + } + } + return CveInfo{}, false +} + +func (c *CveInfos) Delete(cveID string) { + cveInfos := *c + for i, cve := range cveInfos { + if cve.VulnInfo.CveID == cveID { + *c = append(cveInfos[:i], cveInfos[i+1:]...) + break + } + } +} + +func (c *CveInfos) Insert(cveInfo CveInfo) { + *c = append(*c, cveInfo) +} + +func (c CveInfos) Update(cveInfo CveInfo) (ok bool) { + for i, cve := range c { + if cve.VulnInfo.CveID == cveInfo.VulnInfo.CveID { + c[i] = cveInfo + return true + } + } + return false +} + +func (c *CveInfos) Upsert(cveInfo CveInfo) { + ok := c.Update(cveInfo) + if !ok { + c.Insert(cveInfo) + } +} + // CveInfo has Cve Information. type CveInfo struct { - CveDetail cve.CveDetail + CveDetail cve.CveDetail + OvalDetail goval.Definition VulnInfo } diff --git a/oval/debian.go b/oval/debian.go new file mode 100644 index 00000000..e4b4b292 --- /dev/null +++ b/oval/debian.go @@ -0,0 +1,109 @@ +package oval + +import ( + "fmt" + + "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/models" + "github.com/future-architect/vuls/util" + ver "github.com/knqyf263/go-deb-version" + cve "github.com/kotakanbe/go-cve-dictionary/models" + ovalconf "github.com/kotakanbe/goval-dictionary/config" + db "github.com/kotakanbe/goval-dictionary/db" + ovalmodels "github.com/kotakanbe/goval-dictionary/models" +) + +// Debian is the interface for Debian OVAL +type Debian 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) (*models.ScanResult, error) { + util.Log.Debugf("open oval-dictionary db (%s)", config.Conf.OvalDBType) + ovalconf.Conf.DBType = config.Conf.OvalDBType + ovalconf.Conf.DBPath = config.Conf.OvalDBPath + + if err := db.OpenDB(); err != nil { + return nil, fmt.Errorf("Failed to open OVAL DB. err: %s", err) + } + + d := db.NewDebian() + for _, pack := range r.Packages { + // TODO: Set the correct release after implementing LIKE in goval-dictionary + definitions, err := d.GetByPackName("8.2", pack.Name) + if err != nil { + return nil, fmt.Errorf("Failed to get OVAL info by package name: %v", err) + } + for _, definition := range definitions { + current, _ := ver.NewVersion(pack.Version) + for _, p := range definition.AffectedPacks { + if pack.Name != p.Name { + continue + } + affected, _ := ver.NewVersion(p.Version) + if current.LessThan(affected) { + r = o.fillOvalInfo(r, definition) + } + } + } + } + return &r, nil +} + +func (o Debian) fillOvalInfo(r models.ScanResult, definition ovalmodels.Definition) models.ScanResult { + // Update ScannedCves by OVAL info + found := false + cves := []models.VulnInfo{} + for _, cve := range r.ScannedCves { + if cve.CveID == definition.Debian.CveID { + found = true + if cve.Confidence.Score < models.OvalMatch.Score { + cve.Confidence = models.OvalMatch + } + } + cves = append(cves, cve) + } + + packageInfoList := getPackageInfoList(r, definition) + vuln := models.VulnInfo{ + CveID: definition.Debian.CveID, + Confidence: models.OvalMatch, + Packages: packageInfoList, + } + + if !found { + cves = append(cves, vuln) + } + r.ScannedCves = cves + + // Update KnownCves by OVAL info + cveInfo, ok := r.KnownCves.Get(definition.Debian.CveID) + if !ok { + cveInfo.CveDetail = cve.CveDetail{ + CveID: definition.Debian.CveID, + } + cveInfo.VulnInfo = vuln + } + cveInfo.OvalDetail = definition + if cveInfo.VulnInfo.Confidence.Score < models.OvalMatch.Score { + cveInfo.Confidence = models.OvalMatch + } + r.KnownCves.Upsert(cveInfo) + + // Update UnknownCves by OVAL info + cveInfo, ok = r.UnknownCves.Get(definition.Debian.CveID) + if ok { + cveInfo.OvalDetail = definition + if cveInfo.VulnInfo.Confidence.Score < models.OvalMatch.Score { + cveInfo.Confidence = models.OvalMatch + } + r.UnknownCves.Delete(definition.Debian.CveID) + r.KnownCves.Upsert(cveInfo) + } + + return r +} diff --git a/oval/oval.go b/oval/oval.go new file mode 100644 index 00000000..3b4390c4 --- /dev/null +++ b/oval/oval.go @@ -0,0 +1,24 @@ +package oval + +import ( + "github.com/future-architect/vuls/models" + ovalmodels "github.com/kotakanbe/goval-dictionary/models" +) + +// OvalClient is the interface of OVAL client. +type OvalClient interface { + FillCveInfoFromOvalDB(r models.ScanResult) (*models.ScanResult, error) +} + +func getPackageInfoList(r models.ScanResult, d ovalmodels.Definition) models.PackageInfoList { + var packageInfoList models.PackageInfoList + for _, pack := range d.AffectedPacks { + for _, p := range r.Packages { + if pack.Name == p.Name { + packageInfoList = append(packageInfoList, p) + break + } + } + } + return packageInfoList +} diff --git a/oval/redhat.go b/oval/redhat.go new file mode 100644 index 00000000..85e472ce --- /dev/null +++ b/oval/redhat.go @@ -0,0 +1,11 @@ +package oval + +type redhat struct{} + +func NewRedhat() redhat { + return redhat{} +} + +func (o redhat) FillCveInfoFromOvalDB() { + +} From 23c177ed4ab7324f03ed1b8605bd40a560ec0be7 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Mon, 24 Apr 2017 11:56:15 +0900 Subject: [PATCH 002/113] -package-list-only for Debian --- Gopkg.lock | 42 +++++++++++++++++++++++-- scan/debian.go | 83 +++++++++++++++++++++++++++----------------------- 2 files changed, 84 insertions(+), 41 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 73acf56d..4967656b 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,4 +1,4 @@ -memo = "404d058cf6b46d820e153afc5721e1ab2aa2b10ec345d969b9c460097f99add3" +memo = "0851217ca0cf4879a4cf7b2041f2ff852c408df45e075fbaccb7805164db4507" [[projects]] branch = "master" @@ -114,10 +114,16 @@ memo = "404d058cf6b46d820e153afc5721e1ab2aa2b10ec345d969b9c460097f99add3" revision = "612b0b2987ec1a6af46d7008cef1efd4b3898346" [[projects]] + branch = "master" name = "github.com/k0kubun/pp" packages = ["."] - revision = "027a6d1765d673d337e687394dbe780dd64e2a1e" - version = "v2.3.0" + revision = "d1532fc5d94ecdf2da29e24d7b99721f3287de4a" + +[[projects]] + branch = "master" + name = "github.com/knqyf263/go-deb-version" + packages = ["."] + revision = "bec774d791d03b721a20bd3ca1fbdd566fd0f2b9" [[projects]] branch = "master" @@ -131,6 +137,12 @@ memo = "404d058cf6b46d820e153afc5721e1ab2aa2b10ec345d969b9c460097f99add3" revision = "641dc2cc2d3cbf295dad356667b74c69bcbd6f70" version = "v0.1.0" +[[projects]] + branch = "master" + name = "github.com/kotakanbe/goval-dictionary" + packages = ["config","db","log","models"] + revision = "931528ebc56092a6abc0799665cb74f944d0705b" + [[projects]] branch = "master" name = "github.com/kotakanbe/logrus-prefixed-formatter" @@ -143,6 +155,12 @@ memo = "404d058cf6b46d820e153afc5721e1ab2aa2b10ec345d969b9c460097f99add3" packages = [".","hstore","oid"] revision = "2704adc878c21e1329f46f6e56a1c387d788ff94" +[[projects]] + name = "github.com/labstack/gommon" + packages = ["color","log"] + revision = "9cedb429ffbe71a32a3ae7c65fd109cb7ae07804" + version = "v0.2.0" + [[projects]] name = "github.com/mattn/go-colorable" packages = ["."] @@ -203,6 +221,24 @@ memo = "404d058cf6b46d820e153afc5721e1ab2aa2b10ec345d969b9c460097f99add3" revision = "2adb3e0c4ddd8778c4adde609d2dfd4fbe6096ea" version = "1.6" +[[projects]] + branch = "master" + name = "github.com/valyala/bytebufferpool" + packages = ["."] + revision = "e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7" + +[[projects]] + branch = "master" + name = "github.com/valyala/fasttemplate" + packages = ["."] + revision = "dcecefd839c4193db0d35b88ec65b4c12d360ab0" + +[[projects]] + branch = "master" + name = "github.com/ymomoi/goval-parser" + packages = ["oval"] + revision = "fa7d8e949108b0b2b7d124bef9a7f2bda9b6dd69" + [[projects]] branch = "master" name = "golang.org/x/crypto" diff --git a/scan/debian.go b/scan/debian.go index 10bff021..7ff27c26 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -161,27 +161,30 @@ func (o *debian) checkDependencies() error { } func (o *debian) scanPackages() error { - var err error - var packs []models.PackageInfo - if packs, err = o.scanInstalledPackages(); err != nil { + installed, upgradable, err := o.scanInstalledPackages() + if err != nil { o.log.Errorf("Failed to scan installed packages") return err } - o.setPackages(packs) + o.setPackages(installed) - var unsecurePacks []models.VulnInfo - if unsecurePacks, err = o.scanUnsecurePackages(packs); err != nil { + if config.Conf.PackageListOnly { + return nil + } + + unsecure, err := o.scanUnsecurePackages(upgradable) + if err != nil { o.log.Errorf("Failed to scan vulnerable packages") return err } - o.setVulnInfos(unsecurePacks) + o.setVulnInfos(unsecure) return nil } -func (o *debian) scanInstalledPackages() (packs []models.PackageInfo, err error) { +func (o *debian) scanInstalledPackages() (installed models.PackageInfoList, upgradable models.PackageInfoList, err error) { r := o.exec("dpkg-query -W", noSudo) if !r.isSuccess() { - return packs, fmt.Errorf("Failed to SSH: %s", r) + return nil, nil, fmt.Errorf("Failed to SSH: %s", r) } // e.g. @@ -192,15 +195,36 @@ func (o *debian) scanInstalledPackages() (packs []models.PackageInfo, err error) if trimmed := strings.TrimSpace(line); len(trimmed) != 0 { name, version, err := o.parseScannedPackagesLine(trimmed) if err != nil { - return nil, fmt.Errorf( + return nil, nil, fmt.Errorf( "Debian: Failed to parse package line: %s", line) } - packs = append(packs, models.PackageInfo{ + installed = append(installed, models.PackageInfo{ Name: name, Version: version, }) } } + + upgradableNames, err := o.GetUpgradablePackNames() + if err != nil { + return nil, nil, err + } + for _, name := range upgradableNames { + for _, pack := range installed { + if pack.Name == name { + upgradable = append(upgradable, pack) + break + } + } + } + + // Fill the candidate versions of upgradable packages + upgradable, err = o.fillCandidateVersion(upgradable) + if err != nil { + return nil, nil, fmt.Errorf("Failed to fill candidate versions. err: %s", err) + } + installed.MergeNewVersion(upgradable) + return } @@ -221,51 +245,34 @@ func (o *debian) parseScannedPackagesLine(line string) (name, version string, er return "", "", fmt.Errorf("Unknown format: %s", line) } -func (o *debian) scanUnsecurePackages(installed []models.PackageInfo) ([]models.VulnInfo, error) { +func (o *debian) aptGetUpdate() error { o.log.Infof("apt-get update...") cmd := util.PrependProxyEnv("apt-get update") if r := o.exec(cmd, sudo); !r.isSuccess() { - return nil, fmt.Errorf("Failed to SSH: %s", r) + return fmt.Errorf("Failed to SSH: %s", r) } + return nil +} - // Convert the name of upgradable packages to PackageInfo struct - upgradableNames, err := o.GetUpgradablePackNames() - if err != nil { - return nil, err - } - var upgradablePacks []models.PackageInfo - for _, name := range upgradableNames { - for _, pack := range installed { - if pack.Name == name { - upgradablePacks = append(upgradablePacks, pack) - break - } - } - } +func (o *debian) scanUnsecurePackages(upgradable []models.PackageInfo) ([]models.VulnInfo, error) { - // Fill the candidate versions of upgradable packages - upgradablePacks, err = o.fillCandidateVersion(upgradablePacks) - if err != nil { - return nil, fmt.Errorf("Failed to fill candidate versions. err: %s", err) - } - - o.Packages.MergeNewVersion(upgradablePacks) + o.aptGetUpdate() // Setup changelog cache current := cache.Meta{ Name: o.getServerInfo().GetServerName(), Distro: o.getServerInfo().Distro, - Packs: upgradablePacks, + Packs: upgradable, } o.log.Debugf("Ensure changelog cache: %s", current.Name) - var meta *cache.Meta - if meta, err = o.ensureChangelogCache(current); err != nil { + meta, err := o.ensureChangelogCache(current) + if err != nil { return nil, err } // Collect CVE information of upgradable packages - vulnInfos, err := o.scanVulnInfos(upgradablePacks, meta) + vulnInfos, err := o.scanVulnInfos(upgradable, meta) if err != nil { return nil, fmt.Errorf("Failed to scan unsecure packages. err: %s", err) } From e5d32c87644aec35d99d99b7ff47131d75d355c6 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Mon, 24 Apr 2017 14:40:31 +0900 Subject: [PATCH 003/113] Debian Report using OVAL --- Gopkg.lock | 12 ++++++------ Gopkg.toml | 8 ++++++++ oval/debian.go | 3 +-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 4967656b..c76e602a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,4 +1,4 @@ -memo = "0851217ca0cf4879a4cf7b2041f2ff852c408df45e075fbaccb7805164db4507" +memo = "bd95ed8c2b0aa32327ae55d88bff888b8198d238f7a71eee0f8663494664a0ac" [[projects]] branch = "master" @@ -141,7 +141,7 @@ memo = "0851217ca0cf4879a4cf7b2041f2ff852c408df45e075fbaccb7805164db4507" branch = "master" name = "github.com/kotakanbe/goval-dictionary" packages = ["config","db","log","models"] - revision = "931528ebc56092a6abc0799665cb74f944d0705b" + revision = "c33f7c4a77c3522ea25a70b9c38e1ae910f106cb" [[projects]] branch = "master" @@ -243,22 +243,22 @@ memo = "0851217ca0cf4879a4cf7b2041f2ff852c408df45e075fbaccb7805164db4507" branch = "master" name = "golang.org/x/crypto" packages = ["curve25519","ed25519","ed25519/internal/edwards25519","ssh","ssh/agent","ssh/terminal"] - revision = "ed779e1bec0180cdfce8135ca6558067b388777b" + revision = "96846453c37f0876340a66a47f3f75b1f3a6cd2d" [[projects]] branch = "master" name = "golang.org/x/net" packages = ["context","idna","publicsuffix"] - revision = "d1e1b351919c6738fdeb9893d5c998b161464f0c" + revision = "d212a1ef2de2f5d441c327b8f26cf3ea3ea9f265" [[projects]] branch = "master" name = "golang.org/x/sys" packages = ["unix"] - revision = "f3918c30c5c2cb527c0b071a27c35120a6c0719a" + revision = "ea9bcade75cb975a0b9738936568ab388b845617" [[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 = "f4b4367115ec2de254587813edaa901bc1c723a8" + revision = "a9a820217f98f7c8a207ec1e45a874e1fe12c478" diff --git a/Gopkg.toml b/Gopkg.toml index b1a1ac21..e5e7ee17 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -19,10 +19,18 @@ branch = "master" name = "github.com/jroimartin/gocui" +[[dependencies]] + branch = "master" + name = "github.com/k0kubun/pp" + [[dependencies]] branch = "master" name = "github.com/kotakanbe/go-cve-dictionary" +[[dependencies]] + branch = "master" + name = "github.com/kotakanbe/goval-dictionary" + [[dependencies]] branch = "master" name = "github.com/kotakanbe/logrus-prefixed-formatter" diff --git a/oval/debian.go b/oval/debian.go index e4b4b292..f9f31909 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -33,8 +33,7 @@ func (o Debian) FillCveInfoFromOvalDB(r models.ScanResult) (*models.ScanResult, d := db.NewDebian() for _, pack := range r.Packages { - // TODO: Set the correct release after implementing LIKE in goval-dictionary - definitions, err := d.GetByPackName("8.2", pack.Name) + definitions, err := d.GetByPackName(r.Release, pack.Name) if err != nil { return nil, fmt.Errorf("Failed to get OVAL info by package name: %v", err) } From c989c31aebea8623a541d63bc3915ed2423c567c Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Mon, 24 Apr 2017 23:28:46 +0900 Subject: [PATCH 004/113] Support RHEL --- commands/util.go | 5 +- oval/debian.go | 2 +- oval/redhat.go | 120 +++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 119 insertions(+), 8 deletions(-) diff --git a/commands/util.go b/commands/util.go index d34a197a..775f26e0 100644 --- a/commands/util.go +++ b/commands/util.go @@ -187,9 +187,8 @@ func fillCveInfoFromOvalDB(r models.ScanResult) (*models.ScanResult, error) { case "ubuntu", "debian": ovalClient = oval.NewDebian() fmt.Println("hello") - case "redhat": - // TODO: RedHat - // ovalClient = oval.NewRedhat() + case "redhat", "centos": + ovalClient = oval.NewRedhat() } result, err := ovalClient.FillCveInfoFromOvalDB(r) if err != nil { diff --git a/oval/debian.go b/oval/debian.go index f9f31909..2ba1d7c8 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -35,7 +35,7 @@ func (o Debian) FillCveInfoFromOvalDB(r models.ScanResult) (*models.ScanResult, for _, pack := range r.Packages { definitions, err := d.GetByPackName(r.Release, pack.Name) if err != nil { - return nil, fmt.Errorf("Failed to get OVAL info by package name: %v", err) + return nil, fmt.Errorf("Failed to get Debian OVAL info by package name: %v", err) } for _, definition := range definitions { current, _ := ver.NewVersion(pack.Version) diff --git a/oval/redhat.go b/oval/redhat.go index 85e472ce..6cb795d5 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -1,11 +1,123 @@ package oval -type redhat struct{} +import ( + "fmt" -func NewRedhat() redhat { - return redhat{} + "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/models" + "github.com/future-architect/vuls/util" + ver "github.com/knqyf263/go-deb-version" + cve "github.com/kotakanbe/go-cve-dictionary/models" + ovalconf "github.com/kotakanbe/goval-dictionary/config" + db "github.com/kotakanbe/goval-dictionary/db" + ovalmodels "github.com/kotakanbe/goval-dictionary/models" +) + +// Redhat is the interface for Redhat OVAL +type Redhat struct{} + +// NewRedhat creates OVAL client for Redhat +func NewRedhat() Redhat { + return Redhat{} } -func (o redhat) FillCveInfoFromOvalDB() { +// FillCveInfoFromOvalDB returns scan result after updating CVE info by OVAL +func (o Redhat) FillCveInfoFromOvalDB(r models.ScanResult) (*models.ScanResult, error) { + util.Log.Debugf("open oval-dictionary db (%s)", config.Conf.OvalDBType) + ovalconf.Conf.DBType = config.Conf.OvalDBType + ovalconf.Conf.DBPath = config.Conf.OvalDBPath + + if err := db.OpenDB(); err != nil { + return nil, fmt.Errorf("Failed to open OVAL DB. err: %s", err) + } + + d := db.NewRedHat() + + for _, pack := range r.Packages { + definitions, err := d.GetByPackName("6", pack.Name) + if err != nil { + return nil, fmt.Errorf("Failed to get RedHat OVAL info by package name: %v", err) + } + for _, definition := range definitions { + current, _ := ver.NewVersion(fmt.Sprintf("%s-%s", pack.Version, pack.Release)) + for _, p := range definition.AffectedPacks { + if pack.Name != p.Name { + continue + } + affected, _ := ver.NewVersion(p.Version) + if current.LessThan(affected) { + r = o.fillOvalInfo(r, definition) + } + } + } + } + return &r, nil +} + +func (o Redhat) fillOvalInfo(r models.ScanResult, definition ovalmodels.Definition) models.ScanResult { + found := make(map[string]bool) + vulnInfos := make(map[string]models.VulnInfo) + packageInfoList := getPackageInfoList(r, definition) + for _, cve := range definition.Advisory.Cves { + found[cve.CveID] = false + vulnInfos[cve.CveID] = models.VulnInfo{ + CveID: cve.CveID, + Confidence: models.OvalMatch, + Packages: packageInfoList, + } + } + + // Update ScannedCves by OVAL info + cves := []models.VulnInfo{} + for _, scannedCve := range r.ScannedCves { + for _, c := range definition.Advisory.Cves { + if scannedCve.CveID == c.CveID { + found[c.CveID] = true + if scannedCve.Confidence.Score < models.OvalMatch.Score { + scannedCve.Confidence = models.OvalMatch + } + break + } + } + cves = append(cves, scannedCve) + } + + for cveID, found := range found { + if !found { + cves = append(cves, vulnInfos[cveID]) + } + } + r.ScannedCves = cves + + // Update KnownCves by OVAL info + for _, c := range definition.Advisory.Cves { + cveInfo, ok := r.KnownCves.Get(c.CveID) + if !ok { + cveInfo.CveDetail = cve.CveDetail{ + CveID: c.CveID, + } + cveInfo.VulnInfo = vulnInfos[c.CveID] + } + cveInfo.OvalDetail = definition + if cveInfo.VulnInfo.Confidence.Score < models.OvalMatch.Score { + cveInfo.Confidence = models.OvalMatch + } + r.KnownCves.Upsert(cveInfo) + } + + // Update UnknownCves by OVAL info + for _, c := range definition.Advisory.Cves { + cveInfo, ok := r.UnknownCves.Get(c.CveID) + if ok { + cveInfo.OvalDetail = definition + if cveInfo.VulnInfo.Confidence.Score < models.OvalMatch.Score { + cveInfo.Confidence = models.OvalMatch + } + r.UnknownCves.Delete(c.CveID) + r.KnownCves.Upsert(cveInfo) + } + } + + return r } From 1a319859eb5194b45848d1753d12ac309b72dbc0 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Wed, 26 Apr 2017 12:20:21 +0900 Subject: [PATCH 005/113] Include RHEL, CentOS epoch number in version --- Gopkg.lock | 10 +++++----- scan/redhat.go | 17 +++++++++++------ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index c76e602a..3f569b94 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -141,7 +141,7 @@ memo = "bd95ed8c2b0aa32327ae55d88bff888b8198d238f7a71eee0f8663494664a0ac" branch = "master" name = "github.com/kotakanbe/goval-dictionary" packages = ["config","db","log","models"] - revision = "c33f7c4a77c3522ea25a70b9c38e1ae910f106cb" + revision = "9aba0cebf04ef546c7ae8666fea5e142b9e90fc1" [[projects]] branch = "master" @@ -237,25 +237,25 @@ memo = "bd95ed8c2b0aa32327ae55d88bff888b8198d238f7a71eee0f8663494664a0ac" branch = "master" name = "github.com/ymomoi/goval-parser" packages = ["oval"] - revision = "fa7d8e949108b0b2b7d124bef9a7f2bda9b6dd69" + revision = "4ddf6fc4f1a1af026f7cae41f76979c7ff2b2e2f" [[projects]] branch = "master" name = "golang.org/x/crypto" packages = ["curve25519","ed25519","ed25519/internal/edwards25519","ssh","ssh/agent","ssh/terminal"] - revision = "96846453c37f0876340a66a47f3f75b1f3a6cd2d" + revision = "3543873453996aaab2fc6b3928a35fc5ca2b5afb" [[projects]] branch = "master" name = "golang.org/x/net" packages = ["context","idna","publicsuffix"] - revision = "d212a1ef2de2f5d441c327b8f26cf3ea3ea9f265" + revision = "da118f7b8e5954f39d0d2130ab35d4bf0e3cb344" [[projects]] branch = "master" name = "golang.org/x/sys" packages = ["unix"] - revision = "ea9bcade75cb975a0b9738936568ab388b845617" + revision = "8c0a5eacbac818f9011015b17992f53d9cec3e8f" [[projects]] branch = "master" diff --git a/scan/redhat.go b/scan/redhat.go index 4214f341..053e16fa 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -244,7 +244,7 @@ func (o *redhat) scanPackages() error { } func (o *redhat) scanInstalledPackages() (installedPackages models.PackageInfoList, err error) { - cmd := "rpm -qa --queryformat '%{NAME}\t%{VERSION}\t%{RELEASE}\n'" + cmd := "rpm -qa --queryformat '%{NAME}\t%{EPOCHNUM}\t%{VERSION}\t%{RELEASE}\n'" r := o.exec(cmd, noSudo) if r.isSuccess() { // e.g. @@ -269,14 +269,20 @@ func (o *redhat) scanInstalledPackages() (installedPackages models.PackageInfoLi func (o *redhat) parseScannedPackagesLine(line string) (models.PackageInfo, error) { fields := strings.Fields(line) - if len(fields) != 3 { + if len(fields) != 4 { return models.PackageInfo{}, fmt.Errorf("Failed to parse package line: %s", line) } + ver := "" + if fields[1] == "0" { + ver = fields[2] + } else { + ver = fmt.Sprintf("%s:%s", fields[1], fields[2]) + } return models.PackageInfo{ Name: fields[0], - Version: fields[1], - Release: fields[2], + Version: ver, + Release: fields[3], }, nil } @@ -471,13 +477,12 @@ func (o *redhat) parseYumCheckUpdateLine(line string) (models.PackageInfo, error if len(verfields) != 2 { return models.PackageInfo{}, fmt.Errorf("Unknown format: %s", line) } - version := o.regexpReplace(verfields[0], `^[0-9]+:`, "") release := verfields[1] repos := strings.Join(fields[2:len(fields)], " ") return models.PackageInfo{ Name: packName, - NewVersion: version, + NewVersion: verfields[0], NewRelease: release, Repository: repos, }, nil From 587c87b3a0d07ff2d0524b9f6c92ba17231ea256 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Wed, 26 Apr 2017 13:49:18 +0900 Subject: [PATCH 006/113] Fix RHEL oval scan --- commands/util.go | 5 ++++- oval/redhat.go | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/commands/util.go b/commands/util.go index 775f26e0..057c1809 100644 --- a/commands/util.go +++ b/commands/util.go @@ -187,8 +187,11 @@ func fillCveInfoFromOvalDB(r models.ScanResult) (*models.ScanResult, error) { case "ubuntu", "debian": ovalClient = oval.NewDebian() fmt.Println("hello") - case "redhat", "centos": + case "rhel", "centos": ovalClient = oval.NewRedhat() + fmt.Println("good morning") + default: + return nil, fmt.Errorf("Oval %s is not implemented yet", r.Family) } result, err := ovalClient.FillCveInfoFromOvalDB(r) if err != nil { diff --git a/oval/redhat.go b/oval/redhat.go index 6cb795d5..e9fc9f36 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -35,7 +35,7 @@ func (o Redhat) FillCveInfoFromOvalDB(r models.ScanResult) (*models.ScanResult, d := db.NewRedHat() for _, pack := range r.Packages { - definitions, err := d.GetByPackName("6", pack.Name) + definitions, err := d.GetByPackName(r.Release, pack.Name) if err != nil { return nil, fmt.Errorf("Failed to get RedHat OVAL info by package name: %v", err) } @@ -86,6 +86,7 @@ func (o Redhat) fillOvalInfo(r models.ScanResult, definition ovalmodels.Definiti for cveID, found := range found { if !found { cves = append(cves, vulnInfos[cveID]) + util.Log.Debugf("%s is newly detected by OVAL", cveID) } } r.ScannedCves = cves From c9ab956f8f1bdeee5e2c68d0ccc0869df61add10 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Wed, 26 Apr 2017 14:28:02 +0900 Subject: [PATCH 007/113] Make it work on Amazon Linux --- commands/report.go | 2 +- commands/util.go | 6 ++++-- oval/debian.go | 12 ++++++------ oval/oval.go | 8 ++++---- oval/redhat.go | 12 ++++++------ 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/commands/report.go b/commands/report.go index 163d5c6e..a4e744db 100644 --- a/commands/report.go +++ b/commands/report.go @@ -417,7 +417,7 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} } } - filled, err := fillCveInfoFromOvalDB(r) + filled, err := fillCveInfoFromOvalDB(&r) if err != nil { util.Log.Errorf("Failed to fill OVAL information: %s", err) return subcommands.ExitFailure diff --git a/commands/util.go b/commands/util.go index 057c1809..563cbd7b 100644 --- a/commands/util.go +++ b/commands/util.go @@ -181,8 +181,8 @@ func fillCveInfoFromCveDB(r models.ScanResult) (*models.ScanResult, error) { return r.FillCveDetail() } -func fillCveInfoFromOvalDB(r models.ScanResult) (*models.ScanResult, error) { - var ovalClient oval.OvalClient +func fillCveInfoFromOvalDB(r *models.ScanResult) (*models.ScanResult, error) { + var ovalClient oval.Client switch r.Family { case "ubuntu", "debian": ovalClient = oval.NewDebian() @@ -190,6 +190,8 @@ func fillCveInfoFromOvalDB(r models.ScanResult) (*models.ScanResult, error) { case "rhel", "centos": ovalClient = oval.NewRedhat() fmt.Println("good morning") + case "amazon": + return r, nil default: return nil, fmt.Errorf("Oval %s is not implemented yet", r.Family) } diff --git a/oval/debian.go b/oval/debian.go index 2ba1d7c8..9bc291a1 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -22,7 +22,7 @@ func NewDebian() Debian { } // FillCveInfoFromOvalDB returns scan result after updating CVE info by OVAL -func (o Debian) FillCveInfoFromOvalDB(r models.ScanResult) (*models.ScanResult, error) { +func (o Debian) FillCveInfoFromOvalDB(r *models.ScanResult) (*models.ScanResult, error) { util.Log.Debugf("open oval-dictionary db (%s)", config.Conf.OvalDBType) ovalconf.Conf.DBType = config.Conf.OvalDBType ovalconf.Conf.DBPath = config.Conf.OvalDBPath @@ -45,15 +45,15 @@ func (o Debian) FillCveInfoFromOvalDB(r models.ScanResult) (*models.ScanResult, } affected, _ := ver.NewVersion(p.Version) if current.LessThan(affected) { - r = o.fillOvalInfo(r, definition) + r = o.fillOvalInfo(r, &definition) } } } } - return &r, nil + return r, nil } -func (o Debian) fillOvalInfo(r models.ScanResult, definition ovalmodels.Definition) models.ScanResult { +func (o Debian) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Definition) *models.ScanResult { // Update ScannedCves by OVAL info found := false cves := []models.VulnInfo{} @@ -87,7 +87,7 @@ func (o Debian) fillOvalInfo(r models.ScanResult, definition ovalmodels.Definiti } cveInfo.VulnInfo = vuln } - cveInfo.OvalDetail = definition + cveInfo.OvalDetail = *definition if cveInfo.VulnInfo.Confidence.Score < models.OvalMatch.Score { cveInfo.Confidence = models.OvalMatch } @@ -96,7 +96,7 @@ func (o Debian) fillOvalInfo(r models.ScanResult, definition ovalmodels.Definiti // Update UnknownCves by OVAL info cveInfo, ok = r.UnknownCves.Get(definition.Debian.CveID) if ok { - cveInfo.OvalDetail = definition + cveInfo.OvalDetail = *definition if cveInfo.VulnInfo.Confidence.Score < models.OvalMatch.Score { cveInfo.Confidence = models.OvalMatch } diff --git a/oval/oval.go b/oval/oval.go index 3b4390c4..a247386e 100644 --- a/oval/oval.go +++ b/oval/oval.go @@ -5,12 +5,12 @@ import ( ovalmodels "github.com/kotakanbe/goval-dictionary/models" ) -// OvalClient is the interface of OVAL client. -type OvalClient interface { - FillCveInfoFromOvalDB(r models.ScanResult) (*models.ScanResult, error) +// Client is the interface of OVAL client. +type Client interface { + FillCveInfoFromOvalDB(r *models.ScanResult) (*models.ScanResult, error) } -func getPackageInfoList(r models.ScanResult, d ovalmodels.Definition) models.PackageInfoList { +func getPackageInfoList(r *models.ScanResult, d *ovalmodels.Definition) models.PackageInfoList { var packageInfoList models.PackageInfoList for _, pack := range d.AffectedPacks { for _, p := range r.Packages { diff --git a/oval/redhat.go b/oval/redhat.go index e9fc9f36..ae3a127e 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -22,7 +22,7 @@ func NewRedhat() Redhat { } // FillCveInfoFromOvalDB returns scan result after updating CVE info by OVAL -func (o Redhat) FillCveInfoFromOvalDB(r models.ScanResult) (*models.ScanResult, error) { +func (o Redhat) FillCveInfoFromOvalDB(r *models.ScanResult) (*models.ScanResult, error) { util.Log.Debugf("open oval-dictionary db (%s)", config.Conf.OvalDBType) ovalconf.Conf.DBType = config.Conf.OvalDBType @@ -47,15 +47,15 @@ func (o Redhat) FillCveInfoFromOvalDB(r models.ScanResult) (*models.ScanResult, } affected, _ := ver.NewVersion(p.Version) if current.LessThan(affected) { - r = o.fillOvalInfo(r, definition) + r = o.fillOvalInfo(r, &definition) } } } } - return &r, nil + return r, nil } -func (o Redhat) fillOvalInfo(r models.ScanResult, definition ovalmodels.Definition) models.ScanResult { +func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Definition) *models.ScanResult { found := make(map[string]bool) vulnInfos := make(map[string]models.VulnInfo) packageInfoList := getPackageInfoList(r, definition) @@ -100,7 +100,7 @@ func (o Redhat) fillOvalInfo(r models.ScanResult, definition ovalmodels.Definiti } cveInfo.VulnInfo = vulnInfos[c.CveID] } - cveInfo.OvalDetail = definition + cveInfo.OvalDetail = *definition if cveInfo.VulnInfo.Confidence.Score < models.OvalMatch.Score { cveInfo.Confidence = models.OvalMatch } @@ -111,7 +111,7 @@ func (o Redhat) fillOvalInfo(r models.ScanResult, definition ovalmodels.Definiti for _, c := range definition.Advisory.Cves { cveInfo, ok := r.UnknownCves.Get(c.CveID) if ok { - cveInfo.OvalDetail = definition + cveInfo.OvalDetail = *definition if cveInfo.VulnInfo.Confidence.Score < models.OvalMatch.Score { cveInfo.Confidence = models.OvalMatch } From 037e12b0bd203fa66a5bba0e23594bcc22dae206 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Wed, 26 Apr 2017 19:30:38 +0900 Subject: [PATCH 008/113] Add Ubuntu Support --- Gopkg.lock | 10 +++++----- commands/util.go | 5 ++--- oval/debian.go | 9 ++++++++- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 3f569b94..6713d9d8 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -27,8 +27,8 @@ memo = "bd95ed8c2b0aa32327ae55d88bff888b8198d238f7a71eee0f8663494664a0ac" [[projects]] name = "github.com/asaskevich/govalidator" packages = ["."] - revision = "7b3beb6df3c42abd3509abfc3bcacc0fbfb7c877" - version = "v5" + revision = "4918b99a7cb949bb295f3c7bbaf24b577d806e35" + version = "v6" [[projects]] name = "github.com/aws/aws-sdk-go" @@ -141,7 +141,7 @@ memo = "bd95ed8c2b0aa32327ae55d88bff888b8198d238f7a71eee0f8663494664a0ac" branch = "master" name = "github.com/kotakanbe/goval-dictionary" packages = ["config","db","log","models"] - revision = "9aba0cebf04ef546c7ae8666fea5e142b9e90fc1" + revision = "5470d7565a9de51593f53327ce14c97d466b05ab" [[projects]] branch = "master" @@ -243,7 +243,7 @@ memo = "bd95ed8c2b0aa32327ae55d88bff888b8198d238f7a71eee0f8663494664a0ac" branch = "master" name = "golang.org/x/crypto" packages = ["curve25519","ed25519","ed25519/internal/edwards25519","ssh","ssh/agent","ssh/terminal"] - revision = "3543873453996aaab2fc6b3928a35fc5ca2b5afb" + revision = "c7af5bf2638a1164f2eb5467c39c6cffbd13a02e" [[projects]] branch = "master" @@ -255,7 +255,7 @@ memo = "bd95ed8c2b0aa32327ae55d88bff888b8198d238f7a71eee0f8663494664a0ac" branch = "master" name = "golang.org/x/sys" packages = ["unix"] - revision = "8c0a5eacbac818f9011015b17992f53d9cec3e8f" + revision = "9f30dcbe5be197894515a338a9bda9253567ea8f" [[projects]] branch = "master" diff --git a/commands/util.go b/commands/util.go index 563cbd7b..c5378624 100644 --- a/commands/util.go +++ b/commands/util.go @@ -186,11 +186,10 @@ func fillCveInfoFromOvalDB(r *models.ScanResult) (*models.ScanResult, error) { switch r.Family { case "ubuntu", "debian": ovalClient = oval.NewDebian() - fmt.Println("hello") case "rhel", "centos": ovalClient = oval.NewRedhat() - fmt.Println("good morning") - case "amazon": + case "amazon", "oraclelinux", "Raspbian": + //TODO implement OracleLinux return r, nil default: return nil, fmt.Errorf("Oval %s is not implemented yet", r.Family) diff --git a/oval/debian.go b/oval/debian.go index 9bc291a1..ac892097 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -31,7 +31,13 @@ func (o Debian) FillCveInfoFromOvalDB(r *models.ScanResult) (*models.ScanResult, return nil, fmt.Errorf("Failed to open OVAL DB. err: %s", err) } - d := db.NewDebian() + 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) if err != nil { @@ -76,6 +82,7 @@ func (o Debian) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini if !found { cves = append(cves, vuln) + util.Log.Debugf("%s is newly detected by OVAL", vuln.CveID) } r.ScannedCves = cves From 085a9dcb7959fe683471e37f2f9e0b859deb8af6 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Wed, 26 Apr 2017 20:10:18 +0900 Subject: [PATCH 009/113] Fix Test Case --- models/models.go | 5 +++++ scan/redhat_test.go | 14 +++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/models/models.go b/models/models.go index 7b772f61..448d5e3c 100644 --- a/models/models.go +++ b/models/models.go @@ -391,6 +391,7 @@ func (c CveInfos) Less(i, j int) bool { return c[j].CveDetail.CvssScore(lang) < c[i].CveDetail.CvssScore(lang) } +// Get cveInfo by cveID func (c CveInfos) Get(cveID string) (CveInfo, bool) { for _, cve := range c { if cve.VulnInfo.CveID == cveID { @@ -400,6 +401,7 @@ func (c CveInfos) Get(cveID string) (CveInfo, bool) { return CveInfo{}, false } +// Delete by cveID func (c *CveInfos) Delete(cveID string) { cveInfos := *c for i, cve := range cveInfos { @@ -410,10 +412,12 @@ func (c *CveInfos) Delete(cveID string) { } } +// Insert cveInfo func (c *CveInfos) Insert(cveInfo CveInfo) { *c = append(*c, cveInfo) } +// Update cveInfo func (c CveInfos) Update(cveInfo CveInfo) (ok bool) { for i, cve := range c { if cve.VulnInfo.CveID == cveInfo.VulnInfo.CveID { @@ -424,6 +428,7 @@ func (c CveInfos) Update(cveInfo CveInfo) (ok bool) { return false } +// Upsert cveInfo func (c *CveInfos) Upsert(cveInfo CveInfo) { ok := c.Update(cveInfo) if !ok { diff --git a/scan/redhat_test.go b/scan/redhat_test.go index 16097892..9eb78133 100644 --- a/scan/redhat_test.go +++ b/scan/redhat_test.go @@ -41,7 +41,7 @@ func TestParseScanedPackagesLineRedhat(t *testing.T) { pack models.PackageInfo }{ { - "openssl 1.0.1e 30.el6.11", + "openssl 0 1.0.1e 30.el6.11", models.PackageInfo{ Name: "openssl", Version: "1.0.1e", @@ -49,10 +49,10 @@ func TestParseScanedPackagesLineRedhat(t *testing.T) { }, }, { - "Percona-Server-shared-56 5.6.19 rel67.0.el6", + "Percona-Server-shared-56 1 5.6.19 rel67.0.el6", models.PackageInfo{ Name: "Percona-Server-shared-56", - Version: "5.6.19", + Version: "1:5.6.19", Release: "rel67.0.el6", }, }, @@ -359,7 +359,7 @@ Description : [32:9.9.4-38.2] : - Fix and test caching CNAME before DNAME (ISC : change 4558) Severity : Moderate - + =============================================================================== openssl security update =============================================================================== @@ -376,7 +376,7 @@ Description : [1.0.1e-48.4] : - fix CVE-2016-8610 - DoS of single-threaded : servers via excessive alerts Severity : Moderate - + =============================================================================== Unbreakable Enterprise kernel security update =============================================================================== @@ -758,7 +758,7 @@ pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5 Name: "bind-utils", Version: "1.0", Release: "1", - NewVersion: "9.3.6", + NewVersion: "30:9.3.6", NewRelease: "25.P1.el5_11.8", Repository: "updates", }, @@ -828,7 +828,7 @@ if-not-architecture 100-200 amzn-main Name: "bind-libs", Version: "9.8.0", Release: "0.33.rc1.45.amzn1", - NewVersion: "9.8.2", + NewVersion: "32:9.8.2", NewRelease: "0.37.rc1.45.amzn1", Repository: "amzn-main", }, From bb708db89fed0034e48c2fcab3921d959fcb3e90 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Wed, 26 Apr 2017 20:29:32 +0900 Subject: [PATCH 010/113] Make it work on FreeBSD --- commands/util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/util.go b/commands/util.go index c5378624..8052ba38 100644 --- a/commands/util.go +++ b/commands/util.go @@ -188,7 +188,7 @@ func fillCveInfoFromOvalDB(r *models.ScanResult) (*models.ScanResult, error) { ovalClient = oval.NewDebian() case "rhel", "centos": ovalClient = oval.NewRedhat() - case "amazon", "oraclelinux", "Raspbian": + case "amazon", "oraclelinux", "Raspbian", "FreeBSD": //TODO implement OracleLinux return r, nil default: From ec092501c321d5c3f271ddf5d5bf094f22134b36 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Thu, 27 Apr 2017 19:19:14 +0900 Subject: [PATCH 011/113] [BreakingChange]Remove models.ScanHistory --- commands/report.go | 12 +++++------- commands/tui.go | 13 ++++++------- commands/util.go | 34 ++++++++++++++-------------------- models/models.go | 5 ----- report/tui.go | 16 ++++++++-------- 5 files changed, 33 insertions(+), 47 deletions(-) diff --git a/commands/report.go b/commands/report.go index a4e744db..85f2005f 100644 --- a/commands/report.go +++ b/commands/report.go @@ -397,8 +397,7 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} } } - var history models.ScanHistory - history, err = loadOneScanHistory(dir) + rs, err := loadScanResults(dir) if err != nil { util.Log.Error(err) return subcommands.ExitFailure @@ -406,7 +405,7 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} util.Log.Infof("Loaded: %s", dir) var results []models.ScanResult - for _, r := range history.ScanResults { + for _, r := range rs { if p.refreshCve || needToRefreshCve(r) { util.Log.Debugf("need to refresh") if c.Conf.CveDBType == "sqlite3" && c.Conf.CveDBURL == "" { @@ -442,20 +441,19 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} } if p.diff { - currentHistory := models.ScanHistory{ScanResults: results} - previousHistory, err := loadPreviousScanHistory(currentHistory) + previous, err := loadPrevious(results) if err != nil { util.Log.Error(err) return subcommands.ExitFailure } - history, err = diff(currentHistory, previousHistory) + diff, err := diff(results, previous) if err != nil { util.Log.Error(err) return subcommands.ExitFailure } results = []models.ScanResult{} - for _, r := range history.ScanResults { + for _, r := range diff { filled, _ := r.FillCveDetail() results = append(results, *filled) } diff --git a/commands/tui.go b/commands/tui.go index 94347c6a..530b9c32 100644 --- a/commands/tui.go +++ b/commands/tui.go @@ -152,14 +152,14 @@ func (p *TuiCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s return subcommands.ExitFailure } - history, err := loadOneScanHistory(jsonDir) + results, err := loadScanResults(jsonDir) if err != nil { log.Errorf("Failed to read from JSON: %s", err) return subcommands.ExitFailure } - var results []models.ScanResult - for _, r := range history.ScanResults { + var filledResults []models.ScanResult + for _, r := range results { if p.refreshCve || needToRefreshCve(r) { if c.Conf.CveDBType == "sqlite3" { if _, err := os.Stat(c.Conf.CveDBPath); os.IsNotExist(err) { @@ -179,11 +179,10 @@ func (p *TuiCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s log.Errorf("Failed to write JSON: %s", err) return subcommands.ExitFailure } - results = append(results, *filled) + filledResults = append(filledResults, *filled) } else { - results = append(results, r) + filledResults = append(filledResults, r) } } - history.ScanResults = results - return report.RunTui(history) + return report.RunTui(filledResults) } diff --git a/commands/util.go b/commands/util.go index 8052ba38..1e0bee33 100644 --- a/commands/util.go +++ b/commands/util.go @@ -136,13 +136,11 @@ func loadOneServerScanResult(jsonFile string) (result models.ScanResult, err err return } -// loadOneScanHistory read JSON data -func loadOneScanHistory(jsonDir string) (scanHistory models.ScanHistory, err error) { - var results []models.ScanResult +// loadScanResults read JSON data +func loadScanResults(jsonDir string) (results models.ScanResults, err error) { var files []os.FileInfo if files, err = ioutil.ReadDir(jsonDir); err != nil { - err = fmt.Errorf("Failed to read %s: %s", jsonDir, err) - return + return nil, fmt.Errorf("Failed to read %s: %s", jsonDir, err) } for _, f := range files { if filepath.Ext(f.Name()) != ".json" || strings.HasSuffix(f.Name(), "_diff.json") { @@ -152,18 +150,13 @@ func loadOneScanHistory(jsonDir string) (scanHistory models.ScanHistory, err err var r models.ScanResult path := filepath.Join(jsonDir, f.Name()) if r, err = loadOneServerScanResult(path); err != nil { - return + return nil, err } results = append(results, r) } if len(results) == 0 { - err = fmt.Errorf("There is no json file under %s", jsonDir) - return - } - - scanHistory = models.ScanHistory{ - ScanResults: results, + return nil, fmt.Errorf("There is no json file under %s", jsonDir) } return } @@ -201,13 +194,13 @@ func fillCveInfoFromOvalDB(r *models.ScanResult) (*models.ScanResult, error) { return result, nil } -func loadPreviousScanHistory(current models.ScanHistory) (previous models.ScanHistory, err error) { +func loadPrevious(current models.ScanResults) (previous models.ScanResults, err error) { var dirs jsonDirs if dirs, err = lsValidJSONDirs(); err != nil { return } - for _, result := range current.ScanResults { + for _, result := range current { for _, dir := range dirs[1:] { var r models.ScanResult path := filepath.Join(dir, result.ServerName+".json") @@ -215,7 +208,8 @@ func loadPreviousScanHistory(current models.ScanHistory) (previous models.ScanHi continue } if r.Family == result.Family && r.Release == result.Release { - previous.ScanResults = append(previous.ScanResults, r) + previous = append(previous, r) + util.Log.Infof("Privious json found: %s", path) break } } @@ -223,11 +217,11 @@ func loadPreviousScanHistory(current models.ScanHistory) (previous models.ScanHi return previous, nil } -func diff(currentHistory, previousHistory models.ScanHistory) (diffHistory models.ScanHistory, err error) { - for _, currentResult := range currentHistory.ScanResults { +func diff(current, previous models.ScanResults) (diff models.ScanResults, err error) { + for _, currentResult := range current { found := false var previousResult models.ScanResult - for _, previousResult = range previousHistory.ScanResults { + for _, previousResult = range previous { if currentResult.ServerName == previousResult.ServerName { found = true break @@ -247,9 +241,9 @@ func diff(currentHistory, previousHistory models.ScanHistory) (diffHistory model currentResult.Packages = currentResult.Packages.UniqByName() } - diffHistory.ScanResults = append(diffHistory.ScanResults, currentResult) + diff = append(diff, currentResult) } - return diffHistory, err + return diff, err } func getNewCves(previousResult, currentResult models.ScanResult) (newVulninfos []models.VulnInfo) { diff --git a/models/models.go b/models/models.go index 448d5e3c..fe4211d2 100644 --- a/models/models.go +++ b/models/models.go @@ -28,11 +28,6 @@ import ( goval "github.com/kotakanbe/goval-dictionary/models" ) -// ScanHistory is the history of Scanning. -type ScanHistory struct { - ScanResults ScanResults -} - // ScanResults is slice of ScanResult. type ScanResults []ScanResult diff --git a/report/tui.go b/report/tui.go index ec74cf8a..1ee1de60 100644 --- a/report/tui.go +++ b/report/tui.go @@ -34,14 +34,14 @@ import ( cve "github.com/kotakanbe/go-cve-dictionary/models" ) -var scanHistory models.ScanHistory +var scanResults models.ScanResults var currentScanResult models.ScanResult var currentCveInfo int var currentDetailLimitY int // RunTui execute main logic -func RunTui(history models.ScanHistory) subcommands.ExitStatus { - scanHistory = history +func RunTui(results models.ScanResults) subcommands.ExitStatus { + scanResults = results g, err := gocui.NewGui(gocui.OutputNormal) if err != nil { @@ -216,7 +216,7 @@ func previousView(g *gocui.Gui, v *gocui.View) error { func movable(v *gocui.View, nextY int) (ok bool, yLimit int) { switch v.Name() { case "side": - yLimit = len(scanHistory.ScanResults) - 1 + yLimit = len(scanResults) - 1 if yLimit < nextY { return false, yLimit } @@ -439,7 +439,7 @@ func changeHost(g *gocui.Gui, v *gocui.View) error { } serverName := strings.TrimSpace(l) - for _, r := range scanHistory.ScanResults { + for _, r := range scanResults { if serverName == strings.TrimSpace(r.ServerInfoTui()) { currentScanResult = r break @@ -562,13 +562,13 @@ func setSideLayout(g *gocui.Gui) error { } v.Highlight = true - for _, result := range scanHistory.ScanResults { + for _, result := range scanResults { fmt.Fprintln(v, result.ServerInfoTui()) } - if len(scanHistory.ScanResults) == 0 { + if len(scanResults) == 0 { return fmt.Errorf("No scan results") } - currentScanResult = scanHistory.ScanResults[0] + currentScanResult = scanResults[0] if _, err := g.SetCurrentView("side"); err != nil { return err } From aafbdcd34dd7ee2b8d96bd4728b942abc70705aa Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Thu, 27 Apr 2017 19:33:03 +0900 Subject: [PATCH 012/113] Fix testcase --- commands/util_test.go | 321 ++++++++++++++++++++---------------------- 1 file changed, 156 insertions(+), 165 deletions(-) diff --git a/commands/util_test.go b/commands/util_test.go index ddcde598..66bfa9f7 100644 --- a/commands/util_test.go +++ b/commands/util_test.go @@ -32,109 +32,105 @@ func TestDiff(t *testing.T) { atCurrent, _ := time.Parse("2006-01-02", "2014-12-31") atPrevious, _ := time.Parse("2006-01-02", "2014-11-31") var tests = []struct { - inCurrent models.ScanHistory - inPrevious models.ScanHistory + inCurrent models.ScanResults + inPrevious models.ScanResults out models.ScanResult }{ { - models.ScanHistory{ - ScanResults: models.ScanResults{ - { - ScannedAt: atCurrent, - ServerName: "u16", - Family: "ubuntu", - Release: "16.04", - ScannedCves: []models.VulnInfo{ - { - CveID: "CVE-2012-6702", - Packages: models.PackageInfoList{ - { - Name: "libexpat1", - Version: "2.1.0-7", - Release: "", - NewVersion: "2.1.0-7ubuntu0.16.04.2", - NewRelease: "", - Repository: "", - }, + models.ScanResults{ + { + ScannedAt: atCurrent, + ServerName: "u16", + Family: "ubuntu", + Release: "16.04", + ScannedCves: []models.VulnInfo{ + { + CveID: "CVE-2012-6702", + Packages: models.PackageInfoList{ + { + Name: "libexpat1", + Version: "2.1.0-7", + Release: "", + NewVersion: "2.1.0-7ubuntu0.16.04.2", + NewRelease: "", + Repository: "", }, - DistroAdvisories: []models.DistroAdvisory{}, - CpeNames: []string{}, - }, - { - CveID: "CVE-2014-9761", - Packages: models.PackageInfoList{ - { - Name: "libc-bin", - Version: "2.21-0ubuntu5", - Release: "", - NewVersion: "2.23-0ubuntu5", - NewRelease: "", - Repository: "", - }, - }, - DistroAdvisories: []models.DistroAdvisory{}, - CpeNames: []string{}, }, + DistroAdvisories: []models.DistroAdvisory{}, + CpeNames: []string{}, + }, + { + CveID: "CVE-2014-9761", + Packages: models.PackageInfoList{ + { + Name: "libc-bin", + Version: "2.21-0ubuntu5", + Release: "", + NewVersion: "2.23-0ubuntu5", + NewRelease: "", + Repository: "", + }, + }, + DistroAdvisories: []models.DistroAdvisory{}, + CpeNames: []string{}, }, - KnownCves: []models.CveInfo{}, - UnknownCves: []models.CveInfo{}, - IgnoredCves: []models.CveInfo{}, - - Packages: models.PackageInfoList{}, - - Errors: []string{}, - Optional: [][]interface{}{}, }, + KnownCves: []models.CveInfo{}, + UnknownCves: []models.CveInfo{}, + IgnoredCves: []models.CveInfo{}, + + Packages: models.PackageInfoList{}, + + Errors: []string{}, + Optional: [][]interface{}{}, }, }, - models.ScanHistory{ - ScanResults: models.ScanResults{ - { - ScannedAt: atPrevious, - ServerName: "u16", - Family: "ubuntu", - Release: "16.04", - ScannedCves: []models.VulnInfo{ - { - CveID: "CVE-2012-6702", - Packages: models.PackageInfoList{ - { - Name: "libexpat1", - Version: "2.1.0-7", - Release: "", - NewVersion: "2.1.0-7ubuntu0.16.04.2", - NewRelease: "", - Repository: "", - }, + models.ScanResults{ + { + ScannedAt: atPrevious, + ServerName: "u16", + Family: "ubuntu", + Release: "16.04", + ScannedCves: []models.VulnInfo{ + { + CveID: "CVE-2012-6702", + Packages: models.PackageInfoList{ + { + Name: "libexpat1", + Version: "2.1.0-7", + Release: "", + NewVersion: "2.1.0-7ubuntu0.16.04.2", + NewRelease: "", + Repository: "", }, - DistroAdvisories: []models.DistroAdvisory{}, - CpeNames: []string{}, - }, - { - CveID: "CVE-2014-9761", - Packages: models.PackageInfoList{ - { - Name: "libc-bin", - Version: "2.21-0ubuntu5", - Release: "", - NewVersion: "2.23-0ubuntu5", - NewRelease: "", - Repository: "", - }, - }, - DistroAdvisories: []models.DistroAdvisory{}, - CpeNames: []string{}, }, + DistroAdvisories: []models.DistroAdvisory{}, + CpeNames: []string{}, + }, + { + CveID: "CVE-2014-9761", + Packages: models.PackageInfoList{ + { + Name: "libc-bin", + Version: "2.21-0ubuntu5", + Release: "", + NewVersion: "2.23-0ubuntu5", + NewRelease: "", + Repository: "", + }, + }, + DistroAdvisories: []models.DistroAdvisory{}, + CpeNames: []string{}, }, - KnownCves: []models.CveInfo{}, - UnknownCves: []models.CveInfo{}, - IgnoredCves: []models.CveInfo{}, - - Packages: models.PackageInfoList{}, - - Errors: []string{}, - Optional: [][]interface{}{}, }, + KnownCves: []models.CveInfo{}, + UnknownCves: []models.CveInfo{}, + IgnoredCves: []models.CveInfo{}, + + Packages: models.PackageInfoList{}, + + Errors: []string{}, + Optional: [][]interface{}{}, }, }, models.ScanResult{ @@ -153,88 +149,84 @@ func TestDiff(t *testing.T) { }, }, { - models.ScanHistory{ - ScanResults: models.ScanResults{ - { - ScannedAt: atCurrent, - ServerName: "u16", - Family: "ubuntu", - Release: "16.04", - ScannedCves: []models.VulnInfo{ - { - CveID: "CVE-2016-6662", - Packages: models.PackageInfoList{ - { - Name: "mysql-libs", - Version: "5.1.73", - Release: "7.el6", - NewVersion: "5.1.73", - NewRelease: "8.el6_8", - Repository: "", - }, - }, - DistroAdvisories: []models.DistroAdvisory{}, - CpeNames: []string{}, - }, - }, - KnownCves: []models.CveInfo{ - { - CveDetail: cve.CveDetail{ - CveID: "CVE-2016-6662", - Nvd: cve.Nvd{ - LastModifiedDate: time.Date(2016, 1, 1, 0, 0, 0, 0, time.Local), - }, - }, - VulnInfo: models.VulnInfo{ - CveID: "CVE-2016-6662", + models.ScanResults{ + { + ScannedAt: atCurrent, + ServerName: "u16", + Family: "ubuntu", + Release: "16.04", + ScannedCves: []models.VulnInfo{ + { + CveID: "CVE-2016-6662", + Packages: models.PackageInfoList{ + { + Name: "mysql-libs", + Version: "5.1.73", + Release: "7.el6", + NewVersion: "5.1.73", + NewRelease: "8.el6_8", + Repository: "", }, }, + DistroAdvisories: []models.DistroAdvisory{}, + CpeNames: []string{}, }, - UnknownCves: []models.CveInfo{}, - IgnoredCves: []models.CveInfo{}, }, + KnownCves: []models.CveInfo{ + { + CveDetail: cve.CveDetail{ + CveID: "CVE-2016-6662", + Nvd: cve.Nvd{ + LastModifiedDate: time.Date(2016, 1, 1, 0, 0, 0, 0, time.Local), + }, + }, + VulnInfo: models.VulnInfo{ + CveID: "CVE-2016-6662", + }, + }, + }, + UnknownCves: []models.CveInfo{}, + IgnoredCves: []models.CveInfo{}, }, }, - models.ScanHistory{ - ScanResults: models.ScanResults{ - { - ScannedAt: atPrevious, - ServerName: "u16", - Family: "ubuntu", - Release: "16.04", - ScannedCves: []models.VulnInfo{ - { - CveID: "CVE-2016-6662", - Packages: models.PackageInfoList{ - { - Name: "mysql-libs", - Version: "5.1.73", - Release: "7.el6", - NewVersion: "5.1.73", - NewRelease: "8.el6_8", - Repository: "", - }, - }, - DistroAdvisories: []models.DistroAdvisory{}, - CpeNames: []string{}, - }, - }, - KnownCves: []models.CveInfo{ - { - CveDetail: cve.CveDetail{ - CveID: "CVE-2016-6662", - Nvd: cve.Nvd{ - LastModifiedDate: time.Date(2017, 3, 15, 13, 40, 57, 0, time.Local), - }, - }, - VulnInfo: models.VulnInfo{ - CveID: "CVE-2016-6662", + models.ScanResults{ + { + ScannedAt: atPrevious, + ServerName: "u16", + Family: "ubuntu", + Release: "16.04", + ScannedCves: []models.VulnInfo{ + { + CveID: "CVE-2016-6662", + Packages: models.PackageInfoList{ + { + Name: "mysql-libs", + Version: "5.1.73", + Release: "7.el6", + NewVersion: "5.1.73", + NewRelease: "8.el6_8", + Repository: "", }, }, + DistroAdvisories: []models.DistroAdvisory{}, + CpeNames: []string{}, }, - UnknownCves: []models.CveInfo{}, - IgnoredCves: []models.CveInfo{}, }, + KnownCves: []models.CveInfo{ + { + CveDetail: cve.CveDetail{ + CveID: "CVE-2016-6662", + Nvd: cve.Nvd{ + LastModifiedDate: time.Date(2017, 3, 15, 13, 40, 57, 0, time.Local), + }, + }, + VulnInfo: models.VulnInfo{ + CveID: "CVE-2016-6662", + }, + }, + }, + UnknownCves: []models.CveInfo{}, + IgnoredCves: []models.CveInfo{}, }, }, models.ScanResult{ @@ -280,10 +272,9 @@ func TestDiff(t *testing.T) { }, } - var s models.ScanHistory for _, tt := range tests { - s, _ = diff(tt.inCurrent, tt.inPrevious) - for _, actual := range s.ScanResults { + diff, _ := diff(tt.inCurrent, tt.inPrevious) + for _, actual := range diff { if !reflect.DeepEqual(actual, tt.out) { h := pp.Sprint(actual) x := pp.Sprint(tt.out) From 342a1c6cff8c01ffdfaa47f483c32933160604d5 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Mon, 1 May 2017 19:25:25 +0900 Subject: [PATCH 013/113] Refactoring --- README.md | 2 +- models/models.go | 8 ++++---- report/slack.go | 4 ++-- report/tui.go | 4 ++-- report/util.go | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index fb738303..6912dcb7 100644 --- a/README.md +++ b/README.md @@ -342,7 +342,7 @@ CVE-2016-5636 10.0 (High) Integer overflow in the get_data functio View full report. ``` -$ vuls report -format-full-text +$ vuls report -format-full-text | less localhost (amazon 2015.09) ============================ diff --git a/models/models.go b/models/models.go index fe4211d2..277cea56 100644 --- a/models/models.go +++ b/models/models.go @@ -568,8 +568,8 @@ type Changelog struct { Method string } -// ToStringCurrentVersion returns package name-version-release -func (p PackageInfo) ToStringCurrentVersion() string { +// FormatCurrentVer returns package name-version-release +func (p PackageInfo) FormatCurrentVer() string { str := p.Name if 0 < len(p.Version) { str = fmt.Sprintf("%s-%s", str, p.Version) @@ -580,8 +580,8 @@ func (p PackageInfo) ToStringCurrentVersion() string { return str } -// ToStringNewVersion returns package name-version-release -func (p PackageInfo) ToStringNewVersion() string { +// FormatNewVer returns package name-version-release +func (p PackageInfo) FormatNewVer() string { str := p.Name if 0 < len(p.NewVersion) { str = fmt.Sprintf("%s-%s", str, p.NewVersion) diff --git a/report/slack.go b/report/slack.go index 434bc06b..fd9b5bf1 100644 --- a/report/slack.go +++ b/report/slack.go @@ -170,7 +170,7 @@ func toSlackAttachments(scanResult models.ScanResult) (attaches []*attachment) { curentPackages := []string{} for _, p := range cveInfo.Packages { - curentPackages = append(curentPackages, p.ToStringCurrentVersion()) + curentPackages = append(curentPackages, p.FormatCurrentVer()) } for _, n := range cveInfo.CpeNames { curentPackages = append(curentPackages, n) @@ -178,7 +178,7 @@ func toSlackAttachments(scanResult models.ScanResult) (attaches []*attachment) { newPackages := []string{} for _, p := range cveInfo.Packages { - newPackages = append(newPackages, p.ToStringNewVersion()) + newPackages = append(newPackages, p.FormatNewVer()) } a := attachment{ diff --git a/report/tui.go b/report/tui.go index 1ee1de60..564c6154 100644 --- a/report/tui.go +++ b/report/tui.go @@ -799,8 +799,8 @@ func detailLines() (string, error) { packages = append(packages, fmt.Sprintf( "%s -> %s", - pack.ToStringCurrentVersion(), - pack.ToStringNewVersion())) + pack.FormatCurrentVer(), + pack.FormatNewVer())) } data := dataForTmpl{ diff --git a/report/util.go b/report/util.go index adf85f27..51ac589a 100644 --- a/report/util.go +++ b/report/util.go @@ -118,7 +118,7 @@ No CVE-IDs are found in updatable packages. var packsVer string for _, p := range d.Packages { packsVer += fmt.Sprintf( - "%s -> %s\n", p.ToStringCurrentVersion(), p.ToStringNewVersion()) + "%s -> %s\n", p.FormatCurrentVer(), p.FormatNewVer()) } for _, n := range d.CpeNames { packsVer += n @@ -460,7 +460,7 @@ func addPackageInfos(table *uitable.Table, packs []models.PackageInfo) *uitable. title = "Package" } ver := fmt.Sprintf( - "%s -> %s", p.ToStringCurrentVersion(), p.ToStringNewVersion()) + "%s -> %s", p.FormatCurrentVer(), p.FormatNewVer()) table.AddRow(title, ver) } return table @@ -501,7 +501,7 @@ func formatOneChangelog(p models.PackageInfo) string { } packVer := fmt.Sprintf("%s -> %s", - p.ToStringCurrentVersion(), p.ToStringNewVersion()) + p.FormatCurrentVer(), p.FormatNewVer()) var delim bytes.Buffer for i := 0; i < len(packVer); i++ { delim.WriteString("-") From b545b5d0a3d1b44d9209650c608b0672f35185dd Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Tue, 2 May 2017 15:39:22 +0900 Subject: [PATCH 014/113] Unify the models of NVD, JVN, OVAL --- commands/report.go | 2 + commands/util.go | 34 ++++-- commands/util_test.go | 19 +-- models/models.go | 267 ++++++++++++++++++++++++++++++++++++------ oval/debian.go | 75 ++++++++---- oval/oval.go | 1 + oval/redhat.go | 102 +++++++++++----- report/slack.go | 96 +++++++-------- report/tui.go | 134 ++++++++++++--------- report/util.go | 254 +++++++++++++++++++++------------------- util/util.go | 29 +++++ util/util_test.go | 66 +++++++++++ 12 files changed, 736 insertions(+), 343 deletions(-) diff --git a/commands/report.go b/commands/report.go index 85f2005f..5600e440 100644 --- a/commands/report.go +++ b/commands/report.go @@ -30,6 +30,7 @@ import ( "github.com/future-architect/vuls/report" "github.com/future-architect/vuls/util" "github.com/google/subcommands" + "github.com/k0kubun/pp" ) // ReportCmd is subcommand for reporting @@ -421,6 +422,7 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} util.Log.Errorf("Failed to fill OVAL information: %s", err) return subcommands.ExitFailure } + pp.Println(filled) filled, err = fillCveInfoFromCveDB(*filled) if err != nil { diff --git a/commands/util.go b/commands/util.go index 1e0bee33..98306c36 100644 --- a/commands/util.go +++ b/commands/util.go @@ -246,15 +246,15 @@ func diff(current, previous models.ScanResults) (diff models.ScanResults, err er return diff, err } -func getNewCves(previousResult, currentResult models.ScanResult) (newVulninfos []models.VulnInfo) { +func getNewCves(previous, current models.ScanResult) (newVulninfos []models.VulnInfo) { previousCveIDsSet := map[string]bool{} - for _, previousVulnInfo := range previousResult.ScannedCves { + for _, previousVulnInfo := range previous.ScannedCves { previousCveIDsSet[previousVulnInfo.CveID] = true } - for _, v := range currentResult.ScannedCves { + for _, v := range current.ScannedCves { if previousCveIDsSet[v.CveID] { - if isCveInfoUpdated(currentResult, previousResult, v.CveID) { + if isCveInfoUpdated(current, previous, v.CveID) { newVulninfos = append(newVulninfos, v) } } else { @@ -264,25 +264,35 @@ func getNewCves(previousResult, currentResult models.ScanResult) (newVulninfos [ return } -func isCveInfoUpdated(currentResult, previousResult models.ScanResult, CveID string) bool { +func isCveInfoUpdated(current, previous models.ScanResult, CveID string) bool { type lastModified struct { Nvd time.Time Jvn time.Time } previousModifies := lastModified{} - for _, c := range previousResult.KnownCves { + for _, c := range previous.KnownCves { if CveID == c.CveID { - previousModifies.Nvd = c.CveDetail.Nvd.LastModifiedDate - previousModifies.Jvn = c.CveDetail.Jvn.LastModifiedDate + //TODO + if nvd, found := c.Get(models.NVD); found { + previousModifies.Nvd = nvd.LastModified + } + if jvn, found := c.Get(models.JVN); found { + previousModifies.Jvn = jvn.LastModified + } } } currentModifies := lastModified{} - for _, c := range currentResult.KnownCves { - if CveID == c.CveDetail.CveID { - currentModifies.Nvd = c.CveDetail.Nvd.LastModifiedDate - currentModifies.Jvn = c.CveDetail.Jvn.LastModifiedDate + for _, c := range current.KnownCves { + if CveID == c.VulnInfo.CveID { + //TODO + if nvd, found := c.Get(models.NVD); found { + previousModifies.Nvd = nvd.LastModified + } + if jvn, found := c.Get(models.JVN); found { + previousModifies.Jvn = jvn.LastModified + } } } return !currentModifies.Nvd.Equal(previousModifies.Nvd) || diff --git a/commands/util_test.go b/commands/util_test.go index 66bfa9f7..dfa5dc71 100644 --- a/commands/util_test.go +++ b/commands/util_test.go @@ -25,7 +25,6 @@ import ( "github.com/future-architect/vuls/models" "github.com/k0kubun/pp" - cve "github.com/kotakanbe/go-cve-dictionary/models" ) func TestDiff(t *testing.T) { @@ -174,10 +173,11 @@ func TestDiff(t *testing.T) { }, KnownCves: []models.CveInfo{ { - CveDetail: cve.CveDetail{ - CveID: "CVE-2016-6662", - Nvd: cve.Nvd{ - LastModifiedDate: time.Date(2016, 1, 1, 0, 0, 0, 0, time.Local), + CveContents: []models.CveContent{ + { + Type: models.NVD, + CveID: "CVE-2016-6662", + LastModified: time.Date(2016, 1, 1, 0, 0, 0, 0, time.Local), }, }, VulnInfo: models.VulnInfo{ @@ -214,10 +214,11 @@ func TestDiff(t *testing.T) { }, KnownCves: []models.CveInfo{ { - CveDetail: cve.CveDetail{ - CveID: "CVE-2016-6662", - Nvd: cve.Nvd{ - LastModifiedDate: time.Date(2017, 3, 15, 13, 40, 57, 0, time.Local), + CveContents: []models.CveContent{ + { + Type: models.NVD, + CveID: "CVE-2016-6662", + LastModified: time.Date(2017, 3, 15, 13, 40, 57, 0, time.Local), }, }, VulnInfo: models.VulnInfo{ diff --git a/models/models.go b/models/models.go index 277cea56..7eee245e 100644 --- a/models/models.go +++ b/models/models.go @@ -20,12 +20,12 @@ package models import ( "fmt" "sort" + "strings" "time" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/cveapi" - cve "github.com/kotakanbe/go-cve-dictionary/models" - goval "github.com/kotakanbe/goval-dictionary/models" + cvedict "github.com/kotakanbe/go-cve-dictionary/models" ) // ScanResults is slice of ScanResult. @@ -73,8 +73,7 @@ type ScanResult struct { Optional [][]interface{} } -// FillCveDetail fetches CVE detailed information from -// CVE Database, and then set to fields. +// FillCveDetail fetches NVD, JVN from CVE Database, and then set to fields. func (r ScanResult) FillCveDetail() (*ScanResult, error) { set := map[string]VulnInfo{} var cveIDs []string @@ -90,35 +89,45 @@ func (r ScanResult) FillCveDetail() (*ScanResult, error) { r.IgnoredCves = CveInfos{} for _, d := range ds { + nvd := *r.convertNvdToModel(d.CveID, d.Nvd) + jvn := *r.convertJvnToModel(d.CveID, d.Jvn) cinfo := CveInfo{ - CveDetail: d, - VulnInfo: set[d.CveID], + CveContents: []CveContent{nvd, jvn}, + VulnInfo: set[d.CveID], } cinfo.NilSliceToEmpty() // ignored - found := false + ignore := false for _, icve := range config.Conf.Servers[r.ServerName].IgnoreCves { if icve == d.CveID { r.IgnoredCves.Insert(cinfo) - found = true + ignore = true break } } - if found { + if ignore { continue } // Update known if KnownCves already have cinfo if c, ok := r.KnownCves.Get(cinfo.CveID); ok { - c.CveDetail = d + for _, con := range []CveContent{nvd, jvn} { + if !c.Update(con) { + c.Insert(con) + } + } r.KnownCves.Update(c) continue } // Update unknown if UnknownCves already have cinfo if c, ok := r.UnknownCves.Get(cinfo.CveID); ok { - c.CveDetail = d + for _, con := range []CveContent{nvd, jvn} { + if !c.Update(con) { + c.Insert(con) + } + } r.UnknownCves.Update(c) continue } @@ -138,6 +147,93 @@ func (r ScanResult) FillCveDetail() (*ScanResult, error) { return &r, nil } +func (r ScanResult) convertNvdToModel(cveID string, nvd cvedict.Nvd) *CveContent { + var cpes []Cpe + for _, c := range nvd.Cpes { + cpes = append(cpes, Cpe{CpeName: c.CpeName}) + } + + var refs []Reference + for _, r := range nvd.References { + refs = append(refs, Reference{ + Link: r.Link, + Source: r.Source, + }) + } + + validVec := true + for _, v := range []string{ + nvd.AccessVector, + nvd.AccessComplexity, + nvd.Authentication, + nvd.ConfidentialityImpact, + nvd.IntegrityImpact, + nvd.AvailabilityImpact, + } { + if len(v) == 0 { + validVec = false + } + } + + vector := "" + if validVec { + vector = fmt.Sprintf("AV:%s/AC:%s/Au:%s/C:%s/I:%s/A:%s", + string(nvd.AccessVector[0]), + string(nvd.AccessComplexity[0]), + string(nvd.Authentication[0]), + string(nvd.ConfidentialityImpact[0]), + string(nvd.IntegrityImpact[0]), + string(nvd.AvailabilityImpact[0])) + } + + //TODO CVSSv3 + return &CveContent{ + Type: NVD, + CveID: cveID, + Summary: nvd.Summary, + Cvss2Score: nvd.Score, + Cvss2Vector: vector, + Cpes: cpes, + CweID: nvd.CweID, + References: refs, + Published: nvd.PublishedDate, + LastModified: nvd.LastModifiedDate, + } +} + +func (r ScanResult) convertJvnToModel(cveID string, jvn cvedict.Jvn) *CveContent { + var cpes []Cpe + for _, c := range jvn.Cpes { + cpes = append(cpes, Cpe{CpeName: c.CpeName}) + } + + refs := []Reference{{ + Link: jvn.JvnLink, + Source: string(JVN), + }} + for _, r := range jvn.References { + refs = append(refs, Reference{ + Link: r.Link, + Source: r.Source, + }) + } + + vector := strings.TrimSuffix(strings.TrimPrefix(jvn.Vector, "("), ")") + return &CveContent{ + Type: JVN, + CveID: cveID, + Title: jvn.Title, + Summary: jvn.Summary, + Severity: jvn.Severity, + Cvss2Score: jvn.Score, + Cvss2Vector: vector, + Cpes: cpes, + References: refs, + Published: jvn.PublishedDate, + LastModified: jvn.LastModifiedDate, + } +} + // FilterByCvssOver is filter function. func (r ScanResult) FilterByCvssOver() ScanResult { cveInfos := []CveInfo{} @@ -147,7 +243,7 @@ func (r ScanResult) FilterByCvssOver() ScanResult { } for _, cveInfo := range r.KnownCves { - if config.Conf.CvssScoreOver <= cveInfo.CveDetail.CvssScore(config.Conf.Lang) { + if config.Conf.CvssScoreOver <= cveInfo.CvssV2Score() { cveInfos = append(cveInfos, cveInfo) } } @@ -217,7 +313,7 @@ func (r ScanResult) CveSummary() string { var high, medium, low, unknown int cves := append(r.KnownCves, r.UnknownCves...) for _, cveInfo := range cves { - score := cveInfo.CveDetail.CvssScore(config.Conf.Lang) + score := cveInfo.CvssV2Score() switch { case 7.0 <= score: high++ @@ -379,11 +475,10 @@ func (c CveInfos) Swap(i, j int) { } func (c CveInfos) Less(i, j int) bool { - lang := config.Conf.Lang - if c[i].CveDetail.CvssScore(lang) == c[j].CveDetail.CvssScore(lang) { - return c[i].CveDetail.CveID < c[j].CveDetail.CveID + if c[i].CvssV2Score() == c[j].CvssV2Score() { + return c[i].CveID < c[j].CveID } - return c[j].CveDetail.CvssScore(lang) < c[i].CveDetail.CvssScore(lang) + return c[j].CvssV2Score() < c[i].CvssV2Score() } // Get cveInfo by cveID @@ -431,27 +526,120 @@ func (c *CveInfos) Upsert(cveInfo CveInfo) { } } -// CveInfo has Cve Information. +// CveInfo has CVE detailed Information. type CveInfo struct { - CveDetail cve.CveDetail - OvalDetail goval.Definition VulnInfo + CveContents []CveContent +} + +// Get a CveContent specified by arg +func (c *CveInfo) Get(typestr CveContentType) (*CveContent, bool) { + for _, cont := range c.CveContents { + if cont.Type == typestr { + return &cont, true + } + } + return &CveContent{}, false +} + +// Insert a CveContent to specified by arg +func (c *CveInfo) Insert(con CveContent) { + c.CveContents = append(c.CveContents, con) +} + +// Update a CveContent to specified by arg +func (c *CveInfo) Update(to CveContent) bool { + for i, cont := range c.CveContents { + if cont.Type == to.Type { + c.CveContents[i] = to + return true + } + } + return false +} + +// CvssV2Score returns CVSS V2 Score +func (c *CveInfo) CvssV2Score() float64 { + //TODO + if cont, found := c.Get(NVD); found { + return cont.Cvss2Score + } else if cont, found := c.Get(JVN); found { + return cont.Cvss2Score + } else if cont, found := c.Get(RedHat); found { + return cont.Cvss2Score + } + return -1 } // NilSliceToEmpty set nil slice fields to empty slice to avoid null in JSON func (c *CveInfo) NilSliceToEmpty() { - if c.CveDetail.Nvd.Cpes == nil { - c.CveDetail.Nvd.Cpes = []cve.Cpe{} - } - if c.CveDetail.Jvn.Cpes == nil { - c.CveDetail.Jvn.Cpes = []cve.Cpe{} - } - if c.CveDetail.Nvd.References == nil { - c.CveDetail.Nvd.References = []cve.Reference{} - } - if c.CveDetail.Jvn.References == nil { - c.CveDetail.Jvn.References = []cve.Reference{} - } + return + // TODO + // if c.CveDetail.Nvd.Cpes == nil { + // c.CveDetail.Nvd.Cpes = []cve.Cpe{} + // } + // if c.CveDetail.Jvn.Cpes == nil { + // c.CveDetail.Jvn.Cpes = []cve.Cpe{} + // } + // if c.CveDetail.Nvd.References == nil { + // c.CveDetail.Nvd.References = []cve.Reference{} + // } + // if c.CveDetail.Jvn.References == nil { + // c.CveDetail.Jvn.References = []cve.Reference{} + // } +} + +// CveContentType is a source of CVE information +type CveContentType string + +const ( + // NVD is NVD + NVD CveContentType = "nvd" + + // JVN is JVN + JVN CveContentType = "jvn" + + // RedHat is RedHat + RedHat CveContentType = "redhat" + + // CentOS is CentOS + CentOS CveContentType = "centos" + + // Debian is Debian + Debian CveContentType = "debian" + + // Ubuntu is Ubuntu + Ubuntu CveContentType = "ubuntu" +) + +// CveContent has abstraction of various vulnerability information +type CveContent struct { + Type CveContentType + CveID string + Title string + Summary string + Severity string + Cvss2Score float64 + Cvss2Vector string + Cvss3Score float64 + Cvss3Vector string + Cpes []Cpe + References []Reference + CweID string + Published time.Time + LastModified time.Time +} + +// Cpe is Common Platform Enumeration +type Cpe struct { + CpeName string +} + +// Reference has a related link of the CVE +type Reference struct { + RefID string + Source string + Link string } // PackageInfoList is slice of PackageInfo @@ -552,13 +740,14 @@ func (a PackageInfosByName) Less(i, j int) bool { return a[i].Name < a[j].Name } // PackageInfo has installed packages. type PackageInfo struct { - Name string - Version string - Release string - NewVersion string - NewRelease string - Repository string - Changelog Changelog + Name string + Version string + Release string + NewVersion string + NewRelease string + Repository string + Changelog Changelog + NotFixedYet bool // Ubuntu OVAL Only } // Changelog has contents of changelog and how to get it. diff --git a/oval/debian.go b/oval/debian.go index ac892097..b9cf3194 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -7,7 +7,6 @@ import ( "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/util" ver "github.com/knqyf263/go-deb-version" - cve "github.com/kotakanbe/go-cve-dictionary/models" ovalconf "github.com/kotakanbe/goval-dictionary/config" db "github.com/kotakanbe/goval-dictionary/db" ovalmodels "github.com/kotakanbe/goval-dictionary/models" @@ -62,54 +61,78 @@ func (o Debian) FillCveInfoFromOvalDB(r *models.ScanResult) (*models.ScanResult, func (o Debian) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Definition) *models.ScanResult { // Update ScannedCves by OVAL info found := false - cves := []models.VulnInfo{} - for _, cve := range r.ScannedCves { - if cve.CveID == definition.Debian.CveID { + updatedCves := []models.VulnInfo{} + + // Update scanned confidence to ovalmatch + for _, scanned := range r.ScannedCves { + if scanned.CveID == definition.Debian.CveID { found = true - if cve.Confidence.Score < models.OvalMatch.Score { - cve.Confidence = models.OvalMatch + if scanned.Confidence.Score < models.OvalMatch.Score { + scanned.Confidence = models.OvalMatch } } - cves = append(cves, cve) + updatedCves = append(updatedCves, scanned) } - packageInfoList := getPackageInfoList(r, definition) vuln := models.VulnInfo{ CveID: definition.Debian.CveID, Confidence: models.OvalMatch, - Packages: packageInfoList, + Packages: getPackageInfoList(r, definition), } if !found { - cves = append(cves, vuln) util.Log.Debugf("%s is newly detected by OVAL", vuln.CveID) + updatedCves = append(updatedCves, vuln) } - r.ScannedCves = cves + r.ScannedCves = updatedCves // Update KnownCves by OVAL info - cveInfo, ok := r.KnownCves.Get(definition.Debian.CveID) + ovalContent := *o.convertToModel(definition) + ovalContent.Type = models.CveContentType(r.Family) + cInfo, ok := r.KnownCves.Get(definition.Debian.CveID) if !ok { - cveInfo.CveDetail = cve.CveDetail{ - CveID: definition.Debian.CveID, - } - cveInfo.VulnInfo = vuln + cInfo.VulnInfo = vuln + cInfo.CveContents = []models.CveContent{ovalContent} } - cveInfo.OvalDetail = *definition - if cveInfo.VulnInfo.Confidence.Score < models.OvalMatch.Score { - cveInfo.Confidence = models.OvalMatch + if !cInfo.Update(ovalContent) { + cInfo.Insert(ovalContent) } - r.KnownCves.Upsert(cveInfo) + if cInfo.VulnInfo.Confidence.Score < models.OvalMatch.Score { + cInfo.Confidence = models.OvalMatch + } + r.KnownCves.Upsert(cInfo) // Update UnknownCves by OVAL info - cveInfo, ok = r.UnknownCves.Get(definition.Debian.CveID) + cInfo, ok = r.UnknownCves.Get(definition.Debian.CveID) if ok { - cveInfo.OvalDetail = *definition - if cveInfo.VulnInfo.Confidence.Score < models.OvalMatch.Score { - cveInfo.Confidence = models.OvalMatch - } r.UnknownCves.Delete(definition.Debian.CveID) - r.KnownCves.Upsert(cveInfo) + + // Insert new CveInfo + if !cInfo.Update(ovalContent) { + cInfo.Insert(ovalContent) + } + if cInfo.VulnInfo.Confidence.Score < models.OvalMatch.Score { + cInfo.Confidence = models.OvalMatch + } + r.KnownCves.Upsert(cInfo) } return r } + +func (o Debian) convertToModel(def *ovalmodels.Definition) *models.CveContent { + var refs []models.Reference + for _, r := range def.References { + refs = append(refs, models.Reference{ + Link: r.RefURL, + Source: r.Source, + RefID: r.RefID, + }) + } + return &models.CveContent{ + CveID: def.Debian.CveID, + Title: def.Title, + Summary: def.Description, + References: refs, + } +} diff --git a/oval/oval.go b/oval/oval.go index a247386e..0b324da0 100644 --- a/oval/oval.go +++ b/oval/oval.go @@ -15,6 +15,7 @@ func getPackageInfoList(r *models.ScanResult, d *ovalmodels.Definition) models.P for _, pack := range d.AffectedPacks { for _, p := range r.Packages { if pack.Name == p.Name { + p.Changelog = models.Changelog{} packageInfoList = append(packageInfoList, p) break } diff --git a/oval/redhat.go b/oval/redhat.go index ae3a127e..9a0e1abd 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -7,7 +7,6 @@ import ( "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/util" ver "github.com/knqyf263/go-deb-version" - cve "github.com/kotakanbe/go-cve-dictionary/models" ovalconf "github.com/kotakanbe/goval-dictionary/config" db "github.com/kotakanbe/goval-dictionary/db" ovalmodels "github.com/kotakanbe/goval-dictionary/models" @@ -56,69 +55,108 @@ func (o Redhat) FillCveInfoFromOvalDB(r *models.ScanResult) (*models.ScanResult, } func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Definition) *models.ScanResult { - found := make(map[string]bool) - vulnInfos := make(map[string]models.VulnInfo) - packageInfoList := getPackageInfoList(r, definition) + cveIDSet := make(map[string]bool) + cveID2VulnInfo := make(map[string]models.VulnInfo) for _, cve := range definition.Advisory.Cves { - found[cve.CveID] = false - vulnInfos[cve.CveID] = models.VulnInfo{ + cveIDSet[cve.CveID] = false + cveID2VulnInfo[cve.CveID] = models.VulnInfo{ CveID: cve.CveID, Confidence: models.OvalMatch, - Packages: packageInfoList, + Packages: getPackageInfoList(r, definition), } } // Update ScannedCves by OVAL info - cves := []models.VulnInfo{} - for _, scannedCve := range r.ScannedCves { + updatedCves := []models.VulnInfo{} + for _, scanned := range r.ScannedCves { + // Update scanned confidence to ovalmatch for _, c := range definition.Advisory.Cves { - if scannedCve.CveID == c.CveID { - found[c.CveID] = true - if scannedCve.Confidence.Score < models.OvalMatch.Score { - scannedCve.Confidence = models.OvalMatch + if scanned.CveID == c.CveID { + cveIDSet[c.CveID] = true + if scanned.Confidence.Score < models.OvalMatch.Score { + scanned.Confidence = models.OvalMatch } break } } - cves = append(cves, scannedCve) + updatedCves = append(updatedCves, scanned) } - for cveID, found := range found { + for cveID, found := range cveIDSet { if !found { - cves = append(cves, vulnInfos[cveID]) util.Log.Debugf("%s is newly detected by OVAL", cveID) + updatedCves = append(updatedCves, cveID2VulnInfo[cveID]) } } - r.ScannedCves = cves + r.ScannedCves = updatedCves // Update KnownCves by OVAL info for _, c := range definition.Advisory.Cves { - cveInfo, ok := r.KnownCves.Get(c.CveID) + ovalContent := *o.convertToModel(c.CveID, definition) + cInfo, ok := r.KnownCves.Get(c.CveID) if !ok { - cveInfo.CveDetail = cve.CveDetail{ - CveID: c.CveID, - } - cveInfo.VulnInfo = vulnInfos[c.CveID] + cInfo.VulnInfo = cveID2VulnInfo[c.CveID] + cInfo.CveContents = []models.CveContent{ovalContent} } - cveInfo.OvalDetail = *definition - if cveInfo.VulnInfo.Confidence.Score < models.OvalMatch.Score { - cveInfo.Confidence = models.OvalMatch + if !cInfo.Update(ovalContent) { + cInfo.Insert(ovalContent) } - r.KnownCves.Upsert(cveInfo) + if cInfo.VulnInfo.Confidence.Score < models.OvalMatch.Score { + cInfo.Confidence = models.OvalMatch + } + r.KnownCves.Upsert(cInfo) } // Update UnknownCves by OVAL info for _, c := range definition.Advisory.Cves { - cveInfo, ok := r.UnknownCves.Get(c.CveID) + cInfo, ok := r.UnknownCves.Get(c.CveID) if ok { - cveInfo.OvalDetail = *definition - if cveInfo.VulnInfo.Confidence.Score < models.OvalMatch.Score { - cveInfo.Confidence = models.OvalMatch - } r.UnknownCves.Delete(c.CveID) - r.KnownCves.Upsert(cveInfo) + + // Insert new CveInfo + ovalContent := *o.convertToModel(c.CveID, definition) + if !cInfo.Update(ovalContent) { + cInfo.Insert(ovalContent) + } + if cInfo.VulnInfo.Confidence.Score < models.OvalMatch.Score { + cInfo.Confidence = models.OvalMatch + } + r.KnownCves.Upsert(cInfo) } } return r } + +func (o Redhat) convertToModel(cveID string, def *ovalmodels.Definition) *models.CveContent { + for _, cve := range def.Advisory.Cves { + if cve.CveID != cveID { + continue + } + var refs []models.Reference + //TODO RHSAのリンクを入れる + for _, r := range def.References { + refs = append(refs, models.Reference{ + Link: r.RefURL, + Source: r.Source, + RefID: r.RefID, + }) + } + + // util.ParseCvss2() + + return &models.CveContent{ + Type: models.RedHat, + CveID: cve.CveID, + Title: def.Title, + Summary: def.Description, + Severity: def.Advisory.Severity, + // V2Score: v2Score, // TODO divide into score and vector + Cvss2Vector: cve.Cvss2, // TODO divide into score and vector + Cvss3Vector: cve.Cvss3, // TODO divide into score and vector + References: refs, + CweID: cve.Cwe, + } + } + return nil +} diff --git a/report/slack.go b/report/slack.go index fd9b5bf1..3b9c9b74 100644 --- a/report/slack.go +++ b/report/slack.go @@ -166,7 +166,7 @@ func toSlackAttachments(scanResult models.ScanResult) (attaches []*attachment) { } for _, cveInfo := range cves { - cveID := cveInfo.CveDetail.CveID + cveID := cveInfo.VulnInfo.CveID curentPackages := []string{} for _, p := range cveInfo.Packages { @@ -199,7 +199,7 @@ func toSlackAttachments(scanResult models.ScanResult) (attaches []*attachment) { Short: true, }, }, - Color: color(cveInfo.CveDetail.CvssScore(config.Conf.Lang)), + Color: color(cveInfo.CvssV2Score()), } attaches = append(attaches, &a) } @@ -221,57 +221,61 @@ func color(cvssScore float64) string { } func attachmentText(cveInfo models.CveInfo, osFamily string) string { - linkText := links(cveInfo, osFamily) - switch { - case config.Conf.Lang == "ja" && - 0 < cveInfo.CveDetail.Jvn.CvssScore(): + // linkText := links(cveInfo, osFamily) + //TODO + return "" + // switch { + // case config.Conf.Lang == "ja" && + // 0 < cveInfo.CveDetail.Jvn.CvssScore(): - jvn := cveInfo.CveDetail.Jvn - return fmt.Sprintf("*%4.1f (%s)* <%s|%s>\n%s\n%s\n*Confidence:* %v", - cveInfo.CveDetail.CvssScore(config.Conf.Lang), - jvn.CvssSeverity(), - fmt.Sprintf(cvssV2CalcBaseURL, cveInfo.CveDetail.CveID), - jvn.CvssVector(), - jvn.CveTitle(), - linkText, - cveInfo.VulnInfo.Confidence, - ) - case 0 < cveInfo.CveDetail.CvssScore("en"): - nvd := cveInfo.CveDetail.Nvd - return fmt.Sprintf("*%4.1f (%s)* <%s|%s>\n%s\n%s\n*Confidence:* %v", - cveInfo.CveDetail.CvssScore(config.Conf.Lang), - nvd.CvssSeverity(), - fmt.Sprintf(cvssV2CalcBaseURL, cveInfo.CveDetail.CveID), - nvd.CvssVector(), - nvd.CveSummary(), - linkText, - cveInfo.VulnInfo.Confidence, - ) - default: - nvd := cveInfo.CveDetail.Nvd - return fmt.Sprintf("?\n%s\n%s\n*Confidence:* %v", - nvd.CveSummary(), linkText, cveInfo.VulnInfo.Confidence) - } + // jvn := cveInfo.CveDetail.Jvn + // return fmt.Sprintf("*%4.1f (%s)* <%s|%s>\n%s\n%s\n*Confidence:* %v", + // cveInfo.CveDetail.CvssScore(config.Conf.Lang), + // jvn.CvssSeverity(), + // fmt.Sprintf(cvssV2CalcBaseURL, cveInfo.CveDetail.CveID), + // jvn.CvssVector(), + // jvn.CveTitle(), + // linkText, + // cveInfo.VulnInfo.Confidence, + // ) + // case 0 < cveInfo.CveDetail.CvssScore("en"): + // nvd := cveInfo.CveDetail.Nvd + // return fmt.Sprintf("*%4.1f (%s)* <%s|%s>\n%s\n%s\n*Confidence:* %v", + // cveInfo.CveDetail.CvssScore(config.Conf.Lang), + // nvd.CvssSeverity(), + // fmt.Sprintf(cvssV2CalcBaseURL, cveInfo.CveDetail.CveID), + // nvd.CvssVector(), + // nvd.CveSummary(), + // linkText, + // cveInfo.VulnInfo.Confidence, + // ) + // default: + // nvd := cveInfo.CveDetail.Nvd + // return fmt.Sprintf("?\n%s\n%s\n*Confidence:* %v", + // nvd.CveSummary(), linkText, cveInfo.VulnInfo.Confidence) + // } } func links(cveInfo models.CveInfo, osFamily string) string { links := []string{} - cweID := cveInfo.CveDetail.CweID() - if 0 < len(cweID) { - links = append(links, fmt.Sprintf("<%s|%s>", - cweURL(cweID), cweID)) - if config.Conf.Lang == "ja" { - links = append(links, fmt.Sprintf("<%s|%s(JVN)>", - cweJvnURL(cweID), cweID)) - } - } + //TODO + // cweID := cveInfo.CveDetail.CweID() + // if 0 < len(cweID) { + // links = append(links, fmt.Sprintf("<%s|%s>", + // cweURL(cweID), cweID)) + // if config.Conf.Lang == "ja" { + // links = append(links, fmt.Sprintf("<%s|%s(JVN)>", + // cweJvnURL(cweID), cweID)) + // } + // } - cveID := cveInfo.CveDetail.CveID - if config.Conf.Lang == "ja" && 0 < len(cveInfo.CveDetail.Jvn.Link()) { - jvn := fmt.Sprintf("<%s|JVN>", cveInfo.CveDetail.Jvn.Link()) - links = append(links, jvn) - } + cveID := cveInfo.VulnInfo.CveID + //TODO + // if config.Conf.Lang == "ja" && 0 < len(cveInfo.CveDetail.Jvn.Link()) { + // jvn := fmt.Sprintf("<%s|JVN>", cveInfo.CveDetail.Jvn.Link()) + // links = append(links, jvn) + // } dlinks := distroLinks(cveInfo, osFamily) for _, link := range dlinks { links = append(links, diff --git a/report/tui.go b/report/tui.go index 564c6154..74ab3cf2 100644 --- a/report/tui.go +++ b/report/tui.go @@ -26,7 +26,6 @@ import ( "time" log "github.com/Sirupsen/logrus" - "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" "github.com/google/subcommands" "github.com/gosuri/uitable" @@ -613,39 +612,53 @@ func summaryLines() string { for i, d := range currentScanResult.AllCves() { var cols []string - // packs := []string{} - // for _, pack := range d.Packages { - // packs = append(packs, pack.Name) - // } - if config.Conf.Lang == "ja" && 0 < d.CveDetail.Jvn.CvssScore() { - summary := d.CveDetail.Jvn.CveTitle() - cols = []string{ - fmt.Sprintf(indexFormat, i+1), - d.CveDetail.CveID, - fmt.Sprintf("| %4.1f", - d.CveDetail.CvssScore(config.Conf.Lang)), - fmt.Sprintf("| %3d |", d.VulnInfo.Confidence.Score), - summary, - } - } else { - summary := d.CveDetail.Nvd.CveSummary() - - var cvssScore string - if d.CveDetail.CvssScore("en") <= 0 { - cvssScore = "| ?" - } else { - cvssScore = fmt.Sprintf("| %4.1f", - d.CveDetail.CvssScore(config.Conf.Lang)) - } - - cols = []string{ - fmt.Sprintf(indexFormat, i+1), - d.CveDetail.CveID, - cvssScore, - fmt.Sprintf("| %3d |", d.VulnInfo.Confidence.Score), - summary, - } + //TODO + var summary string + if cont, found := d.Get(models.NVD); found { + summary = cont.Summary } + var cvssScore string + if d.CvssV2Score() <= 0 { + cvssScore = "| ?" + } else { + cvssScore = fmt.Sprintf("| %4.1f", d.CvssV2Score()) + } + cols = []string{ + fmt.Sprintf(indexFormat, i+1), + d.VulnInfo.CveID, + cvssScore, + fmt.Sprintf("| %3d |", d.VulnInfo.Confidence.Score), + summary, + } + // if config.Conf.Lang == "ja" && 0 < d.CveDetail.Jvn.CvssScore() { + // summary := d.CveDetail.Jvn.CveTitle() + // cols = []string{ + // fmt.Sprintf(indexFormat, i+1), + // d.CveDetail.CveID, + // fmt.Sprintf("| %4.1f", + // d.CveDetail.CvssScore(config.Conf.Lang)), + // fmt.Sprintf("| %3d |", d.VulnInfo.Confidence.Score), + // summary, + // } + // } else { + // summary := d.CveDetail.Nvd.CveSummary() + + // var cvssScore string + // if d.CveDetail.CvssScore("en") <= 0 { + // cvssScore = "| ?" + // } else { + // cvssScore = fmt.Sprintf("| %4.1f", + // d.CveDetail.CvssScore(config.Conf.Lang)) + // } + + // cols = []string{ + // fmt.Sprintf(indexFormat, i+1), + // d.CveDetail.CveID, + // cvssScore, + // fmt.Sprintf("| %3d |", d.VulnInfo.Confidence.Score), + // summary, + // } + // } icols := make([]interface{}, len(cols)) for j := range cols { @@ -748,7 +761,7 @@ func detailLines() (string, error) { } cveInfo := currentScanResult.AllCves()[currentCveInfo] - cveID := cveInfo.CveDetail.CveID + cveID := cveInfo.VulnInfo.CveID tmpl, err := template.New("detail").Parse(detailTemplate()) if err != nil { @@ -758,22 +771,27 @@ func detailLines() (string, error) { var cvssSeverity, cvssVector, summary string var refs []cve.Reference switch { - case config.Conf.Lang == "ja" && - 0 < cveInfo.CveDetail.Jvn.CvssScore(): - jvn := cveInfo.CveDetail.Jvn - cvssSeverity = jvn.CvssSeverity() - cvssVector = jvn.CvssVector() - summary = fmt.Sprintf("%s\n%s", jvn.CveTitle(), jvn.CveSummary()) - refs = jvn.VulnSiteReferences() + //TODO + // case config.Conf.Lang == "ja" && + // 0 < cveInfo.CveDetail.Jvn.CvssScore(): + // jvn := cveInfo.CveDetail.Jvn + // cvssSeverity = jvn.CvssSeverity() + // cvssVector = jvn.CvssVector() + // summary = fmt.Sprintf("%s\n%s", jvn.CveTitle(), jvn.CveSummary()) + // refs = jvn.VulnSiteReferences() default: - nvd := cveInfo.CveDetail.Nvd - cvssSeverity = nvd.CvssSeverity() - cvssVector = nvd.CvssVector() - summary = nvd.CveSummary() - refs = nvd.VulnSiteReferences() + var nvd *models.CveContent + if cont, found := cveInfo.Get(models.NVD); found { + nvd = cont + } + // cvssSeverity = nvd.CvssSeverity() + // cvssVector = nvd.CvssVector() + summary = nvd.Summary + // refs = nvd.VulnSiteReferences() } - cweURL := cweURL(cveInfo.CveDetail.CweID()) + //TODO + // cweURL := cweURL(cveInfo.CveDetail.CweID()) links := []string{ fmt.Sprintf("[NVD]( %s )", fmt.Sprintf("%s/%s", nvdBaseURL, cveID)), @@ -787,11 +805,12 @@ func detailLines() (string, error) { links = append(links, fmt.Sprintf("[%s]( %s )", link.title, link.url)) } + //TODO var cvssScore string - if cveInfo.CveDetail.CvssScore(config.Conf.Lang) == -1 { + if cveInfo.CvssV2Score() == -1 { cvssScore = "?" - } else { - cvssScore = fmt.Sprintf("%4.1f", cveInfo.CveDetail.CvssScore(config.Conf.Lang)) + // } else { + // cvssScore = fmt.Sprintf("%4.1f", cveInfo.CveDetail.CvssScore(config.Conf.Lang)) } packages := []string{} @@ -804,13 +823,14 @@ func detailLines() (string, error) { } data := dataForTmpl{ - CveID: cveID, - CvssScore: cvssScore, - CvssSeverity: cvssSeverity, - CvssVector: cvssVector, - Summary: summary, - Confidence: cveInfo.VulnInfo.Confidence, - CweURL: cweURL, + CveID: cveID, + CvssScore: cvssScore, + CvssSeverity: cvssSeverity, + CvssVector: cvssVector, + Summary: summary, + Confidence: cveInfo.VulnInfo.Confidence, + //TODO + // CweURL: cweURL, VulnSiteLinks: links, References: refs, Packages: packages, diff --git a/report/util.go b/report/util.go index 51ac589a..e7b6420d 100644 --- a/report/util.go +++ b/report/util.go @@ -126,38 +126,43 @@ No CVE-IDs are found in updatable packages. var scols []string switch { - case config.Conf.Lang == "ja" && - 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, - } + // 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, + // } - case 0 < d.CveDetail.CvssScore("en"): + 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", - d.CveDetail.Nvd.CveSummary(), + nvd.Summary, cveDetailsBaseURL, - d.CveDetail.CveID, + d.VulnInfo.CveID, distroLinks(d, r.Family)[0].url, packsVer, d.VulnInfo.Confidence, ) scols = []string{ - d.CveDetail.CveID, + d.VulnInfo.CveID, fmt.Sprintf("%-4.1f (%s)", - d.CveDetail.CvssScore(config.Conf.Lang), - d.CveDetail.Nvd.CvssSeverity(), + d.CvssV2Score(), + "TODO", ), summary, } @@ -165,7 +170,7 @@ No CVE-IDs are found in updatable packages. summary := fmt.Sprintf("%s\n%sConfidence: %v", distroLinks(d, r.Family)[0].url, packsVer, d.VulnInfo.Confidence) scols = []string{ - d.CveDetail.CveID, + d.VulnInfo.CveID, "?", summary, } @@ -229,39 +234,40 @@ No CVE-IDs are found in updatable packages. return fmt.Sprintf("%s\n%s\n%s", header, detail, formatChangelogs(r)) } +//TODO func formatPlainTextDetails(r models.ScanResult, osFamily string) (scoredReport, unscoredReport []string) { - for _, cve := range r.KnownCves { - switch config.Conf.Lang { - case "en": - if 0 < cve.CveDetail.Nvd.CvssScore() { - scoredReport = append( - scoredReport, formatPlainTextDetailsLangEn(cve, osFamily)) - } else { - scoredReport = append( - scoredReport, formatPlainTextUnknownCve(cve, osFamily)) - } - case "ja": - if 0 < cve.CveDetail.Jvn.CvssScore() { - scoredReport = append( - scoredReport, formatPlainTextDetailsLangJa(cve, osFamily)) - } else if 0 < cve.CveDetail.Nvd.CvssScore() { - scoredReport = append( - scoredReport, formatPlainTextDetailsLangEn(cve, osFamily)) - } else { - scoredReport = append( - scoredReport, formatPlainTextUnknownCve(cve, osFamily)) - } - } - } - for _, cve := range r.UnknownCves { - unscoredReport = append( - unscoredReport, formatPlainTextUnknownCve(cve, osFamily)) - } + // for _, cve := range r.KnownCves { + // switch config.Conf.Lang { + // case "en": + // if 0 < cve.CveDetail.Nvd.CvssScore() { + // scoredReport = append( + // scoredReport, formatPlainTextDetailsLangEn(cve, osFamily)) + // } else { + // scoredReport = append( + // scoredReport, formatPlainTextUnknownCve(cve, osFamily)) + // } + // case "ja": + // if 0 < cve.CveDetail.Jvn.CvssScore() { + // scoredReport = append( + // scoredReport, formatPlainTextDetailsLangJa(cve, osFamily)) + // } else if 0 < cve.CveDetail.Nvd.CvssScore() { + // scoredReport = append( + // scoredReport, formatPlainTextDetailsLangEn(cve, osFamily)) + // } else { + // scoredReport = append( + // scoredReport, formatPlainTextUnknownCve(cve, osFamily)) + // } + // } + // } + // for _, cve := range r.UnknownCves { + // unscoredReport = append( + // unscoredReport, formatPlainTextUnknownCve(cve, osFamily)) + // } return } func formatPlainTextUnknownCve(cveInfo models.CveInfo, osFamily string) string { - cveID := cveInfo.CveDetail.CveID + cveID := cveInfo.VulnInfo.CveID dtable := uitable.New() dtable.MaxColWidth = maxColWidth dtable.Wrap = true @@ -281,90 +287,94 @@ func formatPlainTextUnknownCve(cveInfo models.CveInfo, osFamily string) string { return fmt.Sprintf("%s", dtable) } +//TODO func formatPlainTextDetailsLangJa(cveInfo models.CveInfo, osFamily string) string { - cveDetail := cveInfo.CveDetail - cveID := cveDetail.CveID - jvn := cveDetail.Jvn + return "TODO" + // cveDetail := cveInfo.CveDetail + // cveID := cveDetail.CveID + // jvn := cveDetail.Jvn - dtable := uitable.New() - dtable.MaxColWidth = maxColWidth - 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(cveDetail.CweID(), cweURL(cveDetail.CweID())) - dtable.AddRow(cveDetail.CweID()+"(JVN)", cweJvnURL(cveDetail.CweID())) + // dtable := uitable.New() + // dtable.MaxColWidth = maxColWidth + // 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(cveDetail.CweID(), cweURL(cveDetail.CweID())) + // dtable.AddRow(cveDetail.CweID()+"(JVN)", cweJvnURL(cveDetail.CweID())) - dtable.AddRow("JVN", jvn.Link()) - dtable.AddRow("NVD", fmt.Sprintf("%s/%s", nvdBaseURL, cveID)) - dtable.AddRow("MITRE", fmt.Sprintf("%s%s", mitreBaseURL, cveID)) - dtable.AddRow("CVE Details", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)) - dtable.AddRow("CVSSv2 Clac", fmt.Sprintf(cvssV2CalcBaseURL, cveID)) - dtable.AddRow("CVSSv3 Clac", fmt.Sprintf(cvssV3CalcBaseURL, cveID)) + // dtable.AddRow("JVN", jvn.Link()) + // dtable.AddRow("NVD", fmt.Sprintf("%s/%s", nvdBaseURL, cveID)) + // dtable.AddRow("MITRE", fmt.Sprintf("%s%s", mitreBaseURL, cveID)) + // dtable.AddRow("CVE Details", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)) + // dtable.AddRow("CVSSv2 Clac", fmt.Sprintf(cvssV2CalcBaseURL, cveID)) + // dtable.AddRow("CVSSv3 Clac", fmt.Sprintf(cvssV3CalcBaseURL, cveID)) - dlinks := distroLinks(cveInfo, osFamily) - for _, link := range dlinks { - dtable.AddRow(link.title, link.url) - } + // dlinks := distroLinks(cveInfo, osFamily) + // for _, link := range dlinks { + // dtable.AddRow(link.title, link.url) + // } - dtable = addPackageInfos(dtable, cveInfo.Packages) - dtable = addCpeNames(dtable, cveInfo.CpeNames) - dtable.AddRow("Confidence", cveInfo.VulnInfo.Confidence) + // dtable = addPackageInfos(dtable, cveInfo.Packages) + // dtable = addCpeNames(dtable, cveInfo.CpeNames) + // dtable.AddRow("Confidence", cveInfo.VulnInfo.Confidence) - return fmt.Sprintf("%s", dtable) + // return fmt.Sprintf("%s", dtable) } +//TODO func formatPlainTextDetailsLangEn(d models.CveInfo, osFamily string) string { - cveDetail := d.CveDetail - cveID := cveDetail.CveID - nvd := cveDetail.Nvd + return "" + // cveDetail := d.CveDetail + // cveID := cveDetail.CveID + // nvd := cveDetail.Nvd - dtable := uitable.New() - dtable.MaxColWidth = maxColWidth - dtable.Wrap = true - dtable.AddRow(cveID) - dtable.AddRow("-------------") + // dtable := uitable.New() + // dtable.MaxColWidth = maxColWidth + // 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", "?") - } + // 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("CWE", cweURL(cveDetail.CweID())) + // dtable.AddRow("Vector", nvd.CvssVector()) + // dtable.AddRow("Summary", nvd.CveSummary()) + // dtable.AddRow("CWE", cweURL(cveDetail.CweID())) - dtable.AddRow("NVD", fmt.Sprintf("%s/%s", nvdBaseURL, cveID)) - dtable.AddRow("MITRE", fmt.Sprintf("%s%s", mitreBaseURL, cveID)) - dtable.AddRow("CVE Details", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)) - dtable.AddRow("CVSSv2 Clac", fmt.Sprintf(cvssV2CalcBaseURL, cveID)) - dtable.AddRow("CVSSv3 Clac", fmt.Sprintf(cvssV3CalcBaseURL, cveID)) + // dtable.AddRow("NVD", fmt.Sprintf("%s/%s", nvdBaseURL, cveID)) + // dtable.AddRow("MITRE", fmt.Sprintf("%s%s", mitreBaseURL, cveID)) + // dtable.AddRow("CVE Details", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)) + // dtable.AddRow("CVSSv2 Clac", fmt.Sprintf(cvssV2CalcBaseURL, cveID)) + // dtable.AddRow("CVSSv3 Clac", fmt.Sprintf(cvssV3CalcBaseURL, cveID)) - links := distroLinks(d, osFamily) - for _, link := range links { - dtable.AddRow(link.title, link.url) - } - dtable = addPackageInfos(dtable, d.Packages) - dtable = addCpeNames(dtable, d.CpeNames) - dtable.AddRow("Confidence", d.VulnInfo.Confidence) + // links := distroLinks(d, osFamily) + // for _, link := range links { + // dtable.AddRow(link.title, link.url) + // } + // dtable = addPackageInfos(dtable, d.Packages) + // dtable = addCpeNames(dtable, d.CpeNames) + // dtable.AddRow("Confidence", d.VulnInfo.Confidence) - return fmt.Sprintf("%s\n", dtable) + // return fmt.Sprintf("%s\n", dtable) } type distroLink struct { @@ -374,7 +384,7 @@ type distroLink struct { // distroLinks add Vendor URL of the CVE to table func distroLinks(cveInfo models.CveInfo, osFamily string) []distroLink { - cveID := cveInfo.CveDetail.CveID + cveID := cveInfo.VulnInfo.CveID switch osFamily { case "rhel", "centos": links := []distroLink{ diff --git a/util/util.go b/util/util.go index 3b94e8a6..c022c87d 100644 --- a/util/util.go +++ b/util/util.go @@ -20,6 +20,7 @@ package util import ( "fmt" "net/url" + "strconv" "strings" "github.com/future-architect/vuls/config" @@ -135,3 +136,31 @@ func Truncate(str string, length int) string { } return str } + +// ParseCvss2 divide CVSSv2 string into score and vector +// 5/AV:N/AC:L/Au:N/C:N/I:N/A:P +func ParseCvss2(scoreVector string) (score float64, vector string) { + var err error + ss := strings.Split(scoreVector, "/") + if 1 < len(ss) { + if score, err = strconv.ParseFloat(ss[0], 64); err != nil { + return 0, "" + } + return score, strings.Join(ss[1:len(ss)], "/") + } + return 0, "" +} + +// 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 ParseCvss3(scoreVector string) (score float64, vector string) { + var err error + ss := strings.Split(scoreVector, "/CVSS:3.0/") + if 1 < len(ss) { + if score, err = strconv.ParseFloat(ss[0], 64); err != nil { + return 0, "" + } + return score, strings.Join(ss[1:len(ss)], "/") + } + return 0, "" +} diff --git a/util/util_test.go b/util/util_test.go index 49340a3d..19ce9f92 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -171,3 +171,69 @@ func TestTruncate(t *testing.T) { } } } + +func TestParseCvss2(t *testing.T) { + type out struct { + score float64 + vector string + } + var tests = []struct { + in string + out out + }{ + { + in: "5/AV:N/AC:L/Au:N/C:N/I:N/A:P", + out: out{ + score: 5.0, + vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + }, + }, + { + in: "", + out: out{ + score: 0, + vector: "", + }, + }, + } + for _, tt := range tests { + s, v := 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) + } + } +} + +func TestParseCvss3(t *testing.T) { + type out struct { + score float64 + vector string + } + var tests = []struct { + in string + out out + }{ + { + in: "5.6/CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + out: out{ + score: 5.6, + vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + }, + }, + { + in: "", + out: out{ + score: 0, + vector: "", + }, + }, + } + for _, tt := range tests { + s, v := 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) + } + } +} From c103b79ec24a5ec9e3ff0c5b328d9a71952ed2f5 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Thu, 4 May 2017 13:57:22 +0900 Subject: [PATCH 015/113] Change models structure --- Gopkg.lock | 28 ++- commands/report.go | 8 +- commands/util.go | 33 ++- cveapi/cve_client.go | 1 + models/models.go | 515 ++++++++++++++++++++++------------------ oval/debian.go | 71 ++---- oval/redhat.go | 135 +++++------ oval/redhat_test.go | 69 ++++++ report/azureblob.go | 10 +- report/email.go | 4 +- report/slack.go | 236 +++++++++---------- report/tui.go | 271 ++++++++++----------- report/util.go | 547 ++++++++++++++++++++++--------------------- scan/base.go | 4 +- util/util.go | 29 --- util/util_test.go | 66 ------ 16 files changed, 1022 insertions(+), 1005 deletions(-) create mode 100644 oval/redhat_test.go diff --git a/Gopkg.lock b/Gopkg.lock index 6713d9d8..9fe8c4f2 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -4,13 +4,13 @@ memo = "bd95ed8c2b0aa32327ae55d88bff888b8198d238f7a71eee0f8663494664a0ac" branch = "master" name = "github.com/Azure/azure-storage-go" packages = ["."] - revision = "12ccaadb081cdd217702067d28da9a7ff4305239" + revision = "4fe73b0b4f68bf8a7cad2920ef563fe4c40ac5c0" [[projects]] name = "github.com/Azure/go-autorest" - packages = ["autorest","autorest/azure","autorest/date"] - revision = "a2fdd780c9a50455cecd249b00bdc3eb73a78e31" - version = "v7.3.1" + packages = ["autorest","autorest/adal","autorest/azure","autorest/date"] + revision = "58f6f26e200fa5dfb40c9cd1c83f3e2c860d779d" + version = "v8.0.0" [[projects]] name = "github.com/BurntSushi/toml" @@ -22,7 +22,7 @@ memo = "bd95ed8c2b0aa32327ae55d88bff888b8198d238f7a71eee0f8663494664a0ac" branch = "master" name = "github.com/Sirupsen/logrus" packages = ["."] - revision = "10f801ebc38b33738c9d17d50860f484a0988ff5" + revision = "508f304878257fb578be3e863e3990ed9ec3aa2e" [[projects]] name = "github.com/asaskevich/govalidator" @@ -141,7 +141,7 @@ memo = "bd95ed8c2b0aa32327ae55d88bff888b8198d238f7a71eee0f8663494664a0ac" branch = "master" name = "github.com/kotakanbe/goval-dictionary" packages = ["config","db","log","models"] - revision = "5470d7565a9de51593f53327ce14c97d466b05ab" + revision = "545199055508ae62a6d3bd34ef83034fbfc04d7f" [[projects]] branch = "master" @@ -221,6 +221,12 @@ memo = "bd95ed8c2b0aa32327ae55d88bff888b8198d238f7a71eee0f8663494664a0ac" revision = "2adb3e0c4ddd8778c4adde609d2dfd4fbe6096ea" version = "1.6" +[[projects]] + name = "github.com/satori/uuid" + packages = ["."] + revision = "879c5887cd475cd7864858769793b2ceb0d44feb" + version = "v1.1.0" + [[projects]] branch = "master" name = "github.com/valyala/bytebufferpool" @@ -237,28 +243,28 @@ memo = "bd95ed8c2b0aa32327ae55d88bff888b8198d238f7a71eee0f8663494664a0ac" branch = "master" name = "github.com/ymomoi/goval-parser" packages = ["oval"] - revision = "4ddf6fc4f1a1af026f7cae41f76979c7ff2b2e2f" + revision = "003ac9af5fffac6c97ab1def025d2cb73e88469a" [[projects]] branch = "master" name = "golang.org/x/crypto" packages = ["curve25519","ed25519","ed25519/internal/edwards25519","ssh","ssh/agent","ssh/terminal"] - revision = "c7af5bf2638a1164f2eb5467c39c6cffbd13a02e" + revision = "04eae0b62feaaf659a0ce2c4e8dc70b6ae2fff67" [[projects]] branch = "master" name = "golang.org/x/net" packages = ["context","idna","publicsuffix"] - revision = "da118f7b8e5954f39d0d2130ab35d4bf0e3cb344" + revision = "feeb485667d1fdabe727840fe00adc22431bc86e" [[projects]] branch = "master" name = "golang.org/x/sys" packages = ["unix"] - revision = "9f30dcbe5be197894515a338a9bda9253567ea8f" + revision = "9ccfe848b9db8435a24c424abbc07a921adf1df5" [[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 = "a9a820217f98f7c8a207ec1e45a874e1fe12c478" + revision = "470f45bf29f4147d6fbd7dfd0a02a848e49f5bf4" diff --git a/commands/report.go b/commands/report.go index 5600e440..08b3afae 100644 --- a/commands/report.go +++ b/commands/report.go @@ -422,7 +422,6 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} util.Log.Errorf("Failed to fill OVAL information: %s", err) return subcommands.ExitFailure } - pp.Println(filled) filled, err = fillCveInfoFromCveDB(*filled) if err != nil { @@ -463,6 +462,13 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} var res models.ScanResults for _, r := range results { + //TODO remove + for _, vuln := range r.ScannedCves { + if _, ok := vuln.CveContents.Get(models.CveContentType(r.Family)); !ok { + fmt.Println("not in oval") + pp.Println(vuln) + } + } res = append(res, r.FilterByCvssOver()) } diff --git a/commands/util.go b/commands/util.go index 98306c36..93b99d76 100644 --- a/commands/util.go +++ b/commands/util.go @@ -231,8 +231,9 @@ func diff(current, previous models.ScanResults) (diff models.ScanResults, err er if found { currentResult.ScannedCves = getNewCves(previousResult, currentResult) - currentResult.KnownCves = []models.CveInfo{} - currentResult.UnknownCves = []models.CveInfo{} + //TODO + // currentResult.KnownCves = []models.CveInfo{} + // currentResult.UnknownCves = []models.CveInfo{} currentResult.Packages = models.PackageInfoList{} for _, s := range currentResult.ScannedCves { @@ -270,27 +271,28 @@ func isCveInfoUpdated(current, previous models.ScanResult, CveID string) bool { Jvn time.Time } + //TODO previousModifies := lastModified{} - for _, c := range previous.KnownCves { + for _, c := range previous.ScannedCves { if CveID == c.CveID { //TODO - if nvd, found := c.Get(models.NVD); found { + if nvd, found := c.CveContents.Get(models.NVD); found { previousModifies.Nvd = nvd.LastModified } - if jvn, found := c.Get(models.JVN); found { + if jvn, found := c.CveContents.Get(models.JVN); found { previousModifies.Jvn = jvn.LastModified } } } currentModifies := lastModified{} - for _, c := range current.KnownCves { - if CveID == c.VulnInfo.CveID { + for _, c := range current.ScannedCves { + if CveID == c.CveID { //TODO - if nvd, found := c.Get(models.NVD); found { + if nvd, found := c.CveContents.Get(models.NVD); found { previousModifies.Nvd = nvd.LastModified } - if jvn, found := c.Get(models.JVN); found { + if jvn, found := c.CveContents.Get(models.JVN); found { previousModifies.Jvn = jvn.LastModified } } @@ -352,7 +354,14 @@ func scanVulnByCpeNames(cpeNames []string, scannedVulns []models.VulnInfo) ([]mo } func needToRefreshCve(r models.ScanResult) bool { - return r.Lang != c.Conf.Lang || len(r.KnownCves) == 0 && - len(r.UnknownCves) == 0 && - len(r.IgnoredCves) == 0 + if r.Lang != c.Conf.Lang { + return true + } + + for _, cve := range r.ScannedCves { + if 0 < len(cve.CveContents) { + return false + } + } + return true } diff --git a/cveapi/cve_client.go b/cveapi/cve_client.go index 972f7ae9..af219238 100644 --- a/cveapi/cve_client.go +++ b/cveapi/cve_client.go @@ -69,6 +69,7 @@ type response struct { CveDetail cve.CveDetail } +//TODO rename to FetchCveDictionary func (api cvedictClient) FetchCveDetails(cveIDs []string) (cveDetails cve.CveDetails, err error) { switch config.Conf.CveDBType { case "sqlite3", "mysql", "postgres": diff --git a/models/models.go b/models/models.go index 7eee245e..387b63db 100644 --- a/models/models.go +++ b/models/models.go @@ -60,25 +60,19 @@ type ScanResult struct { Container Container Platform Platform - // Scanned Vulns via SSH + CPE Vulns - ScannedCves []VulnInfo - - KnownCves CveInfos - UnknownCves CveInfos - IgnoredCves CveInfos + // Scanned Vulns by SSH scan + CPE + OVAL + ScannedCves VulnInfos Packages PackageInfoList - Errors []string Optional [][]interface{} } // FillCveDetail fetches NVD, JVN from CVE Database, and then set to fields. +//TODO rename to FillCveDictionary func (r ScanResult) FillCveDetail() (*ScanResult, error) { - set := map[string]VulnInfo{} var cveIDs []string for _, v := range r.ScannedCves { - set[v.CveID] = v cveIDs = append(cveIDs, v.CveID) } @@ -86,64 +80,24 @@ func (r ScanResult) FillCveDetail() (*ScanResult, error) { if err != nil { return nil, err } - - r.IgnoredCves = CveInfos{} for _, d := range ds { nvd := *r.convertNvdToModel(d.CveID, d.Nvd) jvn := *r.convertJvnToModel(d.CveID, d.Jvn) - cinfo := CveInfo{ - CveContents: []CveContent{nvd, jvn}, - VulnInfo: set[d.CveID], - } - cinfo.NilSliceToEmpty() - - // ignored - ignore := false - for _, icve := range config.Conf.Servers[r.ServerName].IgnoreCves { - if icve == d.CveID { - r.IgnoredCves.Insert(cinfo) - ignore = true + for i, sc := range r.ScannedCves { + if sc.CveID == d.CveID { + for _, con := range []CveContent{nvd, jvn} { + if !con.Empty() { + r.ScannedCves[i].CveContents.Upsert(con) + } + } break } } - if ignore { - continue - } - - // Update known if KnownCves already have cinfo - if c, ok := r.KnownCves.Get(cinfo.CveID); ok { - for _, con := range []CveContent{nvd, jvn} { - if !c.Update(con) { - c.Insert(con) - } - } - r.KnownCves.Update(c) - continue - } - - // Update unknown if UnknownCves already have cinfo - if c, ok := r.UnknownCves.Get(cinfo.CveID); ok { - for _, con := range []CveContent{nvd, jvn} { - if !c.Update(con) { - c.Insert(con) - } - } - r.UnknownCves.Update(c) - continue - } - - // unknown - if d.CvssScore(config.Conf.Lang) <= 0 { - r.UnknownCves.Insert(cinfo) - continue - } - - // known - r.KnownCves.Insert(cinfo) } - sort.Sort(r.KnownCves) - sort.Sort(r.UnknownCves) - sort.Sort(r.IgnoredCves) + //TODO sort + // sort.Sort(r.KnownCves) + // sort.Sort(r.UnknownCves) + // sort.Sort(r.IgnoredCves) return &r, nil } @@ -236,19 +190,21 @@ func (r ScanResult) convertJvnToModel(cveID string, jvn cvedict.Jvn) *CveContent // FilterByCvssOver is filter function. func (r ScanResult) FilterByCvssOver() ScanResult { - cveInfos := []CveInfo{} // TODO: Set correct default value if config.Conf.CvssScoreOver == 0 { config.Conf.CvssScoreOver = -1.1 } - for _, cveInfo := range r.KnownCves { - if config.Conf.CvssScoreOver <= cveInfo.CvssV2Score() { - cveInfos = append(cveInfos, cveInfo) + // TODO: Filter by ignore cves??? + filtered := VulnInfos{} + for _, sc := range r.ScannedCves { + if config.Conf.CvssScoreOver <= sc.CveContents.CvssV2Score() { + filtered = append(filtered, sc) } } - r.KnownCves = cveInfos - return r + copiedScanResult := r + copiedScanResult.ScannedCves = filtered + return copiedScanResult } // ReportFileName returns the filename on localhost without extention @@ -311,9 +267,8 @@ func (r ScanResult) FormatServerName() string { // CveSummary summarize the number of CVEs group by CVSSv2 Severity func (r ScanResult) CveSummary() string { var high, medium, low, unknown int - cves := append(r.KnownCves, r.UnknownCves...) - for _, cveInfo := range cves { - score := cveInfo.CvssV2Score() + for _, vInfo := range r.ScannedCves { + score := vInfo.CveContents.CvssV2Score() switch { case 7.0 <= score: high++ @@ -334,18 +289,14 @@ func (r ScanResult) CveSummary() string { high+medium+low+unknown, high, medium, low, unknown) } -// AllCves returns Known and Unknown CVEs -func (r ScanResult) AllCves() []CveInfo { - return append(r.KnownCves, r.UnknownCves...) -} - // NWLink has network link information. -type NWLink struct { - IPAddress string - Netmask string - DevName string - LinkState string -} +//TODO remove +// type NWLink struct { +// IPAddress string +// Netmask string +// DevName string +// LinkState string +// } // Confidence is a ranking how confident the CVE-ID was deteted correctly // Score: 0 - 100 @@ -405,6 +356,89 @@ var ChangelogLenientMatch = Confidence{50, ChangelogLenientMatchStr} // VulnInfos is VulnInfo list, getter/setter, sortable methods. type VulnInfos []VulnInfo +// FindByCveID find by CVEID +// TODO remove +// func (v *VulnInfos) FindByCveID(cveID string) (VulnInfo, bool) { +// for _, p := range s { +// if cveID == p.CveID { +// return p, true +// } +// } +// return VulnInfo{CveID: cveID}, false +// } + +// Get VulnInfo by cveID +func (v *VulnInfos) Get(cveID string) (VulnInfo, bool) { + for _, vv := range *v { + if vv.CveID == cveID { + return vv, true + } + } + return VulnInfo{}, false +} + +// Delete by cveID +func (v *VulnInfos) Delete(cveID string) { + vInfos := *v + for i, vv := range vInfos { + if vv.CveID == cveID { + *v = append(vInfos[:i], vInfos[i+1:]...) + break + } + } +} + +// Insert VulnInfo +func (v *VulnInfos) Insert(vinfo VulnInfo) { + *v = append(*v, vinfo) +} + +// Update VulnInfo +func (v *VulnInfos) Update(vInfo VulnInfo) (ok bool) { + for i, vv := range *v { + if vv.CveID == vInfo.CveID { + (*v)[i] = vInfo + return true + } + } + return false +} + +// Upsert cveInfo +func (v *VulnInfos) Upsert(vInfo VulnInfo) { + ok := v.Update(vInfo) + if !ok { + v.Insert(vInfo) + } +} + +// immutable +// func (v *VulnInfos) set(cveID string, v VulnInfo) VulnInfos { +// for i, p := range s { +// if cveID == p.CveID { +// s[i] = v +// return s +// } +// } +// return append(s, v) +// } + +//TODO GO 1.8 +// Len implement Sort Interface +// func (s VulnInfos) Len() int { +// return len(s) +// } + +// // Swap implement Sort Interface +// func (s VulnInfos) Swap(i, j int) { +// s[i], s[j] = s[j], s[i] +// } + +// // Less implement Sort Interface +// func (s VulnInfos) Less(i, j int) bool { +// return s[i].CveID < s[j].CveID +// } + // VulnInfo holds a vulnerability information and unsecure packages type VulnInfo struct { CveID string @@ -412,6 +446,7 @@ type VulnInfo struct { Packages PackageInfoList DistroAdvisories []DistroAdvisory // for Aamazon, RHEL, FreeBSD CpeNames []string + CveContents CveContents } // NilSliceToEmpty set nil slice fields to empty slice to avoid null in JSON @@ -427,167 +462,132 @@ func (v *VulnInfo) NilSliceToEmpty() { } } -// FindByCveID find by CVEID -func (s VulnInfos) FindByCveID(cveID string) (VulnInfo, bool) { - for _, p := range s { - if cveID == p.CveID { - return p, true - } - } - return VulnInfo{CveID: cveID}, false -} - -// immutable -func (s VulnInfos) set(cveID string, v VulnInfo) VulnInfos { - for i, p := range s { - if cveID == p.CveID { - s[i] = v - return s - } - } - return append(s, v) -} - -// Len implement Sort Interface -func (s VulnInfos) Len() int { - return len(s) -} - -// Swap implement Sort Interface -func (s VulnInfos) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} - -// Less implement Sort Interface -func (s VulnInfos) Less(i, j int) bool { - return s[i].CveID < s[j].CveID -} - // CveInfos is for sorting -type CveInfos []CveInfo +// type CveInfos []CveInfo -func (c CveInfos) Len() int { - return len(c) -} +// func (c CveInfos) Len() int { +// return len(c) +// } -func (c CveInfos) Swap(i, j int) { - c[i], c[j] = c[j], c[i] -} +// func (c CveInfos) Swap(i, j int) { +// c[i], c[j] = c[j], c[i] +// } -func (c CveInfos) Less(i, j int) bool { - if c[i].CvssV2Score() == c[j].CvssV2Score() { - return c[i].CveID < c[j].CveID - } - return c[j].CvssV2Score() < c[i].CvssV2Score() -} +// func (c CveInfos) Less(i, j int) bool { +// if c[i].CvssV2Score() == c[j].CvssV2Score() { +// return c[i].CveID < c[j].CveID +// } +// return c[j].CvssV2Score() < c[i].CvssV2Score() +// } -// Get cveInfo by cveID -func (c CveInfos) Get(cveID string) (CveInfo, bool) { - for _, cve := range c { - if cve.VulnInfo.CveID == cveID { - return cve, true - } - } - return CveInfo{}, false -} +// // Get cveInfo by cveID +// func (c CveInfos) Get(cveID string) (CveInfo, bool) { +// for _, cve := range c { +// if cve.VulnInfo.CveID == cveID { +// return cve, true +// } +// } +// return CveInfo{}, false +// } -// Delete by cveID -func (c *CveInfos) Delete(cveID string) { - cveInfos := *c - for i, cve := range cveInfos { - if cve.VulnInfo.CveID == cveID { - *c = append(cveInfos[:i], cveInfos[i+1:]...) - break - } - } -} +// // Delete by cveID +// func (c *CveInfos) Delete(cveID string) { +// cveInfos := *c +// for i, cve := range cveInfos { +// if cve.VulnInfo.CveID == cveID { +// *c = append(cveInfos[:i], cveInfos[i+1:]...) +// break +// } +// } +// } -// Insert cveInfo -func (c *CveInfos) Insert(cveInfo CveInfo) { - *c = append(*c, cveInfo) -} +// // Insert cveInfo +// func (c *CveInfos) Insert(cveInfo CveInfo) { +// *c = append(*c, cveInfo) +// } -// Update cveInfo -func (c CveInfos) Update(cveInfo CveInfo) (ok bool) { - for i, cve := range c { - if cve.VulnInfo.CveID == cveInfo.VulnInfo.CveID { - c[i] = cveInfo - return true - } - } - return false -} +// // Update cveInfo +// func (c CveInfos) Update(cveInfo CveInfo) (ok bool) { +// for i, cve := range c { +// if cve.VulnInfo.CveID == cveInfo.VulnInfo.CveID { +// c[i] = cveInfo +// return true +// } +// } +// return false +// } -// Upsert cveInfo -func (c *CveInfos) Upsert(cveInfo CveInfo) { - ok := c.Update(cveInfo) - if !ok { - c.Insert(cveInfo) - } -} +// // Upsert cveInfo +// func (c *CveInfos) Upsert(cveInfo CveInfo) { +// ok := c.Update(cveInfo) +// if !ok { +// c.Insert(cveInfo) +// } +// } +//TODO // CveInfo has CVE detailed Information. -type CveInfo struct { - VulnInfo - CveContents []CveContent -} +// type CveInfo struct { +// VulnInfo +// CveContents []CveContent +// } // Get a CveContent specified by arg -func (c *CveInfo) Get(typestr CveContentType) (*CveContent, bool) { - for _, cont := range c.CveContents { - if cont.Type == typestr { - return &cont, true - } - } - return &CveContent{}, false -} +// func (c *CveInfo) Get(typestr CveContentType) (*CveContent, bool) { +// for _, cont := range c.CveContents { +// if cont.Type == typestr { +// return &cont, true +// } +// } +// return &CveContent{}, false +// } -// Insert a CveContent to specified by arg -func (c *CveInfo) Insert(con CveContent) { - c.CveContents = append(c.CveContents, con) -} +// // Insert a CveContent to specified by arg +// func (c *CveInfo) Insert(con CveContent) { +// c.CveContents = append(c.CveContents, con) +// } -// Update a CveContent to specified by arg -func (c *CveInfo) Update(to CveContent) bool { - for i, cont := range c.CveContents { - if cont.Type == to.Type { - c.CveContents[i] = to - return true - } - } - return false -} +// // Update a CveContent to specified by arg +// func (c *CveInfo) Update(to CveContent) bool { +// for i, cont := range c.CveContents { +// if cont.Type == to.Type { +// c.CveContents[i] = to +// return true +// } +// } +// return false +// } -// CvssV2Score returns CVSS V2 Score -func (c *CveInfo) CvssV2Score() float64 { - //TODO - if cont, found := c.Get(NVD); found { - return cont.Cvss2Score - } else if cont, found := c.Get(JVN); found { - return cont.Cvss2Score - } else if cont, found := c.Get(RedHat); found { - return cont.Cvss2Score - } - return -1 -} +// // CvssV2Score returns CVSS V2 Score +// func (c *CveInfo) CvssV2Score() float64 { +// //TODO +// if cont, found := c.Get(NVD); found { +// return cont.Cvss2Score +// } else if cont, found := c.Get(JVN); found { +// return cont.Cvss2Score +// } else if cont, found := c.Get(RedHat); found { +// return cont.Cvss2Score +// } +// return -1 +// } -// NilSliceToEmpty set nil slice fields to empty slice to avoid null in JSON -func (c *CveInfo) NilSliceToEmpty() { - return - // TODO - // if c.CveDetail.Nvd.Cpes == nil { - // c.CveDetail.Nvd.Cpes = []cve.Cpe{} - // } - // if c.CveDetail.Jvn.Cpes == nil { - // c.CveDetail.Jvn.Cpes = []cve.Cpe{} - // } - // if c.CveDetail.Nvd.References == nil { - // c.CveDetail.Nvd.References = []cve.Reference{} - // } - // if c.CveDetail.Jvn.References == nil { - // c.CveDetail.Jvn.References = []cve.Reference{} - // } -} +// // NilSliceToEmpty set nil slice fields to empty slice to avoid null in JSON +// func (c *CveInfo) NilSliceToEmpty() { +// return +// // TODO +// // if c.CveDetail.Nvd.Cpes == nil { +// // c.CveDetail.Nvd.Cpes = []cve.Cpe{} +// // } +// // if c.CveDetail.Jvn.Cpes == nil { +// // c.CveDetail.Jvn.Cpes = []cve.Cpe{} +// // } +// // if c.CveDetail.Nvd.References == nil { +// // c.CveDetail.Nvd.References = []cve.Reference{} +// // } +// // if c.CveDetail.Jvn.References == nil { +// // c.CveDetail.Jvn.References = []cve.Reference{} +// // } +// } // CveContentType is a source of CVE information type CveContentType string @@ -612,6 +612,68 @@ const ( Ubuntu CveContentType = "ubuntu" ) +// CveContents has slice of CveContent +type CveContents []CveContent + +// Get CveContent by cveID +// TODO Pointer +func (v *CveContents) Get(typestr CveContentType) (CveContent, bool) { + for _, vv := range *v { + if vv.Type == typestr { + return vv, true + } + } + return CveContent{}, false +} + +// Delete by cveID +func (v *CveContents) Delete(typestr CveContentType) { + cveContents := *v + for i, cc := range cveContents { + if cc.Type == typestr { + *v = append(cveContents[:i], cveContents[i+1:]...) + break + } + } +} + +// Insert CveContent +func (v *CveContents) Insert(cont CveContent) { + *v = append(*v, cont) +} + +// Update VulnInfo +func (v *CveContents) Update(cont CveContent) (ok bool) { + for i, vv := range *v { + if vv.Type == cont.Type { + (*v)[i] = cont + return true + } + } + return false +} + +// Upsert CveContent +func (v *CveContents) Upsert(cont CveContent) { + ok := v.Update(cont) + if !ok { + v.Insert(cont) + } +} + +// CvssV2Score returns CVSS V2 Score +func (v *CveContents) CvssV2Score() float64 { + //TODO + if cont, found := v.Get(NVD); found { + return cont.Cvss2Score + } else if cont, found := v.Get(JVN); found { + return cont.Cvss2Score + } else if cont, found := v.Get(RedHat); found { + return cont.Cvss2Score + } + return -1 +} + // CveContent has abstraction of various vulnerability information type CveContent struct { Type CveContentType @@ -630,6 +692,11 @@ type CveContent struct { LastModified time.Time } +// Empty checks the content is empty +func (c CveContent) Empty() bool { + return c.Summary == "" +} + // Cpe is Common Platform Enumeration type Cpe struct { CpeName string diff --git a/oval/debian.go b/oval/debian.go index b9cf3194..76720e91 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -59,64 +59,30 @@ func (o Debian) FillCveInfoFromOvalDB(r *models.ScanResult) (*models.ScanResult, } func (o Debian) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Definition) *models.ScanResult { - // Update ScannedCves by OVAL info - found := false - updatedCves := []models.VulnInfo{} - - // Update scanned confidence to ovalmatch - for _, scanned := range r.ScannedCves { - if scanned.CveID == definition.Debian.CveID { - found = true - if scanned.Confidence.Score < models.OvalMatch.Score { - scanned.Confidence = models.OvalMatch - } - } - updatedCves = append(updatedCves, scanned) - } - - vuln := models.VulnInfo{ - CveID: definition.Debian.CveID, - Confidence: models.OvalMatch, - Packages: getPackageInfoList(r, definition), - } - - if !found { - util.Log.Debugf("%s is newly detected by OVAL", vuln.CveID) - updatedCves = append(updatedCves, vuln) - } - r.ScannedCves = updatedCves - - // Update KnownCves by OVAL info ovalContent := *o.convertToModel(definition) ovalContent.Type = models.CveContentType(r.Family) - cInfo, ok := r.KnownCves.Get(definition.Debian.CveID) + vinfo, ok := r.ScannedCves.Get(definition.Debian.CveID) if !ok { - cInfo.VulnInfo = vuln - cInfo.CveContents = []models.CveContent{ovalContent} - } - if !cInfo.Update(ovalContent) { - cInfo.Insert(ovalContent) - } - if cInfo.VulnInfo.Confidence.Score < models.OvalMatch.Score { - cInfo.Confidence = models.OvalMatch - } - r.KnownCves.Upsert(cInfo) - - // Update UnknownCves by OVAL info - cInfo, ok = r.UnknownCves.Get(definition.Debian.CveID) - if ok { - r.UnknownCves.Delete(definition.Debian.CveID) - - // Insert new CveInfo - if !cInfo.Update(ovalContent) { - cInfo.Insert(ovalContent) + util.Log.Infof("%s is newly detected by OVAL", + definition.Debian.CveID) + vinfo = models.VulnInfo{ + CveID: definition.Debian.CveID, + Confidence: models.OvalMatch, + Packages: getPackageInfoList(r, definition), + CveContents: []models.CveContent{ovalContent}, } - if cInfo.VulnInfo.Confidence.Score < models.OvalMatch.Score { - cInfo.Confidence = models.OvalMatch + } else { + if _, ok := vinfo.CveContents.Get(models.CveContentType(r.Family)); !ok { + util.Log.Infof("%s is also detected by OVAL", definition.Debian.CveID) + } else { + util.Log.Infof("%s will be updated by OVAL", definition.Debian.CveID) } - r.KnownCves.Upsert(cInfo) + if vinfo.Confidence.Score < models.OvalMatch.Score { + vinfo.Confidence = models.OvalMatch + } + vinfo.CveContents.Upsert(ovalContent) } - + r.ScannedCves.Upsert(vinfo) return r } @@ -133,6 +99,7 @@ func (o Debian) convertToModel(def *ovalmodels.Definition) *models.CveContent { CveID: def.Debian.CveID, Title: def.Title, Summary: def.Description, + Severity: def.Advisory.Severity, References: refs, } } diff --git a/oval/redhat.go b/oval/redhat.go index 9a0e1abd..da0ff9fb 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -2,6 +2,8 @@ package oval import ( "fmt" + "strconv" + "strings" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" @@ -55,76 +57,26 @@ func (o Redhat) FillCveInfoFromOvalDB(r *models.ScanResult) (*models.ScanResult, } func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Definition) *models.ScanResult { - cveIDSet := make(map[string]bool) - cveID2VulnInfo := make(map[string]models.VulnInfo) for _, cve := range definition.Advisory.Cves { - cveIDSet[cve.CveID] = false - cveID2VulnInfo[cve.CveID] = models.VulnInfo{ - CveID: cve.CveID, - Confidence: models.OvalMatch, - Packages: getPackageInfoList(r, definition), - } - } - - // Update ScannedCves by OVAL info - updatedCves := []models.VulnInfo{} - for _, scanned := range r.ScannedCves { - // Update scanned confidence to ovalmatch - for _, c := range definition.Advisory.Cves { - if scanned.CveID == c.CveID { - cveIDSet[c.CveID] = true - if scanned.Confidence.Score < models.OvalMatch.Score { - scanned.Confidence = models.OvalMatch - } - break - } - } - updatedCves = append(updatedCves, scanned) - } - - for cveID, found := range cveIDSet { - if !found { - util.Log.Debugf("%s is newly detected by OVAL", cveID) - updatedCves = append(updatedCves, cveID2VulnInfo[cveID]) - } - } - r.ScannedCves = updatedCves - - // Update KnownCves by OVAL info - for _, c := range definition.Advisory.Cves { - ovalContent := *o.convertToModel(c.CveID, definition) - cInfo, ok := r.KnownCves.Get(c.CveID) + ovalContent := *o.convertToModel(cve.CveID, definition) + vinfo, ok := r.ScannedCves.Get(cve.CveID) if !ok { - cInfo.VulnInfo = cveID2VulnInfo[c.CveID] - cInfo.CveContents = []models.CveContent{ovalContent} - } - if !cInfo.Update(ovalContent) { - cInfo.Insert(ovalContent) - } - if cInfo.VulnInfo.Confidence.Score < models.OvalMatch.Score { - cInfo.Confidence = models.OvalMatch - } - r.KnownCves.Upsert(cInfo) - } - - // Update UnknownCves by OVAL info - for _, c := range definition.Advisory.Cves { - cInfo, ok := r.UnknownCves.Get(c.CveID) - if ok { - r.UnknownCves.Delete(c.CveID) - - // Insert new CveInfo - ovalContent := *o.convertToModel(c.CveID, definition) - if !cInfo.Update(ovalContent) { - cInfo.Insert(ovalContent) + util.Log.Infof("%s is newly detected by OVAL", + definition.Debian.CveID) + vinfo = models.VulnInfo{ + CveID: cve.CveID, + Confidence: models.OvalMatch, + Packages: getPackageInfoList(r, definition), + CveContents: []models.CveContent{ovalContent}, } - if cInfo.VulnInfo.Confidence.Score < models.OvalMatch.Score { - cInfo.Confidence = models.OvalMatch + } else { + if vinfo.Confidence.Score < models.OvalMatch.Score { + vinfo.Confidence = models.OvalMatch } - r.KnownCves.Upsert(cInfo) + vinfo.CveContents.Upsert(ovalContent) } + r.ScannedCves.Upsert(vinfo) } - return r } @@ -134,7 +86,6 @@ func (o Redhat) convertToModel(cveID string, def *ovalmodels.Definition) *models continue } var refs []models.Reference - //TODO RHSAのリンクを入れる for _, r := range def.References { refs = append(refs, models.Reference{ Link: r.RefURL, @@ -143,20 +94,52 @@ func (o Redhat) convertToModel(cveID string, def *ovalmodels.Definition) *models }) } - // util.ParseCvss2() + score2, vec2 := o.parseCvss2(cve.Cvss2) + score3, vec3 := o.parseCvss3(cve.Cvss3) return &models.CveContent{ - Type: models.RedHat, - CveID: cve.CveID, - Title: def.Title, - Summary: def.Description, - Severity: def.Advisory.Severity, - // V2Score: v2Score, // TODO divide into score and vector - Cvss2Vector: cve.Cvss2, // TODO divide into score and vector - Cvss3Vector: cve.Cvss3, // TODO divide into score and vector - References: refs, - CweID: cve.Cwe, + Type: models.RedHat, + CveID: cve.CveID, + Title: def.Title, + Summary: def.Description, + Severity: def.Advisory.Severity, + Cvss2Score: score2, + Cvss2Vector: vec2, + Cvss3Score: score3, + Cvss3Vector: vec3, + References: refs, + CweID: cve.Cwe, + Published: def.Advisory.Issued, + LastModified: def.Advisory.Updated, } } return nil } + +// 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) { + var err error + ss := strings.Split(scoreVector, "/") + if 1 < len(ss) { + if score, err = strconv.ParseFloat(ss[0], 64); err != nil { + return 0, "" + } + return score, strings.Join(ss[1:len(ss)], "/") + } + return 0, "" +} + +// 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) { + var err error + ss := strings.Split(scoreVector, "/CVSS:3.0/") + if 1 < len(ss) { + if score, err = strconv.ParseFloat(ss[0], 64); err != nil { + return 0, "" + } + return score, strings.Join(ss[1:len(ss)], "/") + } + return 0, "" +} diff --git a/oval/redhat_test.go b/oval/redhat_test.go new file mode 100644 index 00000000..c9b57dc8 --- /dev/null +++ b/oval/redhat_test.go @@ -0,0 +1,69 @@ +package oval + +import "testing" + +func TestParseCvss2(t *testing.T) { + type out struct { + score float64 + vector string + } + var tests = []struct { + in string + out out + }{ + { + in: "5/AV:N/AC:L/Au:N/C:N/I:N/A:P", + out: out{ + score: 5.0, + vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + }, + }, + { + in: "", + out: out{ + score: 0, + vector: "", + }, + }, + } + for _, tt := range tests { + s, v := Redhat{}.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) + } + } +} + +func TestParseCvss3(t *testing.T) { + type out struct { + score float64 + vector string + } + var tests = []struct { + in string + out out + }{ + { + in: "5.6/CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + out: out{ + score: 5.6, + vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + }, + }, + { + in: "", + out: out{ + score: 0, + vector: "", + }, + }, + } + for _, tt := range tests { + s, v := Redhat{}.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/azureblob.go b/report/azureblob.go index e8ac3578..2220e9c1 100644 --- a/report/azureblob.go +++ b/report/azureblob.go @@ -139,13 +139,9 @@ func createBlockBlob(cli storage.BlobStorageClient, k string, b []byte) error { k = k + ".gz" } - if err := cli.CreateBlockBlobFromReader( - c.Conf.AzureContainer, - k, - uint64(len(b)), - bytes.NewReader(b), - map[string]string{}, - ); err != nil { + ref := cli.GetContainerReference(c.Conf.AzureContainer) + blob := ref.GetBlobReference(k) + if err := blob.CreateBlockBlobFromReader(bytes.NewReader(b), nil); err != nil { return fmt.Errorf("Failed to upload data to %s/%s, %s", c.Conf.AzureContainer, k, err) } diff --git a/report/email.go b/report/email.go index 48d7449c..8dd3a941 100644 --- a/report/email.go +++ b/report/email.go @@ -41,8 +41,8 @@ func (w EMailWriter) Write(rs ...models.ScanResult) (err error) { for _, r := range rs { if conf.FormatOneEMail { message += formatFullPlainText(r) + "\r\n\r\n" - totalResult.KnownCves = append(totalResult.KnownCves, r.KnownCves...) - totalResult.UnknownCves = append(totalResult.UnknownCves, r.UnknownCves...) + // totalResult.KnownCves = append(totalResult.KnownCves, r.KnownCves...) + // totalResult.UnknownCves = append(totalResult.UnknownCves, r.UnknownCves...) } else { var subject string if len(r.Errors) != 0 { diff --git a/report/slack.go b/report/slack.go index 3b9c9b74..c1c6733c 100644 --- a/report/slack.go +++ b/report/slack.go @@ -21,7 +21,6 @@ import ( "encoding/json" "fmt" "sort" - "strings" "time" log "github.com/Sirupsen/logrus" @@ -67,11 +66,12 @@ func (w SlackWriter) Write(rs ...models.ScanResult) error { } if 0 < len(r.Errors) { - serverInfo := fmt.Sprintf("*%s*", r.ServerInfo()) - notifyUsers := getNotifyUsers(config.Conf.Slack.NotifyUsers) - txt := fmt.Sprintf("%s\n%s\nError: %s", notifyUsers, serverInfo, r.Errors) + //TODO + // serverInfo := fmt.Sprintf("*%s*", r.ServerInfo()) + // notifyUsers := getNotifyUsers(config.Conf.Slack.NotifyUsers) + // txt := fmt.Sprintf("%s\n%s\nError: %s", notifyUsers, serverInfo, r.Errors) msg := message{ - Text: txt, + // Text: txt, Username: conf.AuthUser, IconEmoji: conf.IconEmoji, Channel: channel, @@ -152,57 +152,57 @@ func send(msg message) error { func msgText(r models.ScanResult) string { notifyUsers := "" - if 0 < len(r.KnownCves) || 0 < len(r.UnknownCves) { - notifyUsers = getNotifyUsers(config.Conf.Slack.NotifyUsers) - } + // if 0 < len(r.KnownCves) || 0 < len(r.UnknownCves) { + // notifyUsers = getNotifyUsers(config.Conf.Slack.NotifyUsers) + // } serverInfo := fmt.Sprintf("*%s*", r.ServerInfo()) return fmt.Sprintf("%s\n%s\n>%s", notifyUsers, serverInfo, r.CveSummary()) } func toSlackAttachments(scanResult models.ScanResult) (attaches []*attachment) { - cves := scanResult.KnownCves - if !config.Conf.IgnoreUnscoredCves { - cves = append(cves, scanResult.UnknownCves...) - } + // cves := scanResult.KnownCves + // if !config.Conf.IgnoreUnscoredCves { + // cves = append(cves, scanResult.UnknownCves...) + // } - for _, cveInfo := range cves { - cveID := cveInfo.VulnInfo.CveID + // for _, cveInfo := range cves { + // cveID := cveInfo.VulnInfo.CveID - curentPackages := []string{} - for _, p := range cveInfo.Packages { - curentPackages = append(curentPackages, p.FormatCurrentVer()) - } - for _, n := range cveInfo.CpeNames { - curentPackages = append(curentPackages, n) - } + // curentPackages := []string{} + // for _, p := range cveInfo.Packages { + // curentPackages = append(curentPackages, p.FormatCurrentVer()) + // } + // for _, n := range cveInfo.CpeNames { + // curentPackages = append(curentPackages, n) + // } - newPackages := []string{} - for _, p := range cveInfo.Packages { - newPackages = append(newPackages, p.FormatNewVer()) - } + // newPackages := []string{} + // for _, p := range cveInfo.Packages { + // newPackages = append(newPackages, p.FormatNewVer()) + // } - a := attachment{ - Title: cveID, - TitleLink: fmt.Sprintf("%s/%s", nvdBaseURL, cveID), - Text: attachmentText(cveInfo, scanResult.Family), - MrkdwnIn: []string{"text", "pretext"}, - Fields: []*field{ - { - // Title: "Current Package/CPE", - Title: "Installed", - Value: strings.Join(curentPackages, "\n"), - Short: true, - }, - { - Title: "Candidate", - Value: strings.Join(newPackages, "\n"), - Short: true, - }, - }, - Color: color(cveInfo.CvssV2Score()), - } - attaches = append(attaches, &a) - } + // a := attachment{ + // Title: cveID, + // TitleLink: fmt.Sprintf("%s/%s", nvdBaseURL, cveID), + // Text: attachmentText(cveInfo, scanResult.Family), + // MrkdwnIn: []string{"text", "pretext"}, + // Fields: []*field{ + // { + // // Title: "Current Package/CPE", + // Title: "Installed", + // Value: strings.Join(curentPackages, "\n"), + // Short: true, + // }, + // { + // Title: "Candidate", + // Value: strings.Join(newPackages, "\n"), + // Short: true, + // }, + // }, + // Color: color(cveInfo.CvssV2Score()), + // } + // attaches = append(attaches, &a) + // } return } @@ -220,80 +220,80 @@ func color(cvssScore float64) string { } } -func attachmentText(cveInfo models.CveInfo, osFamily string) string { - // linkText := links(cveInfo, osFamily) - //TODO - return "" - // switch { - // case config.Conf.Lang == "ja" && - // 0 < cveInfo.CveDetail.Jvn.CvssScore(): +// func attachmentText(cveInfo models.CveInfo, osFamily string) string { +// linkText := links(cveInfo, osFamily) +//TODO +// return "" +// switch { +// case config.Conf.Lang == "ja" && +// 0 < cveInfo.CveDetail.Jvn.CvssScore(): - // jvn := cveInfo.CveDetail.Jvn - // return fmt.Sprintf("*%4.1f (%s)* <%s|%s>\n%s\n%s\n*Confidence:* %v", - // cveInfo.CveDetail.CvssScore(config.Conf.Lang), - // jvn.CvssSeverity(), - // fmt.Sprintf(cvssV2CalcBaseURL, cveInfo.CveDetail.CveID), - // jvn.CvssVector(), - // jvn.CveTitle(), - // linkText, - // cveInfo.VulnInfo.Confidence, - // ) - // case 0 < cveInfo.CveDetail.CvssScore("en"): - // nvd := cveInfo.CveDetail.Nvd - // return fmt.Sprintf("*%4.1f (%s)* <%s|%s>\n%s\n%s\n*Confidence:* %v", - // cveInfo.CveDetail.CvssScore(config.Conf.Lang), - // nvd.CvssSeverity(), - // fmt.Sprintf(cvssV2CalcBaseURL, cveInfo.CveDetail.CveID), - // nvd.CvssVector(), - // nvd.CveSummary(), - // linkText, - // cveInfo.VulnInfo.Confidence, - // ) - // default: - // nvd := cveInfo.CveDetail.Nvd - // return fmt.Sprintf("?\n%s\n%s\n*Confidence:* %v", - // nvd.CveSummary(), linkText, cveInfo.VulnInfo.Confidence) - // } -} +// jvn := cveInfo.CveDetail.Jvn +// return fmt.Sprintf("*%4.1f (%s)* <%s|%s>\n%s\n%s\n*Confidence:* %v", +// cveInfo.CveDetail.CvssScore(config.Conf.Lang), +// jvn.CvssSeverity(), +// fmt.Sprintf(cvssV2CalcBaseURL, cveInfo.CveDetail.CveID), +// jvn.CvssVector(), +// jvn.CveTitle(), +// linkText, +// cveInfo.VulnInfo.Confidence, +// ) +// case 0 < cveInfo.CveDetail.CvssScore("en"): +// nvd := cveInfo.CveDetail.Nvd +// return fmt.Sprintf("*%4.1f (%s)* <%s|%s>\n%s\n%s\n*Confidence:* %v", +// cveInfo.CveDetail.CvssScore(config.Conf.Lang), +// nvd.CvssSeverity(), +// fmt.Sprintf(cvssV2CalcBaseURL, cveInfo.CveDetail.CveID), +// nvd.CvssVector(), +// nvd.CveSummary(), +// linkText, +// cveInfo.VulnInfo.Confidence, +// ) +// default: +// nvd := cveInfo.CveDetail.Nvd +// return fmt.Sprintf("?\n%s\n%s\n*Confidence:* %v", +// nvd.CveSummary(), linkText, cveInfo.VulnInfo.Confidence) +// } +// } -func links(cveInfo models.CveInfo, osFamily string) string { - links := []string{} +// func links(cveInfo models.CveInfo, osFamily string) string { +// links := []string{} - //TODO - // cweID := cveInfo.CveDetail.CweID() - // if 0 < len(cweID) { - // links = append(links, fmt.Sprintf("<%s|%s>", - // cweURL(cweID), cweID)) - // if config.Conf.Lang == "ja" { - // links = append(links, fmt.Sprintf("<%s|%s(JVN)>", - // cweJvnURL(cweID), cweID)) - // } - // } +// //TODO +// // cweID := cveInfo.CveDetail.CweID() +// // if 0 < len(cweID) { +// // links = append(links, fmt.Sprintf("<%s|%s>", +// // cweURL(cweID), cweID)) +// // if config.Conf.Lang == "ja" { +// // links = append(links, fmt.Sprintf("<%s|%s(JVN)>", +// // cweJvnURL(cweID), cweID)) +// // } +// // } - cveID := cveInfo.VulnInfo.CveID - //TODO - // if config.Conf.Lang == "ja" && 0 < len(cveInfo.CveDetail.Jvn.Link()) { - // jvn := fmt.Sprintf("<%s|JVN>", cveInfo.CveDetail.Jvn.Link()) - // links = append(links, jvn) - // } - dlinks := distroLinks(cveInfo, osFamily) - for _, link := range dlinks { - links = append(links, - fmt.Sprintf("<%s|%s>", link.url, link.title)) - } - links = append(links, fmt.Sprintf("<%s|MITRE>", - fmt.Sprintf("%s%s", mitreBaseURL, cveID))) - links = append(links, fmt.Sprintf("<%s|CVEDetails>", - fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID))) +// cveID := cveInfo.VulnInfo.CveID +// //TODO +// // if config.Conf.Lang == "ja" && 0 < len(cveInfo.CveDetail.Jvn.Link()) { +// // jvn := fmt.Sprintf("<%s|JVN>", cveInfo.CveDetail.Jvn.Link()) +// // links = append(links, jvn) +// // } +// dlinks := distroLinks(cveInfo, osFamily) +// for _, link := range dlinks { +// links = append(links, +// fmt.Sprintf("<%s|%s>", link.url, link.title)) +// } +// links = append(links, fmt.Sprintf("<%s|MITRE>", +// fmt.Sprintf("%s%s", mitreBaseURL, cveID))) +// links = append(links, fmt.Sprintf("<%s|CVEDetails>", +// fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID))) - return strings.Join(links, " / ") -} +// return strings.Join(links, " / ") +// } -// See testcase -func getNotifyUsers(notifyUsers []string) string { - slackStyleTexts := []string{} - for _, username := range notifyUsers { - slackStyleTexts = append(slackStyleTexts, fmt.Sprintf("<%s>", username)) - } - return strings.Join(slackStyleTexts, " ") -} +// // See testcase +// func getNotifyUsers(notifyUsers []string) string { +// slackStyleTexts := []string{} +// for _, username := range notifyUsers { +// slackStyleTexts = append(slackStyleTexts, fmt.Sprintf("<%s>", username)) +// } +// return strings.Join(slackStyleTexts, " ") +// } diff --git a/report/tui.go b/report/tui.go index 74ab3cf2..2a3c8fdc 100644 --- a/report/tui.go +++ b/report/tui.go @@ -22,7 +22,6 @@ import ( "fmt" "os" "strings" - "text/template" "time" log "github.com/Sirupsen/logrus" @@ -221,7 +220,8 @@ func movable(v *gocui.View, nextY int) (ok bool, yLimit int) { } return true, yLimit case "summary": - yLimit = len(currentScanResult.AllCves()) - 1 + //TODO + // yLimit = len(currentScanResult.AllCves()) - 1 if yLimit < nextY { return false, yLimit } @@ -601,71 +601,72 @@ func summaryLines() string { return "Error: Scan with --debug to view the details" } - indexFormat := "" - if len(currentScanResult.AllCves()) < 10 { - indexFormat = "[%1d]" - } else if len(currentScanResult.AllCves()) < 100 { - indexFormat = "[%2d]" - } else { - indexFormat = "[%3d]" - } + //TODO + // indexFormat := "" + // if len(currentScanResult.AllCves()) < 10 { + // indexFormat = "[%1d]" + // } else if len(currentScanResult.AllCves()) < 100 { + // indexFormat = "[%2d]" + // } else { + // indexFormat = "[%3d]" + // } - for i, d := range currentScanResult.AllCves() { - var cols []string - //TODO - var summary string - if cont, found := d.Get(models.NVD); found { - summary = cont.Summary - } - var cvssScore string - if d.CvssV2Score() <= 0 { - cvssScore = "| ?" - } else { - cvssScore = fmt.Sprintf("| %4.1f", d.CvssV2Score()) - } - cols = []string{ - fmt.Sprintf(indexFormat, i+1), - d.VulnInfo.CveID, - cvssScore, - fmt.Sprintf("| %3d |", d.VulnInfo.Confidence.Score), - summary, - } - // if config.Conf.Lang == "ja" && 0 < d.CveDetail.Jvn.CvssScore() { - // summary := d.CveDetail.Jvn.CveTitle() - // cols = []string{ - // fmt.Sprintf(indexFormat, i+1), - // d.CveDetail.CveID, - // fmt.Sprintf("| %4.1f", - // d.CveDetail.CvssScore(config.Conf.Lang)), - // fmt.Sprintf("| %3d |", d.VulnInfo.Confidence.Score), - // summary, - // } - // } else { - // summary := d.CveDetail.Nvd.CveSummary() + // for i, d := range currentScanResult.AllCves() { + // var cols []string + // //TODO + // var summary string + // if cont, found := d.Get(models.NVD); found { + // summary = cont.Summary + // } + // var cvssScore string + // if d.CvssV2Score() <= 0 { + // cvssScore = "| ?" + // } else { + // cvssScore = fmt.Sprintf("| %4.1f", d.CvssV2Score()) + // } + // cols = []string{ + // fmt.Sprintf(indexFormat, i+1), + // d.VulnInfo.CveID, + // cvssScore, + // fmt.Sprintf("| %3d |", d.VulnInfo.Confidence.Score), + // summary, + // } + // // if config.Conf.Lang == "ja" && 0 < d.CveDetail.Jvn.CvssScore() { + // // summary := d.CveDetail.Jvn.CveTitle() + // // cols = []string{ + // // fmt.Sprintf(indexFormat, i+1), + // // d.CveDetail.CveID, + // // fmt.Sprintf("| %4.1f", + // // d.CveDetail.CvssScore(config.Conf.Lang)), + // // fmt.Sprintf("| %3d |", d.VulnInfo.Confidence.Score), + // // summary, + // // } + // // } else { + // // summary := d.CveDetail.Nvd.CveSummary() - // var cvssScore string - // if d.CveDetail.CvssScore("en") <= 0 { - // cvssScore = "| ?" - // } else { - // cvssScore = fmt.Sprintf("| %4.1f", - // d.CveDetail.CvssScore(config.Conf.Lang)) - // } + // // var cvssScore string + // // if d.CveDetail.CvssScore("en") <= 0 { + // // cvssScore = "| ?" + // // } else { + // // cvssScore = fmt.Sprintf("| %4.1f", + // // d.CveDetail.CvssScore(config.Conf.Lang)) + // // } - // cols = []string{ - // fmt.Sprintf(indexFormat, i+1), - // d.CveDetail.CveID, - // cvssScore, - // fmt.Sprintf("| %3d |", d.VulnInfo.Confidence.Score), - // summary, - // } - // } + // // cols = []string{ + // // fmt.Sprintf(indexFormat, i+1), + // // d.CveDetail.CveID, + // // cvssScore, + // // fmt.Sprintf("| %3d |", d.VulnInfo.Confidence.Score), + // // summary, + // // } + // // } - icols := make([]interface{}, len(cols)) - for j := range cols { - icols[j] = cols[j] - } - stable.AddRow(icols...) - } + // icols := make([]interface{}, len(cols)) + // for j := range cols { + // icols[j] = cols[j] + // } + // stable.AddRow(icols...) + // } return fmt.Sprintf("%s", stable) } @@ -712,19 +713,21 @@ func setChangelogLayout(g *gocui.Gui) error { if err != gocui.ErrUnknownView { return err } - if len(currentScanResult.Errors) != 0 || len(currentScanResult.AllCves()) == 0 { - return nil - } + //TODO + // if len(currentScanResult.Errors) != 0 || len(currentScanResult.AllCves()) == 0 { + // return nil + // } lines := []string{} - cveInfo := currentScanResult.AllCves()[currentCveInfo] - for _, pack := range cveInfo.Packages { - for _, p := range currentScanResult.Packages { - if pack.Name == p.Name { - lines = append(lines, formatOneChangelog(p), "\n") - } - } - } + //TODO + // cveInfo := currentScanResult.AllCves()[currentCveInfo] + // for _, pack := range cveInfo.Packages { + // for _, p := range currentScanResult.Packages { + // if pack.Name == p.Name { + // lines = append(lines, formatOneChangelog(p), "\n") + // } + // } + // } text := strings.Join(lines, "\n") fmt.Fprint(v, text) v.Editable = false @@ -756,20 +759,20 @@ func detailLines() (string, error) { return "", nil } - if len(currentScanResult.AllCves()) == 0 { - return "No vulnerable packages", nil - } + //TODO + // if len(currentScanResult.AllCves()) == 0 { + // return "No vulnerable packages", nil + // } + // cveInfo := currentScanResult.AllCves()[currentCveInfo] + // cveID := cveInfo.VulnInfo.CveID - cveInfo := currentScanResult.AllCves()[currentCveInfo] - cveID := cveInfo.VulnInfo.CveID + // tmpl, err := template.New("detail").Parse(detailTemplate()) + // if err != nil { + // return "", err + // } - tmpl, err := template.New("detail").Parse(detailTemplate()) - if err != nil { - return "", err - } - - var cvssSeverity, cvssVector, summary string - var refs []cve.Reference + // var cvssSeverity, cvssVector, summary string + // var refs []cve.Reference switch { //TODO // case config.Conf.Lang == "ja" && @@ -780,67 +783,67 @@ func detailLines() (string, error) { // summary = fmt.Sprintf("%s\n%s", jvn.CveTitle(), jvn.CveSummary()) // refs = jvn.VulnSiteReferences() default: - var nvd *models.CveContent - if cont, found := cveInfo.Get(models.NVD); found { - nvd = cont - } + // var nvd *models.CveContent + //TODO + // if cont, found := cveInfo.Get(models.NVD); found { + // nvd = cont + // } // cvssSeverity = nvd.CvssSeverity() // cvssVector = nvd.CvssVector() - summary = nvd.Summary + // summary = nvd.Summary // refs = nvd.VulnSiteReferences() } //TODO // cweURL := cweURL(cveInfo.CveDetail.CweID()) - - links := []string{ - fmt.Sprintf("[NVD]( %s )", fmt.Sprintf("%s/%s", nvdBaseURL, cveID)), - fmt.Sprintf("[MITRE]( %s )", fmt.Sprintf("%s%s", mitreBaseURL, cveID)), - fmt.Sprintf("[CveDetais]( %s )", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)), - fmt.Sprintf("[CVSSv2 Calc]( %s )", fmt.Sprintf(cvssV2CalcBaseURL, cveID)), - fmt.Sprintf("[CVSSv3 Calc]( %s )", fmt.Sprintf(cvssV3CalcBaseURL, cveID)), - } - dlinks := distroLinks(cveInfo, currentScanResult.Family) - for _, link := range dlinks { - links = append(links, fmt.Sprintf("[%s]( %s )", link.title, link.url)) - } + // links := []string{ + // fmt.Sprintf("[NVD]( %s )", fmt.Sprintf("%s/%s", nvdBaseURL, cveID)), + // fmt.Sprintf("[MITRE]( %s )", fmt.Sprintf("%s%s", mitreBaseURL, cveID)), + // fmt.Sprintf("[CveDetais]( %s )", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)), + // fmt.Sprintf("[CVSSv2 Calc]( %s )", fmt.Sprintf(cvssV2CalcBaseURL, cveID)), + // fmt.Sprintf("[CVSSv3 Calc]( %s )", fmt.Sprintf(cvssV3CalcBaseURL, cveID)), + // } + // dlinks := distroLinks(cveInfo, currentScanResult.Family) + // for _, link := range dlinks { + // links = append(links, fmt.Sprintf("[%s]( %s )", link.title, link.url)) + // } //TODO - var cvssScore string - if cveInfo.CvssV2Score() == -1 { - cvssScore = "?" - // } else { - // cvssScore = fmt.Sprintf("%4.1f", cveInfo.CveDetail.CvssScore(config.Conf.Lang)) - } + // var cvssScore string + // if cveInfo.CvssV2Score() == -1 { + // cvssScore = "?" + // // } else { + // // cvssScore = fmt.Sprintf("%4.1f", cveInfo.CveDetail.CvssScore(config.Conf.Lang)) + // } - packages := []string{} - for _, pack := range cveInfo.Packages { - packages = append(packages, - fmt.Sprintf( - "%s -> %s", - pack.FormatCurrentVer(), - pack.FormatNewVer())) - } + // packages := []string{} + // for _, pack := range cveInfo.Packages { + // packages = append(packages, + // fmt.Sprintf( + // "%s -> %s", + // pack.FormatCurrentVer(), + // pack.FormatNewVer())) + // } - data := dataForTmpl{ - CveID: cveID, - CvssScore: cvssScore, - CvssSeverity: cvssSeverity, - CvssVector: cvssVector, - Summary: summary, - Confidence: cveInfo.VulnInfo.Confidence, - //TODO - // CweURL: cweURL, - VulnSiteLinks: links, - References: refs, - Packages: packages, - CpeNames: cveInfo.CpeNames, - } + // data := dataForTmpl{ + // CveID: cveID, + // CvssScore: cvssScore, + // CvssSeverity: cvssSeverity, + // CvssVector: cvssVector, + // Summary: summary, + // Confidence: cveInfo.VulnInfo.Confidence, + // //TODO + // // CweURL: cweURL, + // VulnSiteLinks: links, + // References: refs, + // Packages: packages, + // CpeNames: cveInfo.CpeNames, + // } buf := bytes.NewBuffer(nil) // create empty buffer - if err := tmpl.Execute(buf, data); err != nil { - return "", err - } + // if err := tmpl.Execute(buf, data); err != nil { + // return "", err + // } return string(buf.Bytes()), nil } diff --git a/report/util.go b/report/util.go index e7b6420d..e813e676 100644 --- a/report/util.go +++ b/report/util.go @@ -22,7 +22,6 @@ import ( "fmt" "strings" - "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" "github.com/gosuri/uitable" ) @@ -84,10 +83,11 @@ func formatShortPlainText(r models.ScanResult) string { stable.MaxColWidth = maxColWidth stable.Wrap = true - cves := r.KnownCves - if !config.Conf.IgnoreUnscoredCves { - cves = append(cves, r.UnknownCves...) - } + //TODO + // cves := r.KnownCves + // if !config.Conf.IgnoreUnscoredCves { + // cves = append(cves, r.UnknownCves...) + // } var buf bytes.Buffer for i := 0; i < len(r.ServerInfo()); i++ { @@ -106,83 +106,84 @@ func formatShortPlainText(r models.ScanResult) string { header, r.Errors) } - if len(cves) == 0 { - return fmt.Sprintf(` -%s -No CVE-IDs are found in updatable packages. -%s -`, header, r.Packages.FormatUpdatablePacksSummary()) - } + //TODO + // if len(cves) == 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 _, 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 + // } - 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, - // } + // 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, + // // } - 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, - } - } + // 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, + // } + // } - cols := make([]interface{}, len(scols)) - for i := range cols { - cols[i] = scols[i] - } - stable.AddRow(cols...) - stable.AddRow("") - } + // 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) } @@ -206,32 +207,34 @@ func formatFullPlainText(r models.ScanResult) string { header, r.Errors) } - 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()) - } + //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()) + // } - scoredReport, unscoredReport := []string{}, []string{} - scoredReport, unscoredReport = formatPlainTextDetails(r, r.Family) + // scoredReport, unscoredReport := []string{}, []string{} + // scoredReport, unscoredReport = formatPlainTextDetails(r, r.Family) - unscored := "" - if !config.Conf.IgnoreUnscoredCves { - unscored = strings.Join(unscoredReport, "\n\n") - } + // unscored := "" + // if !config.Conf.IgnoreUnscoredCves { + // unscored = strings.Join(unscoredReport, "\n\n") + // } - scored := strings.Join(scoredReport, "\n\n") - detail := fmt.Sprintf(` -%s + // scored := strings.Join(scoredReport, "\n\n") + // detail := fmt.Sprintf(` + // %s -%s -`, - scored, - unscored, - ) - return fmt.Sprintf("%s\n%s\n%s", header, detail, formatChangelogs(r)) + // %s + // `, + // scored, + // unscored, + // ) + // return fmt.Sprintf("%s\n%s\n%s", header, detail, formatChangelogs(r)) + return "" } //TODO @@ -266,116 +269,116 @@ func formatPlainTextDetails(r models.ScanResult, osFamily string) (scoredReport, return } -func formatPlainTextUnknownCve(cveInfo models.CveInfo, osFamily string) string { - cveID := cveInfo.VulnInfo.CveID - dtable := uitable.New() - dtable.MaxColWidth = maxColWidth - dtable.Wrap = true - dtable.AddRow(cveID) - dtable.AddRow("-------------") - dtable.AddRow("Score", "?") - dtable.AddRow("NVD", fmt.Sprintf("%s/%s", nvdBaseURL, cveID)) - dlinks := distroLinks(cveInfo, osFamily) - for _, link := range dlinks { - dtable.AddRow(link.title, link.url) - } - dtable.AddRow("CVE Details", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)) - dtable = addPackageInfos(dtable, cveInfo.Packages) - dtable = addCpeNames(dtable, cveInfo.CpeNames) - dtable.AddRow("Confidence", cveInfo.VulnInfo.Confidence) +// func formatPlainTextUnknownCve(cveInfo models.CveInfo, osFamily string) string { +// cveID := cveInfo.VulnInfo.CveID +// dtable := uitable.New() +// dtable.MaxColWidth = maxColWidth +// dtable.Wrap = true +// dtable.AddRow(cveID) +// dtable.AddRow("-------------") +// dtable.AddRow("Score", "?") +// dtable.AddRow("NVD", fmt.Sprintf("%s/%s", nvdBaseURL, cveID)) +// dlinks := distroLinks(cveInfo, osFamily) +// for _, link := range dlinks { +// dtable.AddRow(link.title, link.url) +// } +// dtable.AddRow("CVE Details", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)) +// dtable = addPackageInfos(dtable, cveInfo.Packages) +// dtable = addCpeNames(dtable, cveInfo.CpeNames) +// dtable.AddRow("Confidence", cveInfo.VulnInfo.Confidence) - return fmt.Sprintf("%s", dtable) -} +// return fmt.Sprintf("%s", dtable) +// } //TODO -func formatPlainTextDetailsLangJa(cveInfo models.CveInfo, osFamily string) string { - return "TODO" - // cveDetail := cveInfo.CveDetail - // cveID := cveDetail.CveID - // jvn := cveDetail.Jvn +// func formatPlainTextDetailsLangJa(cveInfo models.CveInfo, osFamily string) string { +// return "TODO" +// cveDetail := cveInfo.CveDetail +// cveID := cveDetail.CveID +// jvn := cveDetail.Jvn - // dtable := uitable.New() - // dtable.MaxColWidth = maxColWidth - // 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(cveDetail.CweID(), cweURL(cveDetail.CweID())) - // dtable.AddRow(cveDetail.CweID()+"(JVN)", cweJvnURL(cveDetail.CweID())) +// dtable := uitable.New() +// dtable.MaxColWidth = maxColWidth +// 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(cveDetail.CweID(), cweURL(cveDetail.CweID())) +// dtable.AddRow(cveDetail.CweID()+"(JVN)", cweJvnURL(cveDetail.CweID())) - // dtable.AddRow("JVN", jvn.Link()) - // dtable.AddRow("NVD", fmt.Sprintf("%s/%s", nvdBaseURL, cveID)) - // dtable.AddRow("MITRE", fmt.Sprintf("%s%s", mitreBaseURL, cveID)) - // dtable.AddRow("CVE Details", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)) - // dtable.AddRow("CVSSv2 Clac", fmt.Sprintf(cvssV2CalcBaseURL, cveID)) - // dtable.AddRow("CVSSv3 Clac", fmt.Sprintf(cvssV3CalcBaseURL, cveID)) +// dtable.AddRow("JVN", jvn.Link()) +// dtable.AddRow("NVD", fmt.Sprintf("%s/%s", nvdBaseURL, cveID)) +// dtable.AddRow("MITRE", fmt.Sprintf("%s%s", mitreBaseURL, cveID)) +// dtable.AddRow("CVE Details", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)) +// dtable.AddRow("CVSSv2 Clac", fmt.Sprintf(cvssV2CalcBaseURL, cveID)) +// dtable.AddRow("CVSSv3 Clac", fmt.Sprintf(cvssV3CalcBaseURL, cveID)) - // dlinks := distroLinks(cveInfo, osFamily) - // for _, link := range dlinks { - // dtable.AddRow(link.title, link.url) - // } +// dlinks := distroLinks(cveInfo, osFamily) +// for _, link := range dlinks { +// dtable.AddRow(link.title, link.url) +// } - // dtable = addPackageInfos(dtable, cveInfo.Packages) - // dtable = addCpeNames(dtable, cveInfo.CpeNames) - // dtable.AddRow("Confidence", cveInfo.VulnInfo.Confidence) +// dtable = addPackageInfos(dtable, cveInfo.Packages) +// dtable = addCpeNames(dtable, cveInfo.CpeNames) +// dtable.AddRow("Confidence", cveInfo.VulnInfo.Confidence) - // return fmt.Sprintf("%s", dtable) -} +// return fmt.Sprintf("%s", dtable) +// } //TODO -func formatPlainTextDetailsLangEn(d models.CveInfo, osFamily string) string { - return "" - // cveDetail := d.CveDetail - // cveID := cveDetail.CveID - // nvd := cveDetail.Nvd +// func formatPlainTextDetailsLangEn(d models.CveInfo, osFamily string) string { +// return "" +// cveDetail := d.CveDetail +// cveID := cveDetail.CveID +// nvd := cveDetail.Nvd - // dtable := uitable.New() - // dtable.MaxColWidth = maxColWidth - // dtable.Wrap = true - // dtable.AddRow(cveID) - // dtable.AddRow("-------------") +// dtable := uitable.New() +// dtable.MaxColWidth = maxColWidth +// 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", "?") - // } +// 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("CWE", cweURL(cveDetail.CweID())) +// dtable.AddRow("Vector", nvd.CvssVector()) +// dtable.AddRow("Summary", nvd.CveSummary()) +// dtable.AddRow("CWE", cweURL(cveDetail.CweID())) - // dtable.AddRow("NVD", fmt.Sprintf("%s/%s", nvdBaseURL, cveID)) - // dtable.AddRow("MITRE", fmt.Sprintf("%s%s", mitreBaseURL, cveID)) - // dtable.AddRow("CVE Details", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)) - // dtable.AddRow("CVSSv2 Clac", fmt.Sprintf(cvssV2CalcBaseURL, cveID)) - // dtable.AddRow("CVSSv3 Clac", fmt.Sprintf(cvssV3CalcBaseURL, cveID)) +// dtable.AddRow("NVD", fmt.Sprintf("%s/%s", nvdBaseURL, cveID)) +// dtable.AddRow("MITRE", fmt.Sprintf("%s%s", mitreBaseURL, cveID)) +// dtable.AddRow("CVE Details", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)) +// dtable.AddRow("CVSSv2 Clac", fmt.Sprintf(cvssV2CalcBaseURL, cveID)) +// dtable.AddRow("CVSSv3 Clac", fmt.Sprintf(cvssV3CalcBaseURL, cveID)) - // links := distroLinks(d, osFamily) - // for _, link := range links { - // dtable.AddRow(link.title, link.url) - // } - // dtable = addPackageInfos(dtable, d.Packages) - // dtable = addCpeNames(dtable, d.CpeNames) - // dtable.AddRow("Confidence", d.VulnInfo.Confidence) +// links := distroLinks(d, osFamily) +// for _, link := range links { +// dtable.AddRow(link.title, link.url) +// } +// dtable = addPackageInfos(dtable, d.Packages) +// dtable = addCpeNames(dtable, d.CpeNames) +// dtable.AddRow("Confidence", d.VulnInfo.Confidence) - // return fmt.Sprintf("%s\n", dtable) -} +// return fmt.Sprintf("%s\n", dtable) +// } type distroLink struct { title string @@ -383,84 +386,84 @@ type distroLink struct { } // distroLinks add Vendor URL of the CVE to table -func distroLinks(cveInfo models.CveInfo, osFamily string) []distroLink { - cveID := cveInfo.VulnInfo.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 "oraclelinux": - links := []distroLink{ - { - "Oracle-CVE", - fmt.Sprintf(oracleSecurityBaseURL, cveID), - }, - } - for _, advisory := range cveInfo.DistroAdvisories { - links = append(links, distroLink{ - // "Oracle-ELSA" - advisory.AdvisoryID, - fmt.Sprintf(oracleELSABaseBaseURL, advisory.AdvisoryID), - }) - } - 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{} - } -} +// func distroLinks(cveInfo models.CveInfo, osFamily string) []distroLink { +// cveID := cveInfo.VulnInfo.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 "oraclelinux": +// links := []distroLink{ +// { +// "Oracle-CVE", +// fmt.Sprintf(oracleSecurityBaseURL, cveID), +// }, +// } +// for _, advisory := range cveInfo.DistroAdvisories { +// links = append(links, distroLink{ +// // "Oracle-ELSA" +// advisory.AdvisoryID, +// fmt.Sprintf(oracleELSABaseBaseURL, advisory.AdvisoryID), +// }) +// } +// 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{} +// } +// } // addPackageInfos add package information related the CVE to table func addPackageInfos(table *uitable.Table, packs []models.PackageInfo) *uitable.Table { diff --git a/scan/base.go b/scan/base.go index 99c4eb40..98e6a3d8 100644 --- a/scan/base.go +++ b/scan/base.go @@ -267,9 +267,11 @@ func (l base) isAwsInstanceID(str string) bool { func (l *base) convertToModel() models.ScanResult { for _, p := range l.VulnInfos { + //TODO sort.Sort(models.PackageInfosByName(p.Packages)) } - sort.Sort(l.VulnInfos) + //TODO + // sort.Sort(l.VulnInfos) ctype := l.ServerInfo.Containers.Type if l.ServerInfo.Container.ContainerID != "" && ctype == "" { diff --git a/util/util.go b/util/util.go index c022c87d..3b94e8a6 100644 --- a/util/util.go +++ b/util/util.go @@ -20,7 +20,6 @@ package util import ( "fmt" "net/url" - "strconv" "strings" "github.com/future-architect/vuls/config" @@ -136,31 +135,3 @@ func Truncate(str string, length int) string { } return str } - -// ParseCvss2 divide CVSSv2 string into score and vector -// 5/AV:N/AC:L/Au:N/C:N/I:N/A:P -func ParseCvss2(scoreVector string) (score float64, vector string) { - var err error - ss := strings.Split(scoreVector, "/") - if 1 < len(ss) { - if score, err = strconv.ParseFloat(ss[0], 64); err != nil { - return 0, "" - } - return score, strings.Join(ss[1:len(ss)], "/") - } - return 0, "" -} - -// 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 ParseCvss3(scoreVector string) (score float64, vector string) { - var err error - ss := strings.Split(scoreVector, "/CVSS:3.0/") - if 1 < len(ss) { - if score, err = strconv.ParseFloat(ss[0], 64); err != nil { - return 0, "" - } - return score, strings.Join(ss[1:len(ss)], "/") - } - return 0, "" -} diff --git a/util/util_test.go b/util/util_test.go index 19ce9f92..49340a3d 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -171,69 +171,3 @@ func TestTruncate(t *testing.T) { } } } - -func TestParseCvss2(t *testing.T) { - type out struct { - score float64 - vector string - } - var tests = []struct { - in string - out out - }{ - { - in: "5/AV:N/AC:L/Au:N/C:N/I:N/A:P", - out: out{ - score: 5.0, - vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", - }, - }, - { - in: "", - out: out{ - score: 0, - vector: "", - }, - }, - } - for _, tt := range tests { - s, v := 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) - } - } -} - -func TestParseCvss3(t *testing.T) { - type out struct { - score float64 - vector string - } - var tests = []struct { - in string - out out - }{ - { - in: "5.6/CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", - out: out{ - score: 5.6, - vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", - }, - }, - { - in: "", - out: out{ - score: 0, - vector: "", - }, - }, - } - for _, tt := range tests { - s, v := 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) - } - } -} From 17a4e532c1c210e439438090e344b87b431d404c Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Thu, 4 May 2017 16:27:00 +0900 Subject: [PATCH 016/113] Fix testcase --- commands/util.go | 36 ++++++------ commands/util_test.go | 127 +++++++++++------------------------------- models/models_test.go | 4 +- oval/redhat.go | 8 ++- report/slack_test.go | 36 ++++++------ 5 files changed, 76 insertions(+), 135 deletions(-) diff --git a/commands/util.go b/commands/util.go index 93b99d76..23f43f0a 100644 --- a/commands/util.go +++ b/commands/util.go @@ -217,37 +217,35 @@ func loadPrevious(current models.ScanResults) (previous models.ScanResults, err return previous, nil } -func diff(current, previous models.ScanResults) (diff models.ScanResults, err error) { - for _, currentResult := range current { +func diff(curResults, preResults models.ScanResults) (diffed models.ScanResults, err error) { + for _, current := range curResults { found := false - var previousResult models.ScanResult - for _, previousResult = range previous { - if currentResult.ServerName == previousResult.ServerName { + var previous models.ScanResult + for _, r := range preResults { + if current.ServerName == r.ServerName { found = true + previous = r break } } if found { - currentResult.ScannedCves = getNewCves(previousResult, currentResult) + new, updated := getDiffCves(previous, current) + current.ScannedCves = append(new, updated...) - //TODO - // currentResult.KnownCves = []models.CveInfo{} - // currentResult.UnknownCves = []models.CveInfo{} - - currentResult.Packages = models.PackageInfoList{} - for _, s := range currentResult.ScannedCves { - currentResult.Packages = append(currentResult.Packages, s.Packages...) + current.Packages = models.PackageInfoList{} + for _, s := range current.ScannedCves { + current.Packages = append(current.Packages, s.Packages...) } - currentResult.Packages = currentResult.Packages.UniqByName() + current.Packages = current.Packages.UniqByName() } - diff = append(diff, currentResult) + diffed = append(diffed, current) } - return diff, err + return diffed, err } -func getNewCves(previous, current models.ScanResult) (newVulninfos []models.VulnInfo) { +func getDiffCves(previous, current models.ScanResult) (new, updated []models.VulnInfo) { previousCveIDsSet := map[string]bool{} for _, previousVulnInfo := range previous.ScannedCves { previousCveIDsSet[previousVulnInfo.CveID] = true @@ -256,10 +254,10 @@ func getNewCves(previous, current models.ScanResult) (newVulninfos []models.Vuln for _, v := range current.ScannedCves { if previousCveIDsSet[v.CveID] { if isCveInfoUpdated(current, previous, v.CveID) { - newVulninfos = append(newVulninfos, v) + updated = append(updated, v) } } else { - newVulninfos = append(newVulninfos, v) + new = append(new, v) } } return diff --git a/commands/util_test.go b/commands/util_test.go index dfa5dc71..e32db3e0 100644 --- a/commands/util_test.go +++ b/commands/util_test.go @@ -18,11 +18,10 @@ along with this program. If not, see . package commands import ( + "reflect" "testing" "time" - "reflect" - "github.com/future-architect/vuls/models" "github.com/k0kubun/pp" ) @@ -36,7 +35,7 @@ func TestDiff(t *testing.T) { out models.ScanResult }{ { - models.ScanResults{ + inCurrent: models.ScanResults{ { ScannedAt: atCurrent, ServerName: "u16", @@ -74,17 +73,12 @@ func TestDiff(t *testing.T) { CpeNames: []string{}, }, }, - KnownCves: []models.CveInfo{}, - UnknownCves: []models.CveInfo{}, - IgnoredCves: []models.CveInfo{}, - - Packages: models.PackageInfoList{}, - + Packages: []models.PackageInfo{}, Errors: []string{}, Optional: [][]interface{}{}, }, }, - models.ScanResults{ + inPrevious: models.ScanResults{ { ScannedAt: atPrevious, ServerName: "u16", @@ -122,33 +116,23 @@ func TestDiff(t *testing.T) { CpeNames: []string{}, }, }, - KnownCves: []models.CveInfo{}, - UnknownCves: []models.CveInfo{}, - IgnoredCves: []models.CveInfo{}, - - Packages: models.PackageInfoList{}, - + Packages: []models.PackageInfo{}, Errors: []string{}, Optional: [][]interface{}{}, }, }, - models.ScanResult{ - ScannedAt: atCurrent, - ServerName: "u16", - Family: "ubuntu", - Release: "16.04", - KnownCves: []models.CveInfo{}, - UnknownCves: []models.CveInfo{}, - IgnoredCves: []models.CveInfo{}, - - // Packages: models.PackageInfoList{}, - - Errors: []string{}, - Optional: [][]interface{}{}, + out: models.ScanResult{ + ScannedAt: atCurrent, + ServerName: "u16", + Family: "ubuntu", + Release: "16.04", + Packages: []models.PackageInfo{}, + Errors: []string{}, + Optional: [][]interface{}{}, }, }, { - models.ScanResults{ + inCurrent: models.ScanResults{ { ScannedAt: atCurrent, ServerName: "u16", @@ -171,66 +155,18 @@ func TestDiff(t *testing.T) { CpeNames: []string{}, }, }, - KnownCves: []models.CveInfo{ - { - CveContents: []models.CveContent{ - { - Type: models.NVD, - CveID: "CVE-2016-6662", - LastModified: time.Date(2016, 1, 1, 0, 0, 0, 0, time.Local), - }, - }, - VulnInfo: models.VulnInfo{ - CveID: "CVE-2016-6662", - }, - }, - }, - UnknownCves: []models.CveInfo{}, - IgnoredCves: []models.CveInfo{}, }, }, - models.ScanResults{ + inPrevious: models.ScanResults{ { - ScannedAt: atPrevious, - ServerName: "u16", - Family: "ubuntu", - Release: "16.04", - ScannedCves: []models.VulnInfo{ - { - CveID: "CVE-2016-6662", - Packages: models.PackageInfoList{ - { - Name: "mysql-libs", - Version: "5.1.73", - Release: "7.el6", - NewVersion: "5.1.73", - NewRelease: "8.el6_8", - Repository: "", - }, - }, - DistroAdvisories: []models.DistroAdvisory{}, - CpeNames: []string{}, - }, - }, - KnownCves: []models.CveInfo{ - { - CveContents: []models.CveContent{ - { - Type: models.NVD, - CveID: "CVE-2016-6662", - LastModified: time.Date(2017, 3, 15, 13, 40, 57, 0, time.Local), - }, - }, - VulnInfo: models.VulnInfo{ - CveID: "CVE-2016-6662", - }, - }, - }, - UnknownCves: []models.CveInfo{}, - IgnoredCves: []models.CveInfo{}, + ScannedAt: atPrevious, + ServerName: "u16", + Family: "ubuntu", + Release: "16.04", + ScannedCves: []models.VulnInfo{}, }, }, - models.ScanResult{ + out: models.ScanResult{ ScannedAt: atCurrent, ServerName: "u16", Family: "ubuntu", @@ -252,9 +188,6 @@ func TestDiff(t *testing.T) { CpeNames: []string{}, }, }, - KnownCves: []models.CveInfo{}, - UnknownCves: []models.CveInfo{}, - IgnoredCves: []models.CveInfo{}, Packages: models.PackageInfoList{ models.PackageInfo{ Name: "mysql-libs", @@ -273,13 +206,21 @@ func TestDiff(t *testing.T) { }, } - for _, tt := range tests { + for i, tt := range tests { diff, _ := diff(tt.inCurrent, tt.inPrevious) for _, actual := range diff { - if !reflect.DeepEqual(actual, tt.out) { - h := pp.Sprint(actual) - x := pp.Sprint(tt.out) - t.Errorf("diff result : \n %s \n output result : \n %s", h, x) + if !reflect.DeepEqual(actual.ScannedCves, tt.out.ScannedCves) { + h := pp.Sprint(actual.ScannedCves) + x := pp.Sprint(tt.out.ScannedCves) + t.Errorf("[%d] actual: \n %s \n expected: \n %s", i, h, x) + } + + for j := range tt.out.Packages { + if !reflect.DeepEqual(tt.out.Packages[j], actual.Packages[j]) { + h := pp.Sprint(tt.out.Packages[j]) + x := pp.Sprint(actual.Packages[j]) + t.Errorf("[%d] actual: \n %s \n expected: \n %s", i, x, h) + } } } } diff --git a/models/models_test.go b/models/models_test.go index 0ef1d40e..9c7dcb34 100644 --- a/models/models_test.go +++ b/models/models_test.go @@ -116,7 +116,7 @@ func TestVulnInfosSetGet(t *testing.T) { // var ps packageCveInfos var ps VulnInfos for _, cid := range test.in { - ps = ps.set(cid, VulnInfo{CveID: cid}) + ps.Upsert(VulnInfo{CveID: cid}) } if len(test.out) != len(ps) { @@ -129,7 +129,7 @@ func TestVulnInfosSetGet(t *testing.T) { } } for _, cid := range test.in { - p, _ := ps.FindByCveID(cid) + p, _ := ps.Get(cid) if p.CveID != cid { t.Errorf("expected %s, actual %s", cid, p.CveID) } diff --git a/oval/redhat.go b/oval/redhat.go index da0ff9fb..b9c5e709 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -61,8 +61,7 @@ func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini ovalContent := *o.convertToModel(cve.CveID, definition) vinfo, ok := r.ScannedCves.Get(cve.CveID) if !ok { - util.Log.Infof("%s is newly detected by OVAL", - definition.Debian.CveID) + util.Log.Infof("%s is newly detected by OVAL", definition.Debian.CveID) vinfo = models.VulnInfo{ CveID: cve.CveID, Confidence: models.OvalMatch, @@ -70,6 +69,11 @@ func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini CveContents: []models.CveContent{ovalContent}, } } else { + if _, ok := vinfo.CveContents.Get(models.RedHat); !ok { + util.Log.Infof("%s is also detected by OVAL", definition.Debian.CveID) + } else { + util.Log.Infof("%s will be updated by OVAL", definition.Debian.CveID) + } if vinfo.Confidence.Score < models.OvalMatch.Score { vinfo.Confidence = models.OvalMatch } diff --git a/report/slack_test.go b/report/slack_test.go index 0eae9031..66e77d98 100644 --- a/report/slack_test.go +++ b/report/slack_test.go @@ -1,23 +1,21 @@ package report -import "testing" +// func TestGetNotifyUsers(t *testing.T) { +// var tests = []struct { +// in []string +// expected string +// }{ +// { +// []string{"@user1", "@user2"}, +// "<@user1> <@user2>", +// }, +// } -func TestGetNotifyUsers(t *testing.T) { - var tests = []struct { - in []string - expected string - }{ - { - []string{"@user1", "@user2"}, - "<@user1> <@user2>", - }, - } +// for _, tt := range tests { +// actual := getNotifyUsers(tt.in) +// if tt.expected != actual { +// t.Errorf("expected %s, actual %s", tt.expected, actual) +// } +// } - for _, tt := range tests { - actual := getNotifyUsers(tt.in) - if tt.expected != actual { - t.Errorf("expected %s, actual %s", tt.expected, actual) - } - } - -} +// } From a2c364f9eb04c7772837530c5793e33e8520a206 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Fri, 5 May 2017 09:50:59 +0900 Subject: [PATCH 017/113] Refacotring --- commands/report.go | 108 ++++++++++++++++++++++++++++++++++++++------- commands/tui.go | 9 ++-- commands/util.go | 34 -------------- models/models.go | 93 +++++++++++++++++++------------------- oval/debian.go | 24 +++++----- oval/oval.go | 2 +- oval/redhat.go | 17 ++++--- report/util.go | 18 +++++--- 8 files changed, 175 insertions(+), 130 deletions(-) diff --git a/commands/report.go b/commands/report.go index 08b3afae..7d963663 100644 --- a/commands/report.go +++ b/commands/report.go @@ -27,10 +27,10 @@ import ( c "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/cveapi" "github.com/future-architect/vuls/models" + "github.com/future-architect/vuls/oval" "github.com/future-architect/vuls/report" "github.com/future-architect/vuls/util" "github.com/google/subcommands" - "github.com/k0kubun/pp" ) // ReportCmd is subcommand for reporting @@ -417,24 +417,22 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} } } - filled, err := fillCveInfoFromOvalDB(&r) - if err != nil { + if err := fillCveInfoFromOvalDB(&r); err != nil { util.Log.Errorf("Failed to fill OVAL information: %s", err) return subcommands.ExitFailure } - filled, err = fillCveInfoFromCveDB(*filled) - if err != nil { + if err := fillCveInfoFromCveDB(&r); err != nil { util.Log.Errorf("Failed to fill CVE information: %s", err) return subcommands.ExitFailure } - filled.Lang = c.Conf.Lang - if err := overwriteJSONFile(dir, *filled); err != nil { + r.Lang = c.Conf.Lang + if err := overwriteJSONFile(dir, r); err != nil { util.Log.Errorf("Failed to write JSON: %s", err) return subcommands.ExitFailure } - results = append(results, *filled) + results = append(results, r) } else { util.Log.Debugf("no need to refresh") results = append(results, r) @@ -455,20 +453,30 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} } results = []models.ScanResult{} for _, r := range diff { - filled, _ := r.FillCveDetail() - results = append(results, *filled) + if err := fillCveDetail(&r); err != nil { + util.Log.Error(err) + return subcommands.ExitFailure + } + results = append(results, r) } } var res models.ScanResults for _, r := range results { //TODO remove - for _, vuln := range r.ScannedCves { - if _, ok := vuln.CveContents.Get(models.CveContentType(r.Family)); !ok { - fmt.Println("not in oval") - pp.Println(vuln) - } - } + // for _, vuln := range r.ScannedCves { + // // if _, ok := vuln.CveContents.Get(models.NewCveContentType(r.Family)); !ok { + // // pp.Printf("not in oval: %s %f\n%v\n", + // // vuln.CveID, vuln.CveContents.CvssV2Score(), vuln.Packages) + // // } else { + // // fmt.Printf(" in oval: %s %f\n", + // // vuln.CveID, vuln.CveContents.CvssV2Score()) + // // } + // // if vuln.CveContents.CvssV2Score() < 0.1 && + // // vuln.CveContents.CvssV3Score() < 0.1 { + // // pp.Println(vuln) + // // } + // } res = append(res, r.FilterByCvssOver()) } @@ -480,3 +488,71 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} } return subcommands.ExitSuccess } + +// fillCveDetail fetches NVD, JVN from CVE Database, and then set to fields. +//TODO rename to FillCveDictionary +func fillCveDetail(r *models.ScanResult) error { + var cveIDs []string + for _, v := range r.ScannedCves { + cveIDs = append(cveIDs, v.CveID) + } + + ds, err := cveapi.CveClient.FetchCveDetails(cveIDs) + if err != nil { + return err + } + for _, d := range ds { + nvd := *r.ConvertNvdToModel(d.CveID, d.Nvd) + jvn := *r.ConvertJvnToModel(d.CveID, d.Jvn) + for i, sc := range r.ScannedCves { + if sc.CveID == d.CveID { + for _, con := range []models.CveContent{nvd, jvn} { + if !con.Empty() { + r.ScannedCves[i].CveContents.Upsert(con) + } + } + break + } + } + } + //TODO sort + // sort.Sort(r.KnownCves) + // sort.Sort(r.UnknownCves) + // sort.Sort(r.IgnoredCves) + return nil +} + +func fillCveInfoFromCveDB(r *models.ScanResult) error { + var err error + var vs []models.VulnInfo + + sInfo := c.Conf.Servers[r.ServerName] + vs, err = scanVulnByCpeNames(sInfo.CpeNames, r.ScannedCves) + if err != nil { + return err + } + r.ScannedCves = vs + if err := fillCveDetail(r); err != nil { + return err + } + return nil +} + +func fillCveInfoFromOvalDB(r *models.ScanResult) error { + var ovalClient oval.Client + switch r.Family { + case "ubuntu", "debian": + ovalClient = oval.NewDebian() + case "rhel", "centos": + ovalClient = oval.NewRedhat() + case "amazon", "oraclelinux", "Raspbian", "FreeBSD": + //TODO implement OracleLinux + return nil + default: + return fmt.Errorf("Oval %s is not implemented yet", r.Family) + } + if err := ovalClient.FillCveInfoFromOvalDB(r); err != nil { + return err + } + return nil +} diff --git a/commands/tui.go b/commands/tui.go index 530b9c32..cbf39a02 100644 --- a/commands/tui.go +++ b/commands/tui.go @@ -169,20 +169,17 @@ func (p *TuiCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s } } - filled, err := fillCveInfoFromCveDB(r) - if err != nil { + if err := fillCveInfoFromCveDB(&r); err != nil { log.Errorf("Failed to fill CVE information: %s", err) return subcommands.ExitFailure } - if err := overwriteJSONFile(jsonDir, *filled); err != nil { + if err := overwriteJSONFile(jsonDir, r); err != nil { log.Errorf("Failed to write JSON: %s", err) return subcommands.ExitFailure } - filledResults = append(filledResults, *filled) - } else { - filledResults = append(filledResults, r) } + filledResults = append(filledResults, r) } return report.RunTui(filledResults) } diff --git a/commands/util.go b/commands/util.go index 23f43f0a..feecb61d 100644 --- a/commands/util.go +++ b/commands/util.go @@ -31,7 +31,6 @@ import ( c "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/cveapi" "github.com/future-architect/vuls/models" - "github.com/future-architect/vuls/oval" "github.com/future-architect/vuls/report" "github.com/future-architect/vuls/util" ) @@ -161,39 +160,6 @@ func loadScanResults(jsonDir string) (results models.ScanResults, err error) { return } -func fillCveInfoFromCveDB(r models.ScanResult) (*models.ScanResult, error) { - var err error - var vs []models.VulnInfo - - sInfo := c.Conf.Servers[r.ServerName] - vs, err = scanVulnByCpeNames(sInfo.CpeNames, r.ScannedCves) - if err != nil { - return nil, err - } - r.ScannedCves = vs - return r.FillCveDetail() -} - -func fillCveInfoFromOvalDB(r *models.ScanResult) (*models.ScanResult, error) { - var ovalClient oval.Client - switch r.Family { - case "ubuntu", "debian": - ovalClient = oval.NewDebian() - case "rhel", "centos": - ovalClient = oval.NewRedhat() - case "amazon", "oraclelinux", "Raspbian", "FreeBSD": - //TODO implement OracleLinux - return r, nil - default: - return nil, fmt.Errorf("Oval %s is not implemented yet", r.Family) - } - result, err := ovalClient.FillCveInfoFromOvalDB(r) - if err != nil { - return nil, err - } - return result, nil -} - func loadPrevious(current models.ScanResults) (previous models.ScanResults, err error) { var dirs jsonDirs if dirs, err = lsValidJSONDirs(); err != nil { diff --git a/models/models.go b/models/models.go index 387b63db..ce208b06 100644 --- a/models/models.go +++ b/models/models.go @@ -24,7 +24,6 @@ import ( "time" "github.com/future-architect/vuls/config" - "github.com/future-architect/vuls/cveapi" cvedict "github.com/kotakanbe/go-cve-dictionary/models" ) @@ -68,40 +67,8 @@ type ScanResult struct { Optional [][]interface{} } -// FillCveDetail fetches NVD, JVN from CVE Database, and then set to fields. -//TODO rename to FillCveDictionary -func (r ScanResult) FillCveDetail() (*ScanResult, error) { - var cveIDs []string - for _, v := range r.ScannedCves { - cveIDs = append(cveIDs, v.CveID) - } - - ds, err := cveapi.CveClient.FetchCveDetails(cveIDs) - if err != nil { - return nil, err - } - for _, d := range ds { - nvd := *r.convertNvdToModel(d.CveID, d.Nvd) - jvn := *r.convertJvnToModel(d.CveID, d.Jvn) - for i, sc := range r.ScannedCves { - if sc.CveID == d.CveID { - for _, con := range []CveContent{nvd, jvn} { - if !con.Empty() { - r.ScannedCves[i].CveContents.Upsert(con) - } - } - break - } - } - } - //TODO sort - // sort.Sort(r.KnownCves) - // sort.Sort(r.UnknownCves) - // sort.Sort(r.IgnoredCves) - return &r, nil -} - -func (r ScanResult) convertNvdToModel(cveID string, nvd cvedict.Nvd) *CveContent { +// ConvertNvdToModel convert NVD to CveContent +func (r ScanResult) ConvertNvdToModel(cveID string, nvd cvedict.Nvd) *CveContent { var cpes []Cpe for _, c := range nvd.Cpes { cpes = append(cpes, Cpe{CpeName: c.CpeName}) @@ -155,7 +122,8 @@ func (r ScanResult) convertNvdToModel(cveID string, nvd cvedict.Nvd) *CveContent } } -func (r ScanResult) convertJvnToModel(cveID string, jvn cvedict.Jvn) *CveContent { +// ConvertJvnToModel convert JVN to CveContent +func (r ScanResult) ConvertJvnToModel(cveID string, jvn cvedict.Jvn) *CveContent { var cpes []Cpe for _, c := range jvn.Cpes { cpes = append(cpes, Cpe{CpeName: c.CpeName}) @@ -269,6 +237,9 @@ func (r ScanResult) CveSummary() string { var high, medium, low, unknown int for _, vInfo := range r.ScannedCves { score := vInfo.CveContents.CvssV2Score() + if score < 0.1 { + score = vInfo.CveContents.CvssV3Score() + } switch { case 7.0 <= score: high++ @@ -356,16 +327,15 @@ var ChangelogLenientMatch = Confidence{50, ChangelogLenientMatchStr} // VulnInfos is VulnInfo list, getter/setter, sortable methods. type VulnInfos []VulnInfo -// FindByCveID find by CVEID -// TODO remove -// func (v *VulnInfos) FindByCveID(cveID string) (VulnInfo, bool) { -// for _, p := range s { -// if cveID == p.CveID { -// return p, true -// } -// } -// return VulnInfo{CveID: cveID}, false -// } +// Find elements that matches the function passed in argument +func (v *VulnInfos) Find(f func(VulnInfo) bool) (filtered VulnInfos) { + for _, vv := range *v { + if f(vv) { + filtered = append(filtered, vv) + } + } + return +} // Get VulnInfo by cveID func (v *VulnInfos) Get(cveID string) (VulnInfo, bool) { @@ -592,6 +562,24 @@ func (v *VulnInfo) NilSliceToEmpty() { // CveContentType is a source of CVE information type CveContentType string +// NewCveContentType create CveContentType +func NewCveContentType(name string) CveContentType { + switch name { + case "nvd": + return NVD + case "jvn": + return JVN + case "redhat", "centos": + return RedHat + case "ubuntu": + return Ubuntu + case "debian": + return Debian + default: + return Unknown + } +} + const ( // NVD is NVD NVD CveContentType = "nvd" @@ -610,6 +598,9 @@ const ( // Ubuntu is Ubuntu Ubuntu CveContentType = "ubuntu" + + // Unknown is Unknown + Unknown CveContentType = "unknown" ) // CveContents has slice of CveContent @@ -671,7 +662,15 @@ func (v *CveContents) CvssV2Score() float64 { } else if cont, found := v.Get(RedHat); found { return cont.Cvss2Score } - return -1 + return -1.1 +} + +// CvssV3Score returns CVSS V2 Score +func (v *CveContents) CvssV3Score() float64 { + if cont, found := v.Get(RedHat); found { + return cont.Cvss3Score + } + return -1.1 } // CveContent has abstraction of various vulnerability information diff --git a/oval/debian.go b/oval/debian.go index 76720e91..864b9f73 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -21,13 +21,14 @@ func NewDebian() Debian { } // FillCveInfoFromOvalDB returns scan result after updating CVE info by OVAL -func (o Debian) FillCveInfoFromOvalDB(r *models.ScanResult) (*models.ScanResult, error) { - util.Log.Debugf("open oval-dictionary db (%s)", config.Conf.OvalDBType) +func (o Debian) 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 nil, fmt.Errorf("Failed to open OVAL DB. err: %s", err) + return fmt.Errorf("Failed to open OVAL DB. err: %s", err) } var d db.OvalDB @@ -40,27 +41,27 @@ func (o Debian) FillCveInfoFromOvalDB(r *models.ScanResult) (*models.ScanResult, for _, pack := range r.Packages { definitions, err := d.GetByPackName(r.Release, pack.Name) if err != nil { - return nil, fmt.Errorf("Failed to get Debian OVAL info by package name: %v", err) + return fmt.Errorf("Failed to get Debian OVAL info by package name: %v", err) } - for _, definition := range definitions { + for _, def := range definitions { current, _ := ver.NewVersion(pack.Version) - for _, p := range definition.AffectedPacks { + for _, p := range def.AffectedPacks { if pack.Name != p.Name { continue } affected, _ := ver.NewVersion(p.Version) if current.LessThan(affected) { - r = o.fillOvalInfo(r, &definition) + o.fillOvalInfo(r, &def) } } } } - return r, nil + return nil } -func (o Debian) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Definition) *models.ScanResult { +func (o Debian) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Definition) { ovalContent := *o.convertToModel(definition) - ovalContent.Type = models.CveContentType(r.Family) + ovalContent.Type = models.NewCveContentType(r.Family) vinfo, ok := r.ScannedCves.Get(definition.Debian.CveID) if !ok { util.Log.Infof("%s is newly detected by OVAL", @@ -72,7 +73,7 @@ func (o Debian) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini CveContents: []models.CveContent{ovalContent}, } } else { - if _, ok := vinfo.CveContents.Get(models.CveContentType(r.Family)); !ok { + if _, ok := vinfo.CveContents.Get(models.NewCveContentType(r.Family)); !ok { util.Log.Infof("%s is also detected by OVAL", definition.Debian.CveID) } else { util.Log.Infof("%s will be updated by OVAL", definition.Debian.CveID) @@ -83,7 +84,6 @@ func (o Debian) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini vinfo.CveContents.Upsert(ovalContent) } r.ScannedCves.Upsert(vinfo) - return r } func (o Debian) convertToModel(def *ovalmodels.Definition) *models.CveContent { diff --git a/oval/oval.go b/oval/oval.go index 0b324da0..410b01e1 100644 --- a/oval/oval.go +++ b/oval/oval.go @@ -7,7 +7,7 @@ import ( // Client is the interface of OVAL client. type Client interface { - FillCveInfoFromOvalDB(r *models.ScanResult) (*models.ScanResult, error) + FillCveInfoFromOvalDB(r *models.ScanResult) error } func getPackageInfoList(r *models.ScanResult, d *ovalmodels.Definition) models.PackageInfoList { diff --git a/oval/redhat.go b/oval/redhat.go index b9c5e709..f59f4d00 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -23,14 +23,14 @@ func NewRedhat() Redhat { } // FillCveInfoFromOvalDB returns scan result after updating CVE info by OVAL -func (o Redhat) FillCveInfoFromOvalDB(r *models.ScanResult) (*models.ScanResult, error) { - util.Log.Debugf("open oval-dictionary db (%s)", config.Conf.OvalDBType) - +func (o Redhat) 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 nil, fmt.Errorf("Failed to open OVAL DB. err: %s", err) + return fmt.Errorf("Failed to open OVAL DB. err: %s", err) } d := db.NewRedHat() @@ -38,7 +38,7 @@ func (o Redhat) FillCveInfoFromOvalDB(r *models.ScanResult) (*models.ScanResult, for _, pack := range r.Packages { definitions, err := d.GetByPackName(r.Release, pack.Name) if err != nil { - return nil, fmt.Errorf("Failed to get RedHat OVAL info by package name: %v", err) + return fmt.Errorf("Failed to get RedHat OVAL info by package name: %v", err) } for _, definition := range definitions { current, _ := ver.NewVersion(fmt.Sprintf("%s-%s", pack.Version, pack.Release)) @@ -48,15 +48,15 @@ func (o Redhat) FillCveInfoFromOvalDB(r *models.ScanResult) (*models.ScanResult, } affected, _ := ver.NewVersion(p.Version) if current.LessThan(affected) { - r = o.fillOvalInfo(r, &definition) + o.fillOvalInfo(r, &definition) } } } } - return r, nil + return nil } -func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Definition) *models.ScanResult { +func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Definition) { for _, cve := range definition.Advisory.Cves { ovalContent := *o.convertToModel(cve.CveID, definition) vinfo, ok := r.ScannedCves.Get(cve.CveID) @@ -81,7 +81,6 @@ func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini } r.ScannedCves.Upsert(vinfo) } - return r } func (o Redhat) convertToModel(cveID string, def *ovalmodels.Definition) *models.CveContent { diff --git a/report/util.go b/report/util.go index e813e676..bfee5d11 100644 --- a/report/util.go +++ b/report/util.go @@ -22,8 +22,10 @@ import ( "fmt" "strings" + "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" "github.com/gosuri/uitable" + "github.com/k0kubun/pp" ) const maxColWidth = 80 @@ -83,11 +85,17 @@ func formatShortPlainText(r models.ScanResult) string { stable.MaxColWidth = maxColWidth stable.Wrap = true - //TODO - // cves := r.KnownCves - // if !config.Conf.IgnoreUnscoredCves { - // cves = append(cves, r.UnknownCves...) - // } + 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() { + return true + } + return false + }) + } + pp.Println(vulns) var buf bytes.Buffer for i := 0; i < len(r.ServerInfo()); i++ { From 509fb045b6e3f3fba5302e178f53226edb20703a Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Fri, 5 May 2017 12:12:05 +0900 Subject: [PATCH 018/113] Refactoring diff logic --- commands/util.go | 48 ++++++------- commands/util_test.go | 156 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 24 deletions(-) diff --git a/commands/util.go b/commands/util.go index feecb61d..c0eaa04e 100644 --- a/commands/util.go +++ b/commands/util.go @@ -219,7 +219,7 @@ func getDiffCves(previous, current models.ScanResult) (new, updated []models.Vul for _, v := range current.ScannedCves { if previousCveIDsSet[v.CveID] { - if isCveInfoUpdated(current, previous, v.CveID) { + if isCveInfoUpdated(v.CveID, previous, current) { updated = append(updated, v) } } else { @@ -229,40 +229,40 @@ func getDiffCves(previous, current models.ScanResult) (new, updated []models.Vul return } -func isCveInfoUpdated(current, previous models.ScanResult, CveID string) bool { - type lastModified struct { - Nvd time.Time - Jvn time.Time +func isCveInfoUpdated(cveID string, previous, current models.ScanResult) bool { + cTypes := []models.CveContentType{ + models.NVD, + models.JVN, + models.NewCveContentType(current.Family), } - //TODO - previousModifies := lastModified{} + prevLastModified := map[models.CveContentType]time.Time{} for _, c := range previous.ScannedCves { - if CveID == c.CveID { - //TODO - if nvd, found := c.CveContents.Get(models.NVD); found { - previousModifies.Nvd = nvd.LastModified - } - if jvn, found := c.CveContents.Get(models.JVN); found { - previousModifies.Jvn = jvn.LastModified + if cveID == c.CveID { + for _, cType := range cTypes { + content, _ := c.CveContents.Get(cType) + prevLastModified[cType] = content.LastModified } + break } } - currentModifies := lastModified{} + curLastModified := map[models.CveContentType]time.Time{} for _, c := range current.ScannedCves { - if CveID == c.CveID { - //TODO - if nvd, found := c.CveContents.Get(models.NVD); found { - previousModifies.Nvd = nvd.LastModified - } - if jvn, found := c.CveContents.Get(models.JVN); found { - previousModifies.Jvn = jvn.LastModified + if cveID == c.CveID { + for _, cType := range cTypes { + content, _ := c.CveContents.Get(cType) + curLastModified[cType] = content.LastModified } + break } } - return !currentModifies.Nvd.Equal(previousModifies.Nvd) || - !currentModifies.Jvn.Equal(previousModifies.Jvn) + for _, cType := range cTypes { + if equal := prevLastModified[cType].Equal(curLastModified[cType]); !equal { + return true + } + } + return false } func overwriteJSONFile(dir string, r models.ScanResult) error { diff --git a/commands/util_test.go b/commands/util_test.go index e32db3e0..0c598a01 100644 --- a/commands/util_test.go +++ b/commands/util_test.go @@ -26,6 +26,162 @@ import ( "github.com/k0kubun/pp" ) +func TestIsCveInfoUpdated(t *testing.T) { + f := "2006-01-02" + old, _ := time.Parse(f, "2015-12-15") + new, _ := time.Parse(f, "2015-12-16") + + type In struct { + cveID string + cur models.ScanResult + prev models.ScanResult + } + var tests = []struct { + in In + expected bool + }{ + // NVD compare non-initialized times + { + in: In{ + cveID: "CVE-2017-0001", + cur: models.ScanResult{ + ScannedCves: []models.VulnInfo{ + { + CveID: "CVE-2017-0001", + CveContents: []models.CveContent{ + { + Type: models.NVD, + CveID: "CVE-2017-0001", + LastModified: time.Time{}, + }, + }, + }, + }, + }, + prev: models.ScanResult{ + ScannedCves: []models.VulnInfo{ + { + CveID: "CVE-2017-0001", + CveContents: []models.CveContent{ + { + Type: models.NVD, + CveID: "CVE-2017-0001", + LastModified: time.Time{}, + }, + }, + }, + }, + }, + }, + expected: false, + }, + // JVN not updated + { + in: In{ + cveID: "CVE-2017-0002", + cur: models.ScanResult{ + ScannedCves: []models.VulnInfo{ + { + CveID: "CVE-2017-0002", + CveContents: []models.CveContent{ + { + Type: models.JVN, + CveID: "CVE-2017-0002", + LastModified: old, + }, + }, + }, + }, + }, + prev: models.ScanResult{ + ScannedCves: []models.VulnInfo{ + { + CveID: "CVE-2017-0002", + CveContents: []models.CveContent{ + { + Type: models.JVN, + CveID: "CVE-2017-0002", + LastModified: old, + }, + }, + }, + }, + }, + }, + expected: false, + }, + // OVAL updated + { + in: In{ + cveID: "CVE-2017-0003", + cur: models.ScanResult{ + Family: "ubuntu", + ScannedCves: []models.VulnInfo{ + { + CveID: "CVE-2017-0003", + CveContents: []models.CveContent{ + { + Type: models.Ubuntu, + CveID: "CVE-2017-0003", + LastModified: new, + }, + }, + }, + }, + }, + prev: models.ScanResult{ + Family: "ubuntu", + ScannedCves: []models.VulnInfo{ + { + CveID: "CVE-2017-0003", + CveContents: []models.CveContent{ + { + Type: models.Ubuntu, + CveID: "CVE-2017-0003", + LastModified: old, + }, + }, + }, + }, + }, + }, + expected: true, + }, + // OVAL newly detected + { + in: In{ + cveID: "CVE-2017-0004", + cur: models.ScanResult{ + Family: "redhat", + ScannedCves: []models.VulnInfo{ + { + CveID: "CVE-2017-0004", + CveContents: []models.CveContent{ + { + Type: models.RedHat, + CveID: "CVE-2017-0004", + LastModified: old, + }, + }, + }, + }, + }, + prev: models.ScanResult{ + Family: "redhat", + ScannedCves: []models.VulnInfo{}, + }, + }, + expected: true, + }, + } + for i, tt := range tests { + actual := isCveInfoUpdated(tt.in.cveID, tt.in.prev, tt.in.cur) + if actual != tt.expected { + t.Errorf("[%d] actual: %t, expected: %t", i, actual, tt.expected) + } + } +} + func TestDiff(t *testing.T) { atCurrent, _ := time.Parse("2006-01-02", "2014-12-31") atPrevious, _ := time.Parse("2006-01-02", "2014-11-31") From 2e37d3adc18c4d3d64b95b2bed27af50a7edbba8 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Fri, 5 May 2017 13:14:21 +0900 Subject: [PATCH 019/113] Improve sort logics --- commands/report.go | 19 +- commands/util.go | 15 +- .../owasp-dependency-check/parser/parser.go | 10 +- cveapi/cve_client.go | 8 +- models/models.go | 202 ++---------------- scan/base.go | 16 +- scan/redhat.go | 5 +- 7 files changed, 54 insertions(+), 221 deletions(-) diff --git a/commands/report.go b/commands/report.go index 7d963663..3c77f30f 100644 --- a/commands/report.go +++ b/commands/report.go @@ -463,7 +463,11 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} var res models.ScanResults for _, r := range results { - //TODO remove + res = append(res, r.FilterByCvssOver()) + + // TODO Add sort function to ScanResults + + //remove // for _, vuln := range r.ScannedCves { // // if _, ok := vuln.CveContents.Get(models.NewCveContentType(r.Family)); !ok { // // pp.Printf("not in oval: %s %f\n%v\n", @@ -477,7 +481,6 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} // // pp.Println(vuln) // // } // } - res = append(res, r.FilterByCvssOver()) } for _, w := range reports { @@ -490,7 +493,6 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} } // fillCveDetail fetches NVD, JVN from CVE Database, and then set to fields. -//TODO rename to FillCveDictionary func fillCveDetail(r *models.ScanResult) error { var cveIDs []string for _, v := range r.ScannedCves { @@ -515,10 +517,13 @@ func fillCveDetail(r *models.ScanResult) error { } } } - //TODO sort - // sort.Sort(r.KnownCves) - // sort.Sort(r.UnknownCves) - // sort.Sort(r.IgnoredCves) + //TODO Remove + // sort.Slice(r.ScannedCves, func(i, j int) bool { + // if r.ScannedCves[j].CveContents.CvssV2Score() == r.ScannedCves[i].CveContents.CvssV2Score() { + // return r.ScannedCves[j].CveContents.CvssV2Score() < r.ScannedCves[i].CveContents.CvssV2Score() + // } + // return r.ScannedCves[j].CveContents.CvssV2Score() < r.ScannedCves[i].CveContents.CvssV2Score() + // }) return nil } diff --git a/commands/util.go b/commands/util.go index c0eaa04e..cea5989f 100644 --- a/commands/util.go +++ b/commands/util.go @@ -44,17 +44,6 @@ var jsonDirPattern = regexp.MustCompile( // JSONDirs is array of json files path. type jsonDirs []string -// sort as recent directories are at the head -func (d jsonDirs) Len() int { - return len(d) -} -func (d jsonDirs) Swap(i, j int) { - d[i], d[j] = d[j], d[i] -} -func (d jsonDirs) Less(i, j int) bool { - return d[j] < d[i] -} - // getValidJSONDirs return valid json directory as array // Returned array is sorted so that recent directories are at the head func lsValidJSONDirs() (dirs jsonDirs, err error) { @@ -69,7 +58,9 @@ func lsValidJSONDirs() (dirs jsonDirs, err error) { dirs = append(dirs, jsonDir) } } - sort.Sort(dirs) + sort.Slice(dirs, func(i, j int) bool { + return dirs[j] < dirs[i] + }) return } diff --git a/contrib/owasp-dependency-check/parser/parser.go b/contrib/owasp-dependency-check/parser/parser.go index c0e8e818..b39dec94 100644 --- a/contrib/owasp-dependency-check/parser/parser.go +++ b/contrib/owasp-dependency-check/parser/parser.go @@ -5,7 +5,6 @@ import ( "fmt" "io/ioutil" "os" - "sort" "strings" ) @@ -35,18 +34,18 @@ func appendIfMissing(slice []string, str string) []string { func Parse(path string) ([]string, error) { file, err := os.Open(path) if err != nil { - return []string{}, fmt.Errorf("Failed to open: %s", err) + return nil, fmt.Errorf("Failed to open: %s", err) } defer file.Close() b, err := ioutil.ReadAll(file) if err != nil { - return []string{}, fmt.Errorf("Failed to read: %s", err) + return nil, fmt.Errorf("Failed to read: %s", err) } var anal analysis if err := xml.Unmarshal(b, &anal); err != nil { - fmt.Errorf("Failed to unmarshal: %s", err) + return nil, fmt.Errorf("Failed to unmarshal: %s", err) } cpes := []string{} @@ -59,6 +58,7 @@ func Parse(path string) ([]string, error) { } } } - sort.Strings(cpes) + //TODO remove + // sort.Strings(cpes) return cpes, nil } diff --git a/cveapi/cve_client.go b/cveapi/cve_client.go index af219238..08a73164 100644 --- a/cveapi/cve_client.go +++ b/cveapi/cve_client.go @@ -21,7 +21,6 @@ import ( "encoding/json" "fmt" "net/http" - "sort" "time" "github.com/cenkalti/backoff" @@ -69,7 +68,6 @@ type response struct { CveDetail cve.CveDetail } -//TODO rename to FetchCveDictionary func (api cvedictClient) FetchCveDetails(cveIDs []string) (cveDetails cve.CveDetails, err error) { switch config.Conf.CveDBType { case "sqlite3", "mysql", "postgres": @@ -130,7 +128,8 @@ func (api cvedictClient) FetchCveDetails(cveIDs []string) (cveDetails cve.CveDet fmt.Errorf("Failed to fetch CVE. err: %v", errs) } - sort.Sort(cveDetails) + //TODO + // sort.Sort(cveDetails) return } @@ -158,8 +157,9 @@ func (api cvedictClient) FetchCveDetailsFromCveDB(cveIDs []string) (cveDetails c } } + //TODO // order by CVE ID desc - sort.Sort(cveDetails) + // sort.Sort(cveDetails) return } diff --git a/models/models.go b/models/models.go index ce208b06..726435c2 100644 --- a/models/models.go +++ b/models/models.go @@ -19,7 +19,6 @@ package models import ( "fmt" - "sort" "strings" "time" @@ -30,23 +29,24 @@ import ( // ScanResults is slice of ScanResult. type ScanResults []ScanResult -// Len implement Sort Interface -func (s ScanResults) Len() int { - return len(s) -} +//TODO +// // Len implement Sort Interface +// func (s ScanResults) Len() int { +// return len(s) +// } -// Swap implement Sort Interface -func (s ScanResults) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} +// // Swap implement Sort Interface +// func (s ScanResults) Swap(i, j int) { +// s[i], s[j] = s[j], s[i] +// } -// Less implement Sort Interface -func (s ScanResults) Less(i, j int) bool { - if s[i].ServerName == s[j].ServerName { - return s[i].Container.ContainerID < s[i].Container.ContainerID - } - return s[i].ServerName < s[j].ServerName -} +// // Less implement Sort Interface +// func (s ScanResults) Less(i, j int) bool { +// if s[i].ServerName == s[j].ServerName { +// return s[i].Container.ContainerID < s[i].Container.ContainerID +// } +// return s[i].ServerName < s[j].ServerName +// } // ScanResult has the result of scanned CVE information. type ScanResult struct { @@ -260,15 +260,6 @@ func (r ScanResult) CveSummary() string { high+medium+low+unknown, high, medium, low, unknown) } -// NWLink has network link information. -//TODO remove -// type NWLink struct { -// IPAddress string -// Netmask string -// DevName string -// LinkState string -// } - // Confidence is a ranking how confident the CVE-ID was deteted correctly // Score: 0 - 100 type Confidence struct { @@ -382,33 +373,6 @@ func (v *VulnInfos) Upsert(vInfo VulnInfo) { } } -// immutable -// func (v *VulnInfos) set(cveID string, v VulnInfo) VulnInfos { -// for i, p := range s { -// if cveID == p.CveID { -// s[i] = v -// return s -// } -// } -// return append(s, v) -// } - -//TODO GO 1.8 -// Len implement Sort Interface -// func (s VulnInfos) Len() int { -// return len(s) -// } - -// // Swap implement Sort Interface -// func (s VulnInfos) Swap(i, j int) { -// s[i], s[j] = s[j], s[i] -// } - -// // Less implement Sort Interface -// func (s VulnInfos) Less(i, j int) bool { -// return s[i].CveID < s[j].CveID -// } - // VulnInfo holds a vulnerability information and unsecure packages type VulnInfo struct { CveID string @@ -432,133 +396,6 @@ func (v *VulnInfo) NilSliceToEmpty() { } } -// CveInfos is for sorting -// type CveInfos []CveInfo - -// func (c CveInfos) Len() int { -// return len(c) -// } - -// func (c CveInfos) Swap(i, j int) { -// c[i], c[j] = c[j], c[i] -// } - -// func (c CveInfos) Less(i, j int) bool { -// if c[i].CvssV2Score() == c[j].CvssV2Score() { -// return c[i].CveID < c[j].CveID -// } -// return c[j].CvssV2Score() < c[i].CvssV2Score() -// } - -// // Get cveInfo by cveID -// func (c CveInfos) Get(cveID string) (CveInfo, bool) { -// for _, cve := range c { -// if cve.VulnInfo.CveID == cveID { -// return cve, true -// } -// } -// return CveInfo{}, false -// } - -// // Delete by cveID -// func (c *CveInfos) Delete(cveID string) { -// cveInfos := *c -// for i, cve := range cveInfos { -// if cve.VulnInfo.CveID == cveID { -// *c = append(cveInfos[:i], cveInfos[i+1:]...) -// break -// } -// } -// } - -// // Insert cveInfo -// func (c *CveInfos) Insert(cveInfo CveInfo) { -// *c = append(*c, cveInfo) -// } - -// // Update cveInfo -// func (c CveInfos) Update(cveInfo CveInfo) (ok bool) { -// for i, cve := range c { -// if cve.VulnInfo.CveID == cveInfo.VulnInfo.CveID { -// c[i] = cveInfo -// return true -// } -// } -// return false -// } - -// // Upsert cveInfo -// func (c *CveInfos) Upsert(cveInfo CveInfo) { -// ok := c.Update(cveInfo) -// if !ok { -// c.Insert(cveInfo) -// } -// } - -//TODO -// CveInfo has CVE detailed Information. -// type CveInfo struct { -// VulnInfo -// CveContents []CveContent -// } - -// Get a CveContent specified by arg -// func (c *CveInfo) Get(typestr CveContentType) (*CveContent, bool) { -// for _, cont := range c.CveContents { -// if cont.Type == typestr { -// return &cont, true -// } -// } -// return &CveContent{}, false -// } - -// // Insert a CveContent to specified by arg -// func (c *CveInfo) Insert(con CveContent) { -// c.CveContents = append(c.CveContents, con) -// } - -// // Update a CveContent to specified by arg -// func (c *CveInfo) Update(to CveContent) bool { -// for i, cont := range c.CveContents { -// if cont.Type == to.Type { -// c.CveContents[i] = to -// return true -// } -// } -// return false -// } - -// // CvssV2Score returns CVSS V2 Score -// func (c *CveInfo) CvssV2Score() float64 { -// //TODO -// if cont, found := c.Get(NVD); found { -// return cont.Cvss2Score -// } else if cont, found := c.Get(JVN); found { -// return cont.Cvss2Score -// } else if cont, found := c.Get(RedHat); found { -// return cont.Cvss2Score -// } -// return -1 -// } - -// // NilSliceToEmpty set nil slice fields to empty slice to avoid null in JSON -// func (c *CveInfo) NilSliceToEmpty() { -// return -// // TODO -// // if c.CveDetail.Nvd.Cpes == nil { -// // c.CveDetail.Nvd.Cpes = []cve.Cpe{} -// // } -// // if c.CveDetail.Jvn.Cpes == nil { -// // c.CveDetail.Jvn.Cpes = []cve.Cpe{} -// // } -// // if c.CveDetail.Nvd.References == nil { -// // c.CveDetail.Nvd.References = []cve.Reference{} -// // } -// // if c.CveDetail.Jvn.References == nil { -// // c.CveDetail.Jvn.References = []cve.Reference{} -// // } -// } - // CveContentType is a source of CVE information type CveContentType string @@ -732,7 +569,8 @@ func (ps PackageInfoList) UniqByName() (distincted PackageInfoList) { for key := range set { keys = append(keys, key) } - sort.Strings(keys) + //TODO remove + // sort.Strings(keys) for _, key := range keys { distincted = append(distincted, set[key]) } @@ -800,10 +638,6 @@ func (ps PackageInfoList) FormatUpdatablePacksSummary() string { // the Name field. type PackageInfosByName []PackageInfo -func (a PackageInfosByName) Len() int { return len(a) } -func (a PackageInfosByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a PackageInfosByName) Less(i, j int) bool { return a[i].Name < a[j].Name } - // PackageInfo has installed packages. type PackageInfo struct { Name string diff --git a/scan/base.go b/scan/base.go index 98e6a3d8..01351585 100644 --- a/scan/base.go +++ b/scan/base.go @@ -20,7 +20,6 @@ package scan import ( "fmt" "regexp" - "sort" "strings" "time" @@ -266,12 +265,15 @@ func (l base) isAwsInstanceID(str string) bool { } func (l *base) convertToModel() models.ScanResult { - for _, p := range l.VulnInfos { - //TODO - sort.Sort(models.PackageInfosByName(p.Packages)) - } - //TODO - // sort.Sort(l.VulnInfos) + //TODO Remove + // for _, p := range l.VulnInfos { + // sort.Slice(p.Packages, func(i, j int) bool { + // return p.Packages[i].Name < p.Packages[j].Name + // }) + // } + // sort.Slice(l.VulnInfos, func(i, j int) bool { + // return l.VulnInfos[i].CveID < l.VulnInfos[j].CveID + // }) ctype := l.ServerInfo.Containers.Type if l.ServerInfo.Container.ContainerID != "" && ctype == "" { diff --git a/scan/redhat.go b/scan/redhat.go index 053e16fa..cbbc5dc9 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -20,7 +20,6 @@ package scan import ( "fmt" "regexp" - "sort" "strings" "time" @@ -770,7 +769,9 @@ func (o *redhat) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveID for cveID := range cveIDsSetInThisSection { foundCveIDs = append(foundCveIDs, cveID) } - sort.Strings(foundCveIDs) + //TODO remove + // sort.Strings(foundCveIDs) + result = append(result, distroAdvisoryCveIDs{ DistroAdvisory: advisory, CveIDs: foundCveIDs, From 209ca704de5c911a117f00a5e0dd5d6a9b9c371a Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Fri, 5 May 2017 15:25:07 +0900 Subject: [PATCH 020/113] Fixed a bug caused by capturing epoch number on RedHat.go --- scan/redhat.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scan/redhat.go b/scan/redhat.go index cbbc5dc9..ae7924ef 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -577,8 +577,17 @@ func (o *redhat) divideChangelogByPackage(allChangelog string) (map[string]*stri writePointer = pNewString for _, rpm := range rpms { rpm = strings.TrimSpace(rpm) - rpm = o.regexpReplace(rpm, `^[0-9]+:`, "") rpm = o.regexpReplace(rpm, `\.(i386|i486|i586|i686|k6|athlon|x86_64|noarch|ppc|alpha|sparc)$`, "") + if ss := strings.Split(rpm, ":"); 1 < len(ss) { + epoch := ss[0] + packVersion := strings.Join(ss[1:len(ss)], ":") + if sss := strings.Split(packVersion, "-"); 2 < len(sss) { + version := strings.Join(sss[len(sss)-2:len(sss)], "-") + name := strings.Join(sss[0:len(sss)-2], "-") + rpm = fmt.Sprintf("%s-%s:%s", name, epoch, version) + } + } + rpm2changelog[rpm] = pNewString } } else { From 12c2d3cbc67b42d2d3c1c566f9ca76487d087919 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Sat, 6 May 2017 03:30:01 +0900 Subject: [PATCH 021/113] Fix test cases --- models/models.go | 11 ++--------- models/models_test.go | 9 ++++++++- scan/redhat_test.go | 11 +++++++---- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/models/models.go b/models/models.go index 726435c2..c7aa23aa 100644 --- a/models/models.go +++ b/models/models.go @@ -564,15 +564,8 @@ func (ps PackageInfoList) UniqByName() (distincted PackageInfoList) { for _, p := range ps { set[p.Name] = p } - //sort by key - keys := []string{} - for key := range set { - keys = append(keys, key) - } - //TODO remove - // sort.Strings(keys) - for _, key := range keys { - distincted = append(distincted, set[key]) + for _, v := range set { + distincted = append(distincted, v) } return } diff --git a/models/models_test.go b/models/models_test.go index 9c7dcb34..206d89e4 100644 --- a/models/models_test.go +++ b/models/models_test.go @@ -19,6 +19,7 @@ package models import ( "reflect" + "sort" "testing" "github.com/k0kubun/pp" @@ -51,8 +52,14 @@ func TestPackageInfoListUniqByName(t *testing.T) { } actual := test.in.UniqByName() + sort.Slice(actual, func(i, j int) bool { + return actual[i].Name < actual[j].Name + }) + sort.Slice(test.out, func(i, j int) bool { + return test.out[i].Name < test.out[j].Name + }) for i, ePack := range test.out { - if actual[i].Name == ePack.Name { + if actual[i].Name != ePack.Name { t.Errorf("expected %#v, actual %#v", ePack.Name, actual[i].Name) } } diff --git a/scan/redhat_test.go b/scan/redhat_test.go index 9eb78133..d63b7ef6 100644 --- a/scan/redhat_test.go +++ b/scan/redhat_test.go @@ -19,6 +19,7 @@ package scan import ( "reflect" + "sort" "testing" "time" @@ -653,6 +654,8 @@ Description : Package updates are available for Amazon Linux AMI that fix the for _, tt := range tests { actual, _ := r.parseYumUpdateinfo(tt.in) for i, advisoryCveIDs := range actual { + sort.Strings(tt.out[i].CveIDs) + sort.Strings(actual[i].CveIDs) if !reflect.DeepEqual(tt.out[i], advisoryCveIDs) { e := pp.Sprintf("%v", tt.out[i]) a := pp.Sprintf("%v", advisoryCveIDs) @@ -1115,7 +1118,7 @@ func TestGetChangelogCVELines(t *testing.T) { { models.PackageInfo{ Name: "dhclient", - NewVersion: "4.1.1", + NewVersion: "12:4.1.1", NewRelease: "51.P1.el6.centos", }, `- TESTSTRING CVE-1111-1111 @@ -1124,7 +1127,7 @@ func TestGetChangelogCVELines(t *testing.T) { { models.PackageInfo{ Name: "dhcp-common", - NewVersion: "4.1.1", + NewVersion: "12:4.1.1", NewRelease: "51.P1.el6.centos", }, `- TESTSTRING CVE-1111-1111 @@ -1234,7 +1237,7 @@ func TestGetChangelogCVELines(t *testing.T) { { models.PackageInfo{ Name: "bind-libs", - NewVersion: "9.3.6", + NewVersion: "30:9.3.6", NewRelease: "25.P1.el5_11.8", }, `- Fix issue with patch for CVE-2016-1285 and CVE-2016-1286 found by test suite @@ -1246,7 +1249,7 @@ func TestGetChangelogCVELines(t *testing.T) { { models.PackageInfo{ Name: "bind-utils", - NewVersion: "9.3.6", + NewVersion: "30:9.3.6", NewRelease: "25.P1.el5_11.8", }, `- Fix issue with patch for CVE-2016-1285 and CVE-2016-1286 found by test suite From f26b61d77353a6ec4b097935c799c3f825f8a0f2 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Sat, 6 May 2017 04:52:19 +0900 Subject: [PATCH 022/113] Change CveContents data type to map --- commands/util.go | 2 +- commands/util_test.go | 58 +++++++++++++++++++++---------------------- models/models.go | 58 ++++++++++++++++++++++--------------------- oval/debian.go | 2 +- oval/redhat.go | 2 +- scan/base.go | 2 +- scan/executil.go | 4 ++- 7 files changed, 66 insertions(+), 62 deletions(-) diff --git a/commands/util.go b/commands/util.go index cea5989f..8a17f39a 100644 --- a/commands/util.go +++ b/commands/util.go @@ -295,7 +295,7 @@ func scanVulnByCpeNames(cpeNames []string, scannedVulns []models.VulnInfo) ([]mo CpeNames: []string{name}, Confidence: models.CpeNameMatch, } - v.NilSliceToEmpty() + v.NilToEmpty() set[detail.CveID] = v } } diff --git a/commands/util_test.go b/commands/util_test.go index 0c598a01..a461428b 100644 --- a/commands/util_test.go +++ b/commands/util_test.go @@ -48,13 +48,13 @@ func TestIsCveInfoUpdated(t *testing.T) { ScannedCves: []models.VulnInfo{ { CveID: "CVE-2017-0001", - CveContents: []models.CveContent{ - { + CveContents: models.NewCveContents( + models.CveContent{ Type: models.NVD, CveID: "CVE-2017-0001", LastModified: time.Time{}, }, - }, + ), }, }, }, @@ -62,13 +62,13 @@ func TestIsCveInfoUpdated(t *testing.T) { ScannedCves: []models.VulnInfo{ { CveID: "CVE-2017-0001", - CveContents: []models.CveContent{ - { + CveContents: models.NewCveContents( + models.CveContent{ Type: models.NVD, CveID: "CVE-2017-0001", LastModified: time.Time{}, }, - }, + ), }, }, }, @@ -83,13 +83,13 @@ func TestIsCveInfoUpdated(t *testing.T) { ScannedCves: []models.VulnInfo{ { CveID: "CVE-2017-0002", - CveContents: []models.CveContent{ - { - Type: models.JVN, + CveContents: models.NewCveContents( + models.CveContent{ + Type: models.NVD, CveID: "CVE-2017-0002", LastModified: old, }, - }, + ), }, }, }, @@ -97,13 +97,13 @@ func TestIsCveInfoUpdated(t *testing.T) { ScannedCves: []models.VulnInfo{ { CveID: "CVE-2017-0002", - CveContents: []models.CveContent{ - { - Type: models.JVN, + CveContents: models.NewCveContents( + models.CveContent{ + Type: models.NVD, CveID: "CVE-2017-0002", LastModified: old, }, - }, + ), }, }, }, @@ -119,13 +119,13 @@ func TestIsCveInfoUpdated(t *testing.T) { ScannedCves: []models.VulnInfo{ { CveID: "CVE-2017-0003", - CveContents: []models.CveContent{ - { - Type: models.Ubuntu, - CveID: "CVE-2017-0003", + CveContents: models.NewCveContents( + models.CveContent{ + Type: models.NVD, + CveID: "CVE-2017-0002", LastModified: new, }, - }, + ), }, }, }, @@ -134,13 +134,13 @@ func TestIsCveInfoUpdated(t *testing.T) { ScannedCves: []models.VulnInfo{ { CveID: "CVE-2017-0003", - CveContents: []models.CveContent{ - { - Type: models.Ubuntu, - CveID: "CVE-2017-0003", + CveContents: models.NewCveContents( + models.CveContent{ + Type: models.NVD, + CveID: "CVE-2017-0002", LastModified: old, }, - }, + ), }, }, }, @@ -156,13 +156,13 @@ func TestIsCveInfoUpdated(t *testing.T) { ScannedCves: []models.VulnInfo{ { CveID: "CVE-2017-0004", - CveContents: []models.CveContent{ - { - Type: models.RedHat, - CveID: "CVE-2017-0004", + CveContents: models.NewCveContents( + models.CveContent{ + Type: models.NVD, + CveID: "CVE-2017-0002", LastModified: old, }, - }, + ), }, }, }, diff --git a/models/models.go b/models/models.go index c7aa23aa..55489b3f 100644 --- a/models/models.go +++ b/models/models.go @@ -383,8 +383,8 @@ type VulnInfo struct { CveContents CveContents } -// NilSliceToEmpty set nil slice fields to empty slice to avoid null in JSON -func (v *VulnInfo) NilSliceToEmpty() { +// 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{} } @@ -394,6 +394,9 @@ func (v *VulnInfo) NilSliceToEmpty() { if v.Packages == nil { v.Packages = PackageInfoList{} } + if v.CveContents == nil { + v.CveContents = NewCveContents() + } } // CveContentType is a source of CVE information @@ -440,49 +443,48 @@ const ( Unknown CveContentType = "unknown" ) -// CveContents has slice of CveContent -type CveContents []CveContent +// CveContents has CveContent +type CveContents map[CveContentType]CveContent + +// NewCveContents create CveContents +func NewCveContents(conts ...CveContent) CveContents { + m := make(map[CveContentType]CveContent) + for _, cont := range conts { + m[cont.Type] = cont + } + return m +} // Get CveContent by cveID // TODO Pointer -func (v *CveContents) Get(typestr CveContentType) (CveContent, bool) { - for _, vv := range *v { - if vv.Type == typestr { - return vv, true - } +func (v CveContents) Get(typestr CveContentType) (CveContent, bool) { + if vv, ok := v[typestr]; ok { + return vv, true } return CveContent{}, false } // Delete by cveID -func (v *CveContents) Delete(typestr CveContentType) { - cveContents := *v - for i, cc := range cveContents { - if cc.Type == typestr { - *v = append(cveContents[:i], cveContents[i+1:]...) - break - } - } +func (v CveContents) Delete(typestr CveContentType) { + delete(v, typestr) } // Insert CveContent -func (v *CveContents) Insert(cont CveContent) { - *v = append(*v, cont) +func (v CveContents) Insert(cont CveContent) { + v[cont.Type] = cont } // Update VulnInfo -func (v *CveContents) Update(cont CveContent) (ok bool) { - for i, vv := range *v { - if vv.Type == cont.Type { - (*v)[i] = cont - return true - } +func (v CveContents) Update(cont CveContent) (ok bool) { + if _, ok := v[cont.Type]; ok { + v[cont.Type] = cont + return true } return false } // Upsert CveContent -func (v *CveContents) Upsert(cont CveContent) { +func (v CveContents) Upsert(cont CveContent) { ok := v.Update(cont) if !ok { v.Insert(cont) @@ -490,7 +492,7 @@ func (v *CveContents) Upsert(cont CveContent) { } // CvssV2Score returns CVSS V2 Score -func (v *CveContents) CvssV2Score() float64 { +func (v CveContents) CvssV2Score() float64 { //TODO if cont, found := v.Get(NVD); found { return cont.Cvss2Score @@ -503,7 +505,7 @@ func (v *CveContents) CvssV2Score() float64 { } // CvssV3Score returns CVSS V2 Score -func (v *CveContents) CvssV3Score() float64 { +func (v CveContents) CvssV3Score() float64 { if cont, found := v.Get(RedHat); found { return cont.Cvss3Score } diff --git a/oval/debian.go b/oval/debian.go index 864b9f73..1bbd0877 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -70,7 +70,7 @@ func (o Debian) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini CveID: definition.Debian.CveID, Confidence: models.OvalMatch, Packages: getPackageInfoList(r, definition), - CveContents: []models.CveContent{ovalContent}, + CveContents: models.NewCveContents(ovalContent), } } else { if _, ok := vinfo.CveContents.Get(models.NewCveContentType(r.Family)); !ok { diff --git a/oval/redhat.go b/oval/redhat.go index f59f4d00..58f0dd8f 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -66,7 +66,7 @@ func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini CveID: cve.CveID, Confidence: models.OvalMatch, Packages: getPackageInfoList(r, definition), - CveContents: []models.CveContent{ovalContent}, + CveContents: models.NewCveContents(ovalContent), } } else { if _, ok := vinfo.CveContents.Get(models.RedHat); !ok { diff --git a/scan/base.go b/scan/base.go index 01351585..b2a2da1d 100644 --- a/scan/base.go +++ b/scan/base.go @@ -293,7 +293,7 @@ func (l *base) convertToModel() models.ScanResult { // Avoid null slice being null in JSON for i := range l.VulnInfos { - l.VulnInfos[i].NilSliceToEmpty() + l.VulnInfos[i].NilToEmpty() } return models.ScanResult{ diff --git a/scan/executil.go b/scan/executil.go index 81b45452..e8215272 100644 --- a/scan/executil.go +++ b/scan/executil.go @@ -148,6 +148,9 @@ func parallelExec(fn func(osTypeInterface) error, timeoutSec ...int) { } func exec(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (result execResult) { + logger := getSSHLogger(log...) + logger.Debugf("Executing... %s", strings.Replace(cmd, "\n", "", -1)) + if c.Port == "local" && (c.Host == "127.0.0.1" || c.Host == "localhost") { result = localExec(c, cmd, sudo) @@ -157,7 +160,6 @@ func exec(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (resul result = sshExecExternal(c, cmd, sudo) } - logger := getSSHLogger(log...) logger.Debug(result) return } From d626cc8a8be0e71d20a6d67e46ff798c7c59a808 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Sat, 6 May 2017 04:59:12 +0900 Subject: [PATCH 023/113] Rename PackageInfoList to Packages --- cache/bolt_test.go | 2 +- cache/db.go | 6 +-- commands/util.go | 2 +- commands/util_test.go | 22 +++++----- models/models.go | 57 ++++++++----------------- models/models_test.go | 22 +++++----- oval/debian.go | 2 +- oval/oval.go | 8 ++-- oval/redhat.go | 2 +- report/util.go | 6 +-- scan/debian.go | 34 +++++++-------- scan/debian_test.go | 4 +- scan/freebsd.go | 14 +++---- scan/freebsd_test.go | 4 +- scan/redhat.go | 98 +++++++++++++++++++++---------------------- scan/redhat_test.go | 70 +++++++++++++++---------------- scan/serverapi.go | 4 +- 17 files changed, 168 insertions(+), 189 deletions(-) diff --git a/cache/bolt_test.go b/cache/bolt_test.go index ea2a31b4..d13dd947 100644 --- a/cache/bolt_test.go +++ b/cache/bolt_test.go @@ -37,7 +37,7 @@ var meta = Meta{ Family: "ubuntu", Release: "16.04", }, - Packs: []models.PackageInfo{ + Packs: []models.Package{ { Name: "apt", Version: "1", diff --git a/cache/db.go b/cache/db.go index 30299390..ded0668f 100644 --- a/cache/db.go +++ b/cache/db.go @@ -45,12 +45,12 @@ type Cache interface { type Meta struct { Name string Distro config.Distro - Packs []models.PackageInfo + Packs []models.Package CreatedAt time.Time } -// FindPack search a PackageInfo -func (m Meta) FindPack(name string) (pack models.PackageInfo, found bool) { +// FindPack search a Package +func (m Meta) FindPack(name string) (pack models.Package, found bool) { for _, p := range m.Packs { if name == p.Name { return p, true diff --git a/commands/util.go b/commands/util.go index 8a17f39a..3d2b583b 100644 --- a/commands/util.go +++ b/commands/util.go @@ -190,7 +190,7 @@ func diff(curResults, preResults models.ScanResults) (diffed models.ScanResults, new, updated := getDiffCves(previous, current) current.ScannedCves = append(new, updated...) - current.Packages = models.PackageInfoList{} + current.Packages = models.Packages{} for _, s := range current.ScannedCves { current.Packages = append(current.Packages, s.Packages...) } diff --git a/commands/util_test.go b/commands/util_test.go index a461428b..ee47986e 100644 --- a/commands/util_test.go +++ b/commands/util_test.go @@ -200,7 +200,7 @@ func TestDiff(t *testing.T) { ScannedCves: []models.VulnInfo{ { CveID: "CVE-2012-6702", - Packages: models.PackageInfoList{ + Packages: models.Packages{ { Name: "libexpat1", Version: "2.1.0-7", @@ -215,7 +215,7 @@ func TestDiff(t *testing.T) { }, { CveID: "CVE-2014-9761", - Packages: models.PackageInfoList{ + Packages: models.Packages{ { Name: "libc-bin", Version: "2.21-0ubuntu5", @@ -229,7 +229,7 @@ func TestDiff(t *testing.T) { CpeNames: []string{}, }, }, - Packages: []models.PackageInfo{}, + Packages: []models.Package{}, Errors: []string{}, Optional: [][]interface{}{}, }, @@ -243,7 +243,7 @@ func TestDiff(t *testing.T) { ScannedCves: []models.VulnInfo{ { CveID: "CVE-2012-6702", - Packages: models.PackageInfoList{ + Packages: models.Packages{ { Name: "libexpat1", Version: "2.1.0-7", @@ -258,7 +258,7 @@ func TestDiff(t *testing.T) { }, { CveID: "CVE-2014-9761", - Packages: models.PackageInfoList{ + Packages: models.Packages{ { Name: "libc-bin", Version: "2.21-0ubuntu5", @@ -272,7 +272,7 @@ func TestDiff(t *testing.T) { CpeNames: []string{}, }, }, - Packages: []models.PackageInfo{}, + Packages: []models.Package{}, Errors: []string{}, Optional: [][]interface{}{}, }, @@ -282,7 +282,7 @@ func TestDiff(t *testing.T) { ServerName: "u16", Family: "ubuntu", Release: "16.04", - Packages: []models.PackageInfo{}, + Packages: []models.Package{}, Errors: []string{}, Optional: [][]interface{}{}, }, @@ -297,7 +297,7 @@ func TestDiff(t *testing.T) { ScannedCves: []models.VulnInfo{ { CveID: "CVE-2016-6662", - Packages: models.PackageInfoList{ + Packages: models.Packages{ { Name: "mysql-libs", Version: "5.1.73", @@ -330,7 +330,7 @@ func TestDiff(t *testing.T) { ScannedCves: []models.VulnInfo{ { CveID: "CVE-2016-6662", - Packages: models.PackageInfoList{ + Packages: models.Packages{ { Name: "mysql-libs", Version: "5.1.73", @@ -344,8 +344,8 @@ func TestDiff(t *testing.T) { CpeNames: []string{}, }, }, - Packages: models.PackageInfoList{ - models.PackageInfo{ + Packages: models.Packages{ + models.Package{ Name: "mysql-libs", Version: "5.1.73", Release: "7.el6", diff --git a/models/models.go b/models/models.go index 55489b3f..ead677bc 100644 --- a/models/models.go +++ b/models/models.go @@ -62,7 +62,7 @@ type ScanResult struct { // Scanned Vulns by SSH scan + CPE + OVAL ScannedCves VulnInfos - Packages PackageInfoList + Packages Packages Errors []string Optional [][]interface{} } @@ -377,7 +377,7 @@ func (v *VulnInfos) Upsert(vInfo VulnInfo) { type VulnInfo struct { CveID string Confidence Confidence - Packages PackageInfoList + Packages Packages DistroAdvisories []DistroAdvisory // for Aamazon, RHEL, FreeBSD CpeNames []string CveContents CveContents @@ -392,7 +392,7 @@ func (v *VulnInfo) NilToEmpty() { v.DistroAdvisories = []DistroAdvisory{} } if v.Packages == nil { - v.Packages = PackageInfoList{} + v.Packages = Packages{} } if v.CveContents == nil { v.CveContents = NewCveContents() @@ -547,11 +547,11 @@ type Reference struct { Link string } -// PackageInfoList is slice of PackageInfo -type PackageInfoList []PackageInfo +// Packages is slice of Package +type Packages []Package // Exists returns true if exists the name -func (ps PackageInfoList) Exists(name string) bool { +func (ps Packages) Exists(name string) bool { for _, p := range ps { if p.Name == name { return true @@ -561,8 +561,8 @@ func (ps PackageInfoList) Exists(name string) bool { } // UniqByName be uniq by name. -func (ps PackageInfoList) UniqByName() (distincted PackageInfoList) { - set := make(map[string]PackageInfo) +func (ps Packages) UniqByName() (distincted Packages) { + set := make(map[string]Package) for _, p := range ps { set[p.Name] = p } @@ -572,18 +572,18 @@ func (ps PackageInfoList) UniqByName() (distincted PackageInfoList) { return } -// FindByName search PackageInfo by name -func (ps PackageInfoList) FindByName(name string) (result PackageInfo, found bool) { +// FindByName search Package by name +func (ps Packages) FindByName(name string) (result Package, found bool) { for _, p := range ps { if p.Name == name { return p, true } } - return PackageInfo{}, false + return Package{}, false } // MergeNewVersion merges candidate version information to the receiver struct -func (ps PackageInfoList) MergeNewVersion(as PackageInfoList) { +func (ps Packages) MergeNewVersion(as Packages) { for _, a := range as { for i, p := range ps { if p.Name == a.Name { @@ -594,7 +594,7 @@ func (ps PackageInfoList) MergeNewVersion(as PackageInfoList) { } } -func (ps PackageInfoList) countUpdatablePacks() int { +func (ps Packages) countUpdatablePacks() int { count := 0 set := make(map[string]bool) for _, p := range ps { @@ -607,34 +607,13 @@ func (ps PackageInfoList) countUpdatablePacks() int { } // FormatUpdatablePacksSummary returns a summary of updatable packages -func (ps PackageInfoList) FormatUpdatablePacksSummary() string { +func (ps Packages) FormatUpdatablePacksSummary() string { return fmt.Sprintf("%d updatable packages", ps.countUpdatablePacks()) } -// Find search PackageInfo by name-version-release -// func (ps PackageInfoList) find(nameVersionRelease string) (PackageInfo, bool) { -// for _, p := range ps { -// joined := p.Name -// if 0 < len(p.Version) { -// joined = fmt.Sprintf("%s-%s", joined, p.Version) -// } -// if 0 < len(p.Release) { -// joined = fmt.Sprintf("%s-%s", joined, p.Release) -// } -// if joined == nameVersionRelease { -// return p, true -// } -// } -// return PackageInfo{}, false -// } - -// PackageInfosByName implements sort.Interface for []PackageInfo based on -// the Name field. -type PackageInfosByName []PackageInfo - -// PackageInfo has installed packages. -type PackageInfo struct { +// Package has installed packages. +type Package struct { Name string Version string Release string @@ -653,7 +632,7 @@ type Changelog struct { } // FormatCurrentVer returns package name-version-release -func (p PackageInfo) FormatCurrentVer() string { +func (p Package) FormatCurrentVer() string { str := p.Name if 0 < len(p.Version) { str = fmt.Sprintf("%s-%s", str, p.Version) @@ -665,7 +644,7 @@ func (p PackageInfo) FormatCurrentVer() string { } // FormatNewVer returns package name-version-release -func (p PackageInfo) FormatNewVer() string { +func (p Package) FormatNewVer() string { str := p.Name if 0 < len(p.NewVersion) { str = fmt.Sprintf("%s-%s", str, p.NewVersion) diff --git a/models/models_test.go b/models/models_test.go index 206d89e4..2626d7ab 100644 --- a/models/models_test.go +++ b/models/models_test.go @@ -25,12 +25,12 @@ import ( "github.com/k0kubun/pp" ) -func TestPackageInfoListUniqByName(t *testing.T) { +func TestPackagesUniqByName(t *testing.T) { var test = struct { - in PackageInfoList - out PackageInfoList + in Packages + out Packages }{ - PackageInfoList{ + Packages{ { Name: "hoge", }, @@ -41,7 +41,7 @@ func TestPackageInfoListUniqByName(t *testing.T) { Name: "hoge", }, }, - PackageInfoList{ + Packages{ { Name: "hoge", }, @@ -67,23 +67,23 @@ func TestPackageInfoListUniqByName(t *testing.T) { func TestMergeNewVersion(t *testing.T) { var test = struct { - a PackageInfoList - b PackageInfoList - expected PackageInfoList + a Packages + b Packages + expected Packages }{ - PackageInfoList{ + Packages{ { Name: "hoge", }, }, - PackageInfoList{ + Packages{ { Name: "hoge", NewVersion: "1.0.0", NewRelease: "release1", }, }, - PackageInfoList{ + Packages{ { Name: "hoge", NewVersion: "1.0.0", diff --git a/oval/debian.go b/oval/debian.go index 1bbd0877..c2b298d6 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -69,7 +69,7 @@ func (o Debian) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini vinfo = models.VulnInfo{ CveID: definition.Debian.CveID, Confidence: models.OvalMatch, - Packages: getPackageInfoList(r, definition), + Packages: getPackages(r, definition), CveContents: models.NewCveContents(ovalContent), } } else { diff --git a/oval/oval.go b/oval/oval.go index 410b01e1..d386b6b3 100644 --- a/oval/oval.go +++ b/oval/oval.go @@ -10,16 +10,16 @@ type Client interface { FillCveInfoFromOvalDB(r *models.ScanResult) error } -func getPackageInfoList(r *models.ScanResult, d *ovalmodels.Definition) models.PackageInfoList { - var packageInfoList models.PackageInfoList +func getPackages(r *models.ScanResult, d *ovalmodels.Definition) models.Packages { + var packages models.Packages for _, pack := range d.AffectedPacks { for _, p := range r.Packages { if pack.Name == p.Name { p.Changelog = models.Changelog{} - packageInfoList = append(packageInfoList, p) + packages = append(packages, p) break } } } - return packageInfoList + return packages } diff --git a/oval/redhat.go b/oval/redhat.go index 58f0dd8f..295d1746 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -65,7 +65,7 @@ func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini vinfo = models.VulnInfo{ CveID: cve.CveID, Confidence: models.OvalMatch, - Packages: getPackageInfoList(r, definition), + Packages: getPackages(r, definition), CveContents: models.NewCveContents(ovalContent), } } else { diff --git a/report/util.go b/report/util.go index bfee5d11..f33e71e4 100644 --- a/report/util.go +++ b/report/util.go @@ -473,8 +473,8 @@ type distroLink struct { // } // } -// addPackageInfos add package information related the CVE to table -func addPackageInfos(table *uitable.Table, packs []models.PackageInfo) *uitable.Table { +// addPackages add package information related the CVE to table +func addPackages(table *uitable.Table, packs []models.Package) *uitable.Table { for i, p := range packs { var title string if i == 0 { @@ -515,7 +515,7 @@ func formatChangelogs(r models.ScanResult) string { return strings.Join(buf, "\n") } -func formatOneChangelog(p models.PackageInfo) string { +func formatOneChangelog(p models.Package) string { buf := []string{} if p.NewVersion == "" { return "" diff --git a/scan/debian.go b/scan/debian.go index 7ff27c26..d9843cb6 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -181,7 +181,7 @@ func (o *debian) scanPackages() error { return nil } -func (o *debian) scanInstalledPackages() (installed models.PackageInfoList, upgradable models.PackageInfoList, err error) { +func (o *debian) scanInstalledPackages() (installed models.Packages, upgradable models.Packages, err error) { r := o.exec("dpkg-query -W", noSudo) if !r.isSuccess() { return nil, nil, fmt.Errorf("Failed to SSH: %s", r) @@ -198,7 +198,7 @@ func (o *debian) scanInstalledPackages() (installed models.PackageInfoList, upgr return nil, nil, fmt.Errorf( "Debian: Failed to parse package line: %s", line) } - installed = append(installed, models.PackageInfo{ + installed = append(installed, models.Package{ Name: name, Version: version, }) @@ -254,7 +254,7 @@ func (o *debian) aptGetUpdate() error { return nil } -func (o *debian) scanUnsecurePackages(upgradable []models.PackageInfo) ([]models.VulnInfo, error) { +func (o *debian) scanUnsecurePackages(upgradable []models.Package) ([]models.VulnInfo, error) { o.aptGetUpdate() @@ -315,7 +315,7 @@ func (o *debian) ensureChangelogCache(current cache.Meta) (*cache.Meta, error) { return &cached, nil } -func (o *debian) fillCandidateVersion(before models.PackageInfoList) (filled []models.PackageInfo, err error) { +func (o *debian) fillCandidateVersion(before models.Packages) (filled []models.Package, err error) { names := []string{} for _, p := range before { names = append(names, p.Name) @@ -394,13 +394,13 @@ func (o *debian) parseAptGetUpgrade(stdout string) (upgradableNames []string, er return } -func (o *debian) scanVulnInfos(upgradablePacks []models.PackageInfo, meta *cache.Meta) (models.VulnInfos, error) { +func (o *debian) scanVulnInfos(upgradablePacks []models.Package, meta *cache.Meta) (models.VulnInfos, error) { resChan := make(chan struct { - models.PackageInfo + models.Package DetectedCveIDs }, len(upgradablePacks)) errChan := make(chan error, len(upgradablePacks)) - reqChan := make(chan models.PackageInfo, len(upgradablePacks)) + reqChan := make(chan models.Package, len(upgradablePacks)) defer close(resChan) defer close(errChan) defer close(reqChan) @@ -418,12 +418,12 @@ func (o *debian) scanVulnInfos(upgradablePacks []models.PackageInfo, meta *cache tasks <- func() { select { case pack := <-reqChan: - func(p models.PackageInfo) { + func(p models.Package) { changelog := o.getChangelogCache(meta, p) if 0 < len(changelog) { cveIDs, _ := o.getCveIDsFromChangelog(changelog, p.Name, p.Version) resChan <- struct { - models.PackageInfo + models.Package DetectedCveIDs }{p, cveIDs} return @@ -436,7 +436,7 @@ func (o *debian) scanVulnInfos(upgradablePacks []models.PackageInfo, meta *cache errChan <- err } else { resChan <- struct { - models.PackageInfo + models.Package DetectedCveIDs }{p, cveIDs} } @@ -445,19 +445,19 @@ func (o *debian) scanVulnInfos(upgradablePacks []models.PackageInfo, meta *cache } } - // { DetectedCveID{} : [packageInfo] } - cvePackages := make(map[DetectedCveID][]models.PackageInfo) + // { DetectedCveID{} : [package] } + cvePackages := make(map[DetectedCveID][]models.Package) errs := []error{} for i := 0; i < len(upgradablePacks); i++ { select { case pair := <-resChan: - pack := pair.PackageInfo + pack := pair.Package cveIDs := pair.DetectedCveIDs for _, cveID := range cveIDs { cvePackages[cveID] = appendPackIfMissing(cvePackages[cveID], pack) } o.log.Infof("(%d/%d) Scanned %s-%s : %s", - i+1, len(upgradablePacks), pair.Name, pair.PackageInfo.Version, cveIDs) + i+1, len(upgradablePacks), pair.Name, pair.Package.Version, cveIDs) case err := <-errChan: errs = append(errs, err) case <-timeout: @@ -491,7 +491,7 @@ func (o *debian) scanVulnInfos(upgradablePacks []models.PackageInfo, meta *cache return vinfos, nil } -func (o *debian) getChangelogCache(meta *cache.Meta, pack models.PackageInfo) string { +func (o *debian) getChangelogCache(meta *cache.Meta, pack models.Package) string { cachedPack, found := meta.FindPack(pack.Name) if !found { o.log.Debugf("Not found: %s", pack.Name) @@ -519,7 +519,7 @@ func (o *debian) getChangelogCache(meta *cache.Meta, pack models.PackageInfo) st return changelog } -func (o *debian) scanPackageCveIDs(pack models.PackageInfo) ([]DetectedCveID, error) { +func (o *debian) scanPackageCveIDs(pack models.Package) ([]DetectedCveID, error) { cmd := "" switch o.Distro.Family { case "ubuntu", "raspbian": @@ -730,7 +730,7 @@ func (o *debian) parseAptCachePolicy(stdout, name string) (packCandidateVer, err return ver, fmt.Errorf("Unknown Format: %s", stdout) } -func appendPackIfMissing(slice []models.PackageInfo, s models.PackageInfo) []models.PackageInfo { +func appendPackIfMissing(slice []models.Package, s models.Package) []models.Package { for _, ele := range slice { if ele.Name == s.Name && ele.Version == s.Version && diff --git a/scan/debian_test.go b/scan/debian_test.go index f27597de..6a5f81c6 100644 --- a/scan/debian_test.go +++ b/scan/debian_test.go @@ -613,7 +613,7 @@ Calculating upgrade... Done func TestGetChangelogCache(t *testing.T) { const servername = "server1" - pack := models.PackageInfo{ + pack := models.Package{ Name: "apt", Version: "1.0.0", NewVersion: "1.0.1", @@ -624,7 +624,7 @@ func TestGetChangelogCache(t *testing.T) { Family: "ubuntu", Release: "16.04", }, - Packs: []models.PackageInfo{pack}, + Packs: []models.Package{pack}, } const path = "/tmp/vuls-test-cache-11111111.db" diff --git a/scan/freebsd.go b/scan/freebsd.go index f8712ef6..817e9bd1 100644 --- a/scan/freebsd.go +++ b/scan/freebsd.go @@ -71,7 +71,7 @@ func (o *bsd) checkDependencies() error { func (o *bsd) scanPackages() error { var err error - var packs []models.PackageInfo + var packs []models.Package if packs, err = o.scanInstalledPackages(); err != nil { o.log.Errorf("Failed to scan installed packages") return err @@ -87,7 +87,7 @@ func (o *bsd) scanPackages() error { return nil } -func (o *bsd) scanInstalledPackages() ([]models.PackageInfo, error) { +func (o *bsd) scanInstalledPackages() ([]models.Package, error) { cmd := util.PrependProxyEnv("pkg version -v") r := o.exec(cmd, noSudo) if !r.isSuccess() { @@ -143,7 +143,7 @@ func (o *bsd) scanUnsecurePackages() (vulnInfos []models.VulnInfo, err error) { } for k := range cveIDAdtMap { - packs := []models.PackageInfo{} + packs := []models.Package{} for _, r := range cveIDAdtMap[k] { packs = append(packs, r.pack) } @@ -165,7 +165,7 @@ func (o *bsd) scanUnsecurePackages() (vulnInfos []models.VulnInfo, err error) { return } -func (o *bsd) parsePkgVersion(stdout string) (packs []models.PackageInfo) { +func (o *bsd) parsePkgVersion(stdout string) (packs []models.Package) { lines := strings.Split(stdout, "\n") for _, l := range lines { fields := strings.Fields(l) @@ -180,13 +180,13 @@ func (o *bsd) parsePkgVersion(stdout string) (packs []models.PackageInfo) { switch fields[1] { case "?", "=": - packs = append(packs, models.PackageInfo{ + packs = append(packs, models.Package{ Name: name, Version: ver, }) case "<": candidate := strings.TrimSuffix(fields[6], ")") - packs = append(packs, models.PackageInfo{ + packs = append(packs, models.Package{ Name: name, Version: ver, NewVersion: candidate, @@ -202,7 +202,7 @@ type vulnIDCveIDs struct { } type pkgAuditResult struct { - pack models.PackageInfo + pack models.Package vulnIDCveIDs vulnIDCveIDs } diff --git a/scan/freebsd_test.go b/scan/freebsd_test.go index 48dd62b1..ee2cdc81 100644 --- a/scan/freebsd_test.go +++ b/scan/freebsd_test.go @@ -12,7 +12,7 @@ import ( func TestParsePkgVersion(t *testing.T) { var tests = []struct { in string - expected []models.PackageInfo + expected []models.Package }{ { `Updating FreeBSD repository catalogue... @@ -23,7 +23,7 @@ gettext-0.18.3.1 < needs updating (remote has 0.19.7) tcl84-8.4.20_2,1 = up-to-date with remote teTeX-base-3.0_25 ? orphaned: print/teTeX-base`, - []models.PackageInfo{ + []models.Package{ { Name: "bash", Version: "4.2.45", diff --git a/scan/redhat.go b/scan/redhat.go index ae7924ef..0d49a71a 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -226,7 +226,7 @@ func (o *redhat) checkDependencies() error { func (o *redhat) scanPackages() error { var err error - var packs []models.PackageInfo + var packs []models.Package if packs, err = o.scanInstalledPackages(); err != nil { o.log.Errorf("Failed to scan installed packages") return err @@ -242,7 +242,7 @@ func (o *redhat) scanPackages() error { return nil } -func (o *redhat) scanInstalledPackages() (installedPackages models.PackageInfoList, err error) { +func (o *redhat) scanInstalledPackages() (installedPackages models.Packages, err error) { cmd := "rpm -qa --queryformat '%{NAME}\t%{EPOCHNUM}\t%{VERSION}\t%{RELEASE}\n'" r := o.exec(cmd, noSudo) if r.isSuccess() { @@ -251,11 +251,11 @@ func (o *redhat) scanInstalledPackages() (installedPackages models.PackageInfoLi lines := strings.Split(r.Stdout, "\n") for _, line := range lines { if trimed := strings.TrimSpace(line); len(trimed) != 0 { - var packinfo models.PackageInfo - if packinfo, err = o.parseScannedPackagesLine(line); err != nil { + var pack models.Package + if pack, err = o.parseScannedPackagesLine(line); err != nil { return } - installedPackages = append(installedPackages, packinfo) + installedPackages = append(installedPackages, pack) } } return @@ -266,10 +266,10 @@ func (o *redhat) scanInstalledPackages() (installedPackages models.PackageInfoLi r.ExitStatus, r.Stdout, r.Stderr) } -func (o *redhat) parseScannedPackagesLine(line string) (models.PackageInfo, error) { +func (o *redhat) parseScannedPackagesLine(line string) (models.Package, error) { fields := strings.Fields(line) if len(fields) != 4 { - return models.PackageInfo{}, + return models.Package{}, fmt.Errorf("Failed to parse package line: %s", line) } ver := "" @@ -278,7 +278,7 @@ func (o *redhat) parseScannedPackagesLine(line string) (models.PackageInfo, erro } else { ver = fmt.Sprintf("%s:%s", fields[1], fields[2]) } - return models.PackageInfo{ + return models.Package{ Name: fields[0], Version: ver, Release: fields[3], @@ -312,22 +312,22 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er } // get Updateble package name, installed, candidate version. - packInfoList, err := o.parseYumCheckUpdateLines(r.Stdout) + packages, err := o.parseYumCheckUpdateLines(r.Stdout) if err != nil { return nil, fmt.Errorf("Failed to parse %s. err: %s", cmd, err) } - o.log.Debugf("%s", pp.Sprintf("%v", packInfoList)) + o.log.Debugf("%s", pp.Sprintf("%v", packages)) // set candidate version info - o.Packages.MergeNewVersion(packInfoList) + o.Packages.MergeNewVersion(packages) // Collect CVE-IDs in changelog - type PackInfoCveIDs struct { - PackInfo models.PackageInfo - CveIDs []string + type PackageCveIDs struct { + Package models.Package + CveIDs []string } - allChangelog, err := o.getAllChangelog(packInfoList) + allChangelog, err := o.getAllChangelog(packages) if err != nil { o.log.Errorf("Failed to getAllchangelog. err: %s", err) return nil, err @@ -354,9 +354,9 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er } } - var results []PackInfoCveIDs - for i, packInfo := range packInfoList { - changelog := o.getChangelogCVELines(rpm2changelog, packInfo) + var results []PackageCveIDs + for i, pack := range packages { + changelog := o.getChangelogCVELines(rpm2changelog, pack) // Collect unique set of CVE-ID in each changelog uniqueCveIDMap := make(map[string]bool) @@ -373,20 +373,20 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er for k := range uniqueCveIDMap { cveIDs = append(cveIDs, k) } - p := PackInfoCveIDs{ - PackInfo: packInfo, - CveIDs: cveIDs, + p := PackageCveIDs{ + Package: pack, + CveIDs: cveIDs, } results = append(results, p) o.log.Infof("(%d/%d) Scanned %s-%s-%s -> %s-%s : %s", i+1, - len(packInfoList), - p.PackInfo.Name, - p.PackInfo.Version, - p.PackInfo.Release, - p.PackInfo.NewVersion, - p.PackInfo.NewRelease, + len(packages), + p.Package.Name, + p.Package.Version, + p.Package.Release, + p.Package.NewVersion, + p.Package.NewRelease, p.CveIDs) } @@ -394,24 +394,24 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er // - From // [ // { - // PackInfo: models.PackageInfo, + // Pack: models.Packages, // CveIDs: []string, // }, // ] // - To // map { - // CveID: []models.PackageInfo + // CveID: []models.Package // } - cveIDPackInfoMap := make(map[string][]models.PackageInfo) + cveIDPackMap := make(map[string][]models.Package) for _, res := range results { for _, cveID := range res.CveIDs { - cveIDPackInfoMap[cveID] = append( - cveIDPackInfoMap[cveID], res.PackInfo) + cveIDPackMap[cveID] = append( + cveIDPackMap[cveID], res.Package) } } vinfos := []models.VulnInfo{} - for k, v := range cveIDPackInfoMap { + for k, v := range cveIDPackMap { // Amazon, RHEL do not use this method, so VendorAdvisory do not set. vinfos = append(vinfos, models.VulnInfo{ CveID: k, @@ -423,7 +423,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er } // parseYumCheckUpdateLines parse yum check-update to get package name, candidate version -func (o *redhat) parseYumCheckUpdateLines(stdout string) (results models.PackageInfoList, err error) { +func (o *redhat) parseYumCheckUpdateLines(stdout string) (results models.Packages, err error) { needToParse := false lines := strings.Split(stdout, "\n") for _, line := range lines { @@ -459,10 +459,10 @@ func (o *redhat) parseYumCheckUpdateLines(stdout string) (results models.Package return } -func (o *redhat) parseYumCheckUpdateLine(line string) (models.PackageInfo, error) { +func (o *redhat) parseYumCheckUpdateLine(line string) (models.Package, error) { fields := strings.Fields(line) if len(fields) < 3 { - return models.PackageInfo{}, fmt.Errorf("Unknown format: %s", line) + return models.Package{}, fmt.Errorf("Unknown format: %s", line) } splitted := strings.Split(fields[0], ".") packName := "" @@ -474,12 +474,12 @@ func (o *redhat) parseYumCheckUpdateLine(line string) (models.PackageInfo, error verfields := strings.Split(fields[1], "-") if len(verfields) != 2 { - return models.PackageInfo{}, fmt.Errorf("Unknown format: %s", line) + return models.Package{}, fmt.Errorf("Unknown format: %s", line) } release := verfields[1] repos := strings.Join(fields[2:len(fields)], " ") - return models.PackageInfo{ + return models.Package{ Name: packName, NewVersion: verfields[0], NewRelease: release, @@ -499,8 +499,8 @@ func (o *redhat) regexpReplace(src string, pat string, rep string) string { var changeLogCVEPattern = regexp.MustCompile(`CVE-[0-9]+-[0-9]+`) -func (o *redhat) getChangelogCVELines(rpm2changelog map[string]*string, packInfo models.PackageInfo) string { - rpm := fmt.Sprintf("%s-%s-%s", packInfo.Name, packInfo.NewVersion, packInfo.NewRelease) +func (o *redhat) getChangelogCVELines(rpm2changelog map[string]*string, pack models.Package) string { + rpm := fmt.Sprintf("%s-%s-%s", pack.Name, pack.NewVersion, pack.NewRelease) retLine := "" if rpm2changelog[rpm] != nil { lines := strings.Split(*rpm2changelog[rpm], "\n") @@ -601,10 +601,10 @@ func (o *redhat) divideChangelogByPackage(allChangelog string) (map[string]*stri } // CentOS -func (o *redhat) getAllChangelog(packInfoList models.PackageInfoList) (stdout string, err error) { +func (o *redhat) getAllChangelog(packages models.Packages) (stdout string, err error) { packageNames := "" - for _, packInfo := range packInfoList { - packageNames += fmt.Sprintf("%s ", packInfo.Name) + for _, pack := range packages { + packageNames += fmt.Sprintf("%s ", pack.Name) } command := "" @@ -686,19 +686,19 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, // set candidate version info o.Packages.MergeNewVersion(updatable) - dict := map[string][]models.PackageInfo{} + dict := map[string][]models.Package{} for _, advIDPackNames := range advIDPackNamesList { - packInfoList := models.PackageInfoList{} + packages := models.Packages{} for _, packName := range advIDPackNames.PackNames { - packInfo, found := updatable.FindByName(packName) + pack, found := updatable.FindByName(packName) if !found { return nil, fmt.Errorf( - "PackInfo not found. packInfo: %#v", packName) + "Package not found. pack: %#v", packName) } - packInfoList = append(packInfoList, packInfo) + packages = append(packages, pack) continue } - dict[advIDPackNames.AdvisoryID] = packInfoList + dict[advIDPackNames.AdvisoryID] = packages } // get advisoryID(RHSA, ALAS, ELSA) - CVE IDs diff --git a/scan/redhat_test.go b/scan/redhat_test.go index d63b7ef6..01fd70c1 100644 --- a/scan/redhat_test.go +++ b/scan/redhat_test.go @@ -39,11 +39,11 @@ func TestParseScanedPackagesLineRedhat(t *testing.T) { var packagetests = []struct { in string - pack models.PackageInfo + pack models.Package }{ { "openssl 0 1.0.1e 30.el6.11", - models.PackageInfo{ + models.Package{ Name: "openssl", Version: "1.0.1e", Release: "30.el6.11", @@ -51,7 +51,7 @@ func TestParseScanedPackagesLineRedhat(t *testing.T) { }, { "Percona-Server-shared-56 1 5.6.19 rel67.0.el6", - models.PackageInfo{ + models.Package{ Name: "Percona-Server-shared-56", Version: "1:5.6.19", Release: "rel67.0.el6", @@ -686,7 +686,7 @@ bind-utils.x86_64 30:9.3.6-25.P1.el5_11.8 updates pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5 ` - r.Packages = []models.PackageInfo{ + r.Packages = []models.Package{ { Name: "audit-libs", Version: "2.3.6", @@ -720,11 +720,11 @@ pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5 } var tests = []struct { in string - out models.PackageInfoList + out models.Packages }{ { stdout, - models.PackageInfoList{ + models.Packages{ { Name: "audit-libs", Version: "2.3.6", @@ -778,15 +778,15 @@ pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5 } for _, tt := range tests { - packInfoList, err := r.parseYumCheckUpdateLines(tt.in) + packages, err := r.parseYumCheckUpdateLines(tt.in) if err != nil { t.Errorf("Error has occurred, err: %s\ntt.in: %v", err, tt.in) return } - for i, ePackInfo := range tt.out { - if !reflect.DeepEqual(ePackInfo, packInfoList[i]) { - e := pp.Sprintf("%v", ePackInfo) - a := pp.Sprintf("%v", packInfoList[i]) + for i, ePack := range tt.out { + if !reflect.DeepEqual(ePack, packages[i]) { + e := pp.Sprintf("%v", ePack) + a := pp.Sprintf("%v", packages[i]) t.Errorf("[%d] expected %s, actual %s", i, e, a) } } @@ -803,7 +803,7 @@ bind-libs.x86_64 32:9.8.2-0.37.rc1.45.amzn1 amzn-main java-1.7.0-openjdk.x86_64 1.7.0.95-2.6.4.0.65.amzn1 amzn-main if-not-architecture 100-200 amzn-main ` - r.Packages = []models.PackageInfo{ + r.Packages = []models.Package{ { Name: "bind-libs", Version: "9.8.0", @@ -822,11 +822,11 @@ if-not-architecture 100-200 amzn-main } var tests = []struct { in string - out models.PackageInfoList + out models.Packages }{ { stdout, - models.PackageInfoList{ + models.Packages{ { Name: "bind-libs", Version: "9.8.0", @@ -856,15 +856,15 @@ if-not-architecture 100-200 amzn-main } for _, tt := range tests { - packInfoList, err := r.parseYumCheckUpdateLines(tt.in) + packages, err := r.parseYumCheckUpdateLines(tt.in) if err != nil { t.Errorf("Error has occurred, err: %s\ntt.in: %v", err, tt.in) return } - for i, ePackInfo := range tt.out { - if !reflect.DeepEqual(ePackInfo, packInfoList[i]) { - e := pp.Sprintf("%v", ePackInfo) - a := pp.Sprintf("%v", packInfoList[i]) + for i, ePack := range tt.out { + if !reflect.DeepEqual(ePack, packages[i]) { + e := pp.Sprintf("%v", ePack) + a := pp.Sprintf("%v", packages[i]) t.Errorf("[%d] expected %s, actual %s", i, e, a) } } @@ -1095,11 +1095,11 @@ Dependencies Resolved func TestGetChangelogCVELines(t *testing.T) { var testsCentos6 = []struct { - in models.PackageInfo + in models.Package out string }{ { - models.PackageInfo{ + models.Package{ Name: "binutils", NewVersion: "2.20.51.0.2", NewRelease: "5.44.el6", @@ -1107,7 +1107,7 @@ func TestGetChangelogCVELines(t *testing.T) { "", }, { - models.PackageInfo{ + models.Package{ Name: "centos-release", NewVersion: "6", NewRelease: "8.el6.centos.12.3", @@ -1116,7 +1116,7 @@ func TestGetChangelogCVELines(t *testing.T) { `, }, { - models.PackageInfo{ + models.Package{ Name: "dhclient", NewVersion: "12:4.1.1", NewRelease: "51.P1.el6.centos", @@ -1125,7 +1125,7 @@ func TestGetChangelogCVELines(t *testing.T) { `, }, { - models.PackageInfo{ + models.Package{ Name: "dhcp-common", NewVersion: "12:4.1.1", NewRelease: "51.P1.el6.centos", @@ -1134,7 +1134,7 @@ func TestGetChangelogCVELines(t *testing.T) { `, }, { - models.PackageInfo{ + models.Package{ Name: "coreutils-libs", NewVersion: "8.4", NewRelease: "43.el6", @@ -1142,7 +1142,7 @@ func TestGetChangelogCVELines(t *testing.T) { "", }, { - models.PackageInfo{ + models.Package{ Name: "file", NewVersion: "5.04", NewRelease: "30.el6", @@ -1157,7 +1157,7 @@ func TestGetChangelogCVELines(t *testing.T) { `, }, { - models.PackageInfo{ + models.Package{ Name: "file-libs", NewVersion: "5.04", NewRelease: "30.el6", @@ -1190,11 +1190,11 @@ func TestGetChangelogCVELines(t *testing.T) { } var testsCentos5 = []struct { - in models.PackageInfo + in models.Package out string }{ { - models.PackageInfo{ + models.Package{ Name: "libuser", NewVersion: "0.54.7", NewRelease: "3.el5", @@ -1202,7 +1202,7 @@ func TestGetChangelogCVELines(t *testing.T) { "", }, { - models.PackageInfo{ + models.Package{ Name: "nss_db", NewVersion: "2.2", NewRelease: "38.el5_11", @@ -1210,7 +1210,7 @@ func TestGetChangelogCVELines(t *testing.T) { "", }, { - models.PackageInfo{ + models.Package{ Name: "acpid", NewVersion: "1.0.4", NewRelease: "82.el5", @@ -1218,7 +1218,7 @@ func TestGetChangelogCVELines(t *testing.T) { "", }, { - models.PackageInfo{ + models.Package{ Name: "mkinitrd", NewVersion: "5.1.19.6", NewRelease: "82.el5", @@ -1226,7 +1226,7 @@ func TestGetChangelogCVELines(t *testing.T) { "", }, { - models.PackageInfo{ + models.Package{ Name: "util-linux", NewVersion: "2.13", NewRelease: "0.59.el5_8", @@ -1235,7 +1235,7 @@ func TestGetChangelogCVELines(t *testing.T) { `, }, { - models.PackageInfo{ + models.Package{ Name: "bind-libs", NewVersion: "30:9.3.6", NewRelease: "25.P1.el5_11.8", @@ -1247,7 +1247,7 @@ func TestGetChangelogCVELines(t *testing.T) { `, }, { - models.PackageInfo{ + models.Package{ Name: "bind-utils", NewVersion: "30:9.3.6", NewRelease: "25.P1.el5_11.8", diff --git a/scan/serverapi.go b/scan/serverapi.go index a20d1748..852e5375 100644 --- a/scan/serverapi.go +++ b/scan/serverapi.go @@ -59,13 +59,13 @@ type osTypeInterface interface { // osPackages is included by base struct type osPackages struct { // installed packages - Packages models.PackageInfoList + Packages models.Packages // unsecure packages VulnInfos models.VulnInfos } -func (p *osPackages) setPackages(pi models.PackageInfoList) { +func (p *osPackages) setPackages(pi models.Packages) { p.Packages = pi } From f36671784ea5eecd84912369392e2fb12a5faa53 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Sat, 6 May 2017 05:25:17 +0900 Subject: [PATCH 024/113] Fix testcase --- scan/redhat_test.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scan/redhat_test.go b/scan/redhat_test.go index 01fd70c1..70017135 100644 --- a/scan/redhat_test.go +++ b/scan/redhat_test.go @@ -562,12 +562,14 @@ Description : The Berkeley Internet Name Domain (BIND) is an implementation of } for _, tt := range tests { actual, _ := r.parseYumUpdateinfo(tt.in) - for i, advisoryCveIDs := range actual { - if !reflect.DeepEqual(tt.out[i], advisoryCveIDs) { - e := pp.Sprintf("%v", tt.out[i]) + for j, advisoryCveIDs := range actual { + sort.Strings(tt.out[j].CveIDs) + sort.Strings(advisoryCveIDs.CveIDs) + if !reflect.DeepEqual(tt.out[j], advisoryCveIDs) { + e := pp.Sprintf("%v", tt.out[j]) a := pp.Sprintf("%v", advisoryCveIDs) t.Errorf("[%d] Alas is not same. \nexpected: %s\nactual: %s", - i, e, a) + j, e, a) } } } From 210e3dc9907d73feb5ccbfe1abef7bcf75ca9eec Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Mon, 8 May 2017 22:15:12 +0900 Subject: [PATCH 025/113] Change ScanResult.Packages structure to Map --- cache/bolt_test.go | 4 +- cache/db.go | 12 +----- commands/cmdutil.go | 21 ---------- commands/util.go | 24 ++++++++++-- commands/util_test.go | 24 ++++++------ models/models.go | 78 ++++++++++++++----------------------- models/models_test.go | 47 ++-------------------- oval/oval.go | 14 +++---- oval/redhat.go | 6 +-- scan/debian.go | 91 ++++++++++++++++++++----------------------- scan/debian_test.go | 4 +- scan/freebsd.go | 23 +++++------ scan/freebsd_test.go | 12 +++--- scan/redhat.go | 45 +++++++++++---------- scan/redhat_test.go | 88 +++++++++++++++++++++-------------------- 15 files changed, 209 insertions(+), 284 deletions(-) delete mode 100644 commands/cmdutil.go diff --git a/cache/bolt_test.go b/cache/bolt_test.go index d13dd947..95a9815a 100644 --- a/cache/bolt_test.go +++ b/cache/bolt_test.go @@ -37,8 +37,8 @@ var meta = Meta{ Family: "ubuntu", Release: "16.04", }, - Packs: []models.Package{ - { + Packs: models.Packages{ + "apt": { Name: "apt", Version: "1", }, diff --git a/cache/db.go b/cache/db.go index ded0668f..ce837774 100644 --- a/cache/db.go +++ b/cache/db.go @@ -45,16 +45,6 @@ type Cache interface { type Meta struct { Name string Distro config.Distro - Packs []models.Package + Packs models.Packages CreatedAt time.Time } - -// FindPack search a Package -func (m Meta) FindPack(name string) (pack models.Package, found bool) { - for _, p := range m.Packs { - if name == p.Name { - return p, true - } - } - return pack, false -} diff --git a/commands/cmdutil.go b/commands/cmdutil.go deleted file mode 100644 index 9a58019b..00000000 --- a/commands/cmdutil.go +++ /dev/null @@ -1,21 +0,0 @@ -package commands - -import ( - "fmt" - - "github.com/howeyc/gopass" -) - -func getPasswd(prompt string) (string, error) { - for { - fmt.Print(prompt) - pass, err := gopass.GetPasswdMasked() - if err != nil { - return "", fmt.Errorf("Failed to read password") - } - if 0 < len(pass) { - return string(pass[:]), nil - } - } - -} diff --git a/commands/util.go b/commands/util.go index 3d2b583b..8f90624c 100644 --- a/commands/util.go +++ b/commands/util.go @@ -33,6 +33,7 @@ import ( "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/report" "github.com/future-architect/vuls/util" + "github.com/howeyc/gopass" ) // jsonDirPattern is file name pattern of JSON directory @@ -190,11 +191,14 @@ func diff(curResults, preResults models.ScanResults) (diffed models.ScanResults, new, updated := getDiffCves(previous, current) current.ScannedCves = append(new, updated...) - current.Packages = models.Packages{} + packages := models.Packages{} for _, s := range current.ScannedCves { - current.Packages = append(current.Packages, s.Packages...) + for _, pack := range s.Packages { + p := current.Packages[pack.Name] + packages[pack.Name] = p + } } - current.Packages = current.Packages.UniqByName() + current.Packages = packages } diffed = append(diffed, current) @@ -320,3 +324,17 @@ func needToRefreshCve(r models.ScanResult) bool { } return true } + +func getPasswd(prompt string) (string, error) { + for { + fmt.Print(prompt) + pass, err := gopass.GetPasswdMasked() + if err != nil { + return "", fmt.Errorf("Failed to read password") + } + if 0 < len(pass) { + return string(pass[:]), nil + } + } + +} diff --git a/commands/util_test.go b/commands/util_test.go index ee47986e..6732d716 100644 --- a/commands/util_test.go +++ b/commands/util_test.go @@ -201,7 +201,7 @@ func TestDiff(t *testing.T) { { CveID: "CVE-2012-6702", Packages: models.Packages{ - { + "libexpat1": { Name: "libexpat1", Version: "2.1.0-7", Release: "", @@ -216,7 +216,7 @@ func TestDiff(t *testing.T) { { CveID: "CVE-2014-9761", Packages: models.Packages{ - { + "libc-bin": { Name: "libc-bin", Version: "2.21-0ubuntu5", Release: "", @@ -229,7 +229,7 @@ func TestDiff(t *testing.T) { CpeNames: []string{}, }, }, - Packages: []models.Package{}, + Packages: models.Packages{}, Errors: []string{}, Optional: [][]interface{}{}, }, @@ -244,7 +244,7 @@ func TestDiff(t *testing.T) { { CveID: "CVE-2012-6702", Packages: models.Packages{ - { + "libexpat1": { Name: "libexpat1", Version: "2.1.0-7", Release: "", @@ -259,7 +259,7 @@ func TestDiff(t *testing.T) { { CveID: "CVE-2014-9761", Packages: models.Packages{ - { + "libc-bin": { Name: "libc-bin", Version: "2.21-0ubuntu5", Release: "", @@ -272,7 +272,7 @@ func TestDiff(t *testing.T) { CpeNames: []string{}, }, }, - Packages: []models.Package{}, + Packages: models.Packages{}, Errors: []string{}, Optional: [][]interface{}{}, }, @@ -282,7 +282,7 @@ func TestDiff(t *testing.T) { ServerName: "u16", Family: "ubuntu", Release: "16.04", - Packages: []models.Package{}, + Packages: models.Packages{}, Errors: []string{}, Optional: [][]interface{}{}, }, @@ -298,7 +298,7 @@ func TestDiff(t *testing.T) { { CveID: "CVE-2016-6662", Packages: models.Packages{ - { + "mysql-libs": { Name: "mysql-libs", Version: "5.1.73", Release: "7.el6", @@ -331,7 +331,7 @@ func TestDiff(t *testing.T) { { CveID: "CVE-2016-6662", Packages: models.Packages{ - { + "mysql-libs": { Name: "mysql-libs", Version: "5.1.73", Release: "7.el6", @@ -345,7 +345,7 @@ func TestDiff(t *testing.T) { }, }, Packages: models.Packages{ - models.Package{ + "mysql-libs": { Name: "mysql-libs", Version: "5.1.73", Release: "7.el6", @@ -368,14 +368,14 @@ func TestDiff(t *testing.T) { if !reflect.DeepEqual(actual.ScannedCves, tt.out.ScannedCves) { h := pp.Sprint(actual.ScannedCves) x := pp.Sprint(tt.out.ScannedCves) - t.Errorf("[%d] actual: \n %s \n expected: \n %s", i, h, x) + t.Errorf("[%d] cves actual: \n %s \n expected: \n %s", i, h, x) } for j := range tt.out.Packages { if !reflect.DeepEqual(tt.out.Packages[j], actual.Packages[j]) { h := pp.Sprint(tt.out.Packages[j]) x := pp.Sprint(actual.Packages[j]) - t.Errorf("[%d] actual: \n %s \n expected: \n %s", i, x, h) + t.Errorf("[%d] packages actual: \n %s \n expected: \n %s", i, x, h) } } } diff --git a/models/models.go b/models/models.go index ead677bc..c6923949 100644 --- a/models/models.go +++ b/models/models.go @@ -448,7 +448,7 @@ type CveContents map[CveContentType]CveContent // NewCveContents create CveContents func NewCveContents(conts ...CveContent) CveContents { - m := make(map[CveContentType]CveContent) + m := map[CveContentType]CveContent{} for _, cont := range conts { m[cont.Type] = cont } @@ -547,69 +547,51 @@ type Reference struct { Link string } -// Packages is slice of Package -type Packages []Package +// Packages is Map of Package +// { "package-name": Package } +type Packages map[string]Package -// Exists returns true if exists the name -func (ps Packages) Exists(name string) bool { - for _, p := range ps { - if p.Name == name { - return true - } +// NewPackages create Packages +func NewPackages(packs ...Package) Packages { + m := Packages{} + for _, pack := range packs { + m[pack.Name] = pack } - return false -} - -// UniqByName be uniq by name. -func (ps Packages) UniqByName() (distincted Packages) { - set := make(map[string]Package) - for _, p := range ps { - set[p.Name] = p - } - for _, v := range set { - distincted = append(distincted, v) - } - return -} - -// FindByName search Package by name -func (ps Packages) FindByName(name string) (result Package, found bool) { - for _, p := range ps { - if p.Name == name { - return p, true - } - } - return Package{}, false + return m } // MergeNewVersion merges candidate version information to the receiver struct func (ps Packages) MergeNewVersion(as Packages) { for _, a := range as { - for i, p := range ps { - if p.Name == a.Name { - ps[i].NewVersion = a.NewVersion - ps[i].NewRelease = a.NewRelease - } + if pack, ok := ps[a.Name]; ok { + pack.NewVersion = a.NewVersion + pack.NewRelease = a.NewRelease + ps[a.Name] = pack } } } -func (ps Packages) countUpdatablePacks() int { - count := 0 - set := make(map[string]bool) - for _, p := range ps { - if len(p.NewVersion) != 0 && !set[p.Name] { - count++ - set[p.Name] = true - } +// Merge returns merged map (immutable) +func (ps Packages) Merge(other Packages) Packages { + merged := map[string]Package{} + for k, v := range ps { + merged[k] = v } - return count + for k, v := range other { + merged[k] = v + } + return merged } // FormatUpdatablePacksSummary returns a summary of updatable packages func (ps Packages) FormatUpdatablePacksSummary() string { - return fmt.Sprintf("%d updatable packages", - ps.countUpdatablePacks()) + nUpdatable := 0 + for _, p := range ps { + if p.NewVersion != "" { + nUpdatable++ + } + } + return fmt.Sprintf("%d updatable packages", nUpdatable) } // Package has installed packages. diff --git a/models/models_test.go b/models/models_test.go index 2626d7ab..3a4b428b 100644 --- a/models/models_test.go +++ b/models/models_test.go @@ -19,52 +19,11 @@ package models import ( "reflect" - "sort" "testing" "github.com/k0kubun/pp" ) -func TestPackagesUniqByName(t *testing.T) { - var test = struct { - in Packages - out Packages - }{ - Packages{ - { - Name: "hoge", - }, - { - Name: "fuga", - }, - { - Name: "hoge", - }, - }, - Packages{ - { - Name: "hoge", - }, - { - Name: "fuga", - }, - }, - } - - actual := test.in.UniqByName() - sort.Slice(actual, func(i, j int) bool { - return actual[i].Name < actual[j].Name - }) - sort.Slice(test.out, func(i, j int) bool { - return test.out[i].Name < test.out[j].Name - }) - for i, ePack := range test.out { - if actual[i].Name != ePack.Name { - t.Errorf("expected %#v, actual %#v", ePack.Name, actual[i].Name) - } - } -} - func TestMergeNewVersion(t *testing.T) { var test = struct { a Packages @@ -72,19 +31,19 @@ func TestMergeNewVersion(t *testing.T) { expected Packages }{ Packages{ - { + "hoge": { Name: "hoge", }, }, Packages{ - { + "hoge": { Name: "hoge", NewVersion: "1.0.0", NewRelease: "release1", }, }, Packages{ - { + "hoge": { Name: "hoge", NewVersion: "1.0.0", NewRelease: "release1", diff --git a/oval/oval.go b/oval/oval.go index d386b6b3..557f464c 100644 --- a/oval/oval.go +++ b/oval/oval.go @@ -11,15 +11,11 @@ type Client interface { } func getPackages(r *models.ScanResult, d *ovalmodels.Definition) models.Packages { - var packages models.Packages - for _, pack := range d.AffectedPacks { - for _, p := range r.Packages { - if pack.Name == p.Name { - p.Changelog = models.Changelog{} - packages = append(packages, p) - break - } - } + packages := models.Packages{} + for _, affectedPack := range d.AffectedPacks { + pack, _ := r.Packages[affectedPack.Name] + // pack.Changelog = models.Changelog{} + packages[affectedPack.Name] = pack } return packages } diff --git a/oval/redhat.go b/oval/redhat.go index 295d1746..cf301599 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -61,7 +61,7 @@ func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini ovalContent := *o.convertToModel(cve.CveID, definition) vinfo, ok := r.ScannedCves.Get(cve.CveID) if !ok { - util.Log.Infof("%s is newly detected by OVAL", definition.Debian.CveID) + util.Log.Infof("%s is newly detected by OVAL", cve.CveID) vinfo = models.VulnInfo{ CveID: cve.CveID, Confidence: models.OvalMatch, @@ -70,9 +70,9 @@ func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini } } else { if _, ok := vinfo.CveContents.Get(models.RedHat); !ok { - util.Log.Infof("%s is also detected by OVAL", definition.Debian.CveID) + util.Log.Infof("%s is also detected by OVAL", cve.CveID) } else { - util.Log.Infof("%s will be updated by OVAL", definition.Debian.CveID) + util.Log.Infof("%s will be updated by OVAL", cve.CveID) } if vinfo.Confidence.Score < models.OvalMatch.Score { vinfo.Confidence = models.OvalMatch diff --git a/scan/debian.go b/scan/debian.go index d9843cb6..5db8cbac 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -181,7 +181,10 @@ func (o *debian) scanPackages() error { return nil } -func (o *debian) scanInstalledPackages() (installed models.Packages, upgradable models.Packages, err error) { +func (o *debian) scanInstalledPackages() (models.Packages, models.Packages, error) { + installed := models.Packages{} + upgradable := models.Packages{} + r := o.exec("dpkg-query -W", noSudo) if !r.isSuccess() { return nil, nil, fmt.Errorf("Failed to SSH: %s", r) @@ -198,10 +201,10 @@ func (o *debian) scanInstalledPackages() (installed models.Packages, upgradable return nil, nil, fmt.Errorf( "Debian: Failed to parse package line: %s", line) } - installed = append(installed, models.Package{ + installed[name] = models.Package{ Name: name, Version: version, - }) + } } } @@ -212,20 +215,20 @@ func (o *debian) scanInstalledPackages() (installed models.Packages, upgradable for _, name := range upgradableNames { for _, pack := range installed { if pack.Name == name { - upgradable = append(upgradable, pack) + upgradable[name] = pack break } } } // Fill the candidate versions of upgradable packages - upgradable, err = o.fillCandidateVersion(upgradable) + err = o.fillCandidateVersion(upgradable) if err != nil { return nil, nil, fmt.Errorf("Failed to fill candidate versions. err: %s", err) } installed.MergeNewVersion(upgradable) - return + return installed, upgradable, nil } var packageLinePattern = regexp.MustCompile(`^([^\t']+)\t(.+)$`) @@ -254,7 +257,7 @@ func (o *debian) aptGetUpdate() error { return nil } -func (o *debian) scanUnsecurePackages(upgradable []models.Package) ([]models.VulnInfo, error) { +func (o *debian) scanUnsecurePackages(upgradable models.Packages) ([]models.VulnInfo, error) { o.aptGetUpdate() @@ -315,28 +318,28 @@ func (o *debian) ensureChangelogCache(current cache.Meta) (*cache.Meta, error) { return &cached, nil } -func (o *debian) fillCandidateVersion(before models.Packages) (filled []models.Package, err error) { +func (o *debian) fillCandidateVersion(packages models.Packages) (err error) { names := []string{} - for _, p := range before { - names = append(names, p.Name) + for name := range packages { + names = append(names, name) } cmd := fmt.Sprintf("LANGUAGE=en_US.UTF-8 apt-cache policy %s", strings.Join(names, " ")) r := o.exec(cmd, noSudo) if !r.isSuccess() { - return nil, fmt.Errorf("Failed to SSH: %s", r) + return fmt.Errorf("Failed to SSH: %s", r) } packChangelog := o.splitAptCachePolicy(r.Stdout) for k, v := range packChangelog { ver, err := o.parseAptCachePolicy(v, k) if err != nil { - return nil, fmt.Errorf("Failed to parse %s", err) + return fmt.Errorf("Failed to parse %s", err) } - p, found := before.FindByName(k) - if !found { - return nil, fmt.Errorf("Not found: %s", k) + pack, ok := packages[k] + if !ok { + return fmt.Errorf("Not found: %s", k) } - p.NewVersion = ver.Candidate - filled = append(filled, p) + pack.NewVersion = ver.Candidate + packages[k] = pack } return } @@ -394,7 +397,7 @@ func (o *debian) parseAptGetUpgrade(stdout string) (upgradableNames []string, er return } -func (o *debian) scanVulnInfos(upgradablePacks []models.Package, meta *cache.Meta) (models.VulnInfos, error) { +func (o *debian) scanVulnInfos(upgradablePacks models.Packages, meta *cache.Meta) (models.VulnInfos, error) { resChan := make(chan struct { models.Package DetectedCveIDs @@ -412,7 +415,7 @@ func (o *debian) scanVulnInfos(upgradablePacks []models.Package, meta *cache.Met }() timeout := time.After(30 * 60 * time.Second) - concurrency := 10 + concurrency := 1 tasks := util.GenWorkers(concurrency) for range upgradablePacks { tasks <- func() { @@ -446,18 +449,23 @@ func (o *debian) scanVulnInfos(upgradablePacks []models.Package, meta *cache.Met } // { DetectedCveID{} : [package] } - cvePackages := make(map[DetectedCveID][]models.Package) + cvePackages := make(map[DetectedCveID]models.Packages) errs := []error{} for i := 0; i < len(upgradablePacks); i++ { select { case pair := <-resChan: - pack := pair.Package - cveIDs := pair.DetectedCveIDs - for _, cveID := range cveIDs { - cvePackages[cveID] = appendPackIfMissing(cvePackages[cveID], pack) + cves := pair.DetectedCveIDs + for _, cve := range cves { + packs, ok := cvePackages[cve] + if ok { + packs[cve.CveID] = pair.Package + } else { + packs = models.Packages{} + } + cvePackages[cve] = packs } o.log.Infof("(%d/%d) Scanned %s-%s : %s", - i+1, len(upgradablePacks), pair.Name, pair.Package.Version, cveIDs) + i+1, len(upgradablePacks), pair.Name, pair.Package.Version, cves) case err := <-errChan: errs = append(errs, err) case <-timeout: @@ -492,7 +500,7 @@ func (o *debian) scanVulnInfos(upgradablePacks []models.Package, meta *cache.Met } func (o *debian) getChangelogCache(meta *cache.Meta, pack models.Package) string { - cachedPack, found := meta.FindPack(pack.Name) + cachedPack, found := meta.Packs[pack.Name] if !found { o.log.Debugf("Not found: %s", pack.Name) return "" @@ -602,14 +610,12 @@ func (o *debian) getCveIDsFromChangelog( // Only logging the error. o.log.Error(err) - for i, p := range o.Packages { - if p.Name == name { - o.Packages[i].Changelog = models.Changelog{ - Contents: "", - Method: models.FailedToFindVersionInChangelog, - } - } + pack := o.Packages[name] + pack.Changelog = models.Changelog{ + Contents: "", + Method: models.FailedToFindVersionInChangelog, } + o.Packages[name] = pack // If the version is not in changelog, return entire changelog to put into cache return []DetectedCveID{}, models.Changelog{ @@ -666,11 +672,9 @@ func (o *debian) parseChangelog(changelog, name, ver string, confidence models.C Method: string(confidence.DetectionMethod), } - for i, p := range o.Packages { - if p.Name == name { - o.Packages[i].Changelog = clog - } - } + pack := o.Packages[name] + pack.Changelog = clog + o.Packages[name] = pack cves := []DetectedCveID{} for _, id := range cveIDs { @@ -729,14 +733,3 @@ func (o *debian) parseAptCachePolicy(stdout, name string) (packCandidateVer, err } return ver, fmt.Errorf("Unknown Format: %s", stdout) } - -func appendPackIfMissing(slice []models.Package, s models.Package) []models.Package { - for _, ele := range slice { - if ele.Name == s.Name && - ele.Version == s.Version && - ele.Release == s.Release { - return slice - } - } - return append(slice, s) -} diff --git a/scan/debian_test.go b/scan/debian_test.go index 6a5f81c6..04429aa8 100644 --- a/scan/debian_test.go +++ b/scan/debian_test.go @@ -624,7 +624,9 @@ func TestGetChangelogCache(t *testing.T) { Family: "ubuntu", Release: "16.04", }, - Packs: []models.Package{pack}, + Packs: models.Packages{ + "apt": pack, + }, } const path = "/tmp/vuls-test-cache-11111111.db" diff --git a/scan/freebsd.go b/scan/freebsd.go index 817e9bd1..9be3aad3 100644 --- a/scan/freebsd.go +++ b/scan/freebsd.go @@ -71,7 +71,7 @@ func (o *bsd) checkDependencies() error { func (o *bsd) scanPackages() error { var err error - var packs []models.Package + var packs models.Packages if packs, err = o.scanInstalledPackages(); err != nil { o.log.Errorf("Failed to scan installed packages") return err @@ -87,7 +87,7 @@ func (o *bsd) scanPackages() error { return nil } -func (o *bsd) scanInstalledPackages() ([]models.Package, error) { +func (o *bsd) scanInstalledPackages() (models.Packages, error) { cmd := util.PrependProxyEnv("pkg version -v") r := o.exec(cmd, noSudo) if !r.isSuccess() { @@ -121,7 +121,7 @@ func (o *bsd) scanUnsecurePackages() (vulnInfos []models.VulnInfo, err error) { if len(cveIDs) == 0 { continue } - pack, found := o.Packages.FindByName(name) + pack, found := o.Packages[name] if !found { return nil, fmt.Errorf("Vulnerable package: %s is not found", name) } @@ -143,9 +143,9 @@ func (o *bsd) scanUnsecurePackages() (vulnInfos []models.VulnInfo, err error) { } for k := range cveIDAdtMap { - packs := []models.Package{} + packs := models.Packages{} for _, r := range cveIDAdtMap[k] { - packs = append(packs, r.pack) + packs[r.pack.Name] = r.pack } disAdvs := []models.DistroAdvisory{} @@ -165,7 +165,8 @@ func (o *bsd) scanUnsecurePackages() (vulnInfos []models.VulnInfo, err error) { return } -func (o *bsd) parsePkgVersion(stdout string) (packs []models.Package) { +func (o *bsd) parsePkgVersion(stdout string) models.Packages { + packs := models.Packages{} lines := strings.Split(stdout, "\n") for _, l := range lines { fields := strings.Fields(l) @@ -180,20 +181,20 @@ func (o *bsd) parsePkgVersion(stdout string) (packs []models.Package) { switch fields[1] { case "?", "=": - packs = append(packs, models.Package{ + packs[name] = models.Package{ Name: name, Version: ver, - }) + } case "<": candidate := strings.TrimSuffix(fields[6], ")") - packs = append(packs, models.Package{ + packs[name] = models.Package{ Name: name, Version: ver, NewVersion: candidate, - }) + } } } - return + return packs } type vulnIDCveIDs struct { diff --git a/scan/freebsd_test.go b/scan/freebsd_test.go index ee2cdc81..9ad62ef2 100644 --- a/scan/freebsd_test.go +++ b/scan/freebsd_test.go @@ -12,7 +12,7 @@ import ( func TestParsePkgVersion(t *testing.T) { var tests = []struct { in string - expected []models.Package + expected models.Packages }{ { `Updating FreeBSD repository catalogue... @@ -23,22 +23,22 @@ gettext-0.18.3.1 < needs updating (remote has 0.19.7) tcl84-8.4.20_2,1 = up-to-date with remote teTeX-base-3.0_25 ? orphaned: print/teTeX-base`, - []models.Package{ - { + models.Packages{ + "bash": { Name: "bash", Version: "4.2.45", NewVersion: "4.3.42_1", }, - { + "gettext": { Name: "gettext", Version: "0.18.3.1", NewVersion: "0.19.7", }, - { + "tcl84": { Name: "tcl84", Version: "8.4.20_2,1", }, - { + "teTeX-base": { Name: "teTeX-base", Version: "3.0_25", }, diff --git a/scan/redhat.go b/scan/redhat.go index 0d49a71a..3411ac11 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -231,7 +231,7 @@ func (o *redhat) scanPackages() error { o.log.Errorf("Failed to scan installed packages") return err } - o.setPackages(packs) + o.setPackages(models.NewPackages(packs...)) var vinfos []models.VulnInfo if vinfos, err = o.scanVulnInfos(); err != nil { @@ -242,7 +242,7 @@ func (o *redhat) scanPackages() error { return nil } -func (o *redhat) scanInstalledPackages() (installedPackages models.Packages, err error) { +func (o *redhat) scanInstalledPackages() (installed []models.Package, err error) { cmd := "rpm -qa --queryformat '%{NAME}\t%{EPOCHNUM}\t%{VERSION}\t%{RELEASE}\n'" r := o.exec(cmd, noSudo) if r.isSuccess() { @@ -255,13 +255,13 @@ func (o *redhat) scanInstalledPackages() (installedPackages models.Packages, err if pack, err = o.parseScannedPackagesLine(line); err != nil { return } - installedPackages = append(installedPackages, pack) + installed = append(installed, pack) } } return } - return installedPackages, fmt.Errorf( + return nil, fmt.Errorf( "Scan packages failed. status: %d, stdout: %s, stderr: %s", r.ExitStatus, r.Stdout, r.Stderr) } @@ -341,22 +341,23 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er } for name, clog := range rpm2changelog { - for i, p := range o.Packages { - n := fmt.Sprintf("%s-%s-%s", - p.Name, p.NewVersion, p.NewRelease) + for _, p := range o.Packages { + n := fmt.Sprintf("%s-%s-%s", p.Name, p.NewVersion, p.NewRelease) if name == n { - o.Packages[i].Changelog = models.Changelog{ + p.Changelog = models.Changelog{ Contents: *clog, Method: models.ChangelogExactMatchStr, } + o.Packages[p.Name] = p break } } } var results []PackageCveIDs - for i, pack := range packages { - changelog := o.getChangelogCVELines(rpm2changelog, pack) + i := 0 + for name := range packages { + changelog := o.getChangelogCVELines(rpm2changelog, packages[name]) // Collect unique set of CVE-ID in each changelog uniqueCveIDMap := make(map[string]bool) @@ -374,7 +375,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er cveIDs = append(cveIDs, k) } p := PackageCveIDs{ - Package: pack, + Package: packages[name], CveIDs: cveIDs, } results = append(results, p) @@ -388,6 +389,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er p.Package.NewVersion, p.Package.NewRelease, p.CveIDs) + i++ } // transform datastructure @@ -415,7 +417,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er // Amazon, RHEL do not use this method, so VendorAdvisory do not set. vinfos = append(vinfos, models.VulnInfo{ CveID: k, - Packages: v, + Packages: models.NewPackages(v...), Confidence: models.ChangelogExactMatch, }) } @@ -423,7 +425,8 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er } // parseYumCheckUpdateLines parse yum check-update to get package name, candidate version -func (o *redhat) parseYumCheckUpdateLines(stdout string) (results models.Packages, err error) { +func (o *redhat) parseYumCheckUpdateLines(stdout string) (models.Packages, error) { + results := models.Packages{} needToParse := false lines := strings.Split(stdout, "\n") for _, line := range lines { @@ -443,20 +446,20 @@ func (o *redhat) parseYumCheckUpdateLines(stdout string) (results models.Package return results, err } - installed, found := o.Packages.FindByName(candidate.Name) + installed, found := o.Packages[candidate.Name] if !found { o.log.Warnf("Not found the package in rpm -qa. candidate: %s-%s-%s", candidate.Name, candidate.Version, candidate.Release) - results = append(results, candidate) + results[candidate.Name] = candidate continue } installed.NewVersion = candidate.NewVersion installed.NewRelease = candidate.NewRelease installed.Repository = candidate.Repository - results = append(results, installed) + results[installed.Name] = installed } } - return + return results, nil } func (o *redhat) parseYumCheckUpdateLine(line string) (models.Package, error) { @@ -686,16 +689,16 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, // set candidate version info o.Packages.MergeNewVersion(updatable) - dict := map[string][]models.Package{} + dict := make(map[string]models.Packages) for _, advIDPackNames := range advIDPackNamesList { packages := models.Packages{} for _, packName := range advIDPackNames.PackNames { - pack, found := updatable.FindByName(packName) + pack, found := updatable[packName] if !found { return nil, fmt.Errorf( "Package not found. pack: %#v", packName) } - packages = append(packages, pack) + packages[pack.Name] = pack continue } dict[advIDPackNames.AdvisoryID] = packages @@ -729,7 +732,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, vinfos[i].DistroAdvisories = advAppended packs := dict[advIDCveIDs.DistroAdvisory.AdvisoryID] - vinfos[i].Packages = append(vinfos[i].Packages, packs...) + vinfos[i].Packages = vinfos[i].Packages.Merge(packs) found = true break } diff --git a/scan/redhat_test.go b/scan/redhat_test.go index 70017135..b506b23a 100644 --- a/scan/redhat_test.go +++ b/scan/redhat_test.go @@ -440,11 +440,13 @@ Description : kernel-uek for _, tt := range tests { actual, _ := r.parseYumUpdateinfo(tt.in) for i, advisoryCveIDs := range actual { - if !reflect.DeepEqual(tt.out[i], advisoryCveIDs) { - e := pp.Sprintf("%v", tt.out[i]) - a := pp.Sprintf("%v", advisoryCveIDs) + if tt.out[i].DistroAdvisory != advisoryCveIDs.DistroAdvisory { t.Errorf("[%d] Alas is not same. \nexpected: %s\nactual: %s", - i, e, a) + i, tt.out[i].DistroAdvisory, advisoryCveIDs.DistroAdvisory) + } + if !reflect.DeepEqual(tt.out[i].CveIDs, advisoryCveIDs.CveIDs) { + t.Errorf("[%d] Alas is not same. \nexpected: %s\nactual: %s", + i, tt.out[i].CveIDs, advisoryCveIDs.CveIDs) } } } @@ -562,14 +564,14 @@ Description : The Berkeley Internet Name Domain (BIND) is an implementation of } for _, tt := range tests { actual, _ := r.parseYumUpdateinfo(tt.in) - for j, advisoryCveIDs := range actual { - sort.Strings(tt.out[j].CveIDs) + for i, advisoryCveIDs := range actual { + sort.Strings(tt.out[i].CveIDs) sort.Strings(advisoryCveIDs.CveIDs) - if !reflect.DeepEqual(tt.out[j], advisoryCveIDs) { - e := pp.Sprintf("%v", tt.out[j]) + if !reflect.DeepEqual(tt.out[i], advisoryCveIDs) { + e := pp.Sprintf("%v", tt.out[i]) a := pp.Sprintf("%v", advisoryCveIDs) t.Errorf("[%d] Alas is not same. \nexpected: %s\nactual: %s", - j, e, a) + i, e, a) } } } @@ -688,46 +690,46 @@ bind-utils.x86_64 30:9.3.6-25.P1.el5_11.8 updates pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5 ` - r.Packages = []models.Package{ - { + r.setPackages(models.NewPackages( + models.Package{ Name: "audit-libs", Version: "2.3.6", Release: "4.el6", }, - { + models.Package{ Name: "bash", Version: "4.1.1", Release: "33", }, - { + models.Package{ Name: "python-libs", Version: "2.6.0", Release: "1.1-0", }, - { + models.Package{ Name: "python-ordereddict", Version: "1.0", Release: "1", }, - { + models.Package{ Name: "bind-utils", Version: "1.0", Release: "1", }, - { + models.Package{ Name: "pytalloc", Version: "2.0.1", Release: "0", }, - } + )) var tests = []struct { in string out models.Packages }{ { stdout, - models.Packages{ - { + models.NewPackages( + models.Package{ Name: "audit-libs", Version: "2.3.6", Release: "4.el6", @@ -735,7 +737,7 @@ pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5 NewRelease: "5.el6", Repository: "base", }, - { + models.Package{ Name: "bash", Version: "4.1.1", Release: "33", @@ -743,7 +745,7 @@ pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5 NewRelease: "33.el6_7.1", Repository: "updates", }, - { + models.Package{ Name: "python-libs", Version: "2.6.0", Release: "1.1-0", @@ -751,7 +753,7 @@ pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5 NewRelease: "64.el6", Repository: "rhui-REGION-rhel-server-releases", }, - { + models.Package{ Name: "python-ordereddict", Version: "1.0", Release: "1", @@ -759,7 +761,7 @@ pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5 NewRelease: "3.el6ev", Repository: "installed", }, - { + models.Package{ Name: "bind-utils", Version: "1.0", Release: "1", @@ -767,7 +769,7 @@ pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5 NewRelease: "25.P1.el5_11.8", Repository: "updates", }, - { + models.Package{ Name: "pytalloc", Version: "2.0.1", Release: "0", @@ -775,7 +777,7 @@ pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5 NewRelease: "2.el6", Repository: "@CentOS 6.5/6.5", }, - }, + ), }, } @@ -785,11 +787,11 @@ pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5 t.Errorf("Error has occurred, err: %s\ntt.in: %v", err, tt.in) return } - for i, ePack := range tt.out { - if !reflect.DeepEqual(ePack, packages[i]) { + for name, ePack := range tt.out { + if !reflect.DeepEqual(ePack, packages[name]) { e := pp.Sprintf("%v", ePack) - a := pp.Sprintf("%v", packages[i]) - t.Errorf("[%d] expected %s, actual %s", i, e, a) + a := pp.Sprintf("%v", packages[name]) + t.Errorf("expected %s, actual %s", e, a) } } } @@ -805,31 +807,31 @@ bind-libs.x86_64 32:9.8.2-0.37.rc1.45.amzn1 amzn-main java-1.7.0-openjdk.x86_64 1.7.0.95-2.6.4.0.65.amzn1 amzn-main if-not-architecture 100-200 amzn-main ` - r.Packages = []models.Package{ - { + r.Packages = models.NewPackages( + models.Package{ Name: "bind-libs", Version: "9.8.0", Release: "0.33.rc1.45.amzn1", }, - { + models.Package{ Name: "java-1.7.0-openjdk", Version: "1.7.0.0", Release: "2.6.4.0.0.amzn1", }, - { + models.Package{ Name: "if-not-architecture", Version: "10", Release: "20", }, - } + ) var tests = []struct { in string out models.Packages }{ { stdout, - models.Packages{ - { + models.NewPackages( + models.Package{ Name: "bind-libs", Version: "9.8.0", Release: "0.33.rc1.45.amzn1", @@ -837,7 +839,7 @@ if-not-architecture 100-200 amzn-main NewRelease: "0.37.rc1.45.amzn1", Repository: "amzn-main", }, - { + models.Package{ Name: "java-1.7.0-openjdk", Version: "1.7.0.0", Release: "2.6.4.0.0.amzn1", @@ -845,7 +847,7 @@ if-not-architecture 100-200 amzn-main NewRelease: "2.6.4.0.65.amzn1", Repository: "amzn-main", }, - { + models.Package{ Name: "if-not-architecture", Version: "10", Release: "20", @@ -853,7 +855,7 @@ if-not-architecture 100-200 amzn-main NewRelease: "200", Repository: "amzn-main", }, - }, + ), }, } @@ -863,11 +865,11 @@ if-not-architecture 100-200 amzn-main t.Errorf("Error has occurred, err: %s\ntt.in: %v", err, tt.in) return } - for i, ePack := range tt.out { - if !reflect.DeepEqual(ePack, packages[i]) { + for name, ePack := range tt.out { + if !reflect.DeepEqual(ePack, packages[name]) { e := pp.Sprintf("%v", ePack) - a := pp.Sprintf("%v", packages[i]) - t.Errorf("[%d] expected %s, actual %s", i, e, a) + a := pp.Sprintf("%v", packages[name]) + t.Errorf("[%s] expected %s, actual %s", name, e, a) } } } From b977558f38a3b7b604fff4ae2ac5800c09d1d0e7 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Tue, 9 May 2017 00:20:11 +0900 Subject: [PATCH 026/113] Change structure of VulnInfo.Pacakges to []string --- commands/util.go | 6 +-- commands/util_test.go | 92 ++++++++++++------------------------------- models/models.go | 6 +-- oval/debian.go | 8 ++-- oval/oval.go | 9 ++--- oval/redhat.go | 8 ++-- scan/debian.go | 76 +++++++++++++++++------------------ scan/freebsd.go | 15 ++++++- scan/redhat.go | 45 +++++++++++++++------ 9 files changed, 128 insertions(+), 137 deletions(-) diff --git a/commands/util.go b/commands/util.go index 8f90624c..8c692632 100644 --- a/commands/util.go +++ b/commands/util.go @@ -193,9 +193,9 @@ func diff(curResults, preResults models.ScanResults) (diffed models.ScanResults, packages := models.Packages{} for _, s := range current.ScannedCves { - for _, pack := range s.Packages { - p := current.Packages[pack.Name] - packages[pack.Name] = p + for _, name := range s.PackageNames { + p := current.Packages[name] + packages[name] = p } } current.Packages = packages diff --git a/commands/util_test.go b/commands/util_test.go index 6732d716..3b6f4540 100644 --- a/commands/util_test.go +++ b/commands/util_test.go @@ -199,32 +199,14 @@ func TestDiff(t *testing.T) { Release: "16.04", ScannedCves: []models.VulnInfo{ { - CveID: "CVE-2012-6702", - Packages: models.Packages{ - "libexpat1": { - Name: "libexpat1", - Version: "2.1.0-7", - Release: "", - NewVersion: "2.1.0-7ubuntu0.16.04.2", - NewRelease: "", - Repository: "", - }, - }, + CveID: "CVE-2012-6702", + PackageNames: []string{"libexpat1"}, DistroAdvisories: []models.DistroAdvisory{}, CpeNames: []string{}, }, { - CveID: "CVE-2014-9761", - Packages: models.Packages{ - "libc-bin": { - Name: "libc-bin", - Version: "2.21-0ubuntu5", - Release: "", - NewVersion: "2.23-0ubuntu5", - NewRelease: "", - Repository: "", - }, - }, + CveID: "CVE-2014-9761", + PackageNames: []string{"libc-bin"}, DistroAdvisories: []models.DistroAdvisory{}, CpeNames: []string{}, }, @@ -242,32 +224,14 @@ func TestDiff(t *testing.T) { Release: "16.04", ScannedCves: []models.VulnInfo{ { - CveID: "CVE-2012-6702", - Packages: models.Packages{ - "libexpat1": { - Name: "libexpat1", - Version: "2.1.0-7", - Release: "", - NewVersion: "2.1.0-7ubuntu0.16.04.2", - NewRelease: "", - Repository: "", - }, - }, + CveID: "CVE-2012-6702", + PackageNames: []string{"libexpat1"}, DistroAdvisories: []models.DistroAdvisory{}, CpeNames: []string{}, }, { - CveID: "CVE-2014-9761", - Packages: models.Packages{ - "libc-bin": { - Name: "libc-bin", - Version: "2.21-0ubuntu5", - Release: "", - NewVersion: "2.23-0ubuntu5", - NewRelease: "", - Repository: "", - }, - }, + CveID: "CVE-2014-9761", + PackageNames: []string{"libc-bin"}, DistroAdvisories: []models.DistroAdvisory{}, CpeNames: []string{}, }, @@ -296,21 +260,26 @@ func TestDiff(t *testing.T) { Release: "16.04", ScannedCves: []models.VulnInfo{ { - CveID: "CVE-2016-6662", - Packages: models.Packages{ - "mysql-libs": { - Name: "mysql-libs", - Version: "5.1.73", - Release: "7.el6", - NewVersion: "5.1.73", - NewRelease: "8.el6_8", - Repository: "", - }, - }, + CveID: "CVE-2016-6662", + PackageNames: []string{"mysql-libs"}, DistroAdvisories: []models.DistroAdvisory{}, CpeNames: []string{}, }, }, + Packages: models.Packages{ + "mysql-libs": { + Name: "mysql-libs", + Version: "5.1.73", + Release: "7.el6", + NewVersion: "5.1.73", + NewRelease: "8.el6_8", + Repository: "", + Changelog: models.Changelog{ + Contents: "", + Method: "", + }, + }, + }, }, }, inPrevious: models.ScanResults{ @@ -329,17 +298,8 @@ func TestDiff(t *testing.T) { Release: "16.04", ScannedCves: []models.VulnInfo{ { - CveID: "CVE-2016-6662", - Packages: models.Packages{ - "mysql-libs": { - Name: "mysql-libs", - Version: "5.1.73", - Release: "7.el6", - NewVersion: "5.1.73", - NewRelease: "8.el6_8", - Repository: "", - }, - }, + CveID: "CVE-2016-6662", + PackageNames: []string{"mysql-libs"}, DistroAdvisories: []models.DistroAdvisory{}, CpeNames: []string{}, }, diff --git a/models/models.go b/models/models.go index c6923949..e18f8a65 100644 --- a/models/models.go +++ b/models/models.go @@ -377,7 +377,7 @@ func (v *VulnInfos) Upsert(vInfo VulnInfo) { type VulnInfo struct { CveID string Confidence Confidence - Packages Packages + PackageNames []string DistroAdvisories []DistroAdvisory // for Aamazon, RHEL, FreeBSD CpeNames []string CveContents CveContents @@ -391,8 +391,8 @@ func (v *VulnInfo) NilToEmpty() { if v.DistroAdvisories == nil { v.DistroAdvisories = []DistroAdvisory{} } - if v.Packages == nil { - v.Packages = Packages{} + if v.PackageNames == nil { + v.PackageNames = []string{} } if v.CveContents == nil { v.CveContents = NewCveContents() diff --git a/oval/debian.go b/oval/debian.go index c2b298d6..598badb4 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -67,10 +67,10 @@ func (o Debian) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini util.Log.Infof("%s is newly detected by OVAL", definition.Debian.CveID) vinfo = models.VulnInfo{ - CveID: definition.Debian.CveID, - Confidence: models.OvalMatch, - Packages: getPackages(r, definition), - CveContents: models.NewCveContents(ovalContent), + CveID: definition.Debian.CveID, + Confidence: models.OvalMatch, + PackageNames: getPackages(r, definition), + CveContents: models.NewCveContents(ovalContent), } } else { if _, ok := vinfo.CveContents.Get(models.NewCveContentType(r.Family)); !ok { diff --git a/oval/oval.go b/oval/oval.go index 557f464c..e222e4b5 100644 --- a/oval/oval.go +++ b/oval/oval.go @@ -10,12 +10,9 @@ type Client interface { FillCveInfoFromOvalDB(r *models.ScanResult) error } -func getPackages(r *models.ScanResult, d *ovalmodels.Definition) models.Packages { - packages := models.Packages{} +func getPackages(r *models.ScanResult, d *ovalmodels.Definition) (names []string) { for _, affectedPack := range d.AffectedPacks { - pack, _ := r.Packages[affectedPack.Name] - // pack.Changelog = models.Changelog{} - packages[affectedPack.Name] = pack + names = append(names, affectedPack.Name) } - return packages + return } diff --git a/oval/redhat.go b/oval/redhat.go index cf301599..ec2da4c8 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -63,10 +63,10 @@ func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini if !ok { util.Log.Infof("%s is newly detected by OVAL", cve.CveID) vinfo = models.VulnInfo{ - CveID: cve.CveID, - Confidence: models.OvalMatch, - Packages: getPackages(r, definition), - CveContents: models.NewCveContents(ovalContent), + CveID: cve.CveID, + Confidence: models.OvalMatch, + PackageNames: getPackages(r, definition), + CveContents: models.NewCveContents(ovalContent), } } else { if _, ok := vinfo.CveContents.Get(models.RedHat); !ok { diff --git a/scan/debian.go b/scan/debian.go index 5db8cbac..fc915fd3 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -37,7 +37,14 @@ type debian struct { // NewDebian is constructor func newDebian(c config.ServerInfo) *debian { - d := &debian{} + d := &debian{ + base: base{ + osPackages: osPackages{ + Packages: models.Packages{}, + VulnInfos: models.VulnInfos{}, + }, + }, + } d.log = util.NewCustomLogger(c) d.setServerInfo(c) return d @@ -397,11 +404,20 @@ func (o *debian) parseAptGetUpgrade(stdout string) (upgradableNames []string, er return } +// DetectedCveID has CveID, Confidence and DetectionMethod fields +// LenientMatching will be true if this vulnerability is not detected by accurate version matching. +// see https://github.com/future-architect/vuls/pull/328 +type DetectedCveID struct { + CveID string + Confidence models.Confidence +} + func (o *debian) scanVulnInfos(upgradablePacks models.Packages, meta *cache.Meta) (models.VulnInfos, error) { - resChan := make(chan struct { - models.Package - DetectedCveIDs - }, len(upgradablePacks)) + type response struct { + packName string + DetectedCveIDs []DetectedCveID + } + resChan := make(chan response, len(upgradablePacks)) errChan := make(chan error, len(upgradablePacks)) reqChan := make(chan models.Package, len(upgradablePacks)) defer close(resChan) @@ -415,7 +431,7 @@ func (o *debian) scanVulnInfos(upgradablePacks models.Packages, meta *cache.Meta }() timeout := time.After(30 * 60 * time.Second) - concurrency := 1 + concurrency := 10 tasks := util.GenWorkers(concurrency) for range upgradablePacks { tasks <- func() { @@ -425,10 +441,7 @@ func (o *debian) scanVulnInfos(upgradablePacks models.Packages, meta *cache.Meta changelog := o.getChangelogCache(meta, p) if 0 < len(changelog) { cveIDs, _ := o.getCveIDsFromChangelog(changelog, p.Name, p.Version) - resChan <- struct { - models.Package - DetectedCveIDs - }{p, cveIDs} + resChan <- response{p.Name, cveIDs} return } @@ -438,10 +451,7 @@ func (o *debian) scanVulnInfos(upgradablePacks models.Packages, meta *cache.Meta if cveIDs, err := o.scanPackageCveIDs(p); err != nil { errChan <- err } else { - resChan <- struct { - models.Package - DetectedCveIDs - }{p, cveIDs} + resChan <- response{p.Name, cveIDs} } }(pack) } @@ -449,23 +459,23 @@ func (o *debian) scanVulnInfos(upgradablePacks models.Packages, meta *cache.Meta } // { DetectedCveID{} : [package] } - cvePackages := make(map[DetectedCveID]models.Packages) + cvePackages := make(map[DetectedCveID][]string) errs := []error{} for i := 0; i < len(upgradablePacks); i++ { select { - case pair := <-resChan: - cves := pair.DetectedCveIDs + case response := <-resChan: + cves := response.DetectedCveIDs for _, cve := range cves { - packs, ok := cvePackages[cve] + packNames, ok := cvePackages[cve] if ok { - packs[cve.CveID] = pair.Package + packNames = append(packNames, response.packName) } else { - packs = models.Packages{} + packNames = []string{response.packName} } - cvePackages[cve] = packs + cvePackages[cve] = packNames } - o.log.Infof("(%d/%d) Scanned %s-%s : %s", - i+1, len(upgradablePacks), pair.Name, pair.Package.Version, cves) + o.log.Infof("(%d/%d) Scanned %s: %s", + i+1, len(upgradablePacks), response.packName, cves) case err := <-errChan: errs = append(errs, err) case <-timeout: @@ -482,11 +492,11 @@ func (o *debian) scanVulnInfos(upgradablePacks models.Packages, meta *cache.Meta } o.log.Debugf("%d Cves are found. cves: %v", len(cveIDs), cveIDs) var vinfos models.VulnInfos - for k, v := range cvePackages { + for cveID, names := range cvePackages { vinfos = append(vinfos, models.VulnInfo{ - CveID: k.CveID, - Confidence: k.Confidence, - Packages: v, + CveID: cveID.CveID, + Confidence: cveID.Confidence, + PackageNames: names, }) } @@ -615,6 +625,7 @@ func (o *debian) getCveIDsFromChangelog( Contents: "", Method: models.FailedToFindVersionInChangelog, } + //TODO Mutex o.Packages[name] = pack // If the version is not in changelog, return entire changelog to put into cache @@ -624,17 +635,6 @@ func (o *debian) getCveIDsFromChangelog( } } -// DetectedCveID has CveID, Confidence and DetectionMethod fields -// LenientMatching will be true if this vulnerability is not detected by accurate version matching. -// see https://github.com/future-architect/vuls/pull/328 -type DetectedCveID struct { - CveID string - Confidence models.Confidence -} - -// DetectedCveIDs is a slice of DetectedCveID -type DetectedCveIDs []DetectedCveID - var cveRe = regexp.MustCompile(`(CVE-\d{4}-\d{4,})`) // Collect CVE-IDs included in the changelog. diff --git a/scan/freebsd.go b/scan/freebsd.go index 9be3aad3..23cf06fa 100644 --- a/scan/freebsd.go +++ b/scan/freebsd.go @@ -33,7 +33,14 @@ type bsd struct { // NewBSD constructor func newBsd(c config.ServerInfo) *bsd { - d := &bsd{} + d := &bsd{ + base: base{ + osPackages: osPackages{ + Packages: models.Packages{}, + VulnInfos: models.VulnInfos{}, + }, + }, + } d.log = util.NewCustomLogger(c) d.setServerInfo(c) return d @@ -155,9 +162,13 @@ func (o *bsd) scanUnsecurePackages() (vulnInfos []models.VulnInfo, err error) { }) } + names := []string{} + for name := range packs { + names = append(names, name) + } vulnInfos = append(vulnInfos, models.VulnInfo{ CveID: k, - Packages: packs, + PackageNames: names, DistroAdvisories: disAdvs, Confidence: models.PkgAuditMatch, }) diff --git a/scan/redhat.go b/scan/redhat.go index 3411ac11..7c28b933 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -37,7 +37,14 @@ type redhat struct { // NewRedhat is constructor func newRedhat(c config.ServerInfo) *redhat { - r := &redhat{} + r := &redhat{ + base: base{ + osPackages: osPackages{ + Packages: models.Packages{}, + VulnInfos: models.VulnInfos{}, + }, + }, + } r.log = util.NewCustomLogger(c) r.setServerInfo(c) return r @@ -402,23 +409,32 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er // ] // - To // map { - // CveID: []models.Package + // CveID: models.Packages{} // } - cveIDPackMap := make(map[string][]models.Package) + cveIDPackages := make(map[string]models.Packages) for _, res := range results { for _, cveID := range res.CveIDs { - cveIDPackMap[cveID] = append( - cveIDPackMap[cveID], res.Package) + if packages, ok := cveIDPackages[cveID]; ok { + packages[res.Package.Name] = res.Package + cveIDPackages[cveID] = packages + } else { + cveIDPackages[cveID] = models.NewPackages(res.Package) + } } } vinfos := []models.VulnInfo{} - for k, v := range cveIDPackMap { + for cveID, packs := range cveIDPackages { + names := []string{} + for name := range packs { + names = append(names, name) + } + // Amazon, RHEL do not use this method, so VendorAdvisory do not set. vinfos = append(vinfos, models.VulnInfo{ - CveID: k, - Packages: models.NewPackages(v...), - Confidence: models.ChangelogExactMatch, + CveID: cveID, + PackageNames: names, + Confidence: models.ChangelogExactMatch, }) } return vinfos, nil @@ -732,17 +748,24 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, vinfos[i].DistroAdvisories = advAppended packs := dict[advIDCveIDs.DistroAdvisory.AdvisoryID] - vinfos[i].Packages = vinfos[i].Packages.Merge(packs) + for _, pack := range packs { + vinfos[i].PackageNames = append(vinfos[i].PackageNames, pack.Name) + } found = true break } } if !found { + names := []string{} + packs := dict[advIDCveIDs.DistroAdvisory.AdvisoryID] + for _, pack := range packs { + names = append(names, pack.Name) + } cpinfo := models.VulnInfo{ CveID: cveID, DistroAdvisories: []models.DistroAdvisory{advIDCveIDs.DistroAdvisory}, - Packages: dict[advIDCveIDs.DistroAdvisory.AdvisoryID], + PackageNames: names, Confidence: models.YumUpdateSecurityMatch, } vinfos = append(vinfos, cpinfo) From cfb848918f4993296fc617755296190be4fc54d8 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Tue, 9 May 2017 21:03:54 +0900 Subject: [PATCH 027/113] Change structure of ScanResult.[]VulnInfo to Map --- commands/report.go | 23 +++++++-------- commands/util.go | 44 ++++++++++++---------------- commands/util_test.go | 67 ++++++++++++++++++++++--------------------- models/models.go | 66 +++++++----------------------------------- models/models_test.go | 43 --------------------------- oval/debian.go | 18 ++++++------ oval/redhat.go | 16 +++++++---- scan/base.go | 7 +++-- scan/debian.go | 9 +++--- scan/freebsd.go | 21 +++++++------- scan/redhat.go | 37 ++++++++++-------------- scan/serverapi.go | 2 +- 12 files changed, 129 insertions(+), 224 deletions(-) diff --git a/commands/report.go b/commands/report.go index 3c77f30f..76a0bae5 100644 --- a/commands/report.go +++ b/commands/report.go @@ -504,15 +504,19 @@ func fillCveDetail(r *models.ScanResult) error { return err } for _, d := range ds { - nvd := *r.ConvertNvdToModel(d.CveID, d.Nvd) - jvn := *r.ConvertJvnToModel(d.CveID, d.Jvn) - for i, sc := range r.ScannedCves { - if sc.CveID == d.CveID { - for _, con := range []models.CveContent{nvd, jvn} { + nvd := r.ConvertNvdToModel(d.CveID, d.Nvd) + jvn := r.ConvertJvnToModel(d.CveID, d.Jvn) + for cveID, vinfo := range r.ScannedCves { + if vinfo.CveID == d.CveID { + if vinfo.CveContents == nil { + vinfo.CveContents = models.CveContents{} + } + for _, con := range []models.CveContent{*nvd, *jvn} { if !con.Empty() { - r.ScannedCves[i].CveContents.Upsert(con) + vinfo.CveContents.Upsert(con) } } + r.ScannedCves[cveID] = vinfo break } } @@ -528,15 +532,10 @@ func fillCveDetail(r *models.ScanResult) error { } func fillCveInfoFromCveDB(r *models.ScanResult) error { - var err error - var vs []models.VulnInfo - sInfo := c.Conf.Servers[r.ServerName] - vs, err = scanVulnByCpeNames(sInfo.CpeNames, r.ScannedCves) - if err != nil { + if err := fillVulnByCpeNames(sInfo.CpeNames, r.ScannedCves); err != nil { return err } - r.ScannedCves = vs if err := fillCveDetail(r); err != nil { return err } diff --git a/commands/util.go b/commands/util.go index 8c692632..97bf0464 100644 --- a/commands/util.go +++ b/commands/util.go @@ -188,9 +188,7 @@ func diff(curResults, preResults models.ScanResults) (diffed models.ScanResults, } if found { - new, updated := getDiffCves(previous, current) - current.ScannedCves = append(new, updated...) - + current.ScannedCves = getDiffCves(previous, current) packages := models.Packages{} for _, s := range current.ScannedCves { for _, name := range s.PackageNames { @@ -206,22 +204,28 @@ func diff(curResults, preResults models.ScanResults) (diffed models.ScanResults, return diffed, err } -func getDiffCves(previous, current models.ScanResult) (new, updated []models.VulnInfo) { +func getDiffCves(previous, current models.ScanResult) models.VulnInfos { previousCveIDsSet := map[string]bool{} for _, previousVulnInfo := range previous.ScannedCves { previousCveIDsSet[previousVulnInfo.CveID] = true } + new := models.VulnInfos{} + updated := models.VulnInfos{} for _, v := range current.ScannedCves { if previousCveIDsSet[v.CveID] { if isCveInfoUpdated(v.CveID, previous, current) { - updated = append(updated, v) + updated[v.CveID] = v } } else { - new = append(new, v) + new[v.CveID] = v } } - return + + for cveID, vuln := range new { + updated[cveID] = vuln + } + return updated } func isCveInfoUpdated(cveID string, previous, current models.ScanResult) bool { @@ -274,42 +278,32 @@ func overwriteJSONFile(dir string, r models.ScanResult) error { return nil } -func scanVulnByCpeNames(cpeNames []string, scannedVulns []models.VulnInfo) ([]models.VulnInfo, error) { - // To remove duplicate - set := map[string]models.VulnInfo{} - for _, v := range scannedVulns { - set[v.CveID] = v - } - +func fillVulnByCpeNames(cpeNames []string, scannedVulns models.VulnInfos) error { for _, name := range cpeNames { details, err := cveapi.CveClient.FetchCveDetailsByCpeName(name) if err != nil { - return nil, err + return err } for _, detail := range details { - if val, ok := set[detail.CveID]; ok { + if val, ok := scannedVulns[detail.CveID]; ok { names := val.CpeNames names = util.AppendIfMissing(names, name) val.CpeNames = names val.Confidence = models.CpeNameMatch - set[detail.CveID] = val + scannedVulns[detail.CveID] = val } else { v := models.VulnInfo{ CveID: detail.CveID, CpeNames: []string{name}, Confidence: models.CpeNameMatch, } - v.NilToEmpty() - set[detail.CveID] = v + //TODO + // v.NilToEmpty() + scannedVulns[detail.CveID] = v } } } - - vinfos := []models.VulnInfo{} - for key := range set { - vinfos = append(vinfos, set[key]) - } - return vinfos, nil + return nil } func needToRefreshCve(r models.ScanResult) bool { diff --git a/commands/util_test.go b/commands/util_test.go index 3b6f4540..25d872fc 100644 --- a/commands/util_test.go +++ b/commands/util_test.go @@ -45,8 +45,8 @@ func TestIsCveInfoUpdated(t *testing.T) { in: In{ cveID: "CVE-2017-0001", cur: models.ScanResult{ - ScannedCves: []models.VulnInfo{ - { + ScannedCves: models.VulnInfos{ + "CVE-2017-0001": { CveID: "CVE-2017-0001", CveContents: models.NewCveContents( models.CveContent{ @@ -59,8 +59,8 @@ func TestIsCveInfoUpdated(t *testing.T) { }, }, prev: models.ScanResult{ - ScannedCves: []models.VulnInfo{ - { + ScannedCves: models.VulnInfos{ + "CVE-2017-0001": { CveID: "CVE-2017-0001", CveContents: models.NewCveContents( models.CveContent{ @@ -80,8 +80,8 @@ func TestIsCveInfoUpdated(t *testing.T) { in: In{ cveID: "CVE-2017-0002", cur: models.ScanResult{ - ScannedCves: []models.VulnInfo{ - { + ScannedCves: models.VulnInfos{ + "CVE-2017-0002": { CveID: "CVE-2017-0002", CveContents: models.NewCveContents( models.CveContent{ @@ -94,8 +94,8 @@ func TestIsCveInfoUpdated(t *testing.T) { }, }, prev: models.ScanResult{ - ScannedCves: []models.VulnInfo{ - { + ScannedCves: models.VulnInfos{ + "CVE-2017-0002": { CveID: "CVE-2017-0002", CveContents: models.NewCveContents( models.CveContent{ @@ -116,8 +116,8 @@ func TestIsCveInfoUpdated(t *testing.T) { cveID: "CVE-2017-0003", cur: models.ScanResult{ Family: "ubuntu", - ScannedCves: []models.VulnInfo{ - { + ScannedCves: models.VulnInfos{ + "CVE-2017-0003": { CveID: "CVE-2017-0003", CveContents: models.NewCveContents( models.CveContent{ @@ -131,8 +131,8 @@ func TestIsCveInfoUpdated(t *testing.T) { }, prev: models.ScanResult{ Family: "ubuntu", - ScannedCves: []models.VulnInfo{ - { + ScannedCves: models.VulnInfos{ + "CVE-2017-0003": { CveID: "CVE-2017-0003", CveContents: models.NewCveContents( models.CveContent{ @@ -153,8 +153,8 @@ func TestIsCveInfoUpdated(t *testing.T) { cveID: "CVE-2017-0004", cur: models.ScanResult{ Family: "redhat", - ScannedCves: []models.VulnInfo{ - { + ScannedCves: models.VulnInfos{ + "CVE-2017-0004": { CveID: "CVE-2017-0004", CveContents: models.NewCveContents( models.CveContent{ @@ -168,7 +168,7 @@ func TestIsCveInfoUpdated(t *testing.T) { }, prev: models.ScanResult{ Family: "redhat", - ScannedCves: []models.VulnInfo{}, + ScannedCves: models.VulnInfos{}, }, }, expected: true, @@ -197,14 +197,14 @@ func TestDiff(t *testing.T) { ServerName: "u16", Family: "ubuntu", Release: "16.04", - ScannedCves: []models.VulnInfo{ - { + ScannedCves: models.VulnInfos{ + "CVE-2012-6702": { CveID: "CVE-2012-6702", PackageNames: []string{"libexpat1"}, DistroAdvisories: []models.DistroAdvisory{}, CpeNames: []string{}, }, - { + "CVE-2014-9761": { CveID: "CVE-2014-9761", PackageNames: []string{"libc-bin"}, DistroAdvisories: []models.DistroAdvisory{}, @@ -222,14 +222,14 @@ func TestDiff(t *testing.T) { ServerName: "u16", Family: "ubuntu", Release: "16.04", - ScannedCves: []models.VulnInfo{ - { + ScannedCves: models.VulnInfos{ + "CVE-2012-6702": { CveID: "CVE-2012-6702", PackageNames: []string{"libexpat1"}, DistroAdvisories: []models.DistroAdvisory{}, CpeNames: []string{}, }, - { + "CVE-2014-9761": { CveID: "CVE-2014-9761", PackageNames: []string{"libc-bin"}, DistroAdvisories: []models.DistroAdvisory{}, @@ -242,13 +242,14 @@ func TestDiff(t *testing.T) { }, }, out: models.ScanResult{ - ScannedAt: atCurrent, - ServerName: "u16", - Family: "ubuntu", - Release: "16.04", - Packages: models.Packages{}, - Errors: []string{}, - Optional: [][]interface{}{}, + ScannedAt: atCurrent, + ServerName: "u16", + Family: "ubuntu", + Release: "16.04", + Packages: models.Packages{}, + ScannedCves: models.VulnInfos{}, + Errors: []string{}, + Optional: [][]interface{}{}, }, }, { @@ -258,8 +259,8 @@ func TestDiff(t *testing.T) { ServerName: "u16", Family: "ubuntu", Release: "16.04", - ScannedCves: []models.VulnInfo{ - { + ScannedCves: models.VulnInfos{ + "CVE-2016-6662": { CveID: "CVE-2016-6662", PackageNames: []string{"mysql-libs"}, DistroAdvisories: []models.DistroAdvisory{}, @@ -288,7 +289,7 @@ func TestDiff(t *testing.T) { ServerName: "u16", Family: "ubuntu", Release: "16.04", - ScannedCves: []models.VulnInfo{}, + ScannedCves: models.VulnInfos{}, }, }, out: models.ScanResult{ @@ -296,8 +297,8 @@ func TestDiff(t *testing.T) { ServerName: "u16", Family: "ubuntu", Release: "16.04", - ScannedCves: []models.VulnInfo{ - { + ScannedCves: models.VulnInfos{ + "CVE-2016-6662": { CveID: "CVE-2016-6662", PackageNames: []string{"mysql-libs"}, DistroAdvisories: []models.DistroAdvisory{}, diff --git a/models/models.go b/models/models.go index e18f8a65..92f1db54 100644 --- a/models/models.go +++ b/models/models.go @@ -164,12 +164,10 @@ func (r ScanResult) FilterByCvssOver() ScanResult { } // TODO: Filter by ignore cves??? - filtered := VulnInfos{} - for _, sc := range r.ScannedCves { - if config.Conf.CvssScoreOver <= sc.CveContents.CvssV2Score() { - filtered = append(filtered, sc) - } - } + filtered := r.ScannedCves.Find(func(v VulnInfo) bool { + return config.Conf.CvssScoreOver <= v.CveContents.CvssV2Score() + }) + copiedScanResult := r copiedScanResult.ScannedCves = filtered return copiedScanResult @@ -316,61 +314,17 @@ var ChangelogExactMatch = Confidence{95, ChangelogExactMatchStr} var ChangelogLenientMatch = Confidence{50, ChangelogLenientMatchStr} // VulnInfos is VulnInfo list, getter/setter, sortable methods. -type VulnInfos []VulnInfo +type VulnInfos map[string]VulnInfo // Find elements that matches the function passed in argument -func (v *VulnInfos) Find(f func(VulnInfo) bool) (filtered VulnInfos) { - for _, vv := range *v { +func (v VulnInfos) Find(f func(VulnInfo) bool) VulnInfos { + filtered := VulnInfos{} + for _, vv := range v { if f(vv) { - filtered = append(filtered, vv) + filtered[vv.CveID] = vv } } - return -} - -// Get VulnInfo by cveID -func (v *VulnInfos) Get(cveID string) (VulnInfo, bool) { - for _, vv := range *v { - if vv.CveID == cveID { - return vv, true - } - } - return VulnInfo{}, false -} - -// Delete by cveID -func (v *VulnInfos) Delete(cveID string) { - vInfos := *v - for i, vv := range vInfos { - if vv.CveID == cveID { - *v = append(vInfos[:i], vInfos[i+1:]...) - break - } - } -} - -// Insert VulnInfo -func (v *VulnInfos) Insert(vinfo VulnInfo) { - *v = append(*v, vinfo) -} - -// Update VulnInfo -func (v *VulnInfos) Update(vInfo VulnInfo) (ok bool) { - for i, vv := range *v { - if vv.CveID == vInfo.CveID { - (*v)[i] = vInfo - return true - } - } - return false -} - -// Upsert cveInfo -func (v *VulnInfos) Upsert(vInfo VulnInfo) { - ok := v.Update(vInfo) - if !ok { - v.Insert(vInfo) - } + return filtered } // VulnInfo holds a vulnerability information and unsecure packages diff --git a/models/models_test.go b/models/models_test.go index 3a4b428b..7229f0ee 100644 --- a/models/models_test.go +++ b/models/models_test.go @@ -58,46 +58,3 @@ func TestMergeNewVersion(t *testing.T) { t.Errorf("expected %s, actual %s", e, a) } } -func TestVulnInfosSetGet(t *testing.T) { - var test = struct { - in []string - out []string - }{ - []string{ - "CVE1", - "CVE2", - "CVE3", - "CVE1", - "CVE1", - "CVE2", - "CVE3", - }, - []string{ - "CVE1", - "CVE2", - "CVE3", - }, - } - - // var ps packageCveInfos - var ps VulnInfos - for _, cid := range test.in { - ps.Upsert(VulnInfo{CveID: cid}) - } - - if len(test.out) != len(ps) { - t.Errorf("length: expected %d, actual %d", len(test.out), len(ps)) - } - - for i, expectedCid := range test.out { - if expectedCid != ps[i].CveID { - t.Errorf("expected %s, actual %s", expectedCid, ps[i].CveID) - } - } - for _, cid := range test.in { - p, _ := ps.Get(cid) - if p.CveID != cid { - t.Errorf("expected %s, actual %s", cid, p.CveID) - } - } -} diff --git a/oval/debian.go b/oval/debian.go index 598badb4..d8384afd 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -62,10 +62,9 @@ func (o Debian) FillCveInfoFromOvalDB(r *models.ScanResult) error { func (o Debian) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Definition) { ovalContent := *o.convertToModel(definition) ovalContent.Type = models.NewCveContentType(r.Family) - vinfo, ok := r.ScannedCves.Get(definition.Debian.CveID) + vinfo, ok := r.ScannedCves[definition.Debian.CveID] if !ok { - util.Log.Infof("%s is newly detected by OVAL", - definition.Debian.CveID) + util.Log.Infof("%s is newly detected by OVAL", definition.Debian.CveID) vinfo = models.VulnInfo{ CveID: definition.Debian.CveID, Confidence: models.OvalMatch, @@ -73,17 +72,20 @@ func (o Debian) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini CveContents: models.NewCveContents(ovalContent), } } else { - if _, ok := vinfo.CveContents.Get(models.NewCveContentType(r.Family)); !ok { - util.Log.Infof("%s is also detected by OVAL", definition.Debian.CveID) - } else { + cveContents := vinfo.CveContents + if _, ok := vinfo.CveContents.Get(models.NewCveContentType(r.Family)); ok { util.Log.Infof("%s will be updated by OVAL", definition.Debian.CveID) + } else { + util.Log.Infof("%s is also detected by OVAL", definition.Debian.CveID) + cveContents = models.CveContents{} } if vinfo.Confidence.Score < models.OvalMatch.Score { vinfo.Confidence = models.OvalMatch } - vinfo.CveContents.Upsert(ovalContent) + cveContents.Upsert(ovalContent) + vinfo.CveContents = cveContents } - r.ScannedCves.Upsert(vinfo) + r.ScannedCves[definition.Debian.CveID] = vinfo } func (o Debian) convertToModel(def *ovalmodels.Definition) *models.CveContent { diff --git a/oval/redhat.go b/oval/redhat.go index ec2da4c8..980c4f85 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -59,7 +59,7 @@ func (o Redhat) FillCveInfoFromOvalDB(r *models.ScanResult) error { func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Definition) { for _, cve := range definition.Advisory.Cves { ovalContent := *o.convertToModel(cve.CveID, definition) - vinfo, ok := r.ScannedCves.Get(cve.CveID) + vinfo, ok := r.ScannedCves[cve.CveID] if !ok { util.Log.Infof("%s is newly detected by OVAL", cve.CveID) vinfo = models.VulnInfo{ @@ -69,17 +69,21 @@ func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini CveContents: models.NewCveContents(ovalContent), } } else { - if _, ok := vinfo.CveContents.Get(models.RedHat); !ok { - util.Log.Infof("%s is also detected by OVAL", cve.CveID) - } else { + cveContents := vinfo.CveContents + if _, ok := vinfo.CveContents.Get(models.RedHat); ok { util.Log.Infof("%s will be updated by OVAL", cve.CveID) + } else { + util.Log.Infof("%s is also detected by OVAL", cve.CveID) + cveContents = models.CveContents{} } + if vinfo.Confidence.Score < models.OvalMatch.Score { vinfo.Confidence = models.OvalMatch } - vinfo.CveContents.Upsert(ovalContent) + cveContents.Upsert(ovalContent) + vinfo.CveContents = cveContents } - r.ScannedCves.Upsert(vinfo) + r.ScannedCves[cve.CveID] = vinfo } } diff --git a/scan/base.go b/scan/base.go index b2a2da1d..8c8472ff 100644 --- a/scan/base.go +++ b/scan/base.go @@ -291,10 +291,11 @@ func (l *base) convertToModel() models.ScanResult { errs = append(errs, fmt.Sprintf("%s", e)) } + //TODO Remove // Avoid null slice being null in JSON - for i := range l.VulnInfos { - l.VulnInfos[i].NilToEmpty() - } + // for cveID := range l.VulnInfos { + // l.VulnInfos[i].NilToEmpty() + // } return models.ScanResult{ ServerName: l.ServerInfo.ServerName, diff --git a/scan/debian.go b/scan/debian.go index fc915fd3..ddcef4cd 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -264,8 +264,7 @@ func (o *debian) aptGetUpdate() error { return nil } -func (o *debian) scanUnsecurePackages(upgradable models.Packages) ([]models.VulnInfo, error) { - +func (o *debian) scanUnsecurePackages(upgradable models.Packages) (models.VulnInfos, error) { o.aptGetUpdate() // Setup changelog cache @@ -491,13 +490,13 @@ func (o *debian) scanVulnInfos(upgradablePacks models.Packages, meta *cache.Meta cveIDs = append(cveIDs, k) } o.log.Debugf("%d Cves are found. cves: %v", len(cveIDs), cveIDs) - var vinfos models.VulnInfos + vinfos := models.VulnInfos{} for cveID, names := range cvePackages { - vinfos = append(vinfos, models.VulnInfo{ + vinfos[cveID.CveID] = models.VulnInfo{ CveID: cveID.CveID, Confidence: cveID.Confidence, PackageNames: names, - }) + } } // Update meta package information of changelog cache to the latest one. diff --git a/scan/freebsd.go b/scan/freebsd.go index 23cf06fa..03fb08d7 100644 --- a/scan/freebsd.go +++ b/scan/freebsd.go @@ -85,7 +85,7 @@ func (o *bsd) scanPackages() error { } o.setPackages(packs) - var vinfos []models.VulnInfo + var vinfos models.VulnInfos if vinfos, err = o.scanUnsecurePackages(); err != nil { o.log.Errorf("Failed to scan vulnerable packages") return err @@ -103,7 +103,7 @@ func (o *bsd) scanInstalledPackages() (models.Packages, error) { return o.parsePkgVersion(r.Stdout), nil } -func (o *bsd) scanUnsecurePackages() (vulnInfos []models.VulnInfo, err error) { +func (o *bsd) scanUnsecurePackages() (models.VulnInfos, error) { const vulndbPath = "/tmp/vuln.db" cmd := "rm -f " + vulndbPath r := o.exec(cmd, noSudo) @@ -118,7 +118,7 @@ func (o *bsd) scanUnsecurePackages() (vulnInfos []models.VulnInfo, err error) { } if r.ExitStatus == 0 { // no vulnerabilities - return []models.VulnInfo{}, nil + return nil, nil } var packAdtRslt []pkgAuditResult @@ -149,14 +149,15 @@ func (o *bsd) scanUnsecurePackages() (vulnInfos []models.VulnInfo, err error) { } } - for k := range cveIDAdtMap { + vinfos := models.VulnInfos{} + for cveID := range cveIDAdtMap { packs := models.Packages{} - for _, r := range cveIDAdtMap[k] { + for _, r := range cveIDAdtMap[cveID] { packs[r.pack.Name] = r.pack } disAdvs := []models.DistroAdvisory{} - for _, r := range cveIDAdtMap[k] { + for _, r := range cveIDAdtMap[cveID] { disAdvs = append(disAdvs, models.DistroAdvisory{ AdvisoryID: r.vulnIDCveIDs.vulnID, }) @@ -166,14 +167,14 @@ func (o *bsd) scanUnsecurePackages() (vulnInfos []models.VulnInfo, err error) { for name := range packs { names = append(names, name) } - vulnInfos = append(vulnInfos, models.VulnInfo{ - CveID: k, + vinfos[cveID] = models.VulnInfo{ + CveID: cveID, PackageNames: names, DistroAdvisories: disAdvs, Confidence: models.PkgAuditMatch, - }) + } } - return + return vinfos, nil } func (o *bsd) parsePkgVersion(stdout string) models.Packages { diff --git a/scan/redhat.go b/scan/redhat.go index 7c28b933..013f64a7 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -240,7 +240,7 @@ func (o *redhat) scanPackages() error { } o.setPackages(models.NewPackages(packs...)) - var vinfos []models.VulnInfo + var vinfos models.VulnInfos if vinfos, err = o.scanVulnInfos(); err != nil { o.log.Errorf("Failed to scan vulnerable packages") return err @@ -292,7 +292,7 @@ func (o *redhat) parseScannedPackagesLine(line string) (models.Package, error) { }, nil } -func (o *redhat) scanVulnInfos() ([]models.VulnInfo, error) { +func (o *redhat) scanVulnInfos() (models.VulnInfos, error) { if o.Distro.Family != "centos" { // Amazon, RHEL, Oracle Linux has yum updateinfo as default // yum updateinfo can collenct vendor advisory information. @@ -423,7 +423,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er } } - vinfos := []models.VulnInfo{} + vinfos := models.VulnInfos{} for cveID, packs := range cveIDPackages { names := []string{} for name := range packs { @@ -431,11 +431,11 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er } // Amazon, RHEL do not use this method, so VendorAdvisory do not set. - vinfos = append(vinfos, models.VulnInfo{ + vinfos[cveID] = models.VulnInfo{ CveID: cveID, PackageNames: names, Confidence: models.ChangelogExactMatch, - }) + } } return vinfos, nil } @@ -741,36 +741,29 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, vinfos := models.VulnInfos{} for _, advIDCveIDs := range advisoryCveIDsList { for _, cveID := range advIDCveIDs.CveIDs { - found := false - for i, p := range vinfos { - if cveID == p.CveID { - advAppended := append(p.DistroAdvisories, advIDCveIDs.DistroAdvisory) - vinfos[i].DistroAdvisories = advAppended + vinfo, found := vinfos[cveID] + if found { + advAppended := append(vinfo.DistroAdvisories, advIDCveIDs.DistroAdvisory) + vinfo.DistroAdvisories = advAppended - packs := dict[advIDCveIDs.DistroAdvisory.AdvisoryID] - for _, pack := range packs { - vinfos[i].PackageNames = append(vinfos[i].PackageNames, pack.Name) - } - found = true - break + packs := dict[advIDCveIDs.DistroAdvisory.AdvisoryID] + for _, pack := range packs { + vinfo.PackageNames = append(vinfo.PackageNames, pack.Name) } - } - - if !found { + } else { names := []string{} packs := dict[advIDCveIDs.DistroAdvisory.AdvisoryID] for _, pack := range packs { names = append(names, pack.Name) } - cpinfo := models.VulnInfo{ + vinfo = models.VulnInfo{ CveID: cveID, DistroAdvisories: []models.DistroAdvisory{advIDCveIDs.DistroAdvisory}, PackageNames: names, Confidence: models.YumUpdateSecurityMatch, } - vinfos = append(vinfos, cpinfo) } - + vinfos[cveID] = vinfo } } return vinfos, nil diff --git a/scan/serverapi.go b/scan/serverapi.go index 852e5375..1ebf37ae 100644 --- a/scan/serverapi.go +++ b/scan/serverapi.go @@ -69,7 +69,7 @@ func (p *osPackages) setPackages(pi models.Packages) { p.Packages = pi } -func (p *osPackages) setVulnInfos(vi []models.VulnInfo) { +func (p *osPackages) setVulnInfos(vi models.VulnInfos) { p.VulnInfos = vi } From dd5a7920e58b73fbfed4ad231a776cab6602de01 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Tue, 9 May 2017 21:52:14 +0900 Subject: [PATCH 028/113] Add JSON Version --- models/models.go | 19 +++++++++++-------- scan/base.go | 1 + 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/models/models.go b/models/models.go index 92f1db54..5efc7028 100644 --- a/models/models.go +++ b/models/models.go @@ -26,6 +26,9 @@ import ( cvedict "github.com/kotakanbe/go-cve-dictionary/models" ) +// JSONVersion is JSON Version +const JSONVersion = "0.3.0" + // ScanResults is slice of ScanResult. type ScanResults []ScanResult @@ -50,14 +53,14 @@ type ScanResults []ScanResult // ScanResult has the result of scanned CVE information. type ScanResult struct { - ScannedAt time.Time - - Lang string - ServerName string // TOML Section key - Family string - Release string - Container Container - Platform Platform + ScannedAt time.Time + JSONVersion string + Lang string + ServerName string // TOML Section key + Family string + Release string + Container Container + Platform Platform // Scanned Vulns by SSH scan + CPE + OVAL ScannedCves VulnInfos diff --git a/scan/base.go b/scan/base.go index 8c8472ff..ba4dfe6d 100644 --- a/scan/base.go +++ b/scan/base.go @@ -298,6 +298,7 @@ func (l *base) convertToModel() models.ScanResult { // } return models.ScanResult{ + JSONVersion: models.JSONVersion, ServerName: l.ServerInfo.ServerName, ScannedAt: time.Now(), Family: l.Distro.Family, From b285cb0e57e46dea7582f9a1a57e0dfc1488ac12 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Wed, 10 May 2017 11:43:10 +0900 Subject: [PATCH 029/113] Remove CRUD funcs of CveContents --- commands/report.go | 2 +- commands/util.go | 4 ++-- models/models.go | 44 ++++---------------------------------------- oval/debian.go | 5 +++-- oval/redhat.go | 4 ++-- 5 files changed, 12 insertions(+), 47 deletions(-) diff --git a/commands/report.go b/commands/report.go index 76a0bae5..96223beb 100644 --- a/commands/report.go +++ b/commands/report.go @@ -513,7 +513,7 @@ func fillCveDetail(r *models.ScanResult) error { } for _, con := range []models.CveContent{*nvd, *jvn} { if !con.Empty() { - vinfo.CveContents.Upsert(con) + vinfo.CveContents[con.Type] = con } } r.ScannedCves[cveID] = vinfo diff --git a/commands/util.go b/commands/util.go index 97bf0464..73d73001 100644 --- a/commands/util.go +++ b/commands/util.go @@ -239,7 +239,7 @@ func isCveInfoUpdated(cveID string, previous, current models.ScanResult) bool { for _, c := range previous.ScannedCves { if cveID == c.CveID { for _, cType := range cTypes { - content, _ := c.CveContents.Get(cType) + content, _ := c.CveContents[cType] prevLastModified[cType] = content.LastModified } break @@ -250,7 +250,7 @@ func isCveInfoUpdated(cveID string, previous, current models.ScanResult) bool { for _, c := range current.ScannedCves { if cveID == c.CveID { for _, cType := range cTypes { - content, _ := c.CveContents.Get(cType) + content, _ := c.CveContents[cType] curLastModified[cType] = content.LastModified } break diff --git a/models/models.go b/models/models.go index 5efc7028..4030475f 100644 --- a/models/models.go +++ b/models/models.go @@ -412,50 +412,14 @@ func NewCveContents(conts ...CveContent) CveContents { return m } -// Get CveContent by cveID -// TODO Pointer -func (v CveContents) Get(typestr CveContentType) (CveContent, bool) { - if vv, ok := v[typestr]; ok { - return vv, true - } - return CveContent{}, false -} - -// Delete by cveID -func (v CveContents) Delete(typestr CveContentType) { - delete(v, typestr) -} - -// Insert CveContent -func (v CveContents) Insert(cont CveContent) { - v[cont.Type] = cont -} - -// Update VulnInfo -func (v CveContents) Update(cont CveContent) (ok bool) { - if _, ok := v[cont.Type]; ok { - v[cont.Type] = cont - return true - } - return false -} - -// Upsert CveContent -func (v CveContents) Upsert(cont CveContent) { - ok := v.Update(cont) - if !ok { - v.Insert(cont) - } -} - // CvssV2Score returns CVSS V2 Score func (v CveContents) CvssV2Score() float64 { //TODO - if cont, found := v.Get(NVD); found { + if cont, found := v[NVD]; found { return cont.Cvss2Score - } else if cont, found := v.Get(JVN); found { + } else if cont, found := v[JVN]; found { return cont.Cvss2Score - } else if cont, found := v.Get(RedHat); found { + } else if cont, found := v[RedHat]; found { return cont.Cvss2Score } return -1.1 @@ -463,7 +427,7 @@ func (v CveContents) CvssV2Score() float64 { // CvssV3Score returns CVSS V2 Score func (v CveContents) CvssV3Score() float64 { - if cont, found := v.Get(RedHat); found { + if cont, found := v[RedHat]; found { return cont.Cvss3Score } return -1.1 diff --git a/oval/debian.go b/oval/debian.go index d8384afd..ac108d9d 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -73,7 +73,8 @@ func (o Debian) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini } } else { cveContents := vinfo.CveContents - if _, ok := vinfo.CveContents.Get(models.NewCveContentType(r.Family)); ok { + ctype := models.NewCveContentType(r.Family) + if _, ok := vinfo.CveContents[ctype]; ok { util.Log.Infof("%s will be updated by OVAL", definition.Debian.CveID) } else { util.Log.Infof("%s is also detected by OVAL", definition.Debian.CveID) @@ -82,7 +83,7 @@ func (o Debian) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini if vinfo.Confidence.Score < models.OvalMatch.Score { vinfo.Confidence = models.OvalMatch } - cveContents.Upsert(ovalContent) + cveContents[ctype] = ovalContent vinfo.CveContents = cveContents } r.ScannedCves[definition.Debian.CveID] = vinfo diff --git a/oval/redhat.go b/oval/redhat.go index 980c4f85..009324c9 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -70,7 +70,7 @@ func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini } } else { cveContents := vinfo.CveContents - if _, ok := vinfo.CveContents.Get(models.RedHat); ok { + if _, ok := vinfo.CveContents[models.RedHat]; ok { util.Log.Infof("%s will be updated by OVAL", cve.CveID) } else { util.Log.Infof("%s is also detected by OVAL", cve.CveID) @@ -80,7 +80,7 @@ func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini if vinfo.Confidence.Score < models.OvalMatch.Score { vinfo.Confidence = models.OvalMatch } - cveContents.Upsert(ovalContent) + cveContents[models.RedHat] = ovalContent vinfo.CveContents = cveContents } r.ScannedCves[cve.CveID] = vinfo From 3be11cf52f1a6ae2fe891c801c98d069ab7d22bb Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Mon, 15 May 2017 23:39:28 +0900 Subject: [PATCH 030/113] Implement format-short-text --- Gopkg.lock | 28 +-- Gopkg.toml | 2 +- commands/report.go | 10 +- models/models.go | 517 ++++++++++++++++++++++++++++++++++++++------ oval/debian.go | 80 +++++-- oval/redhat.go | 55 +++-- oval/redhat_test.go | 4 +- report/email.go | 6 +- report/slack.go | 5 +- report/util.go | 173 ++++++++------- scan/debian.go | 1 + scan/redhat.go | 2 + 12 files changed, 671 insertions(+), 212 deletions(-) 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 } From 4fcdea3ccbf31649ea48c4ffaf80ebc681945c38 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Wed, 17 May 2017 10:18:13 +0900 Subject: [PATCH 031/113] 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 "" +} From 8b6c841b1e008312ddc93cc10d87b023a0cc5042 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Wed, 17 May 2017 18:15:53 +0900 Subject: [PATCH 032/113] Fix TestCase --- scan/redhat_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scan/redhat_test.go b/scan/redhat_test.go index b506b23a..f3bf219a 100644 --- a/scan/redhat_test.go +++ b/scan/redhat_test.go @@ -444,6 +444,8 @@ Description : kernel-uek t.Errorf("[%d] Alas is not same. \nexpected: %s\nactual: %s", i, tt.out[i].DistroAdvisory, advisoryCveIDs.DistroAdvisory) } + sort.Strings(tt.out[i].CveIDs) + sort.Strings(advisoryCveIDs.CveIDs) if !reflect.DeepEqual(tt.out[i].CveIDs, advisoryCveIDs.CveIDs) { t.Errorf("[%d] Alas is not same. \nexpected: %s\nactual: %s", i, tt.out[i].CveIDs, advisoryCveIDs.CveIDs) From 7f8c975bd72a4c9190ffd84117482e2ea7c30f90 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Thu, 18 May 2017 15:50:25 +0900 Subject: [PATCH 033/113] Avoid concurrent Map writes --- scan/debian.go | 92 +++++++++++++++++++++------------------------ scan/debian_test.go | 10 ++--- scan/redhat.go | 3 -- 3 files changed, 48 insertions(+), 57 deletions(-) diff --git a/scan/debian.go b/scan/debian.go index 28498cb8..4ce0b356 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -413,7 +413,7 @@ type DetectedCveID struct { func (o *debian) scanVulnInfos(upgradablePacks models.Packages, meta *cache.Meta) (models.VulnInfos, error) { type response struct { - packName string + pack *models.Package DetectedCveIDs []DetectedCveID } resChan := make(chan response, len(upgradablePacks)) @@ -439,18 +439,18 @@ func (o *debian) scanVulnInfos(upgradablePacks models.Packages, meta *cache.Meta func(p models.Package) { changelog := o.getChangelogCache(meta, p) if 0 < len(changelog) { - cveIDs, _ := o.getCveIDsFromChangelog(changelog, p.Name, p.Version) - resChan <- response{p.Name, cveIDs} + cveIDs, pack := o.getCveIDsFromChangelog(changelog, p.Name, p.Version) + resChan <- response{pack, cveIDs} return } // if the changelog is not in cache or failed to get from local cache, // get the changelog of the package via internet. // After that, store it in the cache. - if cveIDs, err := o.scanPackageCveIDs(p); err != nil { + if cveIDs, pack, err := o.scanPackageCveIDs(p); err != nil { errChan <- err } else { - resChan <- response{p.Name, cveIDs} + resChan <- response{pack, cveIDs} } }(pack) } @@ -463,18 +463,19 @@ func (o *debian) scanVulnInfos(upgradablePacks models.Packages, meta *cache.Meta for i := 0; i < len(upgradablePacks); i++ { select { case response := <-resChan: + o.Packages[response.pack.Name] = *response.pack cves := response.DetectedCveIDs for _, cve := range cves { packNames, ok := cvePackages[cve] if ok { - packNames = append(packNames, response.packName) + packNames = append(packNames, response.pack.Name) } else { - packNames = []string{response.packName} + packNames = []string{response.pack.Name} } cvePackages[cve] = packNames } o.log.Infof("(%d/%d) Scanned %s: %s", - i+1, len(upgradablePacks), response.packName, cves) + i+1, len(upgradablePacks), response.pack.Name, cves) case err := <-errChan: errs = append(errs, err) case <-timeout: @@ -536,7 +537,7 @@ func (o *debian) getChangelogCache(meta *cache.Meta, pack models.Package) string return changelog } -func (o *debian) scanPackageCveIDs(pack models.Package) ([]DetectedCveID, error) { +func (o *debian) scanPackageCveIDs(pack models.Package) ([]DetectedCveID, *models.Package, error) { cmd := "" switch o.Distro.Family { case "ubuntu", "raspbian": @@ -550,43 +551,43 @@ func (o *debian) scanPackageCveIDs(pack models.Package) ([]DetectedCveID, error) if !r.isSuccess() { o.log.Warnf("Failed to SSH: %s", r) // Ignore this Error. - return nil, nil + return nil, nil, nil } stdout := strings.Replace(r.Stdout, "\r", "", -1) - cveIDs, clog := o.getCveIDsFromChangelog( - stdout, pack.Name, pack.Version) + cveIDs, clogFilledPack := o.getCveIDsFromChangelog(stdout, pack.Name, pack.Version) - if clog.Method != models.FailedToGetChangelog { - err := cache.DB.PutChangelog(o.getServerInfo().GetServerName(), pack.Name, clog.Contents) + if clogFilledPack.Changelog.Method != models.FailedToGetChangelog { + err := cache.DB.PutChangelog( + o.getServerInfo().GetServerName(), pack.Name, pack.Changelog.Contents) if err != nil { - return nil, fmt.Errorf("Failed to put changelog into cache") + return nil, nil, fmt.Errorf("Failed to put changelog into cache") } } // No error will be returned. Only logging. - return cveIDs, nil + return cveIDs, clogFilledPack, nil } // Debian Version Numbers // https://readme.phys.ethz.ch/documentation/debian_version_numbers/ +// TODO Changed to parse and compare versions func (o *debian) getCveIDsFromChangelog( - changelog, name, ver string) ([]DetectedCveID, models.Changelog) { + changelog, name, ver string) ([]DetectedCveID, *models.Package) { - if cveIDs, relevant, err := o.parseChangelog( + if cveIDs, pack, err := o.parseChangelog( changelog, name, ver, models.ChangelogExactMatch); err == nil { - return cveIDs, relevant + return cveIDs, pack } var verAfterColon string - var err error splittedByColon := strings.Split(ver, ":") if 1 < len(splittedByColon) { verAfterColon = splittedByColon[1] - if cveIDs, relevant, err := o.parseChangelog( + if cveIDs, pack, err := o.parseChangelog( changelog, name, verAfterColon, models.ChangelogLenientMatch); err == nil { - return cveIDs, relevant + return cveIDs, pack } } @@ -601,44 +602,40 @@ func (o *debian) getCveIDsFromChangelog( for _, d := range delim { ss := strings.Split(ver, d) if 1 < len(ss) { - if cveIDs, relevant, err := o.parseChangelog( + if cveIDs, pack, err := o.parseChangelog( changelog, name, ss[0], models.ChangelogLenientMatch); err == nil { - return cveIDs, relevant + return cveIDs, pack } } ss = strings.Split(verAfterColon, d) if 1 < len(ss) { - if cveIDs, relevant, err := o.parseChangelog( + if cveIDs, pack, err := o.parseChangelog( changelog, name, ss[0], models.ChangelogLenientMatch); err == nil { - return cveIDs, relevant + return cveIDs, pack } } } // Only logging the error. - o.log.Error(err) - - pack := o.Packages[name] - pack.Changelog = models.Changelog{ - Contents: "", - Method: models.FailedToFindVersionInChangelog, - } - //TODO Mutex - o.Packages[name] = pack + o.log.Warnf("Failed to find the version in changelog: %s-%s", name, ver) + o.log.Debugf("Changelog of : %s-%s", name, ver, changelog) // If the version is not in changelog, return entire changelog to put into cache - return []DetectedCveID{}, models.Changelog{ + pack := o.Packages[name] + pack.Changelog = models.Changelog{ Contents: changelog, Method: models.FailedToFindVersionInChangelog, } + + return []DetectedCveID{}, &pack } var cveRe = regexp.MustCompile(`(CVE-\d{4}-\d{4,})`) // Collect CVE-IDs included in the changelog. // The version which specified in argument(versionOrLater) is excluded. -func (o *debian) parseChangelog(changelog, name, ver string, confidence models.Confidence) ([]DetectedCveID, models.Changelog, error) { +func (o *debian) parseChangelog(changelog, name, ver string, confidence models.Confidence) ([]DetectedCveID, *models.Package, error) { buf, cveIDs := []string{}, []string{} stopRe := regexp.MustCompile(fmt.Sprintf(`\(%s\)`, regexp.QuoteMeta(ver))) stopLineFound := false @@ -656,32 +653,29 @@ func (o *debian) parseChangelog(changelog, name, ver string, confidence models.C } } if !stopLineFound { - return nil, models.Changelog{ - Contents: "", - Method: models.FailedToFindVersionInChangelog, - }, fmt.Errorf( - "Failed to scan CVE IDs. The version is not in changelog. name: %s, version: %s", - name, - ver, - ) + pack := o.Packages[name] + pack.Changelog = models.Changelog{ + Contents: "", + Method: models.FailedToFindVersionInChangelog, + } + return nil, &pack, fmt.Errorf( + "Failed to scan CVE IDs. The version is not in changelog. name: %s, version: %s", + name, ver) } clog := models.Changelog{ Contents: strings.Join(buf, "\n"), Method: string(confidence.DetectionMethod), } - pack := o.Packages[name] pack.Changelog = clog - // TODO Mutex - o.Packages[name] = pack cves := []DetectedCveID{} for _, id := range cveIDs { cves = append(cves, DetectedCveID{id, confidence}) } - return cves, clog, nil + return cves, &pack, nil } func (o *debian) splitAptCachePolicy(stdout string) map[string]string { diff --git a/scan/debian_test.go b/scan/debian_test.go index 04429aa8..0fb48040 100644 --- a/scan/debian_test.go +++ b/scan/debian_test.go @@ -273,7 +273,7 @@ systemd (228-4) unstable; urgency=medium`, d := newDebian(config.ServerInfo{}) d.Distro.Family = "ubuntu" for i, tt := range tests { - aCveIDs, aClog := d.getCveIDsFromChangelog(tt.in[2], tt.in[0], tt.in[1]) + aCveIDs, aPack := d.getCveIDsFromChangelog(tt.in[2], tt.in[0], tt.in[1]) if len(aCveIDs) != len(tt.cveIDs) { t.Errorf("[%d] Len of return array are'nt same. expected %#v, actual %#v", i, tt.cveIDs, aCveIDs) t.Errorf(pp.Sprintf("%s", tt.in)) @@ -285,12 +285,12 @@ systemd (228-4) unstable; urgency=medium`, } } - if aClog.Contents != tt.changelog.Contents { - t.Errorf(pp.Sprintf("expected: %s, actual: %s", tt.changelog.Contents, aClog.Contents)) + if aPack.Changelog.Contents != tt.changelog.Contents { + t.Errorf(pp.Sprintf("expected: %s, actual: %s", tt.changelog.Contents, aPack.Changelog.Contents)) } - if aClog.Method != tt.changelog.Method { - t.Errorf(pp.Sprintf("expected: %s, actual: %s", tt.changelog.Method, aClog.Method)) + if aPack.Changelog.Method != tt.changelog.Method { + t.Errorf(pp.Sprintf("expected: %s, actual: %s", tt.changelog.Method, aPack.Changelog.Method)) } } } diff --git a/scan/redhat.go b/scan/redhat.go index ef1c0cd9..5a6ffd4f 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -326,7 +326,6 @@ 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 @@ -356,7 +355,6 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er Contents: *clog, Method: models.ChangelogExactMatchStr, } - //TODO Mutex o.Packages[p.Name] = p break } @@ -736,7 +734,6 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, if err != nil { return nil, err } - // pp.Println(advisoryCveIDsList) // All information collected. // Convert to VulnInfos. From 9128e2748bfe84455f7ddf5b56d19aa3dc5f8c43 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Thu, 18 May 2017 16:04:44 +0900 Subject: [PATCH 034/113] Refactoring --- scan/base.go | 14 +++++++------- scan/unknownDistro.go | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/scan/base.go b/scan/base.go index ba4dfe6d..0ddac031 100644 --- a/scan/base.go +++ b/scan/base.go @@ -47,7 +47,7 @@ func (l *base) setServerInfo(c config.ServerInfo) { l.ServerInfo = c } -func (l base) getServerInfo() config.ServerInfo { +func (l *base) getServerInfo() config.ServerInfo { return l.ServerInfo } @@ -63,7 +63,7 @@ func (l *base) setDistro(fam, rel string) { l.setServerInfo(s) } -func (l base) getDistro() config.Distro { +func (l *base) getDistro() config.Distro { return l.Distro } @@ -71,11 +71,11 @@ func (l *base) setPlatform(p models.Platform) { l.Platform = p } -func (l base) getPlatform() models.Platform { +func (l *base) getPlatform() models.Platform { return l.Platform } -func (l base) allContainers() (containers []config.Container, err error) { +func (l *base) allContainers() (containers []config.Container, err error) { switch l.ServerInfo.Containers.Type { case "", "docker": stdout, err := l.dockerPs("-a --format '{{.ID}} {{.Names}} {{.Image}}'") @@ -212,7 +212,7 @@ func (l *base) detectPlatform() { return } -func (l base) detectRunningOnAws() (ok bool, instanceID string, err error) { +func (l *base) detectRunningOnAws() (ok bool, instanceID string, err error) { if r := l.exec("type curl", noSudo); r.isSuccess() { cmd := "curl --max-time 1 --retry 3 --noproxy 169.254.169.254 http://169.254.169.254/latest/meta-data/instance-id" r := l.exec(cmd, noSudo) @@ -260,7 +260,7 @@ func (l base) detectRunningOnAws() (ok bool, instanceID string, err error) { // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/resource-ids.html var awsInstanceIDPattern = regexp.MustCompile(`^i-[0-9a-f]+$`) -func (l base) isAwsInstanceID(str string) bool { +func (l *base) isAwsInstanceID(str string) bool { return awsInstanceIDPattern.MatchString(str) } @@ -316,6 +316,6 @@ func (l *base) setErrs(errs []error) { l.errs = errs } -func (l base) getErrs() []error { +func (l *base) getErrs() []error { return l.errs } diff --git a/scan/unknownDistro.go b/scan/unknownDistro.go index 30b0ab76..cceaef35 100644 --- a/scan/unknownDistro.go +++ b/scan/unknownDistro.go @@ -26,7 +26,7 @@ func (o *unknown) checkIfSudoNoPasswd() error { return nil } -func (o unknown) checkDependencies() error { +func (o *unknown) checkDependencies() error { return nil } From d9bc4499a49e0bc109ea0613da9bd3ada65f57aa Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Sun, 21 May 2017 11:45:21 +0900 Subject: [PATCH 035/113] Refactoring --- GNUmakefile | 2 +- commands/history.go | 6 +- commands/report.go | 183 +----- commands/tui.go | 67 +- commands/util.go | 296 --------- commands/util_test.go | 326 ---------- config/config.go | 2 + models/cvecontents.go | 527 ++++++++++++++++ models/cvecontents_test.go | 17 + models/models.go | 1012 ------------------------------ models/models_test.go | 42 -- models/packages.go | 127 ++++ models/packages_test.go | 59 ++ models/scanresults.go | 297 +++++++++ models/scanresults_test.go | 17 + models/vulninfos.go | 150 +++++ models/vulninfos_test.go | 17 + {cveapi => report}/cve_client.go | 2 +- report/report.go | 204 ++++++ report/report_test.go | 1 + report/util.go | 261 ++++++++ report/util_test.go | 327 ++++++++++ util/util.go | 29 +- 23 files changed, 2086 insertions(+), 1885 deletions(-) create mode 100644 models/cvecontents.go create mode 100644 models/cvecontents_test.go create mode 100644 models/packages.go create mode 100644 models/packages_test.go create mode 100644 models/scanresults.go create mode 100644 models/scanresults_test.go create mode 100644 models/vulninfos.go create mode 100644 models/vulninfos_test.go rename {cveapi => report}/cve_client.go (99%) create mode 100644 report/report.go create mode 100644 report/report_test.go create mode 100644 report/util_test.go diff --git a/GNUmakefile b/GNUmakefile index 3cc9b7ae..d44cb749 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -15,7 +15,7 @@ clean SRCS = $(shell git ls-files '*.go') -PKGS = ./. ./config ./models ./report ./cveapi ./scan ./util ./commands ./cache +PKGS = ./. ./cache ./commands ./config ./models ./oval ./report ./scan ./util VERSION := $(shell git describe --tags --abbrev=0) REVISION := $(shell git rev-parse --short HEAD) LDFLAGS := -X 'main.version=$(VERSION)' \ diff --git a/commands/history.go b/commands/history.go index 1c6acf4b..2a6e02fb 100644 --- a/commands/history.go +++ b/commands/history.go @@ -27,6 +27,7 @@ import ( "strings" c "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/report" "github.com/google/subcommands" ) @@ -68,9 +69,8 @@ func (p *HistoryCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{ c.Conf.DebugSQL = p.debugSQL c.Conf.ResultsDir = p.resultsDir - var err error - var dirs jsonDirs - if dirs, err = lsValidJSONDirs(); err != nil { + dirs, err := report.ListValidJSONDirs() + if err != nil { return subcommands.ExitFailure } for _, d := range dirs { diff --git a/commands/report.go b/commands/report.go index 1fe6c93b..442b936c 100644 --- a/commands/report.go +++ b/commands/report.go @@ -25,9 +25,7 @@ import ( "path/filepath" c "github.com/future-architect/vuls/config" - "github.com/future-architect/vuls/cveapi" "github.com/future-architect/vuls/models" - "github.com/future-architect/vuls/oval" "github.com/future-architect/vuls/report" "github.com/future-architect/vuls/util" "github.com/google/subcommands" @@ -290,6 +288,8 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} c.Conf.Lang = p.lang c.Conf.ResultsDir = p.resultsDir + c.Conf.RefreshCve = p.refreshCve + c.Conf.Diff = p.diff c.Conf.CveDBType = p.cvedbtype c.Conf.CveDBPath = p.cvedbpath c.Conf.CveDBURL = p.cvedbURL @@ -314,9 +314,9 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} var dir string var err error if p.diff { - dir, err = jsonDir([]string{}) + dir, err = report.JSONDir([]string{}) } else { - dir, err = jsonDir(f.Args()) + dir, err = report.JSONDir(f.Args()) } if err != nil { util.Log.Errorf("Failed to read from JSON: %s", err) @@ -385,7 +385,7 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} if !c.Conf.ValidateOnReport() { return subcommands.ExitUsageError } - if ok, err := cveapi.CveClient.CheckHealth(); !ok { + if ok, err := report.CveClient.CheckHealth(); !ok { util.Log.Errorf("CVE HTTP server is not running. err: %s", err) util.Log.Errorf("Run go-cve-dictionary as server mode before reporting or run with --cvedb-path option") return subcommands.ExitFailure @@ -398,90 +398,36 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} } } - rs, err := loadScanResults(dir) - if err != nil { + var res models.ScanResults + if res, err = report.LoadScanResults(dir); err != nil { util.Log.Error(err) return subcommands.ExitFailure } util.Log.Infof("Loaded: %s", dir) - var results []models.ScanResult - for _, r := range rs { - if p.refreshCve || needToRefreshCve(r) { - util.Log.Debugf("need to refresh") - if c.Conf.CveDBType == "sqlite3" && c.Conf.CveDBURL == "" { - if _, err := os.Stat(c.Conf.CveDBPath); os.IsNotExist(err) { - util.Log.Errorf("SQLite3 DB(CVE-Dictionary) is not exist: %s", - c.Conf.CveDBPath) - return subcommands.ExitFailure - } - } - - if err := fillCveInfoFromOvalDB(&r); err != nil { - util.Log.Errorf("Failed to fill OVAL information: %s", err) - return subcommands.ExitFailure - } - - if err := fillCveInfoFromCveDB(&r); err != nil { - util.Log.Errorf("Failed to fill CVE information: %s", err) - return subcommands.ExitFailure - } - - r.Lang = c.Conf.Lang - if err := overwriteJSONFile(dir, r); err != nil { - util.Log.Errorf("Failed to write JSON: %s", err) - return subcommands.ExitFailure - } - results = append(results, r) - } else { - util.Log.Debugf("no need to refresh") - results = append(results, r) - } + //TODO dir + if res, err = report.FillCveInfos(res, dir); err != nil { + util.Log.Error(err) + return subcommands.ExitFailure } - if p.diff { - previous, err := loadPrevious(results) - if err != nil { - util.Log.Error(err) - return subcommands.ExitFailure - } - - diff, err := diff(results, previous) - if err != nil { - util.Log.Error(err) - return subcommands.ExitFailure - } - results = []models.ScanResult{} - for _, r := range diff { - if err := fillCveDetail(&r); err != nil { - util.Log.Error(err) - return subcommands.ExitFailure - } - results = append(results, r) - } - } - - var res models.ScanResults - for _, r := range results { - res = append(res, r.FilterByCvssOver(c.Conf.CvssScoreOver)) - - // TODO Add sort function to ScanResults - - //remove - // for _, vuln := range r.ScannedCves { - // // if _, ok := vuln.CveContents.Get(models.NewCveContentType(r.Family)); !ok { - // // pp.Printf("not in oval: %s %f\n%v\n", - // // vuln.CveID, vuln.CveContents.CvssV2Score(), vuln.Packages) - // // } else { - // // fmt.Printf(" in oval: %s %f\n", - // // vuln.CveID, vuln.CveContents.CvssV2Score()) - // // } - // // if vuln.CveContents.CvssV2Score() < 0.1 && - // // vuln.CveContents.CvssV3Score() < 0.1 { - // // pp.Println(vuln) - // // } - // } - } + // TODO Filter, Sort + // TODO Add sort function to ScanResults + //remove + // for _, vuln := range r.ScannedCves { + // // if _, ok := vuln.CveContents.Get(models.NewCveContentType(r.Family)); !ok { + // // pp.Printf("not in oval: %s %f\n%v\n", + // // vuln.CveID, vuln.CveContents.CvssV2Score(), vuln.Packages) + // // } else { + // // fmt.Printf(" in oval: %s %f\n", + // // vuln.CveID, vuln.CveContents.CvssV2Score()) + // // } + // // if vuln.CveContents.CvssV2Score() < 0.1 && + // // vuln.CveContents.CvssV3Score() < 0.1 { + // // pp.Println(vuln) + // // } + // } + // } for _, w := range reports { if err := w.Write(res...); err != nil { @@ -491,76 +437,3 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} } return subcommands.ExitSuccess } - -// fillCveDetail fetches NVD, JVN from CVE Database, and then set to fields. -func fillCveDetail(r *models.ScanResult) error { - var cveIDs []string - for _, v := range r.ScannedCves { - cveIDs = append(cveIDs, v.CveID) - } - - ds, err := cveapi.CveClient.FetchCveDetails(cveIDs) - if err != nil { - return err - } - for _, d := range ds { - nvd := r.ConvertNvdToModel(d.CveID, d.Nvd) - jvn := r.ConvertJvnToModel(d.CveID, d.Jvn) - for cveID, vinfo := range r.ScannedCves { - if vinfo.CveID == d.CveID { - if vinfo.CveContents == nil { - vinfo.CveContents = models.CveContents{} - } - for _, con := range []models.CveContent{*nvd, *jvn} { - if !con.Empty() { - vinfo.CveContents[con.Type] = con - } - } - r.ScannedCves[cveID] = vinfo - break - } - } - } - //TODO Remove - // sort.Slice(r.ScannedCves, func(i, j int) bool { - // if r.ScannedCves[j].CveContents.CvssV2Score() == r.ScannedCves[i].CveContents.CvssV2Score() { - // return r.ScannedCves[j].CveContents.CvssV2Score() < r.ScannedCves[i].CveContents.CvssV2Score() - // } - // return r.ScannedCves[j].CveContents.CvssV2Score() < r.ScannedCves[i].CveContents.CvssV2Score() - // }) - return nil -} - -func fillCveInfoFromCveDB(r *models.ScanResult) error { - sInfo := c.Conf.Servers[r.ServerName] - if err := fillVulnByCpeNames(sInfo.CpeNames, r.ScannedCves); err != nil { - return err - } - if err := fillCveDetail(r); err != nil { - return err - } - return nil -} - -func fillCveInfoFromOvalDB(r *models.ScanResult) error { - var ovalClient oval.Client - switch r.Family { - case "debian": - ovalClient = oval.NewDebian() - 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 - default: - return fmt.Errorf("Oval %s is not implemented yet", r.Family) - } - if err := ovalClient.FillCveInfoFromOvalDB(r); err != nil { - return err - } - return nil -} diff --git a/commands/tui.go b/commands/tui.go index cbf39a02..cd661a50 100644 --- a/commands/tui.go +++ b/commands/tui.go @@ -24,8 +24,6 @@ import ( "path/filepath" c "github.com/future-architect/vuls/config" - "github.com/future-architect/vuls/models" - "github.com/future-architect/vuls/report" "github.com/future-architect/vuls/util" "github.com/google/subcommands" ) @@ -146,40 +144,41 @@ func (p *TuiCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s } c.Conf.Pipe = p.pipe - jsonDir, err := jsonDir(f.Args()) - if err != nil { - log.Errorf("Failed to read json dir under results: %s", err) - return subcommands.ExitFailure - } + // jsonDir, err := report.JSONDir(f.Args()) + // if err != nil { + // log.Errorf("Failed to read json dir under results: %s", err) + // return subcommands.ExitFailure + // } - results, err := loadScanResults(jsonDir) - if err != nil { - log.Errorf("Failed to read from JSON: %s", err) - return subcommands.ExitFailure - } + // results, err := report.LoadScanResults(jsonDir) + // if err != nil { + // log.Errorf("Failed to read from JSON: %s", err) + // return subcommands.ExitFailure + // } - var filledResults []models.ScanResult - for _, r := range results { - if p.refreshCve || needToRefreshCve(r) { - if c.Conf.CveDBType == "sqlite3" { - if _, err := os.Stat(c.Conf.CveDBPath); os.IsNotExist(err) { - log.Errorf("SQLite3 DB(CVE-Dictionary) is not exist: %s", - c.Conf.CveDBPath) - return subcommands.ExitFailure - } - } + // var filledResults []models.ScanResult + // for _, r := range results { + // if p.refreshCve || needToRefreshCve(r) { + // if c.Conf.CveDBType == "sqlite3" { + // if _, err := os.Stat(c.Conf.CveDBPath); os.IsNotExist(err) { + // log.Errorf("SQLite3 DB(CVE-Dictionary) is not exist: %s", + // c.Conf.CveDBPath) + // return subcommands.ExitFailure + // } + // } - if err := fillCveInfoFromCveDB(&r); err != nil { - log.Errorf("Failed to fill CVE information: %s", err) - return subcommands.ExitFailure - } + // if err := fillCveInfoFromCveDB(&r); err != nil { + // log.Errorf("Failed to fill CVE information: %s", err) + // return subcommands.ExitFailure + // } - if err := overwriteJSONFile(jsonDir, r); err != nil { - log.Errorf("Failed to write JSON: %s", err) - return subcommands.ExitFailure - } - } - filledResults = append(filledResults, r) - } - return report.RunTui(filledResults) + // if err := overwriteJSONFile(jsonDir, r); err != nil { + // log.Errorf("Failed to write JSON: %s", err) + // return subcommands.ExitFailure + // } + // } + // filledResults = append(filledResults, r) + // } + // return report.RunTui(filledResults) + return subcommands.ExitFailure } diff --git a/commands/util.go b/commands/util.go index 73d73001..d0a08b5f 100644 --- a/commands/util.go +++ b/commands/util.go @@ -18,307 +18,11 @@ along with this program. If not, see . package commands import ( - "encoding/json" "fmt" - "io/ioutil" - "os" - "path/filepath" - "regexp" - "sort" - "strings" - "time" - c "github.com/future-architect/vuls/config" - "github.com/future-architect/vuls/cveapi" - "github.com/future-architect/vuls/models" - "github.com/future-architect/vuls/report" - "github.com/future-architect/vuls/util" "github.com/howeyc/gopass" ) -// jsonDirPattern is file name pattern of JSON directory -// 2016-11-16T10:43:28+09:00 -// 2016-11-16T10:43:28Z -var jsonDirPattern = regexp.MustCompile( - `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|[+-]\d{2}:\d{2})$`) - -// JSONDirs is array of json files path. -type jsonDirs []string - -// getValidJSONDirs return valid json directory as array -// Returned array is sorted so that recent directories are at the head -func lsValidJSONDirs() (dirs jsonDirs, err error) { - var dirInfo []os.FileInfo - if dirInfo, err = ioutil.ReadDir(c.Conf.ResultsDir); err != nil { - err = fmt.Errorf("Failed to read %s: %s", c.Conf.ResultsDir, err) - return - } - for _, d := range dirInfo { - if d.IsDir() && jsonDirPattern.MatchString(d.Name()) { - jsonDir := filepath.Join(c.Conf.ResultsDir, d.Name()) - dirs = append(dirs, jsonDir) - } - } - sort.Slice(dirs, func(i, j int) bool { - return dirs[j] < dirs[i] - }) - return -} - -// jsonDir returns -// If there is an arg, check if it is a valid format and return the corresponding path under results. -// If arg passed via PIPE (such as history subcommand), return that path. -// Otherwise, returns the path of the latest directory -func jsonDir(args []string) (string, error) { - var err error - var dirs jsonDirs - - if 0 < len(args) { - if dirs, err = lsValidJSONDirs(); err != nil { - return "", err - } - - path := filepath.Join(c.Conf.ResultsDir, args[0]) - for _, d := range dirs { - ss := strings.Split(d, string(os.PathSeparator)) - timedir := ss[len(ss)-1] - if timedir == args[0] { - return path, nil - } - } - - return "", fmt.Errorf("Invalid path: %s", path) - } - - // PIPE - if c.Conf.Pipe { - bytes, err := ioutil.ReadAll(os.Stdin) - if err != nil { - return "", fmt.Errorf("Failed to read stdin: %s", err) - } - fields := strings.Fields(string(bytes)) - if 0 < len(fields) { - return filepath.Join(c.Conf.ResultsDir, fields[0]), nil - } - return "", fmt.Errorf("Stdin is invalid: %s", string(bytes)) - } - - // returns latest dir when no args or no PIPE - if dirs, err = lsValidJSONDirs(); err != nil { - return "", err - } - if len(dirs) == 0 { - return "", fmt.Errorf("No results under %s", - c.Conf.ResultsDir) - } - return dirs[0], nil -} - -// loadOneServerScanResult read JSON data of one server -func loadOneServerScanResult(jsonFile string) (result models.ScanResult, err error) { - var data []byte - if data, err = ioutil.ReadFile(jsonFile); err != nil { - err = fmt.Errorf("Failed to read %s: %s", jsonFile, err) - return - } - if json.Unmarshal(data, &result) != nil { - err = fmt.Errorf("Failed to parse %s: %s", jsonFile, err) - } - return -} - -// loadScanResults read JSON data -func loadScanResults(jsonDir string) (results models.ScanResults, err error) { - var files []os.FileInfo - if files, err = ioutil.ReadDir(jsonDir); err != nil { - return nil, fmt.Errorf("Failed to read %s: %s", jsonDir, err) - } - for _, f := range files { - if filepath.Ext(f.Name()) != ".json" || strings.HasSuffix(f.Name(), "_diff.json") { - continue - } - - var r models.ScanResult - path := filepath.Join(jsonDir, f.Name()) - if r, err = loadOneServerScanResult(path); err != nil { - return nil, err - } - - results = append(results, r) - } - if len(results) == 0 { - return nil, fmt.Errorf("There is no json file under %s", jsonDir) - } - return -} - -func loadPrevious(current models.ScanResults) (previous models.ScanResults, err error) { - var dirs jsonDirs - if dirs, err = lsValidJSONDirs(); err != nil { - return - } - - for _, result := range current { - for _, dir := range dirs[1:] { - var r models.ScanResult - path := filepath.Join(dir, result.ServerName+".json") - if r, err = loadOneServerScanResult(path); err != nil { - continue - } - if r.Family == result.Family && r.Release == result.Release { - previous = append(previous, r) - util.Log.Infof("Privious json found: %s", path) - break - } - } - } - return previous, nil -} - -func diff(curResults, preResults models.ScanResults) (diffed models.ScanResults, err error) { - for _, current := range curResults { - found := false - var previous models.ScanResult - for _, r := range preResults { - if current.ServerName == r.ServerName { - found = true - previous = r - break - } - } - - if found { - current.ScannedCves = getDiffCves(previous, current) - packages := models.Packages{} - for _, s := range current.ScannedCves { - for _, name := range s.PackageNames { - p := current.Packages[name] - packages[name] = p - } - } - current.Packages = packages - } - - diffed = append(diffed, current) - } - return diffed, err -} - -func getDiffCves(previous, current models.ScanResult) models.VulnInfos { - previousCveIDsSet := map[string]bool{} - for _, previousVulnInfo := range previous.ScannedCves { - previousCveIDsSet[previousVulnInfo.CveID] = true - } - - new := models.VulnInfos{} - updated := models.VulnInfos{} - for _, v := range current.ScannedCves { - if previousCveIDsSet[v.CveID] { - if isCveInfoUpdated(v.CveID, previous, current) { - updated[v.CveID] = v - } - } else { - new[v.CveID] = v - } - } - - for cveID, vuln := range new { - updated[cveID] = vuln - } - return updated -} - -func isCveInfoUpdated(cveID string, previous, current models.ScanResult) bool { - cTypes := []models.CveContentType{ - models.NVD, - models.JVN, - models.NewCveContentType(current.Family), - } - - prevLastModified := map[models.CveContentType]time.Time{} - for _, c := range previous.ScannedCves { - if cveID == c.CveID { - for _, cType := range cTypes { - content, _ := c.CveContents[cType] - prevLastModified[cType] = content.LastModified - } - break - } - } - - curLastModified := map[models.CveContentType]time.Time{} - for _, c := range current.ScannedCves { - if cveID == c.CveID { - for _, cType := range cTypes { - content, _ := c.CveContents[cType] - curLastModified[cType] = content.LastModified - } - break - } - } - for _, cType := range cTypes { - if equal := prevLastModified[cType].Equal(curLastModified[cType]); !equal { - return true - } - } - return false -} - -func overwriteJSONFile(dir string, r models.ScanResult) error { - before := c.Conf.FormatJSON - beforeDiff := c.Conf.Diff - c.Conf.FormatJSON = true - c.Conf.Diff = false - w := report.LocalFileWriter{CurrentDir: dir} - if err := w.Write(r); err != nil { - return fmt.Errorf("Failed to write summary report: %s", err) - } - c.Conf.FormatJSON = before - c.Conf.Diff = beforeDiff - return nil -} - -func fillVulnByCpeNames(cpeNames []string, scannedVulns models.VulnInfos) error { - for _, name := range cpeNames { - details, err := cveapi.CveClient.FetchCveDetailsByCpeName(name) - if err != nil { - return err - } - for _, detail := range details { - if val, ok := scannedVulns[detail.CveID]; ok { - names := val.CpeNames - names = util.AppendIfMissing(names, name) - val.CpeNames = names - val.Confidence = models.CpeNameMatch - scannedVulns[detail.CveID] = val - } else { - v := models.VulnInfo{ - CveID: detail.CveID, - CpeNames: []string{name}, - Confidence: models.CpeNameMatch, - } - //TODO - // v.NilToEmpty() - scannedVulns[detail.CveID] = v - } - } - } - return nil -} - -func needToRefreshCve(r models.ScanResult) bool { - if r.Lang != c.Conf.Lang { - return true - } - - for _, cve := range r.ScannedCves { - if 0 < len(cve.CveContents) { - return false - } - } - return true -} - func getPasswd(prompt string) (string, error) { for { fmt.Print(prompt) diff --git a/commands/util_test.go b/commands/util_test.go index 25d872fc..e9ab19df 100644 --- a/commands/util_test.go +++ b/commands/util_test.go @@ -16,329 +16,3 @@ along with this program. If not, see . */ package commands - -import ( - "reflect" - "testing" - "time" - - "github.com/future-architect/vuls/models" - "github.com/k0kubun/pp" -) - -func TestIsCveInfoUpdated(t *testing.T) { - f := "2006-01-02" - old, _ := time.Parse(f, "2015-12-15") - new, _ := time.Parse(f, "2015-12-16") - - type In struct { - cveID string - cur models.ScanResult - prev models.ScanResult - } - var tests = []struct { - in In - expected bool - }{ - // NVD compare non-initialized times - { - in: In{ - cveID: "CVE-2017-0001", - cur: models.ScanResult{ - ScannedCves: models.VulnInfos{ - "CVE-2017-0001": { - CveID: "CVE-2017-0001", - CveContents: models.NewCveContents( - models.CveContent{ - Type: models.NVD, - CveID: "CVE-2017-0001", - LastModified: time.Time{}, - }, - ), - }, - }, - }, - prev: models.ScanResult{ - ScannedCves: models.VulnInfos{ - "CVE-2017-0001": { - CveID: "CVE-2017-0001", - CveContents: models.NewCveContents( - models.CveContent{ - Type: models.NVD, - CveID: "CVE-2017-0001", - LastModified: time.Time{}, - }, - ), - }, - }, - }, - }, - expected: false, - }, - // JVN not updated - { - in: In{ - cveID: "CVE-2017-0002", - cur: models.ScanResult{ - ScannedCves: models.VulnInfos{ - "CVE-2017-0002": { - CveID: "CVE-2017-0002", - CveContents: models.NewCveContents( - models.CveContent{ - Type: models.NVD, - CveID: "CVE-2017-0002", - LastModified: old, - }, - ), - }, - }, - }, - prev: models.ScanResult{ - ScannedCves: models.VulnInfos{ - "CVE-2017-0002": { - CveID: "CVE-2017-0002", - CveContents: models.NewCveContents( - models.CveContent{ - Type: models.NVD, - CveID: "CVE-2017-0002", - LastModified: old, - }, - ), - }, - }, - }, - }, - expected: false, - }, - // OVAL updated - { - in: In{ - cveID: "CVE-2017-0003", - cur: models.ScanResult{ - Family: "ubuntu", - ScannedCves: models.VulnInfos{ - "CVE-2017-0003": { - CveID: "CVE-2017-0003", - CveContents: models.NewCveContents( - models.CveContent{ - Type: models.NVD, - CveID: "CVE-2017-0002", - LastModified: new, - }, - ), - }, - }, - }, - prev: models.ScanResult{ - Family: "ubuntu", - ScannedCves: models.VulnInfos{ - "CVE-2017-0003": { - CveID: "CVE-2017-0003", - CveContents: models.NewCveContents( - models.CveContent{ - Type: models.NVD, - CveID: "CVE-2017-0002", - LastModified: old, - }, - ), - }, - }, - }, - }, - expected: true, - }, - // OVAL newly detected - { - in: In{ - cveID: "CVE-2017-0004", - cur: models.ScanResult{ - Family: "redhat", - ScannedCves: models.VulnInfos{ - "CVE-2017-0004": { - CveID: "CVE-2017-0004", - CveContents: models.NewCveContents( - models.CveContent{ - Type: models.NVD, - CveID: "CVE-2017-0002", - LastModified: old, - }, - ), - }, - }, - }, - prev: models.ScanResult{ - Family: "redhat", - ScannedCves: models.VulnInfos{}, - }, - }, - expected: true, - }, - } - for i, tt := range tests { - actual := isCveInfoUpdated(tt.in.cveID, tt.in.prev, tt.in.cur) - if actual != tt.expected { - t.Errorf("[%d] actual: %t, expected: %t", i, actual, tt.expected) - } - } -} - -func TestDiff(t *testing.T) { - atCurrent, _ := time.Parse("2006-01-02", "2014-12-31") - atPrevious, _ := time.Parse("2006-01-02", "2014-11-31") - var tests = []struct { - inCurrent models.ScanResults - inPrevious models.ScanResults - out models.ScanResult - }{ - { - inCurrent: models.ScanResults{ - { - ScannedAt: atCurrent, - ServerName: "u16", - Family: "ubuntu", - Release: "16.04", - ScannedCves: models.VulnInfos{ - "CVE-2012-6702": { - CveID: "CVE-2012-6702", - PackageNames: []string{"libexpat1"}, - DistroAdvisories: []models.DistroAdvisory{}, - CpeNames: []string{}, - }, - "CVE-2014-9761": { - CveID: "CVE-2014-9761", - PackageNames: []string{"libc-bin"}, - DistroAdvisories: []models.DistroAdvisory{}, - CpeNames: []string{}, - }, - }, - Packages: models.Packages{}, - Errors: []string{}, - Optional: [][]interface{}{}, - }, - }, - inPrevious: models.ScanResults{ - { - ScannedAt: atPrevious, - ServerName: "u16", - Family: "ubuntu", - Release: "16.04", - ScannedCves: models.VulnInfos{ - "CVE-2012-6702": { - CveID: "CVE-2012-6702", - PackageNames: []string{"libexpat1"}, - DistroAdvisories: []models.DistroAdvisory{}, - CpeNames: []string{}, - }, - "CVE-2014-9761": { - CveID: "CVE-2014-9761", - PackageNames: []string{"libc-bin"}, - DistroAdvisories: []models.DistroAdvisory{}, - CpeNames: []string{}, - }, - }, - Packages: models.Packages{}, - Errors: []string{}, - Optional: [][]interface{}{}, - }, - }, - out: models.ScanResult{ - ScannedAt: atCurrent, - ServerName: "u16", - Family: "ubuntu", - Release: "16.04", - Packages: models.Packages{}, - ScannedCves: models.VulnInfos{}, - Errors: []string{}, - Optional: [][]interface{}{}, - }, - }, - { - inCurrent: models.ScanResults{ - { - ScannedAt: atCurrent, - ServerName: "u16", - Family: "ubuntu", - Release: "16.04", - ScannedCves: models.VulnInfos{ - "CVE-2016-6662": { - CveID: "CVE-2016-6662", - PackageNames: []string{"mysql-libs"}, - DistroAdvisories: []models.DistroAdvisory{}, - CpeNames: []string{}, - }, - }, - Packages: models.Packages{ - "mysql-libs": { - Name: "mysql-libs", - Version: "5.1.73", - Release: "7.el6", - NewVersion: "5.1.73", - NewRelease: "8.el6_8", - Repository: "", - Changelog: models.Changelog{ - Contents: "", - Method: "", - }, - }, - }, - }, - }, - inPrevious: models.ScanResults{ - { - ScannedAt: atPrevious, - ServerName: "u16", - Family: "ubuntu", - Release: "16.04", - ScannedCves: models.VulnInfos{}, - }, - }, - out: models.ScanResult{ - ScannedAt: atCurrent, - ServerName: "u16", - Family: "ubuntu", - Release: "16.04", - ScannedCves: models.VulnInfos{ - "CVE-2016-6662": { - CveID: "CVE-2016-6662", - PackageNames: []string{"mysql-libs"}, - DistroAdvisories: []models.DistroAdvisory{}, - CpeNames: []string{}, - }, - }, - Packages: models.Packages{ - "mysql-libs": { - Name: "mysql-libs", - Version: "5.1.73", - Release: "7.el6", - NewVersion: "5.1.73", - NewRelease: "8.el6_8", - Repository: "", - Changelog: models.Changelog{ - Contents: "", - Method: "", - }, - }, - }, - }, - }, - } - - for i, tt := range tests { - diff, _ := diff(tt.inCurrent, tt.inPrevious) - for _, actual := range diff { - if !reflect.DeepEqual(actual.ScannedCves, tt.out.ScannedCves) { - h := pp.Sprint(actual.ScannedCves) - x := pp.Sprint(tt.out.ScannedCves) - t.Errorf("[%d] cves actual: \n %s \n expected: \n %s", i, h, x) - } - - for j := range tt.out.Packages { - if !reflect.DeepEqual(tt.out.Packages[j], actual.Packages[j]) { - h := pp.Sprint(tt.out.Packages[j]) - x := pp.Sprint(actual.Packages[j]) - t.Errorf("[%d] packages actual: \n %s \n expected: \n %s", i, x, h) - } - } - } - } -} diff --git a/config/config.go b/config/config.go index db813314..243efbe9 100644 --- a/config/config.go +++ b/config/config.go @@ -61,6 +61,8 @@ type Config struct { OvalDBType string OvalDBPath string + RefreshCve bool + FormatXML bool FormatJSON bool FormatOneEMail bool diff --git a/models/cvecontents.go b/models/cvecontents.go new file mode 100644 index 00000000..33ab332f --- /dev/null +++ b/models/cvecontents.go @@ -0,0 +1,527 @@ +/* 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 models + +import ( + "fmt" + "strings" + "time" +) + +// CveContents has CveContent +type CveContents map[CveContentType]CveContent + +// NewCveContents create CveContents +func NewCveContents(conts ...CveContent) CveContents { + m := map[CveContentType]CveContent{} + for _, cont := range conts { + m[cont.Type] = cont + } + return m +} + +// CveContentStr has CveContentType and Value +type CveContentStr struct { + Type CveContentType + Value string +} + +// 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 +} + +// 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 +} + +// 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" + } 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 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" + } 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 +} + +// 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" { + 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, cveID 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}) + } + } + + 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 +// 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 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) { + // 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 +} + +// CveContent has abstraction of various vulnerability information +type CveContent struct { + Type CveContentType + CveID string + Title string + Summary string + Severity string + Cvss2Score float64 + Cvss2Vector string + Cvss3Score float64 + Cvss3Vector string + SourceLink string + Cpes []Cpe + References References + CweID string + Published time.Time + LastModified time.Time +} + +// Empty checks the content is empty +func (c CveContent) Empty() bool { + return c.Summary == "" +} + +// CveContentType is a source of CVE information +type CveContentType string + +// NewCveContentType create CveContentType +func NewCveContentType(name string) CveContentType { + switch name { + case "nvd": + return NVD + case "jvn": + return JVN + case "redhat", "centos": + return RedHat + case "ubuntu": + return Ubuntu + case "debian": + return Debian + default: + return Unknown + } +} + +const ( + // NVD is NVD + NVD CveContentType = "nvd" + + // JVN is JVN + JVN CveContentType = "jvn" + + // RedHat is RedHat + RedHat CveContentType = "redhat" + + // Debian is Debian + Debian CveContentType = "debian" + + // Ubuntu is Ubuntu + Ubuntu CveContentType = "ubuntu" + + // Unknown is Unknown + 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 +} + +// Cpe is Common Platform Enumeration +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 { + Source string + Link string + RefID string +} diff --git a/models/cvecontents_test.go b/models/cvecontents_test.go new file mode 100644 index 00000000..fced7012 --- /dev/null +++ b/models/cvecontents_test.go @@ -0,0 +1,17 @@ +/* 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 models diff --git a/models/models.go b/models/models.go index c92f8164..bd7ab57f 100644 --- a/models/models.go +++ b/models/models.go @@ -17,1017 +17,5 @@ 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" -) - // JSONVersion is JSON Version const JSONVersion = "0.3.0" - -// ScanResults is slice of ScanResult. -type ScanResults []ScanResult - -//TODO -// // Len implement Sort Interface -// func (s ScanResults) Len() int { -// return len(s) -// } - -// // Swap implement Sort Interface -// func (s ScanResults) Swap(i, j int) { -// s[i], s[j] = s[j], s[i] -// } - -// // Less implement Sort Interface -// func (s ScanResults) Less(i, j int) bool { -// if s[i].ServerName == s[j].ServerName { -// return s[i].Container.ContainerID < s[i].Container.ContainerID -// } -// return s[i].ServerName < s[j].ServerName -// } - -// ScanResult has the result of scanned CVE information. -type ScanResult struct { - ScannedAt time.Time - JSONVersion string - Lang string - ServerName string // TOML Section key - Family string - Release string - Container Container - Platform Platform - - // Scanned Vulns by SSH scan + CPE + OVAL - ScannedCves VulnInfos - - Packages Packages - Errors []string - Optional [][]interface{} -} - -// ConvertNvdToModel convert NVD to CveContent -func (r ScanResult) ConvertNvdToModel(cveID string, nvd cvedict.Nvd) *CveContent { - var cpes []Cpe - for _, c := range nvd.Cpes { - cpes = append(cpes, Cpe{CpeName: c.CpeName}) - } - - var refs []Reference - for _, r := range nvd.References { - refs = append(refs, Reference{ - Link: r.Link, - Source: r.Source, - }) - } - - validVec := true - for _, v := range []string{ - nvd.AccessVector, - nvd.AccessComplexity, - nvd.Authentication, - nvd.ConfidentialityImpact, - nvd.IntegrityImpact, - nvd.AvailabilityImpact, - } { - if len(v) == 0 { - validVec = false - } - } - - vector := "" - if validVec { - vector = fmt.Sprintf("AV:%s/AC:%s/Au:%s/C:%s/I:%s/A:%s", - string(nvd.AccessVector[0]), - string(nvd.AccessComplexity[0]), - string(nvd.Authentication[0]), - string(nvd.ConfidentialityImpact[0]), - string(nvd.IntegrityImpact[0]), - string(nvd.AvailabilityImpact[0])) - } - - //TODO CVSSv3 - return &CveContent{ - Type: NVD, - CveID: cveID, - 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, - Published: nvd.PublishedDate, - LastModified: nvd.LastModifiedDate, - } -} - -// ConvertJvnToModel convert JVN to CveContent -func (r ScanResult) ConvertJvnToModel(cveID string, jvn cvedict.Jvn) *CveContent { - var cpes []Cpe - for _, c := range jvn.Cpes { - cpes = append(cpes, Cpe{CpeName: c.CpeName}) - } - - refs := []Reference{} - for _, r := range jvn.References { - refs = append(refs, Reference{ - Link: r.Link, - Source: r.Source, - }) - } - - vector := strings.TrimSuffix(strings.TrimPrefix(jvn.Vector, "("), ")") - return &CveContent{ - Type: JVN, - CveID: cveID, - Title: jvn.Title, - Summary: jvn.Summary, - Severity: jvn.Severity, - Cvss2Score: jvn.Score, - Cvss2Vector: vector, - SourceLink: jvn.JvnLink, - Cpes: cpes, - References: refs, - Published: jvn.PublishedDate, - LastModified: jvn.LastModifiedDate, - } -} - -// FilterByCvssOver is filter function. -func (r ScanResult) FilterByCvssOver(over float64) ScanResult { - // TODO: Set correct default value - if over == 0 { - over = -1.1 - } - - // TODO: Filter by ignore cves??? - filtered := r.ScannedCves.Find(func(v VulnInfo) bool { - values := v.CveContents.Cvss2Scores() - for _, v := range values { - score := v.Value.Score - if over <= score { - return true - } - } - return false - }) - - copiedScanResult := r - copiedScanResult.ScannedCves = filtered - return copiedScanResult -} - -// ReportFileName returns the filename on localhost without extention -func (r ScanResult) ReportFileName() (name string) { - if len(r.Container.ContainerID) == 0 { - return fmt.Sprintf("%s", r.ServerName) - } - return fmt.Sprintf("%s@%s", r.Container.Name, r.ServerName) -} - -// ReportKeyName returns the name of key on S3, Azure-Blob without extention -func (r ScanResult) ReportKeyName() (name string) { - timestr := r.ScannedAt.Format(time.RFC3339) - if len(r.Container.ContainerID) == 0 { - return fmt.Sprintf("%s/%s", timestr, r.ServerName) - } - return fmt.Sprintf("%s/%s@%s", timestr, r.Container.Name, r.ServerName) -} - -// ServerInfo returns server name one line -func (r ScanResult) ServerInfo() string { - if len(r.Container.ContainerID) == 0 { - return fmt.Sprintf("%s (%s%s)", - r.ServerName, r.Family, r.Release) - } - return fmt.Sprintf( - "%s / %s (%s%s) on %s", - r.Container.Name, - r.Container.ContainerID, - r.Family, - r.Release, - r.ServerName, - ) -} - -// ServerInfoTui returns server infromation for TUI sidebar -func (r ScanResult) ServerInfoTui() string { - if len(r.Container.ContainerID) == 0 { - return fmt.Sprintf("%s (%s%s)", - r.ServerName, r.Family, r.Release) - } - return fmt.Sprintf( - "|-- %s (%s%s)", - r.Container.Name, - r.Family, - r.Release, - // r.Container.ContainerID, - ) -} - -// FormatServerName returns server and container name -func (r ScanResult) FormatServerName() string { - if len(r.Container.ContainerID) == 0 { - return r.ServerName - } - return fmt.Sprintf("%s@%s", - r.Container.Name, r.ServerName) -} - -// CveSummary summarize the number of CVEs group by CVSSv2 Severity -func (r ScanResult) CveSummary(ignoreUnscoreCves bool) string { - var high, medium, low, unknown int - for _, vInfo := range r.ScannedCves { - score := vInfo.CveContents.MaxCvss2Score().Value.Score - if score < 0.1 { - score = vInfo.CveContents.MaxCvss3Score().Value.Score - } - switch { - case 7.0 <= score: - high++ - case 4.0 <= score: - medium++ - case 0 < score: - low++ - default: - unknown++ - } - } - - if ignoreUnscoreCves { - return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d)", - high+medium+low, high, medium, low) - } - return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d ?:%d)", - 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 { - Score int - DetectionMethod string -} - -func (c Confidence) String() string { - return fmt.Sprintf("%d / %s", c.Score, c.DetectionMethod) -} - -const ( - // CpeNameMatchStr is a String representation of CpeNameMatch - CpeNameMatchStr = "CpeNameMatch" - - // YumUpdateSecurityMatchStr is a String representation of YumUpdateSecurityMatch - YumUpdateSecurityMatchStr = "YumUpdateSecurityMatch" - - // PkgAuditMatchStr is a String representation of PkgAuditMatch - PkgAuditMatchStr = "PkgAuditMatch" - - // OvalMatchStr is a String representation of OvalMatch - OvalMatchStr = "OvalMatch" - - // ChangelogExactMatchStr is a String representation of ChangelogExactMatch - ChangelogExactMatchStr = "ChangelogExactMatch" - - // ChangelogLenientMatchStr is a String representation of ChangelogLenientMatch - ChangelogLenientMatchStr = "ChangelogLenientMatch" - - // FailedToGetChangelog is a String representation of FailedToGetChangelog - FailedToGetChangelog = "FailedToGetChangelog" - - // FailedToFindVersionInChangelog is a String representation of FailedToFindVersionInChangelog - FailedToFindVersionInChangelog = "FailedToFindVersionInChangelog" -) - -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 - YumUpdateSecurityMatch = Confidence{100, YumUpdateSecurityMatchStr} - - // 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 - OvalMatch = Confidence{100, OvalMatchStr} - - // 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 - ChangelogLenientMatch = Confidence{50, ChangelogLenientMatchStr} -) - -// VulnInfos is VulnInfo list, getter/setter, sortable methods. -type VulnInfos map[string]VulnInfo - -// Find elements that matches the function passed in argument -func (v VulnInfos) Find(f func(VulnInfo) bool) VulnInfos { - filtered := VulnInfos{} - for _, vv := range v { - if f(vv) { - filtered[vv.CveID] = vv - } - } - 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 - Confidence Confidence - PackageNames []string - DistroAdvisories []DistroAdvisory // for Aamazon, RHEL, FreeBSD - CpeNames []string - CveContents CveContents -} - -// 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 - -// NewCveContentType create CveContentType -func NewCveContentType(name string) CveContentType { - switch name { - case "nvd": - return NVD - case "jvn": - return JVN - case "redhat", "centos": - return RedHat - case "ubuntu": - return Ubuntu - case "debian": - return Debian - default: - return Unknown - } -} - -const ( - // NVD is NVD - NVD CveContentType = "nvd" - - // JVN is JVN - JVN CveContentType = "jvn" - - // RedHat is RedHat - RedHat CveContentType = "redhat" - - // Debian is Debian - Debian CveContentType = "debian" - - // Ubuntu is Ubuntu - Ubuntu CveContentType = "ubuntu" - - // Unknown is Unknown - 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 - -// NewCveContents create CveContents -func NewCveContents(conts ...CveContent) CveContents { - m := map[CveContentType]CveContent{} - for _, cont := range conts { - m[cont.Type] = cont - } - return m -} - -// CveContentStr has CveContentType and Value -type CveContentStr struct { - Type CveContentType - Value string -} - -// 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 -} - -// 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 -} - -// 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" - } 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 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" - } 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 -} - -// 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" { - 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, cveID 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}) - } - } - - 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 -// 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 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) { - // 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 -} - -// CveContent has abstraction of various vulnerability information -type CveContent struct { - Type CveContentType - CveID string - Title string - Summary string - Severity string - Cvss2Score float64 - Cvss2Vector string - Cvss3Score float64 - Cvss3Vector string - SourceLink string - Cpes []Cpe - References References - CweID string - Published time.Time - LastModified time.Time -} - -// Empty checks the content is empty -func (c CveContent) Empty() bool { - return c.Summary == "" -} - -// Cpe is Common Platform Enumeration -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 { - Source string - Link string - RefID string -} - -// Packages is Map of Package -// { "package-name": Package } -type Packages map[string]Package - -// NewPackages create Packages -func NewPackages(packs ...Package) Packages { - m := Packages{} - for _, pack := range packs { - m[pack.Name] = pack - } - return m -} - -// MergeNewVersion merges candidate version information to the receiver struct -func (ps Packages) MergeNewVersion(as Packages) { - for _, a := range as { - if pack, ok := ps[a.Name]; ok { - pack.NewVersion = a.NewVersion - pack.NewRelease = a.NewRelease - ps[a.Name] = pack - } - } -} - -// Merge returns merged map (immutable) -func (ps Packages) Merge(other Packages) Packages { - merged := map[string]Package{} - for k, v := range ps { - merged[k] = v - } - for k, v := range other { - merged[k] = v - } - 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 - for _, p := range ps { - if p.NewVersion != "" { - nUpdatable++ - } - } - return fmt.Sprintf("%d updatable packages", nUpdatable) -} - -// Package has installed packages. -type Package struct { - Name string - Version string - Release string - NewVersion string - NewRelease string - Repository string - Changelog Changelog - NotFixedYet bool // Ubuntu OVAL Only -} - -// 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) - } - if 0 < len(p.Release) { - str = fmt.Sprintf("%s-%s", str, p.Release) - } - return str -} - -// FormatNewVer returns package name-version-release -func (p Package) FormatNewVer() string { - str := p.Name - if 0 < len(p.NewVersion) { - str = fmt.Sprintf("%s-%s", str, p.NewVersion) - } - if 0 < len(p.NewRelease) { - str = fmt.Sprintf("%s-%s", str, p.NewRelease) - } - 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 - Severity string - Issued time.Time - Updated time.Time -} - -// Container has Container information -type Container struct { - ContainerID string - Name string - Image string - Type string -} - -// Platform has platform information -type Platform struct { - Name string // aws or azure or gcp or other... - InstanceID string -} diff --git a/models/models_test.go b/models/models_test.go index 7229f0ee..aee32778 100644 --- a/models/models_test.go +++ b/models/models_test.go @@ -16,45 +16,3 @@ along with this program. If not, see . */ package models - -import ( - "reflect" - "testing" - - "github.com/k0kubun/pp" -) - -func TestMergeNewVersion(t *testing.T) { - var test = struct { - a Packages - b Packages - expected Packages - }{ - Packages{ - "hoge": { - Name: "hoge", - }, - }, - Packages{ - "hoge": { - Name: "hoge", - NewVersion: "1.0.0", - NewRelease: "release1", - }, - }, - Packages{ - "hoge": { - Name: "hoge", - NewVersion: "1.0.0", - NewRelease: "release1", - }, - }, - } - - test.a.MergeNewVersion(test.b) - if !reflect.DeepEqual(test.a, test.expected) { - e := pp.Sprintf("%v", test.a) - a := pp.Sprintf("%v", test.expected) - t.Errorf("expected %s, actual %s", e, a) - } -} diff --git a/models/packages.go b/models/packages.go new file mode 100644 index 00000000..d923dfc5 --- /dev/null +++ b/models/packages.go @@ -0,0 +1,127 @@ +/* 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 models + +import ( + "fmt" + "strings" +) + +// Packages is Map of Package +// { "package-name": Package } +type Packages map[string]Package + +// NewPackages create Packages +func NewPackages(packs ...Package) Packages { + m := Packages{} + for _, pack := range packs { + m[pack.Name] = pack + } + return m +} + +// MergeNewVersion merges candidate version information to the receiver struct +func (ps Packages) MergeNewVersion(as Packages) { + for _, a := range as { + if pack, ok := ps[a.Name]; ok { + pack.NewVersion = a.NewVersion + pack.NewRelease = a.NewRelease + ps[a.Name] = pack + } + } +} + +// Merge returns merged map (immutable) +func (ps Packages) Merge(other Packages) Packages { + merged := map[string]Package{} + for k, v := range ps { + merged[k] = v + } + for k, v := range other { + merged[k] = v + } + 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 + for _, p := range ps { + if p.NewVersion != "" { + nUpdatable++ + } + } + return fmt.Sprintf("%d updatable packages", nUpdatable) +} + +// Package has installed packages. +type Package struct { + Name string + Version string + Release string + NewVersion string + NewRelease string + Repository string + Changelog Changelog + NotFixedYet bool // Ubuntu OVAL Only +} + +// 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) + } + if 0 < len(p.Release) { + str = fmt.Sprintf("%s-%s", str, p.Release) + } + return str +} + +// FormatNewVer returns package name-version-release +func (p Package) FormatNewVer() string { + str := p.Name + if 0 < len(p.NewVersion) { + str = fmt.Sprintf("%s-%s", str, p.NewVersion) + } + if 0 < len(p.NewRelease) { + str = fmt.Sprintf("%s-%s", str, p.NewRelease) + } + 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 +} diff --git a/models/packages_test.go b/models/packages_test.go new file mode 100644 index 00000000..321b07db --- /dev/null +++ b/models/packages_test.go @@ -0,0 +1,59 @@ +/* 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 models + +import ( + "reflect" + "testing" + + "github.com/k0kubun/pp" +) + +func TestMergeNewVersion(t *testing.T) { + var test = struct { + a Packages + b Packages + expected Packages + }{ + Packages{ + "hoge": { + Name: "hoge", + }, + }, + Packages{ + "hoge": { + Name: "hoge", + NewVersion: "1.0.0", + NewRelease: "release1", + }, + }, + Packages{ + "hoge": { + Name: "hoge", + NewVersion: "1.0.0", + NewRelease: "release1", + }, + }, + } + + test.a.MergeNewVersion(test.b) + if !reflect.DeepEqual(test.a, test.expected) { + e := pp.Sprintf("%v", test.a) + a := pp.Sprintf("%v", test.expected) + t.Errorf("expected %s, actual %s", e, a) + } +} diff --git a/models/scanresults.go b/models/scanresults.go new file mode 100644 index 00000000..a276ef25 --- /dev/null +++ b/models/scanresults.go @@ -0,0 +1,297 @@ +/* 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 models + +import ( + "bytes" + "fmt" + "strings" + "time" + + "github.com/future-architect/vuls/config" + cvedict "github.com/kotakanbe/go-cve-dictionary/models" +) + +// ScanResults is slice of ScanResult. +type ScanResults []ScanResult + +//TODO +// // Len implement Sort Interface +// func (s ScanResults) Len() int { +// return len(s) +// } + +// // Swap implement Sort Interface +// func (s ScanResults) Swap(i, j int) { +// s[i], s[j] = s[j], s[i] +// } + +// // Less implement Sort Interface +// func (s ScanResults) Less(i, j int) bool { +// if s[i].ServerName == s[j].ServerName { +// return s[i].Container.ContainerID < s[i].Container.ContainerID +// } +// return s[i].ServerName < s[j].ServerName +// } + +// ScanResult has the result of scanned CVE information. +type ScanResult struct { + ScannedAt time.Time + JSONVersion string + Lang string + ServerName string // TOML Section key + Family string + Release string + Container Container + Platform Platform + + // Scanned Vulns by SSH scan + CPE + OVAL + ScannedCves VulnInfos + + Packages Packages + Errors []string + Optional [][]interface{} +} + +// ConvertNvdToModel convert NVD to CveContent +func (r ScanResult) ConvertNvdToModel(cveID string, nvd cvedict.Nvd) *CveContent { + var cpes []Cpe + for _, c := range nvd.Cpes { + cpes = append(cpes, Cpe{CpeName: c.CpeName}) + } + + var refs []Reference + for _, r := range nvd.References { + refs = append(refs, Reference{ + Link: r.Link, + Source: r.Source, + }) + } + + validVec := true + for _, v := range []string{ + nvd.AccessVector, + nvd.AccessComplexity, + nvd.Authentication, + nvd.ConfidentialityImpact, + nvd.IntegrityImpact, + nvd.AvailabilityImpact, + } { + if len(v) == 0 { + validVec = false + } + } + + vector := "" + if validVec { + vector = fmt.Sprintf("AV:%s/AC:%s/Au:%s/C:%s/I:%s/A:%s", + string(nvd.AccessVector[0]), + string(nvd.AccessComplexity[0]), + string(nvd.Authentication[0]), + string(nvd.ConfidentialityImpact[0]), + string(nvd.IntegrityImpact[0]), + string(nvd.AvailabilityImpact[0])) + } + + //TODO CVSSv3 + return &CveContent{ + Type: NVD, + CveID: cveID, + 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, + Published: nvd.PublishedDate, + LastModified: nvd.LastModifiedDate, + } +} + +// ConvertJvnToModel convert JVN to CveContent +func (r ScanResult) ConvertJvnToModel(cveID string, jvn cvedict.Jvn) *CveContent { + var cpes []Cpe + for _, c := range jvn.Cpes { + cpes = append(cpes, Cpe{CpeName: c.CpeName}) + } + + refs := []Reference{} + for _, r := range jvn.References { + refs = append(refs, Reference{ + Link: r.Link, + Source: r.Source, + }) + } + + vector := strings.TrimSuffix(strings.TrimPrefix(jvn.Vector, "("), ")") + return &CveContent{ + Type: JVN, + CveID: cveID, + Title: jvn.Title, + Summary: jvn.Summary, + Severity: jvn.Severity, + Cvss2Score: jvn.Score, + Cvss2Vector: vector, + SourceLink: jvn.JvnLink, + Cpes: cpes, + References: refs, + Published: jvn.PublishedDate, + LastModified: jvn.LastModifiedDate, + } +} + +// FilterByCvssOver is filter function. +func (r ScanResult) FilterByCvssOver(over float64) ScanResult { + // TODO: Set correct default value + if over == 0 { + over = -1.1 + } + + // TODO: Filter by ignore cves??? + filtered := r.ScannedCves.Find(func(v VulnInfo) bool { + //TODO in the case of only oval, no cvecontents + values := v.CveContents.Cvss2Scores() + for _, vals := range values { + score := vals.Value.Score + if over <= score { + return true + } + } + return false + }) + + copiedScanResult := r + copiedScanResult.ScannedCves = filtered + return copiedScanResult +} + +// ReportFileName returns the filename on localhost without extention +func (r ScanResult) ReportFileName() (name string) { + if len(r.Container.ContainerID) == 0 { + return fmt.Sprintf("%s", r.ServerName) + } + return fmt.Sprintf("%s@%s", r.Container.Name, r.ServerName) +} + +// ReportKeyName returns the name of key on S3, Azure-Blob without extention +func (r ScanResult) ReportKeyName() (name string) { + timestr := r.ScannedAt.Format(time.RFC3339) + if len(r.Container.ContainerID) == 0 { + return fmt.Sprintf("%s/%s", timestr, r.ServerName) + } + return fmt.Sprintf("%s/%s@%s", timestr, r.Container.Name, r.ServerName) +} + +// ServerInfo returns server name one line +func (r ScanResult) ServerInfo() string { + if len(r.Container.ContainerID) == 0 { + return fmt.Sprintf("%s (%s%s)", + r.ServerName, r.Family, r.Release) + } + return fmt.Sprintf( + "%s / %s (%s%s) on %s", + r.Container.Name, + r.Container.ContainerID, + r.Family, + r.Release, + r.ServerName, + ) +} + +// ServerInfoTui returns server infromation for TUI sidebar +func (r ScanResult) ServerInfoTui() string { + if len(r.Container.ContainerID) == 0 { + return fmt.Sprintf("%s (%s%s)", + r.ServerName, r.Family, r.Release) + } + return fmt.Sprintf( + "|-- %s (%s%s)", + r.Container.Name, + r.Family, + r.Release, + // r.Container.ContainerID, + ) +} + +// FormatServerName returns server and container name +func (r ScanResult) FormatServerName() string { + if len(r.Container.ContainerID) == 0 { + return r.ServerName + } + return fmt.Sprintf("%s@%s", + r.Container.Name, r.ServerName) +} + +// CveSummary summarize the number of CVEs group by CVSSv2 Severity +func (r ScanResult) CveSummary(ignoreUnscoreCves bool) string { + var high, medium, low, unknown int + for _, vInfo := range r.ScannedCves { + score := vInfo.CveContents.MaxCvss2Score().Value.Score + if score < 0.1 { + score = vInfo.CveContents.MaxCvss3Score().Value.Score + } + switch { + case 7.0 <= score: + high++ + case 4.0 <= score: + medium++ + case 0 < score: + low++ + default: + unknown++ + } + } + + if ignoreUnscoreCves { + return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d)", + high+medium+low, high, medium, low) + } + return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d ?:%d)", + 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(), + ) +} + +// Container has Container information +type Container struct { + ContainerID string + Name string + Image string + Type string +} + +// Platform has platform information +type Platform struct { + Name string // aws or azure or gcp or other... + InstanceID string +} diff --git a/models/scanresults_test.go b/models/scanresults_test.go new file mode 100644 index 00000000..fced7012 --- /dev/null +++ b/models/scanresults_test.go @@ -0,0 +1,17 @@ +/* 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 models diff --git a/models/vulninfos.go b/models/vulninfos.go new file mode 100644 index 00000000..a1a03540 --- /dev/null +++ b/models/vulninfos.go @@ -0,0 +1,150 @@ +/* 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 models + +import ( + "fmt" + "time" +) + +// VulnInfos is VulnInfo list, getter/setter, sortable methods. +type VulnInfos map[string]VulnInfo + +// Find elements that matches the function passed in argument +func (v VulnInfos) Find(f func(VulnInfo) bool) VulnInfos { + filtered := VulnInfos{} + for _, vv := range v { + if f(vv) { + filtered[vv.CveID] = vv + } + } + 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 + Confidence Confidence + PackageNames []string + DistroAdvisories []DistroAdvisory // for Aamazon, RHEL, FreeBSD + CpeNames []string + CveContents CveContents +} + +// 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() +// } +// } + +// DistroAdvisory has Amazon Linux, RHEL, FreeBSD Security Advisory information. +type DistroAdvisory struct { + AdvisoryID string + Severity string + Issued time.Time + Updated time.Time +} + +// Confidence is a ranking how confident the CVE-ID was deteted correctly +// Score: 0 - 100 +type Confidence struct { + Score int + DetectionMethod string +} + +func (c Confidence) String() string { + return fmt.Sprintf("%d / %s", c.Score, c.DetectionMethod) +} + +const ( + // CpeNameMatchStr is a String representation of CpeNameMatch + CpeNameMatchStr = "CpeNameMatch" + + // YumUpdateSecurityMatchStr is a String representation of YumUpdateSecurityMatch + YumUpdateSecurityMatchStr = "YumUpdateSecurityMatch" + + // PkgAuditMatchStr is a String representation of PkgAuditMatch + PkgAuditMatchStr = "PkgAuditMatch" + + // OvalMatchStr is a String representation of OvalMatch + OvalMatchStr = "OvalMatch" + + // ChangelogExactMatchStr is a String representation of ChangelogExactMatch + ChangelogExactMatchStr = "ChangelogExactMatch" + + // ChangelogLenientMatchStr is a String representation of ChangelogLenientMatch + ChangelogLenientMatchStr = "ChangelogLenientMatch" + + // FailedToGetChangelog is a String representation of FailedToGetChangelog + FailedToGetChangelog = "FailedToGetChangelog" + + // FailedToFindVersionInChangelog is a String representation of FailedToFindVersionInChangelog + FailedToFindVersionInChangelog = "FailedToFindVersionInChangelog" +) + +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 + YumUpdateSecurityMatch = Confidence{100, YumUpdateSecurityMatchStr} + + // 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 + OvalMatch = Confidence{100, OvalMatchStr} + + // 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 + ChangelogLenientMatch = Confidence{50, ChangelogLenientMatchStr} +) diff --git a/models/vulninfos_test.go b/models/vulninfos_test.go new file mode 100644 index 00000000..fced7012 --- /dev/null +++ b/models/vulninfos_test.go @@ -0,0 +1,17 @@ +/* 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 models diff --git a/cveapi/cve_client.go b/report/cve_client.go similarity index 99% rename from cveapi/cve_client.go rename to report/cve_client.go index 08a73164..fad79470 100644 --- a/cveapi/cve_client.go +++ b/report/cve_client.go @@ -15,7 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -package cveapi +package report import ( "encoding/json" diff --git a/report/report.go b/report/report.go new file mode 100644 index 00000000..0d77f766 --- /dev/null +++ b/report/report.go @@ -0,0 +1,204 @@ +/* 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 ( + "fmt" + "os" + + c "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/models" + "github.com/future-architect/vuls/oval" + "github.com/future-architect/vuls/util" + "github.com/k0kubun/pp" +) + +// FillCveInfos fills CVE Detailed Information +func FillCveInfos(rs []models.ScanResult, dir string) ([]models.ScanResult, error) { + var filled []models.ScanResult + for _, r := range rs { + if c.Conf.RefreshCve || needToRefreshCve(r) { + if err := fillCveInfo(&r); err != nil { + return nil, err + } + r.Lang = c.Conf.Lang + if err := overwriteJSONFile(dir, r); err != nil { + return nil, fmt.Errorf("Failed to write JSON: %s", err) + } + filled = append(filled, r) + } else { + util.Log.Debugf("No need to refresh") + filled = append(filled, r) + } + } + + if c.Conf.Diff { + previous, err := loadPrevious(filled) + if err != nil { + return nil, err + } + + diff, err := diff(filled, previous) + if err != nil { + return nil, err + } + filled = []models.ScanResult{} + for _, r := range diff { + if err := fillCveDetail(&r); err != nil { + return nil, err + } + filled = append(filled, r) + } + } + + for _, r := range filled { + pp.Printf("filled: %d\n", len(r.ScannedCves)) + } + + filtered := []models.ScanResult{} + for _, r := range filled { + filtered = append(filtered, r.FilterByCvssOver(c.Conf.CvssScoreOver)) + } + + for _, r := range filtered { + pp.Printf("filtered: %d\n", len(r.ScannedCves)) + } + + // TODO Sort + return filtered, nil +} + +func fillCveInfo(r *models.ScanResult) error { + util.Log.Debugf("need to refresh") + if c.Conf.CveDBType == "sqlite3" && c.Conf.CveDBURL == "" { + if _, err := os.Stat(c.Conf.CveDBPath); os.IsNotExist(err) { + return fmt.Errorf("SQLite3 DB(CVE-Dictionary) is not exist: %s", + c.Conf.CveDBPath) + } + } + + if err := fillCveInfoFromOvalDB(r); err != nil { + return fmt.Errorf("Failed to fill OVAL information: %s", err) + } + + if err := fillCveInfoFromCveDB(r); err != nil { + return fmt.Errorf("Failed to fill CVE information: %s", err) + } + return nil +} + +// fillCveDetail fetches NVD, JVN from CVE Database, and then set to fields. +func fillCveDetail(r *models.ScanResult) error { + var cveIDs []string + for _, v := range r.ScannedCves { + cveIDs = append(cveIDs, v.CveID) + } + + ds, err := CveClient.FetchCveDetails(cveIDs) + if err != nil { + return err + } + for _, d := range ds { + nvd := r.ConvertNvdToModel(d.CveID, d.Nvd) + jvn := r.ConvertJvnToModel(d.CveID, d.Jvn) + for cveID, vinfo := range r.ScannedCves { + if vinfo.CveID == d.CveID { + if vinfo.CveContents == nil { + vinfo.CveContents = models.CveContents{} + } + for _, con := range []models.CveContent{*nvd, *jvn} { + if !con.Empty() { + vinfo.CveContents[con.Type] = con + } + } + r.ScannedCves[cveID] = vinfo + break + } + } + } + //TODO Remove + // sort.Slice(r.ScannedCves, func(i, j int) bool { + // if r.ScannedCves[j].CveContents.CvssV2Score() == r.ScannedCves[i].CveContents.CvssV2Score() { + // return r.ScannedCves[j].CveContents.CvssV2Score() < r.ScannedCves[i].CveContents.CvssV2Score() + // } + // return r.ScannedCves[j].CveContents.CvssV2Score() < r.ScannedCves[i].CveContents.CvssV2Score() + // }) + return nil +} + +func fillCveInfoFromCveDB(r *models.ScanResult) error { + sInfo := c.Conf.Servers[r.ServerName] + if err := fillVulnByCpeNames(sInfo.CpeNames, r.ScannedCves); err != nil { + return err + } + if err := fillCveDetail(r); err != nil { + return err + } + return nil +} + +func fillCveInfoFromOvalDB(r *models.ScanResult) error { + var ovalClient oval.Client + switch r.Family { + case "debian": + ovalClient = oval.NewDebian() + 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 + default: + return fmt.Errorf("Oval %s is not implemented yet", r.Family) + } + if err := ovalClient.FillCveInfoFromOvalDB(r); err != nil { + return err + } + return nil +} + +func fillVulnByCpeNames(cpeNames []string, scannedVulns models.VulnInfos) error { + for _, name := range cpeNames { + details, err := CveClient.FetchCveDetailsByCpeName(name) + if err != nil { + return err + } + for _, detail := range details { + if val, ok := scannedVulns[detail.CveID]; ok { + names := val.CpeNames + names = util.AppendIfMissing(names, name) + val.CpeNames = names + val.Confidence = models.CpeNameMatch + scannedVulns[detail.CveID] = val + } else { + v := models.VulnInfo{ + CveID: detail.CveID, + CpeNames: []string{name}, + Confidence: models.CpeNameMatch, + } + //TODO + // v.NilToEmpty() + scannedVulns[detail.CveID] = v + } + } + } + return nil +} diff --git a/report/report_test.go b/report/report_test.go new file mode 100644 index 00000000..80c499fb --- /dev/null +++ b/report/report_test.go @@ -0,0 +1 @@ +package report diff --git a/report/util.go b/report/util.go index 405d7fb1..9e4eb230 100644 --- a/report/util.go +++ b/report/util.go @@ -19,11 +19,19 @@ package report import ( "bytes" + "encoding/json" "fmt" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "sort" "strings" + "time" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" + "github.com/future-architect/vuls/util" "github.com/gosuri/uitable" ) @@ -516,3 +524,256 @@ func formatOneChangelog(p models.Package) string { buf = append(buf, packVer, delim.String(), clog) return strings.Join(buf, "\n") } + +func needToRefreshCve(r models.ScanResult) bool { + if r.Lang != config.Conf.Lang { + return true + } + + for _, cve := range r.ScannedCves { + if 0 < len(cve.CveContents) { + return false + } + } + return true +} + +func overwriteJSONFile(dir string, r models.ScanResult) error { + before := config.Conf.FormatJSON + beforeDiff := config.Conf.Diff + config.Conf.FormatJSON = true + config.Conf.Diff = false + w := LocalFileWriter{CurrentDir: dir} + if err := w.Write(r); err != nil { + return fmt.Errorf("Failed to write summary report: %s", err) + } + config.Conf.FormatJSON = before + config.Conf.Diff = beforeDiff + return nil +} + +func loadPrevious(current models.ScanResults) (previous models.ScanResults, err error) { + dirs, err := ListValidJSONDirs() + if err != nil { + return + } + + for _, result := range current { + for _, dir := range dirs[1:] { + var r models.ScanResult + path := filepath.Join(dir, result.ServerName+".json") + if r, err = loadOneServerScanResult(path); err != nil { + continue + } + if r.Family == result.Family && r.Release == result.Release { + previous = append(previous, r) + util.Log.Infof("Privious json found: %s", path) + break + } + } + } + return previous, nil +} + +func diff(curResults, preResults models.ScanResults) (diffed models.ScanResults, err error) { + for _, current := range curResults { + found := false + var previous models.ScanResult + for _, r := range preResults { + if current.ServerName == r.ServerName { + found = true + previous = r + break + } + } + + if found { + current.ScannedCves = getDiffCves(previous, current) + packages := models.Packages{} + for _, s := range current.ScannedCves { + for _, name := range s.PackageNames { + p := current.Packages[name] + packages[name] = p + } + } + current.Packages = packages + } + + diffed = append(diffed, current) + } + return diffed, err +} + +func getDiffCves(previous, current models.ScanResult) models.VulnInfos { + previousCveIDsSet := map[string]bool{} + for _, previousVulnInfo := range previous.ScannedCves { + previousCveIDsSet[previousVulnInfo.CveID] = true + } + + new := models.VulnInfos{} + updated := models.VulnInfos{} + for _, v := range current.ScannedCves { + if previousCveIDsSet[v.CveID] { + if isCveInfoUpdated(v.CveID, previous, current) { + updated[v.CveID] = v + } + } else { + new[v.CveID] = v + } + } + + for cveID, vuln := range new { + updated[cveID] = vuln + } + return updated +} + +func isCveInfoUpdated(cveID string, previous, current models.ScanResult) bool { + cTypes := []models.CveContentType{ + models.NVD, + models.JVN, + models.NewCveContentType(current.Family), + } + + prevLastModified := map[models.CveContentType]time.Time{} + for _, c := range previous.ScannedCves { + if cveID == c.CveID { + for _, cType := range cTypes { + content, _ := c.CveContents[cType] + prevLastModified[cType] = content.LastModified + } + break + } + } + + curLastModified := map[models.CveContentType]time.Time{} + for _, c := range current.ScannedCves { + if cveID == c.CveID { + for _, cType := range cTypes { + content, _ := c.CveContents[cType] + curLastModified[cType] = content.LastModified + } + break + } + } + for _, cType := range cTypes { + if equal := prevLastModified[cType].Equal(curLastModified[cType]); !equal { + return true + } + } + return false +} + +// jsonDirPattern is file name pattern of JSON directory +// 2016-11-16T10:43:28+09:00 +// 2016-11-16T10:43:28Z +var jsonDirPattern = regexp.MustCompile( + `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|[+-]\d{2}:\d{2})$`) + +// ListValidJSONDirs returns valid json directory as array +// Returned array is sorted so that recent directories are at the head +func ListValidJSONDirs() (dirs []string, err error) { + var dirInfo []os.FileInfo + if dirInfo, err = ioutil.ReadDir(config.Conf.ResultsDir); err != nil { + err = fmt.Errorf("Failed to read %s: %s", + config.Conf.ResultsDir, err) + return + } + for _, d := range dirInfo { + if d.IsDir() && jsonDirPattern.MatchString(d.Name()) { + jsonDir := filepath.Join(config.Conf.ResultsDir, d.Name()) + dirs = append(dirs, jsonDir) + } + } + sort.Slice(dirs, func(i, j int) bool { + return dirs[j] < dirs[i] + }) + return +} + +// JSONDir returns +// If there is an arg, check if it is a valid format and return the corresponding path under results. +// If arg passed via PIPE (such as history subcommand), return that path. +// Otherwise, returns the path of the latest directory +func JSONDir(args []string) (string, error) { + var err error + dirs := []string{} + + if 0 < len(args) { + if dirs, err = ListValidJSONDirs(); err != nil { + return "", err + } + + path := filepath.Join(config.Conf.ResultsDir, args[0]) + for _, d := range dirs { + ss := strings.Split(d, string(os.PathSeparator)) + timedir := ss[len(ss)-1] + if timedir == args[0] { + return path, nil + } + } + + return "", fmt.Errorf("Invalid path: %s", path) + } + + // PIPE + if config.Conf.Pipe { + bytes, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return "", fmt.Errorf("Failed to read stdin: %s", err) + } + fields := strings.Fields(string(bytes)) + if 0 < len(fields) { + return filepath.Join(config.Conf.ResultsDir, fields[0]), nil + } + return "", fmt.Errorf("Stdin is invalid: %s", string(bytes)) + } + + // returns latest dir when no args or no PIPE + if dirs, err = ListValidJSONDirs(); err != nil { + return "", err + } + if len(dirs) == 0 { + return "", fmt.Errorf("No results under %s", + config.Conf.ResultsDir) + } + return dirs[0], nil +} + +// LoadScanResults read JSON data +func LoadScanResults(jsonDir string) (results models.ScanResults, err error) { + var files []os.FileInfo + if files, err = ioutil.ReadDir(jsonDir); err != nil { + return nil, fmt.Errorf("Failed to read %s: %s", jsonDir, err) + } + for _, f := range files { + if filepath.Ext(f.Name()) != ".json" || strings.HasSuffix(f.Name(), "_diff.json") { + continue + } + + var r models.ScanResult + path := filepath.Join(jsonDir, f.Name()) + if r, err = loadOneServerScanResult(path); err != nil { + return nil, err + } + + results = append(results, r) + } + if len(results) == 0 { + return nil, fmt.Errorf("There is no json file under %s", jsonDir) + } + return +} + +// loadOneServerScanResult read JSON data of one server +func loadOneServerScanResult(jsonFile string) (result models.ScanResult, err error) { + var data []byte + if data, err = ioutil.ReadFile(jsonFile); err != nil { + err = fmt.Errorf("Failed to read %s: %s", jsonFile, err) + return + } + if json.Unmarshal(data, &result) != nil { + err = fmt.Errorf("Failed to parse %s: %s", jsonFile, err) + } + return +} diff --git a/report/util_test.go b/report/util_test.go new file mode 100644 index 00000000..6cf3c1f9 --- /dev/null +++ b/report/util_test.go @@ -0,0 +1,327 @@ +package report + +import ( + "reflect" + "testing" + "time" + + "github.com/future-architect/vuls/models" + "github.com/k0kubun/pp" +) + +func TestIsCveInfoUpdated(t *testing.T) { + f := "2006-01-02" + old, _ := time.Parse(f, "2015-12-15") + new, _ := time.Parse(f, "2015-12-16") + + type In struct { + cveID string + cur models.ScanResult + prev models.ScanResult + } + var tests = []struct { + in In + expected bool + }{ + // NVD compare non-initialized times + { + in: In{ + cveID: "CVE-2017-0001", + cur: models.ScanResult{ + ScannedCves: models.VulnInfos{ + "CVE-2017-0001": { + CveID: "CVE-2017-0001", + CveContents: models.NewCveContents( + models.CveContent{ + Type: models.NVD, + CveID: "CVE-2017-0001", + LastModified: time.Time{}, + }, + ), + }, + }, + }, + prev: models.ScanResult{ + ScannedCves: models.VulnInfos{ + "CVE-2017-0001": { + CveID: "CVE-2017-0001", + CveContents: models.NewCveContents( + models.CveContent{ + Type: models.NVD, + CveID: "CVE-2017-0001", + LastModified: time.Time{}, + }, + ), + }, + }, + }, + }, + expected: false, + }, + // JVN not updated + { + in: In{ + cveID: "CVE-2017-0002", + cur: models.ScanResult{ + ScannedCves: models.VulnInfos{ + "CVE-2017-0002": { + CveID: "CVE-2017-0002", + CveContents: models.NewCveContents( + models.CveContent{ + Type: models.NVD, + CveID: "CVE-2017-0002", + LastModified: old, + }, + ), + }, + }, + }, + prev: models.ScanResult{ + ScannedCves: models.VulnInfos{ + "CVE-2017-0002": { + CveID: "CVE-2017-0002", + CveContents: models.NewCveContents( + models.CveContent{ + Type: models.NVD, + CveID: "CVE-2017-0002", + LastModified: old, + }, + ), + }, + }, + }, + }, + expected: false, + }, + // OVAL updated + { + in: In{ + cveID: "CVE-2017-0003", + cur: models.ScanResult{ + Family: "ubuntu", + ScannedCves: models.VulnInfos{ + "CVE-2017-0003": { + CveID: "CVE-2017-0003", + CveContents: models.NewCveContents( + models.CveContent{ + Type: models.NVD, + CveID: "CVE-2017-0002", + LastModified: new, + }, + ), + }, + }, + }, + prev: models.ScanResult{ + Family: "ubuntu", + ScannedCves: models.VulnInfos{ + "CVE-2017-0003": { + CveID: "CVE-2017-0003", + CveContents: models.NewCveContents( + models.CveContent{ + Type: models.NVD, + CveID: "CVE-2017-0002", + LastModified: old, + }, + ), + }, + }, + }, + }, + expected: true, + }, + // OVAL newly detected + { + in: In{ + cveID: "CVE-2017-0004", + cur: models.ScanResult{ + Family: "redhat", + ScannedCves: models.VulnInfos{ + "CVE-2017-0004": { + CveID: "CVE-2017-0004", + CveContents: models.NewCveContents( + models.CveContent{ + Type: models.NVD, + CveID: "CVE-2017-0002", + LastModified: old, + }, + ), + }, + }, + }, + prev: models.ScanResult{ + Family: "redhat", + ScannedCves: models.VulnInfos{}, + }, + }, + expected: true, + }, + } + for i, tt := range tests { + actual := isCveInfoUpdated(tt.in.cveID, tt.in.prev, tt.in.cur) + if actual != tt.expected { + t.Errorf("[%d] actual: %t, expected: %t", i, actual, tt.expected) + } + } +} + +func TestDiff(t *testing.T) { + atCurrent, _ := time.Parse("2006-01-02", "2014-12-31") + atPrevious, _ := time.Parse("2006-01-02", "2014-11-31") + var tests = []struct { + inCurrent models.ScanResults + inPrevious models.ScanResults + out models.ScanResult + }{ + { + inCurrent: models.ScanResults{ + { + ScannedAt: atCurrent, + ServerName: "u16", + Family: "ubuntu", + Release: "16.04", + ScannedCves: models.VulnInfos{ + "CVE-2012-6702": { + CveID: "CVE-2012-6702", + PackageNames: []string{"libexpat1"}, + DistroAdvisories: []models.DistroAdvisory{}, + CpeNames: []string{}, + }, + "CVE-2014-9761": { + CveID: "CVE-2014-9761", + PackageNames: []string{"libc-bin"}, + DistroAdvisories: []models.DistroAdvisory{}, + CpeNames: []string{}, + }, + }, + Packages: models.Packages{}, + Errors: []string{}, + Optional: [][]interface{}{}, + }, + }, + inPrevious: models.ScanResults{ + { + ScannedAt: atPrevious, + ServerName: "u16", + Family: "ubuntu", + Release: "16.04", + ScannedCves: models.VulnInfos{ + "CVE-2012-6702": { + CveID: "CVE-2012-6702", + PackageNames: []string{"libexpat1"}, + DistroAdvisories: []models.DistroAdvisory{}, + CpeNames: []string{}, + }, + "CVE-2014-9761": { + CveID: "CVE-2014-9761", + PackageNames: []string{"libc-bin"}, + DistroAdvisories: []models.DistroAdvisory{}, + CpeNames: []string{}, + }, + }, + Packages: models.Packages{}, + Errors: []string{}, + Optional: [][]interface{}{}, + }, + }, + out: models.ScanResult{ + ScannedAt: atCurrent, + ServerName: "u16", + Family: "ubuntu", + Release: "16.04", + Packages: models.Packages{}, + ScannedCves: models.VulnInfos{}, + Errors: []string{}, + Optional: [][]interface{}{}, + }, + }, + { + inCurrent: models.ScanResults{ + { + ScannedAt: atCurrent, + ServerName: "u16", + Family: "ubuntu", + Release: "16.04", + ScannedCves: models.VulnInfos{ + "CVE-2016-6662": { + CveID: "CVE-2016-6662", + PackageNames: []string{"mysql-libs"}, + DistroAdvisories: []models.DistroAdvisory{}, + CpeNames: []string{}, + }, + }, + Packages: models.Packages{ + "mysql-libs": { + Name: "mysql-libs", + Version: "5.1.73", + Release: "7.el6", + NewVersion: "5.1.73", + NewRelease: "8.el6_8", + Repository: "", + Changelog: models.Changelog{ + Contents: "", + Method: "", + }, + }, + }, + }, + }, + inPrevious: models.ScanResults{ + { + ScannedAt: atPrevious, + ServerName: "u16", + Family: "ubuntu", + Release: "16.04", + ScannedCves: models.VulnInfos{}, + }, + }, + out: models.ScanResult{ + ScannedAt: atCurrent, + ServerName: "u16", + Family: "ubuntu", + Release: "16.04", + ScannedCves: models.VulnInfos{ + "CVE-2016-6662": { + CveID: "CVE-2016-6662", + PackageNames: []string{"mysql-libs"}, + DistroAdvisories: []models.DistroAdvisory{}, + CpeNames: []string{}, + }, + }, + Packages: models.Packages{ + "mysql-libs": { + Name: "mysql-libs", + Version: "5.1.73", + Release: "7.el6", + NewVersion: "5.1.73", + NewRelease: "8.el6_8", + Repository: "", + Changelog: models.Changelog{ + Contents: "", + Method: "", + }, + }, + }, + }, + }, + } + + for i, tt := range tests { + diff, _ := diff(tt.inCurrent, tt.inPrevious) + for _, actual := range diff { + if !reflect.DeepEqual(actual.ScannedCves, tt.out.ScannedCves) { + h := pp.Sprint(actual.ScannedCves) + x := pp.Sprint(tt.out.ScannedCves) + t.Errorf("[%d] cves actual: \n %s \n expected: \n %s", i, h, x) + } + + for j := range tt.out.Packages { + if !reflect.DeepEqual(tt.out.Packages[j], actual.Packages[j]) { + h := pp.Sprint(tt.out.Packages[j]) + x := pp.Sprint(actual.Packages[j]) + t.Errorf("[%d] packages actual: \n %s \n expected: \n %s", i, x, h) + } + } + } + } +} diff --git a/util/util.go b/util/util.go index b7dcbc6e..31f27d9e 100644 --- a/util/util.go +++ b/util/util.go @@ -23,7 +23,6 @@ import ( "strings" "github.com/future-architect/vuls/config" - "github.com/future-architect/vuls/models" ) // GenWorkers generates goroutine @@ -139,18 +138,18 @@ func Truncate(str string, length int) string { // 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 - } +// 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 "" -} +// return "" +// } From 74805c6be87f1d4a7b9d04eba691c397e43ce626 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Sun, 21 May 2017 17:07:17 +0900 Subject: [PATCH 036/113] Add test cases of CveContents --- models/cvecontents.go | 11 +- models/cvecontents_test.go | 657 +++++++++++++++++++++++++++++++++++++ models/scanresults.go | 2 +- oval/redhat_test.go | 16 + 4 files changed, 680 insertions(+), 6 deletions(-) diff --git a/models/cvecontents.go b/models/cvecontents.go index 33ab332f..3e549672 100644 --- a/models/cvecontents.go +++ b/models/cvecontents.go @@ -28,7 +28,7 @@ type CveContents map[CveContentType]CveContent // NewCveContents create CveContents func NewCveContents(conts ...CveContent) CveContents { - m := map[CveContentType]CveContent{} + m := CveContents{} for _, cont := range conts { m[cont.Type] = cont } @@ -43,6 +43,7 @@ type CveContentStr struct { // Except returns CveContents except given keys for enumeration func (v CveContents) Except(exceptCtypes ...CveContentType) (values CveContents) { + values = CveContents{} for ctype, content := range v { found := false for _, exceptCtype := range exceptCtypes { @@ -252,12 +253,12 @@ func (v CveContents) Titles(lang, myFamily string) (values []CveContentStr) { 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) + if index < len(summary) { + summary = summary[0:index] + "..." } values = append(values, CveContentStr{ Type: ctype, - Value: summary[0:index] + "...", + Value: summary, }) } } @@ -306,7 +307,7 @@ func (v CveContents) Summaries(lang, myFamily string) (values []CveContentStr) { // SourceLinks returns link of source func (v CveContents) SourceLinks(lang, myFamily, cveID string) (values []CveContentStr) { if lang == "ja" { - if cont, found := v[JVN]; found && !cont.Empty() { + if cont, found := v[JVN]; found && 0 < len(cont.SourceLink) { values = append(values, CveContentStr{JVN, cont.SourceLink}) } } diff --git a/models/cvecontents_test.go b/models/cvecontents_test.go index fced7012..1a178811 100644 --- a/models/cvecontents_test.go +++ b/models/cvecontents_test.go @@ -15,3 +15,660 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ package models + +import ( + "reflect" + "testing" +) + +var m = CveContent{ + Type: RedHat, + CveID: "CVE-2017-0001", + Title: "title", + Summary: "summary", + Severity: "High", + Cvss2Score: 8.0, + Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + Cvss3Score: 9.0, + Cvss3Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", +} + +func TestExcept(t *testing.T) { + var tests = []struct { + in CveContents + out CveContents + }{{ + in: CveContents{ + RedHat: {Type: RedHat}, + Ubuntu: {Type: Ubuntu}, + Debian: {Type: Debian}, + }, + out: CveContents{ + RedHat: {Type: RedHat}, + }, + }, + } + for _, tt := range tests { + actual := tt.in.Except(Ubuntu, Debian) + if !reflect.DeepEqual(tt.out, actual) { + t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + } + } +} + +func TestCvss2Scores(t *testing.T) { + var tests = []struct { + in CveContents + out []CveContentCvss2 + }{ + { + in: CveContents{ + JVN: { + Type: JVN, + Severity: "HIGH", + Cvss2Score: 8.2, + Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + }, + RedHat: { + Type: RedHat, + Severity: "HIGH", + Cvss2Score: 8.0, + Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + }, + NVD: { + Type: NVD, + Cvss2Score: 8.1, + Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + // Severity is NIOT included in NVD + }, + }, + out: []CveContentCvss2{ + { + Type: NVD, + Value: Cvss2{ + Score: 8.1, + Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + Severity: "HIGH", + }, + }, + { + Type: RedHat, + Value: Cvss2{ + Score: 8.0, + Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + Severity: "HIGH", + }, + }, + { + Type: JVN, + Value: Cvss2{ + Score: 8.2, + Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + Severity: "HIGH", + }, + }, + }, + }, + // Empty + { + in: CveContents{}, + out: nil, + }, + } + for _, tt := range tests { + actual := tt.in.Cvss2Scores() + if !reflect.DeepEqual(tt.out, actual) { + t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + } + } +} + +func TestMaxCvss2Scores(t *testing.T) { + var tests = []struct { + in CveContents + out CveContentCvss2 + }{ + { + in: CveContents{ + JVN: { + Type: JVN, + Severity: "HIGH", + Cvss2Score: 8.2, + Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + }, + RedHat: { + Type: RedHat, + Severity: "HIGH", + Cvss2Score: 8.0, + Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + }, + NVD: { + Type: NVD, + Cvss2Score: 8.1, + Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + // Severity is NIOT included in NVD + }, + }, + out: CveContentCvss2{ + Type: JVN, + Value: Cvss2{ + Score: 8.2, + Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + Severity: "HIGH", + }, + }, + }, + // Empty + { + in: CveContents{}, + out: CveContentCvss2{ + Type: Unknown, + Value: Cvss2{ + Score: 0.0, + Vector: "", + Severity: "", + }, + }, + }, + } + for _, tt := range tests { + actual := tt.in.MaxCvss2Score() + if !reflect.DeepEqual(tt.out, actual) { + t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + } + } +} + +func TestCvss3Scores(t *testing.T) { + var tests = []struct { + in CveContents + out []CveContentCvss3 + }{ + { + in: CveContents{ + RedHat: { + Type: RedHat, + Severity: "HIGH", + Cvss3Score: 8.0, + Cvss3Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + }, + NVD: { + Type: NVD, + Cvss3Score: 8.1, + Cvss3Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + // Severity is NIOT included in NVD + }, + }, + out: []CveContentCvss3{ + { + Type: RedHat, + Value: Cvss3{ + Score: 8.0, + Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + Severity: "HIGH", + }, + }, + }, + }, + // Empty + { + in: CveContents{}, + out: nil, + }, + } + for _, tt := range tests { + actual := tt.in.Cvss3Scores() + if !reflect.DeepEqual(tt.out, actual) { + t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + } + } +} + +func TestMaxCvss3Scores(t *testing.T) { + var tests = []struct { + in CveContents + out CveContentCvss3 + }{ + { + in: CveContents{ + RedHat: { + Type: RedHat, + Severity: "HIGH", + Cvss3Score: 8.0, + Cvss3Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + }, + }, + out: CveContentCvss3{ + Type: RedHat, + Value: Cvss3{ + Score: 8.0, + Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + Severity: "HIGH", + }, + }, + }, + // Empty + { + in: CveContents{}, + out: CveContentCvss3{ + Type: Unknown, + Value: Cvss3{ + Score: 0.0, + Vector: "", + Severity: "", + }, + }, + }, + } + for _, tt := range tests { + actual := tt.in.MaxCvss3Score() + if !reflect.DeepEqual(tt.out, actual) { + t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + } + } +} + +func TestFormatMaxCvssScore(t *testing.T) { + var tests = []struct { + in CveContents + out string + }{ + { + in: CveContents{ + JVN: { + Type: JVN, + Severity: "HIGH", + Cvss2Score: 8.3, + }, + RedHat: { + Type: RedHat, + Severity: "HIGH", + Cvss3Score: 8.0, + }, + NVD: { + Type: NVD, + Cvss2Score: 8.1, + // Severity is NIOT included in NVD + }, + }, + out: "8.3 HIGH (jvn)", + }, + { + in: CveContents{ + JVN: { + Type: JVN, + Severity: "HIGH", + Cvss2Score: 8.3, + }, + RedHat: { + Type: RedHat, + Severity: "HIGH", + Cvss2Score: 8.0, + Cvss3Score: 9.9, + }, + NVD: { + Type: NVD, + Cvss2Score: 8.1, + }, + }, + out: "9.9 HIGH (redhat)", + }, + } + for _, tt := range tests { + actual := tt.in.FormatMaxCvssScore() + if !reflect.DeepEqual(tt.out, actual) { + t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + } + } +} + +func TestTitles(t *testing.T) { + type in struct { + lang string + cont CveContents + } + var tests = []struct { + in in + out []CveContentStr + }{ + // lang: ja + { + in: in{ + lang: "ja", + cont: CveContents{ + JVN: { + Type: JVN, + Title: "Title1", + }, + RedHat: { + Type: RedHat, + Summary: "Summary RedHat", + }, + NVD: { + Type: NVD, + Summary: "Summary NVD", + // Severity is NIOT included in NVD + }, + }, + }, + out: []CveContentStr{ + { + Type: JVN, + Value: "Title1", + }, + { + Type: NVD, + Value: "Summary NVD", + }, + { + Type: RedHat, + Value: "Summary RedHat", + }, + }, + }, + // lang: en + { + in: in{ + lang: "en", + cont: CveContents{ + JVN: { + Type: JVN, + Title: "Title1", + }, + RedHat: { + Type: RedHat, + Summary: "Summary RedHat", + }, + NVD: { + Type: NVD, + Summary: "Summary NVD", + // Severity is NIOT included in NVD + }, + }, + }, + out: []CveContentStr{ + { + Type: NVD, + Value: "Summary NVD", + }, + { + Type: RedHat, + Value: "Summary RedHat", + }, + }, + }, + // lang: empty + { + in: in{ + lang: "en", + cont: CveContents{}, + }, + out: []CveContentStr{ + { + Type: Unknown, + Value: "-", + }, + }, + }, + } + for _, tt := range tests { + actual := tt.in.cont.Titles(tt.in.lang, "redhat") + if !reflect.DeepEqual(tt.out, actual) { + t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + } + } +} + +func TestSummaries(t *testing.T) { + type in struct { + lang string + cont CveContents + } + var tests = []struct { + in in + out []CveContentStr + }{ + // lang: ja + { + in: in{ + lang: "ja", + cont: CveContents{ + JVN: { + Type: JVN, + Title: "Title JVN", + Summary: "Summary JVN", + }, + RedHat: { + Type: RedHat, + Summary: "Summary RedHat", + }, + NVD: { + Type: NVD, + Summary: "Summary NVD", + // Severity is NIOT included in NVD + }, + }, + }, + out: []CveContentStr{ + { + Type: JVN, + Value: "Title JVN\nSummary JVN", + }, + { + Type: NVD, + Value: "Summary NVD", + }, + { + Type: RedHat, + Value: "Summary RedHat", + }, + }, + }, + // lang: en + { + in: in{ + lang: "en", + cont: CveContents{ + JVN: { + Type: JVN, + Title: "Title JVN", + Summary: "Summary JVN", + }, + RedHat: { + Type: RedHat, + Summary: "Summary RedHat", + }, + NVD: { + Type: NVD, + Summary: "Summary NVD", + // Severity is NIOT included in NVD + }, + }, + }, + out: []CveContentStr{ + { + Type: NVD, + Value: "Summary NVD", + }, + { + Type: RedHat, + Value: "Summary RedHat", + }, + }, + }, + // lang: empty + { + in: in{ + lang: "en", + cont: CveContents{}, + }, + out: []CveContentStr{ + { + Type: Unknown, + Value: "-", + }, + }, + }, + } + for _, tt := range tests { + actual := tt.in.cont.Summaries(tt.in.lang, "redhat") + if !reflect.DeepEqual(tt.out, actual) { + t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + } + } +} + +func TestSourceLinks(t *testing.T) { + type in struct { + lang string + cveID string + cont CveContents + } + var tests = []struct { + in in + out []CveContentStr + }{ + // lang: ja + { + in: in{ + lang: "ja", + cveID: "CVE-2017-6074", + cont: CveContents{ + JVN: { + Type: JVN, + SourceLink: "https://jvn.jp/vu/JVNVU93610402/", + }, + RedHat: { + Type: RedHat, + SourceLink: "https://access.redhat.com/security/cve/CVE-2017-6074", + }, + NVD: { + Type: NVD, + SourceLink: "https://nvd.nist.gov/vuln/detail/CVE-2017-6074", + }, + }, + }, + out: []CveContentStr{ + { + Type: JVN, + Value: "https://jvn.jp/vu/JVNVU93610402/", + }, + { + Type: NVD, + Value: "https://nvd.nist.gov/vuln/detail/CVE-2017-6074", + }, + { + Type: RedHat, + Value: "https://access.redhat.com/security/cve/CVE-2017-6074", + }, + }, + }, + // lang: en + { + in: in{ + lang: "en", + cveID: "CVE-2017-6074", + cont: CveContents{ + JVN: { + Type: JVN, + SourceLink: "https://jvn.jp/vu/JVNVU93610402/", + }, + RedHat: { + Type: RedHat, + SourceLink: "https://access.redhat.com/security/cve/CVE-2017-6074", + }, + NVD: { + Type: NVD, + SourceLink: "https://nvd.nist.gov/vuln/detail/CVE-2017-6074", + }, + }, + }, + out: []CveContentStr{ + { + Type: NVD, + Value: "https://nvd.nist.gov/vuln/detail/CVE-2017-6074", + }, + { + Type: RedHat, + Value: "https://access.redhat.com/security/cve/CVE-2017-6074", + }, + }, + }, + // lang: empty + { + in: in{ + lang: "en", + cveID: "CVE-2017-6074", + cont: CveContents{}, + }, + out: []CveContentStr{ + { + Type: NVD, + Value: "https://nvd.nist.gov/vuln/detail/CVE-2017-6074", + }, + }, + }, + } + for _, tt := range tests { + actual := tt.in.cont.SourceLinks(tt.in.lang, "redhat", tt.in.cveID) + if !reflect.DeepEqual(tt.out, actual) { + t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + } + } +} + +func TestVendorLink(t *testing.T) { + type in struct { + family string + cont CveContents + } + var tests = []struct { + in in + out CveContentStr + }{ + { + in: in{ + family: "redhat", + cont: CveContents{ + JVN: { + Type: JVN, + SourceLink: "https://jvn.jp/vu/JVNVU93610402/", + }, + RedHat: { + Type: RedHat, + SourceLink: "https://access.redhat.com/security/cve/CVE-2017-6074", + }, + NVD: { + Type: NVD, + SourceLink: "https://nvd.nist.gov/vuln/detail/CVE-2017-6074", + }, + }, + }, + out: CveContentStr{ + Type: RedHat, + Value: "https://access.redhat.com/security/cve/CVE-2017-6074", + }, + }, + { + in: in{ + family: "ubuntu", + cont: CveContents{ + RedHat: { + Type: RedHat, + SourceLink: "https://access.redhat.com/security/cve/CVE-2017-6074", + }, + }, + }, + out: CveContentStr{ + Type: Ubuntu, + Value: "", + }, + }, + } + for _, tt := range tests { + actual := tt.in.cont.VendorLink(tt.in.family) + if !reflect.DeepEqual(tt.out, actual) { + t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + } + } +} diff --git a/models/scanresults.go b/models/scanresults.go index a276ef25..d3c45ecd 100644 --- a/models/scanresults.go +++ b/models/scanresults.go @@ -27,7 +27,7 @@ import ( cvedict "github.com/kotakanbe/go-cve-dictionary/models" ) -// ScanResults is slice of ScanResult. +// ScanResults is a slide of ScanResult type ScanResults []ScanResult //TODO diff --git a/oval/redhat_test.go b/oval/redhat_test.go index c186e1d8..79b90eab 100644 --- a/oval/redhat_test.go +++ b/oval/redhat_test.go @@ -1,3 +1,19 @@ +/* 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 oval import "testing" From eb02bdd95aa60045a933efbf6b5fec198c28be97 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Sun, 21 May 2017 19:22:19 +0900 Subject: [PATCH 037/113] Add test cases of models.Packages --- models/packages.go | 2 +- models/packages_test.go | 88 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/models/packages.go b/models/packages.go index d923dfc5..056c4f83 100644 --- a/models/packages.go +++ b/models/packages.go @@ -48,7 +48,7 @@ func (ps Packages) MergeNewVersion(as Packages) { // Merge returns merged map (immutable) func (ps Packages) Merge(other Packages) Packages { - merged := map[string]Package{} + merged := Packages{} for k, v := range ps { merged[k] = v } diff --git a/models/packages_test.go b/models/packages_test.go index 321b07db..ae92e410 100644 --- a/models/packages_test.go +++ b/models/packages_test.go @@ -57,3 +57,91 @@ func TestMergeNewVersion(t *testing.T) { t.Errorf("expected %s, actual %s", e, a) } } + +func TestMerge(t *testing.T) { + var test = struct { + a Packages + b Packages + expected Packages + }{ + Packages{ + "hoge": {Name: "hoge"}, + "fuga": {Name: "fuga"}, + }, + Packages{ + "hega": {Name: "hega"}, + "hage": {Name: "hage"}, + }, + Packages{ + "hoge": {Name: "hoge"}, + "fuga": {Name: "fuga"}, + "hega": {Name: "hega"}, + "hage": {Name: "hage"}, + }, + } + + actual := test.a.Merge(test.b) + if !reflect.DeepEqual(actual, test.expected) { + e := pp.Sprintf("%v", test.expected) + a := pp.Sprintf("%v", actual) + t.Errorf("expected %s, actual %s", e, a) + } +} + +func TestFormatVersionsFromTo(t *testing.T) { + var tests = []struct { + packs Packages + expected string + }{ + { + packs: Packages{ + "hoge": { + Name: "hoge", + Version: "1.0.0", + Release: "release1", + NewVersion: "1.0.1", + NewRelease: "release2", + }, + }, + expected: "hoge-1.0.0-release1 -> hoge-1.0.1-release2", + }, + { + packs: Packages{ + "hoge": { + Name: "hoge", + Version: "1.0.0", + Release: "", + NewVersion: "1.0.1", + NewRelease: "", + }, + }, + expected: "hoge-1.0.0 -> hoge-1.0.1", + }, + { + packs: Packages{ + "hoge": { + Name: "hoge", + Version: "1.0.0", + Release: "", + NewVersion: "1.0.1", + NewRelease: "", + }, + "fuga": { + Name: "fuga", + Version: "2.0.0", + Release: "", + NewVersion: "2.0.1", + NewRelease: "", + }, + }, + expected: "hoge-1.0.0 -> hoge-1.0.1\nfuga-2.0.0 -> fuga-2.0.1", + }, + } + + for _, tt := range tests { + actual := tt.packs.FormatVersionsFromTo() + if !reflect.DeepEqual(tt.expected, actual) { + t.Errorf("\nexpected: %v\n actual: %v\n", tt.expected, actual) + } + } +} From a31974a3c0d334b4a5d6ace28212dfc619115e25 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Sun, 21 May 2017 23:04:21 +0900 Subject: [PATCH 038/113] Use Severity ranking in OVAL when the CVSS scores are empty. --- models/cvecontents.go | 64 +++++++++++++ models/cvecontents_test.go | 16 ++++ models/scanresults.go | 15 +-- models/scanresults_test.go | 190 +++++++++++++++++++++++++++++++++++++ 4 files changed, 278 insertions(+), 7 deletions(-) diff --git a/models/cvecontents.go b/models/cvecontents.go index 3e549672..d0322089 100644 --- a/models/cvecontents.go +++ b/models/cvecontents.go @@ -106,6 +106,7 @@ func (v CveContents) Cvss2Scores() (values []CveContentCvss2) { }) } } + return } @@ -136,9 +137,69 @@ func (v CveContents) MaxCvss2Score() CveContentCvss2 { max = cont.Cvss2Score } } + if 0 < max { + return value + } + + // If CVSS score isn't on NVD, RedHat and JVN use OVAL's Severity information. + // Convert severity to cvss srore, then returns max severity. + // Only Ubuntu, RedHat and Oracle OVAL has severity data. + order = []CveContentType{Ubuntu, RedHat, Oracle} + for _, ctype := range order { + if cont, found := v[ctype]; found && 0 < len(cont.Severity) { + score := 0.0 + switch cont.Type { + case Ubuntu: + score = severityToScoreForUbuntu(cont.Severity) + case Oracle, RedHat: + score = severityToScoreForRedHat(cont.Severity) + } + if max < score { + value = CveContentCvss2{ + Type: ctype, + Value: Cvss2{ + Score: score, + Vector: cont.Cvss2Vector, + Severity: cont.Severity, + }, + } + } + max = score + } + } return value } +// Convert Severity to Score for Ubuntu OVAL +func severityToScoreForUbuntu(severity string) float64 { + switch strings.ToUpper(severity) { + case "HIGH": + return 10.0 + case "MEDIUM": + return 6.9 + case "LOW": + return 3.9 + } + return 0 +} + +// Convert Severity to Score for RedHat, Oracle OVAL +// https://access.redhat.com/security/updates/classification +// Since I don't know the definition, Use the definition of CVSSv3 +func severityToScoreForRedHat(severity string) float64 { + switch strings.ToUpper(severity) { + case "CRITICAL": + return 10.0 + case "IMPORTANT": + return 8.9 + case "MODERATE": + return 6.9 + case "LOW": + return 3.9 + } + return 0 +} + // CveContentCvss3 has CveContentType and Cvss3 type CveContentCvss3 struct { Type CveContentType @@ -477,6 +538,9 @@ const ( // Ubuntu is Ubuntu Ubuntu CveContentType = "ubuntu" + // Oracle is Oracle Linux + Oracle CveContentType = "oracle" + // Unknown is Unknown Unknown CveContentType = "unknown" ) diff --git a/models/cvecontents_test.go b/models/cvecontents_test.go index 1a178811..2428259b 100644 --- a/models/cvecontents_test.go +++ b/models/cvecontents_test.go @@ -158,6 +158,22 @@ func TestMaxCvss2Scores(t *testing.T) { }, }, }, + // Severity in OVAL + { + in: CveContents{ + Ubuntu: { + Type: Ubuntu, + Severity: "HIGH", + }, + }, + out: CveContentCvss2{ + Type: Ubuntu, + Value: Cvss2{ + Score: 10, + Severity: "HIGH", + }, + }, + }, // Empty { in: CveContents{}, diff --git a/models/scanresults.go b/models/scanresults.go index d3c45ecd..c6f207a1 100644 --- a/models/scanresults.go +++ b/models/scanresults.go @@ -166,13 +166,14 @@ func (r ScanResult) FilterByCvssOver(over float64) ScanResult { // TODO: Filter by ignore cves??? filtered := r.ScannedCves.Find(func(v VulnInfo) bool { - //TODO in the case of only oval, no cvecontents - values := v.CveContents.Cvss2Scores() - for _, vals := range values { - score := vals.Value.Score - if over <= score { - return true - } + v2Max := v.CveContents.MaxCvss2Score() + v3Max := v.CveContents.MaxCvss3Score() + max := v2Max.Value.Score + if max < v3Max.Value.Score { + max = v3Max.Value.Score + } + if over <= max { + return true } return false }) diff --git a/models/scanresults_test.go b/models/scanresults_test.go index fced7012..5cd44b8e 100644 --- a/models/scanresults_test.go +++ b/models/scanresults_test.go @@ -15,3 +15,193 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ package models + +import ( + "reflect" + "testing" + "time" + + "github.com/k0kubun/pp" +) + +func TestFilterByCvssOver(t *testing.T) { + type in struct { + over float64 + rs ScanResult + } + var tests = []struct { + in in + out ScanResult + }{ + { + in: in{ + over: 7.0, + rs: ScanResult{ + ScannedCves: VulnInfos{ + "CVE-2017-0001": { + CveID: "CVE-2017-0001", + CveContents: NewCveContents( + CveContent{ + Type: NVD, + CveID: "CVE-2017-0001", + Cvss2Score: 7.1, + LastModified: time.Time{}, + }, + ), + }, + "CVE-2017-0002": { + CveID: "CVE-2017-0002", + CveContents: NewCveContents( + CveContent{ + Type: NVD, + CveID: "CVE-2017-0002", + Cvss2Score: 6.9, + LastModified: time.Time{}, + }, + ), + }, + "CVE-2017-0003": { + CveID: "CVE-2017-0003", + CveContents: NewCveContents( + CveContent{ + Type: NVD, + CveID: "CVE-2017-0003", + Cvss2Score: 6.9, + LastModified: time.Time{}, + }, + CveContent{ + Type: JVN, + CveID: "CVE-2017-0003", + Cvss2Score: 7.2, + LastModified: time.Time{}, + }, + ), + }, + }, + }, + }, + out: ScanResult{ + ScannedCves: VulnInfos{ + "CVE-2017-0001": { + CveID: "CVE-2017-0001", + CveContents: NewCveContents( + CveContent{ + Type: NVD, + CveID: "CVE-2017-0001", + Cvss2Score: 7.1, + LastModified: time.Time{}, + }, + ), + }, + "CVE-2017-0003": { + CveID: "CVE-2017-0003", + CveContents: NewCveContents( + CveContent{ + Type: NVD, + CveID: "CVE-2017-0003", + Cvss2Score: 6.9, + LastModified: time.Time{}, + }, + CveContent{ + Type: JVN, + CveID: "CVE-2017-0003", + Cvss2Score: 7.2, + LastModified: time.Time{}, + }, + ), + }, + }, + }, + }, + // OVAL Severity + { + in: in{ + over: 7.0, + rs: ScanResult{ + ScannedCves: VulnInfos{ + "CVE-2017-0001": { + CveID: "CVE-2017-0001", + CveContents: NewCveContents( + CveContent{ + Type: Ubuntu, + CveID: "CVE-2017-0001", + Severity: "HIGH", + LastModified: time.Time{}, + }, + ), + }, + "CVE-2017-0002": { + CveID: "CVE-2017-0002", + CveContents: NewCveContents( + CveContent{ + Type: RedHat, + CveID: "CVE-2017-0002", + Severity: "CRITICAL", + LastModified: time.Time{}, + }, + ), + }, + "CVE-2017-0003": { + CveID: "CVE-2017-0003", + CveContents: NewCveContents( + CveContent{ + Type: Oracle, + CveID: "CVE-2017-0003", + Severity: "IMPORTANT", + LastModified: time.Time{}, + }, + ), + }, + }, + }, + }, + out: ScanResult{ + ScannedCves: VulnInfos{ + "CVE-2017-0001": { + CveID: "CVE-2017-0001", + CveContents: NewCveContents( + CveContent{ + Type: Ubuntu, + CveID: "CVE-2017-0001", + Severity: "HIGH", + LastModified: time.Time{}, + }, + ), + }, + "CVE-2017-0002": { + CveID: "CVE-2017-0002", + CveContents: NewCveContents( + CveContent{ + Type: RedHat, + CveID: "CVE-2017-0002", + Severity: "CRITICAL", + LastModified: time.Time{}, + }, + ), + }, + "CVE-2017-0003": { + CveID: "CVE-2017-0003", + CveContents: NewCveContents( + CveContent{ + Type: Oracle, + CveID: "CVE-2017-0003", + Severity: "IMPORTANT", + LastModified: time.Time{}, + }, + ), + }, + }, + }, + }, + } + for _, tt := range tests { + actual := tt.in.rs.FilterByCvssOver(tt.in.over) + for k := range tt.out.ScannedCves { + if !reflect.DeepEqual(tt.out.ScannedCves[k], actual.ScannedCves[k]) { + o := pp.Sprintf("%v", tt.out.ScannedCves[k]) + a := pp.Sprintf("%v", actual.ScannedCves[k]) + t.Errorf("[%s] expected: %v\n actual: %v\n", k, o, a) + } + } + } +} From 73b011eba73d6d2599acd7dd5a4c86e8e2d06b7a Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Tue, 23 May 2017 15:48:59 +0900 Subject: [PATCH 039/113] Sort results order by CVSS score, CVE-ID --- commands/report.go | 22 +---- models/cvecontents.go | 16 +++- models/cvecontents_test.go | 75 +++++++++++++--- models/vulninfos.go | 19 ++++- models/vulninfos_test.go | 171 +++++++++++++++++++++++++++++++++++++ report/util.go | 6 +- 6 files changed, 270 insertions(+), 39 deletions(-) diff --git a/commands/report.go b/commands/report.go index 442b936c..ee2ea724 100644 --- a/commands/report.go +++ b/commands/report.go @@ -299,8 +299,6 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} c.Conf.IgnoreUnscoredCves = p.ignoreUnscoredCves c.Conf.HTTPProxy = p.httpProxy - c.Conf.Pipe = p.pipe - c.Conf.FormatXML = p.formatXML c.Conf.FormatJSON = p.formatJSON c.Conf.FormatOneEMail = p.formatOneEMail @@ -310,6 +308,7 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} c.Conf.GZIP = p.gzip c.Conf.Diff = p.diff + c.Conf.Pipe = p.pipe var dir string var err error @@ -405,30 +404,11 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} } util.Log.Infof("Loaded: %s", dir) - //TODO dir if res, err = report.FillCveInfos(res, dir); err != nil { util.Log.Error(err) return subcommands.ExitFailure } - // TODO Filter, Sort - // TODO Add sort function to ScanResults - //remove - // for _, vuln := range r.ScannedCves { - // // if _, ok := vuln.CveContents.Get(models.NewCveContentType(r.Family)); !ok { - // // pp.Printf("not in oval: %s %f\n%v\n", - // // vuln.CveID, vuln.CveContents.CvssV2Score(), vuln.Packages) - // // } else { - // // fmt.Printf(" in oval: %s %f\n", - // // vuln.CveID, vuln.CveContents.CvssV2Score()) - // // } - // // if vuln.CveContents.CvssV2Score() < 0.1 && - // // vuln.CveContents.CvssV3Score() < 0.1 { - // // pp.Println(vuln) - // // } - // } - // } - for _, w := range reports { if err := w.Write(res...); err != nil { util.Log.Errorf("Failed to report: %s", err) diff --git a/models/cvecontents.go b/models/cvecontents.go index d0322089..21168e1a 100644 --- a/models/cvecontents.go +++ b/models/cvecontents.go @@ -185,7 +185,7 @@ func severityToScoreForUbuntu(severity string) float64 { // Convert Severity to Score for RedHat, Oracle OVAL // https://access.redhat.com/security/updates/classification -// Since I don't know the definition, Use the definition of CVSSv3 +// Use the definition of CVSSv3 because the exact definition of severity and score is not described. func severityToScoreForRedHat(severity string) float64 { switch strings.ToUpper(severity) { case "CRITICAL": @@ -231,7 +231,6 @@ func cvss3ScoreToSeverity(score float64) string { // 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 { @@ -255,7 +254,6 @@ func (v CveContents) Cvss3Scores() (values []CveContentCvss3) { // MaxCvss3Score returns Max CVSS V3 Score func (v CveContents) MaxCvss3Score() CveContentCvss3 { - //TODO Severity Ubuntu, Debian... order := []CveContentType{RedHat} max := 0.0 value := CveContentCvss3{ @@ -283,6 +281,18 @@ func (v CveContents) MaxCvss3Score() CveContentCvss3 { return value } +// MaxCvssScore returns max CVSS Score +// If there is no CVSS Score, return Severity as a numerical value. +func (v CveContents) MaxCvssScore() float64 { + v3Max := v.MaxCvss3Score() + v2Max := v.MaxCvss2Score() + max := v3Max.Value.Score + if max < v2Max.Value.Score { + max = v2Max.Value.Score + } + return max +} + // FormatMaxCvssScore returns Max CVSS Score func (v CveContents) FormatMaxCvssScore() string { v2Max := v.MaxCvss2Score() diff --git a/models/cvecontents_test.go b/models/cvecontents_test.go index 2428259b..dbafb037 100644 --- a/models/cvecontents_test.go +++ b/models/cvecontents_test.go @@ -21,18 +21,6 @@ import ( "testing" ) -var m = CveContent{ - Type: RedHat, - CveID: "CVE-2017-0001", - Title: "title", - Summary: "summary", - Severity: "High", - Cvss2Score: 8.0, - Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", - Cvss3Score: 9.0, - Cvss3Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", -} - func TestExcept(t *testing.T) { var tests = []struct { in CveContents @@ -284,6 +272,69 @@ func TestMaxCvss3Scores(t *testing.T) { } } +func TestMaxCvssScores(t *testing.T) { + var tests = []struct { + in CveContents + out float64 + }{ + { + in: CveContents{ + NVD: { + Type: NVD, + Cvss3Score: 7.0, + }, + RedHat: { + Type: RedHat, + Cvss2Score: 8.0, + }, + }, + out: 8.0, + }, + { + in: CveContents{ + RedHat: { + Type: RedHat, + Cvss3Score: 8.0, + }, + }, + out: 8.0, + }, + { + in: CveContents{ + Ubuntu: { + Type: Ubuntu, + Severity: "HIGH", + }, + }, + out: 10.0, + }, + { + in: CveContents{ + Ubuntu: { + Type: Ubuntu, + Severity: "MEDIUM", + }, + NVD: { + Type: NVD, + Cvss2Score: 7.0, + }, + }, + out: 7.0, + }, + // Empty + { + in: CveContents{}, + out: 0, + }, + } + for i, tt := range tests { + actual := tt.in.MaxCvssScore() + if !reflect.DeepEqual(tt.out, actual) { + t.Errorf("\n[%d] expected: %v\n actual: %v\n", i, tt.out, actual) + } + } +} + func TestFormatMaxCvssScore(t *testing.T) { var tests = []struct { in CveContents diff --git a/models/vulninfos.go b/models/vulninfos.go index a1a03540..25a4757c 100644 --- a/models/vulninfos.go +++ b/models/vulninfos.go @@ -19,6 +19,7 @@ package models import ( "fmt" + "sort" "time" ) @@ -36,7 +37,7 @@ func (v VulnInfos) Find(f func(VulnInfo) bool) VulnInfos { return filtered } -// FindScoredVulns return socred vulnerabilities +// FindScoredVulns return scored vulnerabilities func (v VulnInfos) FindScoredVulns() VulnInfos { return v.Find(func(vv VulnInfo) bool { if 0 < vv.CveContents.MaxCvss2Score().Value.Score || @@ -47,6 +48,22 @@ func (v VulnInfos) FindScoredVulns() VulnInfos { }) } +// ToSortedSlice returns slice of VulnInfos that is sorted by CVE-ID +func (v VulnInfos) ToSortedSlice() (sorted []VulnInfo) { + for k := range v { + sorted = append(sorted, v[k]) + } + sort.Slice(sorted, func(i, j int) bool { + maxI := sorted[i].CveContents.MaxCvssScore() + maxJ := sorted[j].CveContents.MaxCvssScore() + if maxI != maxJ { + return maxJ < maxI + } + return sorted[i].CveID < sorted[j].CveID + }) + return +} + // VulnInfo holds a vulnerability information and unsecure packages type VulnInfo struct { CveID string diff --git a/models/vulninfos_test.go b/models/vulninfos_test.go index fced7012..1e796a3a 100644 --- a/models/vulninfos_test.go +++ b/models/vulninfos_test.go @@ -15,3 +15,174 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ package models + +import ( + "reflect" + "testing" +) + +func TestToSortedSlice(t *testing.T) { + var tests = []struct { + in VulnInfos + out []VulnInfo + }{ + { + in: VulnInfos{ + "CVE-2017-0002": { + CveID: "CVE-2017-0002", + CveContents: CveContents{ + NVD: { + Type: NVD, + Cvss3Score: 6.0, + }, + RedHat: { + Type: RedHat, + Cvss2Score: 7.0, + }, + }, + }, + "CVE-2017-0001": { + CveID: "CVE-2017-0001", + CveContents: CveContents{ + NVD: { + Type: NVD, + Cvss3Score: 7.0, + }, + RedHat: { + Type: RedHat, + Cvss2Score: 8.0, + }, + }, + }, + }, + out: []VulnInfo{ + { + CveID: "CVE-2017-0001", + CveContents: CveContents{ + NVD: { + Type: NVD, + Cvss3Score: 7.0, + }, + RedHat: { + Type: RedHat, + Cvss2Score: 8.0, + }, + }, + }, + { + CveID: "CVE-2017-0002", + CveContents: CveContents{ + NVD: { + Type: NVD, + Cvss3Score: 6.0, + }, + RedHat: { + Type: RedHat, + Cvss2Score: 7.0, + }, + }, + }, + }, + }, + // When max scores are the same, sort by CVE-ID + { + in: VulnInfos{ + "CVE-2017-0002": { + CveID: "CVE-2017-0002", + CveContents: CveContents{ + NVD: { + Type: NVD, + Cvss3Score: 6.0, + }, + RedHat: { + Type: RedHat, + Cvss2Score: 7.0, + }, + }, + }, + "CVE-2017-0001": { + CveID: "CVE-2017-0001", + CveContents: CveContents{ + RedHat: { + Type: RedHat, + Cvss2Score: 7.0, + }, + }, + }, + }, + out: []VulnInfo{ + { + CveID: "CVE-2017-0001", + CveContents: CveContents{ + RedHat: { + Type: RedHat, + Cvss2Score: 7.0, + }, + }, + }, + { + CveID: "CVE-2017-0002", + CveContents: CveContents{ + NVD: { + Type: NVD, + Cvss3Score: 6.0, + }, + RedHat: { + Type: RedHat, + Cvss2Score: 7.0, + }, + }, + }, + }, + }, + // When there are no cvss scores, sort by severity + { + in: VulnInfos{ + "CVE-2017-0002": { + CveID: "CVE-2017-0002", + CveContents: CveContents{ + Ubuntu: { + Type: Ubuntu, + Severity: "High", + }, + }, + }, + "CVE-2017-0001": { + CveID: "CVE-2017-0001", + CveContents: CveContents{ + Ubuntu: { + Type: Ubuntu, + Severity: "Low", + }, + }, + }, + }, + out: []VulnInfo{ + { + CveID: "CVE-2017-0002", + CveContents: CveContents{ + Ubuntu: { + Type: Ubuntu, + Severity: "High", + }, + }, + }, + { + CveID: "CVE-2017-0001", + CveContents: CveContents{ + Ubuntu: { + Type: Ubuntu, + Severity: "Low", + }, + }, + }, + }, + }, + } + for _, tt := range tests { + actual := tt.in.ToSortedSlice() + if !reflect.DeepEqual(tt.out, actual) { + t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + } + } +} diff --git a/report/util.go b/report/util.go index 9e4eb230..de848095 100644 --- a/report/util.go +++ b/report/util.go @@ -111,7 +111,7 @@ func formatShortPlainText(r models.ScanResult) string { stable := uitable.New() stable.MaxColWidth = maxColWidth stable.Wrap = true - for _, vuln := range vulns { + for _, vuln := range vulns.ToSortedSlice() { summaries := vuln.CveContents.Summaries(config.Conf.Lang, r.Family) links := vuln.CveContents.SourceLinks( config.Conf.Lang, r.Family, vuln.CveID) @@ -178,7 +178,7 @@ func formatFullPlainText(r models.ScanResult) string { table := uitable.New() table.MaxColWidth = maxColWidth table.Wrap = true - for _, vuln := range vulns { + for _, vuln := range vulns.ToSortedSlice() { table.AddRow(vuln.CveID) table.AddRow("----------------") table.AddRow("Max Score", vuln.CveContents.FormatMaxCvssScore()) @@ -209,12 +209,14 @@ func formatFullPlainText(r models.ScanResult) string { } packsVer := []string{} + sort.Strings(vuln.PackageNames) 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()) } } + sort.Strings(vuln.CpeNames) for _, name := range vuln.CpeNames { packsVer = append(packsVer, name) } From 0a012273ec855de153775995261856fa354c1c53 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Tue, 23 May 2017 17:47:41 +0900 Subject: [PATCH 040/113] Fix -ignore-unscored-cves --- models/scanresults.go | 31 +++---------------------------- report/email.go | 4 ++-- report/report.go | 3 ++- report/slack.go | 2 +- report/util.go | 6 +++--- 5 files changed, 11 insertions(+), 35 deletions(-) diff --git a/models/scanresults.go b/models/scanresults.go index c6f207a1..8aa6623d 100644 --- a/models/scanresults.go +++ b/models/scanresults.go @@ -30,25 +30,6 @@ import ( // ScanResults is a slide of ScanResult type ScanResults []ScanResult -//TODO -// // Len implement Sort Interface -// func (s ScanResults) Len() int { -// return len(s) -// } - -// // Swap implement Sort Interface -// func (s ScanResults) Swap(i, j int) { -// s[i], s[j] = s[j], s[i] -// } - -// // Less implement Sort Interface -// func (s ScanResults) Less(i, j int) bool { -// if s[i].ServerName == s[j].ServerName { -// return s[i].Container.ContainerID < s[i].Container.ContainerID -// } -// return s[i].ServerName < s[j].ServerName -// } - // ScanResult has the result of scanned CVE information. type ScanResult struct { ScannedAt time.Time @@ -159,12 +140,6 @@ func (r ScanResult) ConvertJvnToModel(cveID string, jvn cvedict.Jvn) *CveContent // FilterByCvssOver is filter function. func (r ScanResult) FilterByCvssOver(over float64) ScanResult { - // TODO: Set correct default value - if over == 0 { - over = -1.1 - } - - // TODO: Filter by ignore cves??? filtered := r.ScannedCves.Find(func(v VulnInfo) bool { v2Max := v.CveContents.MaxCvss2Score() v3Max := v.CveContents.MaxCvss3Score() @@ -241,7 +216,7 @@ func (r ScanResult) FormatServerName() string { } // CveSummary summarize the number of CVEs group by CVSSv2 Severity -func (r ScanResult) CveSummary(ignoreUnscoreCves bool) string { +func (r ScanResult) CveSummary() string { var high, medium, low, unknown int for _, vInfo := range r.ScannedCves { score := vInfo.CveContents.MaxCvss2Score().Value.Score @@ -260,7 +235,7 @@ func (r ScanResult) CveSummary(ignoreUnscoreCves bool) string { } } - if ignoreUnscoreCves { + if config.Conf.IgnoreUnscoredCves { return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d)", high+medium+low, high, medium, low) } @@ -278,7 +253,7 @@ func (r ScanResult) FormatTextReportHeadedr() string { return fmt.Sprintf("%s\n%s\n%s\t%s\n", r.ServerInfo(), buf.String(), - r.CveSummary(config.Conf.IgnoreUnscoredCves), + r.CveSummary(), r.Packages.FormatUpdatablePacksSummary(), ) } diff --git a/report/email.go b/report/email.go index 5dc06ba9..b08de9e3 100644 --- a/report/email.go +++ b/report/email.go @@ -52,7 +52,7 @@ func (w EMailWriter) Write(rs ...models.ScanResult) (err error) { subject = fmt.Sprintf("%s%s %s", conf.EMail.SubjectPrefix, r.ServerInfo(), - r.CveSummary(config.Conf.IgnoreUnscoredCves)) + r.CveSummary()) } message = formatFullPlainText(r) if err := sender.Send(subject, message); err != nil { @@ -74,7 +74,7 @@ One Line Summary subject := fmt.Sprintf("%s %s", conf.EMail.SubjectPrefix, - totalResult.CveSummary(config.Conf.IgnoreUnscoredCves), + totalResult.CveSummary(), ) return sender.Send(subject, message) } diff --git a/report/report.go b/report/report.go index 0d77f766..45228eb9 100644 --- a/report/report.go +++ b/report/report.go @@ -66,6 +66,7 @@ func FillCveInfos(rs []models.ScanResult, dir string) ([]models.ScanResult, erro } } + //TODO remove debug code for _, r := range filled { pp.Printf("filled: %d\n", len(r.ScannedCves)) } @@ -75,11 +76,11 @@ func FillCveInfos(rs []models.ScanResult, dir string) ([]models.ScanResult, erro filtered = append(filtered, r.FilterByCvssOver(c.Conf.CvssScoreOver)) } + //TODO remove debug code for _, r := range filtered { pp.Printf("filtered: %d\n", len(r.ScannedCves)) } - // TODO Sort return filtered, nil } diff --git a/report/slack.go b/report/slack.go index 166622ad..2b1e143e 100644 --- a/report/slack.go +++ b/report/slack.go @@ -159,7 +159,7 @@ func msgText(r models.ScanResult) string { return fmt.Sprintf("%s\n%s\n>%s", notifyUsers, serverInfo, - r.CveSummary(config.Conf.IgnoreUnscoredCves)) + r.CveSummary()) } func toSlackAttachments(scanResult models.ScanResult) (attaches []*attachment) { diff --git a/report/util.go b/report/util.go index de848095..309f4357 100644 --- a/report/util.go +++ b/report/util.go @@ -72,7 +72,7 @@ func formatOneLineSummary(rs ...models.ScanResult) string { if len(r.Errors) == 0 { cols = []interface{}{ r.FormatServerName(), - r.CveSummary(config.Conf.IgnoreUnscoredCves), + r.CveSummary(), r.Packages.FormatUpdatablePacksSummary(), } } else { @@ -96,7 +96,7 @@ func formatShortPlainText(r models.ScanResult) string { } vulns := r.ScannedCves - if !config.Conf.IgnoreUnscoredCves { + if config.Conf.IgnoreUnscoredCves { vulns = vulns.FindScoredVulns() } @@ -163,7 +163,7 @@ func formatFullPlainText(r models.ScanResult) string { } vulns := r.ScannedCves - if !config.Conf.IgnoreUnscoredCves { + if config.Conf.IgnoreUnscoredCves { vulns = vulns.FindScoredVulns() } From af66e44427ac336cd9f8e32bf2034015e176856a Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Thu, 25 May 2017 15:06:01 +0900 Subject: [PATCH 041/113] SHow Vendor Links in text report --- models/cvecontents.go | 9 --------- models/vulninfos.go | 41 +++++++++++++++++++++++++++++++++++++++++ report/report.go | 5 +++++ report/util.go | 27 ++++----------------------- report/writer.go | 22 ---------------------- 5 files changed, 50 insertions(+), 54 deletions(-) diff --git a/models/cvecontents.go b/models/cvecontents.go index 21168e1a..a1330deb 100644 --- a/models/cvecontents.go +++ b/models/cvecontents.go @@ -399,15 +399,6 @@ func (v CveContents) SourceLinks(lang, myFamily, cveID string) (values []CveCont 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 // func (v CveContents) Severities(myFamily string) (values []CveContentValue) { // order := CveContentTypes{NVD, NewCveContentType(myFamily)} diff --git a/models/vulninfos.go b/models/vulninfos.go index 25a4757c..043d1176 100644 --- a/models/vulninfos.go +++ b/models/vulninfos.go @@ -20,6 +20,7 @@ package models import ( "fmt" "sort" + "strings" "time" ) @@ -84,6 +85,46 @@ func (v VulnInfo) Cvss3CalcURL() string { return "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?name=" + v.CveID } +// VendorLinks returns links of vendor support's URL +func (v VulnInfo) VendorLinks(family string) map[string]string { + links := map[string]string{} + switch family { + case "rhel", "centos": + links["RHEL-CVE"] = "https://access.redhat.com/security/cve/" + v.CveID + for _, advisory := range v.DistroAdvisories { + aidURL := strings.Replace(advisory.AdvisoryID, ":", "-", -1) + links[advisory.AdvisoryID] = fmt.Sprintf("https://rhn.redhat.com/errata/%s.html", aidURL) + } + return links + case "oraclelinux": + links["Oracle-CVE"] = fmt.Sprintf("https://linux.oracle.com/cve/%s.html", v.CveID) + for _, advisory := range v.DistroAdvisories { + links[advisory.AdvisoryID] = + fmt.Sprintf("https://linux.oracle.com/errata/%s.html", advisory.AdvisoryID) + } + return links + case "amazon": + links["RHEL-CVE"] = "https://access.redhat.com/security/cve/" + v.CveID + for _, advisory := range v.DistroAdvisories { + links[advisory.AdvisoryID] = + fmt.Sprintf("https://alas.aws.amazon.com/%s.html", advisory.AdvisoryID) + } + return links + case "ubuntu": + links["Ubuntu-CVE"] = "http://people.ubuntu.com/~ubuntu-security/cve/" + v.CveID + return links + case "debian": + links["Debian-CVE"] = "https://security-tracker.debian.org/tracker/" + v.CveID + case "FreeBSD": + for _, advisory := range v.DistroAdvisories { + links["FreeBSD-VuXML"] = fmt.Sprintf("https://vuxml.freebsd.org/freebsd/%s.html", advisory.AdvisoryID) + + } + return links + } + return links +} + // TODO // NilToEmpty set nil slice or map fields to empty to avoid null in JSON // func (v *VulnInfo) NilToEmpty() { diff --git a/report/report.go b/report/report.go index 45228eb9..17619a2b 100644 --- a/report/report.go +++ b/report/report.go @@ -28,6 +28,11 @@ import ( "github.com/k0kubun/pp" ) +const ( + vulsOpenTag = "" + vulsCloseTag = "" +) + // FillCveInfos fills CVE Detailed Information func FillCveInfos(rs []models.ScanResult, dir string) ([]models.ScanResult, error) { var filled []models.ScanResult diff --git a/report/util.go b/report/util.go index 309f4357..d0139a63 100644 --- a/report/util.go +++ b/report/util.go @@ -201,8 +201,10 @@ func formatFullPlainText(r models.ScanResult) string { config.Conf.Lang, r.Family, vuln.CveID) table.AddRow("Source", links[0].Value) - vendorLink := vuln.CveContents.VendorLink(r.Family) - table.AddRow(fmt.Sprintf("Vendor (%s)", vendorLink.Type), vendorLink.Value) + vlinks := vuln.VendorLinks(r.Family) + for name, url := range vlinks { + table.AddRow(name, url) + } for _, v := range vuln.CveContents.CweIDs(r.Family) { table.AddRow(fmt.Sprintf("%s (%s)", v.Value, v.Type), cweURL(v.Value)) @@ -457,27 +459,6 @@ func formatPlainTextDetails(r models.ScanResult, osFamily string) (scoredReport, // } // } -// addPackages add package information related the CVE to table -func addPackages(table *uitable.Table, packs []models.Package) *uitable.Table { - for i, p := range packs { - var title string - if i == 0 { - title = "Package" - } - ver := fmt.Sprintf( - "%s -> %s", p.FormatVer(), p.FormatNewVer()) - table.AddRow(title, ver) - } - return table -} - -func addCpeNames(table *uitable.Table, names []string) *uitable.Table { - for _, n := range names { - table.AddRow("CPE", fmt.Sprintf("%s", n)) - } - return table -} - func cweURL(cweID string) string { return fmt.Sprintf("https://cwe.mitre.org/data/definitions/%s.html", strings.TrimPrefix(cweID, "CWE-")) diff --git a/report/writer.go b/report/writer.go index fa45a55b..66a760e0 100644 --- a/report/writer.go +++ b/report/writer.go @@ -24,28 +24,6 @@ import ( "github.com/future-architect/vuls/models" ) -const ( - nvdBaseURL = "https://nvd.nist.gov/vuln/detail" - mitreBaseURL = "https://cve.mitre.org/cgi-bin/cvename.cgi?name=" - cveDetailsBaseURL = "http://www.cvedetails.com/cve" - cvssV2CalcBaseURL = "https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?name=%s" - cvssV3CalcBaseURL = "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?name=%s" - - redhatSecurityBaseURL = "https://access.redhat.com/security/cve" - redhatRHSABaseBaseURL = "https://rhn.redhat.com/errata/%s.html" - amazonSecurityBaseURL = "https://alas.aws.amazon.com/%s.html" - oracleSecurityBaseURL = "https://linux.oracle.com/cve/%s.html" - oracleELSABaseBaseURL = "https://linux.oracle.com/errata/%s.html" - - ubuntuSecurityBaseURL = "http://people.ubuntu.com/~ubuntu-security/cve" - debianTrackerBaseURL = "https://security-tracker.debian.org/tracker" - - freeBSDVuXMLBaseURL = "https://vuxml.freebsd.org/freebsd/%s.html" - - vulsOpenTag = "" - vulsCloseTag = "" -) - // ResultWriter Interface type ResultWriter interface { Write(...models.ScanResult) error From ad096196ee758d42888939350e585d7bd285e948 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Thu, 25 May 2017 18:17:20 +0900 Subject: [PATCH 042/113] Add vendor links to -format-shor-text --- report/email.go | 2 -- report/util.go | 7 +++++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/report/email.go b/report/email.go index b08de9e3..f20194c4 100644 --- a/report/email.go +++ b/report/email.go @@ -41,8 +41,6 @@ func (w EMailWriter) Write(rs ...models.ScanResult) (err error) { for _, r := range rs { if conf.FormatOneEMail { message += formatFullPlainText(r) + "\r\n\r\n" - // totalResult.KnownCves = append(totalResult.KnownCves, r.KnownCves...) - // totalResult.UnknownCves = append(totalResult.UnknownCves, r.UnknownCves...) } else { var subject string if len(r.Errors) != 0 { diff --git a/report/util.go b/report/util.go index d0139a63..d5a68d2e 100644 --- a/report/util.go +++ b/report/util.go @@ -116,6 +116,11 @@ func formatShortPlainText(r models.ScanResult) string { links := vuln.CveContents.SourceLinks( config.Conf.Lang, r.Family, vuln.CveID) + vlinks := []string{} + for name, url := range vuln.VendorLinks(r.Family) { + vlinks = append(vlinks, fmt.Sprintf("%s (%s)", url, name)) + } + cvsses := "" for _, cvss := range vuln.CveContents.Cvss2Scores() { cvsses += fmt.Sprintf("%s (%s)\n", cvss.Value.Format(), cvss.Type) @@ -133,10 +138,12 @@ func formatShortPlainText(r models.ScanResult) string { %s --- %s +%s %sConfidence: %v`, maxCvss, summaries[0].Value, links[0].Value, + strings.Join(vlinks, "\n"), cvsses, // packsVer, vuln.Confidence, From 306182e2aea0c3cd906cfaf062ba8fd933bfc4c4 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Thu, 25 May 2017 18:55:41 +0900 Subject: [PATCH 043/113] Fix test cases --- models/cvecontents_test.go | 62 +++++++++++++++++++++----------------- models/packages_test.go | 58 ----------------------------------- 2 files changed, 34 insertions(+), 86 deletions(-) diff --git a/models/cvecontents_test.go b/models/cvecontents_test.go index dbafb037..5eabfb10 100644 --- a/models/cvecontents_test.go +++ b/models/cvecontents_test.go @@ -687,55 +687,61 @@ func TestSourceLinks(t *testing.T) { func TestVendorLink(t *testing.T) { type in struct { family string - cont CveContents + vinfo VulnInfo } var tests = []struct { in in - out CveContentStr + out map[string]string }{ { in: in{ - family: "redhat", - cont: CveContents{ - JVN: { - Type: JVN, - SourceLink: "https://jvn.jp/vu/JVNVU93610402/", - }, - RedHat: { - Type: RedHat, - SourceLink: "https://access.redhat.com/security/cve/CVE-2017-6074", - }, - NVD: { - Type: NVD, - SourceLink: "https://nvd.nist.gov/vuln/detail/CVE-2017-6074", + family: "rhel", + vinfo: VulnInfo{ + CveID: "CVE-2017-6074", + CveContents: CveContents{ + JVN: { + Type: JVN, + SourceLink: "https://jvn.jp/vu/JVNVU93610402/", + }, + RedHat: { + Type: RedHat, + SourceLink: "https://access.redhat.com/security/cve/CVE-2017-6074", + }, + NVD: { + Type: NVD, + SourceLink: "https://nvd.nist.gov/vuln/detail/CVE-2017-6074", + }, }, }, }, - out: CveContentStr{ - Type: RedHat, - Value: "https://access.redhat.com/security/cve/CVE-2017-6074", + out: map[string]string{ + "RHEL-CVE": "https://access.redhat.com/security/cve/CVE-2017-6074", }, }, { in: in{ family: "ubuntu", - cont: CveContents{ - RedHat: { - Type: RedHat, - SourceLink: "https://access.redhat.com/security/cve/CVE-2017-6074", + vinfo: VulnInfo{ + CveID: "CVE-2017-6074", + CveContents: CveContents{ + RedHat: { + Type: Ubuntu, + SourceLink: "https://access.redhat.com/security/cve/CVE-2017-6074", + }, }, }, }, - out: CveContentStr{ - Type: Ubuntu, - Value: "", + out: map[string]string{ + "Ubuntu-CVE": "http://people.ubuntu.com/~ubuntu-security/cve/CVE-2017-6074", }, }, } for _, tt := range tests { - actual := tt.in.cont.VendorLink(tt.in.family) - if !reflect.DeepEqual(tt.out, actual) { - t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + actual := tt.in.vinfo.VendorLinks(tt.in.family) + for k := range tt.out { + if tt.out[k] != actual[k] { + t.Errorf("\nexpected: %s\n actual: %s\n", tt.out[k], actual[k]) + } } } } diff --git a/models/packages_test.go b/models/packages_test.go index ae92e410..48eb99d8 100644 --- a/models/packages_test.go +++ b/models/packages_test.go @@ -87,61 +87,3 @@ func TestMerge(t *testing.T) { t.Errorf("expected %s, actual %s", e, a) } } - -func TestFormatVersionsFromTo(t *testing.T) { - var tests = []struct { - packs Packages - expected string - }{ - { - packs: Packages{ - "hoge": { - Name: "hoge", - Version: "1.0.0", - Release: "release1", - NewVersion: "1.0.1", - NewRelease: "release2", - }, - }, - expected: "hoge-1.0.0-release1 -> hoge-1.0.1-release2", - }, - { - packs: Packages{ - "hoge": { - Name: "hoge", - Version: "1.0.0", - Release: "", - NewVersion: "1.0.1", - NewRelease: "", - }, - }, - expected: "hoge-1.0.0 -> hoge-1.0.1", - }, - { - packs: Packages{ - "hoge": { - Name: "hoge", - Version: "1.0.0", - Release: "", - NewVersion: "1.0.1", - NewRelease: "", - }, - "fuga": { - Name: "fuga", - Version: "2.0.0", - Release: "", - NewVersion: "2.0.1", - NewRelease: "", - }, - }, - expected: "hoge-1.0.0 -> hoge-1.0.1\nfuga-2.0.0 -> fuga-2.0.1", - }, - } - - for _, tt := range tests { - actual := tt.packs.FormatVersionsFromTo() - if !reflect.DeepEqual(tt.expected, actual) { - t.Errorf("\nexpected: %v\n actual: %v\n", tt.expected, actual) - } - } -} From bc5a95ebb360ef332211bebe45b253fda80b44bc Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Fri, 26 May 2017 12:50:12 +0900 Subject: [PATCH 044/113] Fix -to-email --- models/cvecontents.go | 8 +--- models/scanresults.go | 31 +-------------- models/vulninfos.go | 39 +++++++++++++++++- models/vulninfos_test.go | 85 ++++++++++++++++++++++++++++++++++------ report/email.go | 23 ++++++++--- report/slack.go | 2 +- report/util.go | 2 +- 7 files changed, 134 insertions(+), 56 deletions(-) diff --git a/models/cvecontents.go b/models/cvecontents.go index a1330deb..1af139c1 100644 --- a/models/cvecontents.go +++ b/models/cvecontents.go @@ -231,14 +231,12 @@ func cvss3ScoreToSeverity(score float64) string { // Cvss3Scores returns CVSS V3 Score func (v CveContents) Cvss3Scores() (values []CveContentCvss3) { + // TODO implement NVD 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{ @@ -254,6 +252,7 @@ func (v CveContents) Cvss3Scores() (values []CveContentCvss3) { // MaxCvss3Score returns Max CVSS V3 Score func (v CveContents) MaxCvss3Score() CveContentCvss3 { + // TODO implement NVD order := []CveContentType{RedHat} max := 0.0 value := CveContentCvss3{ @@ -264,9 +263,6 @@ func (v CveContents) MaxCvss3Score() CveContentCvss3 { 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{ diff --git a/models/scanresults.go b/models/scanresults.go index 8aa6623d..aa538249 100644 --- a/models/scanresults.go +++ b/models/scanresults.go @@ -23,7 +23,6 @@ import ( "strings" "time" - "github.com/future-architect/vuls/config" cvedict "github.com/kotakanbe/go-cve-dictionary/models" ) @@ -215,34 +214,6 @@ func (r ScanResult) FormatServerName() string { r.Container.Name, r.ServerName) } -// CveSummary summarize the number of CVEs group by CVSSv2 Severity -func (r ScanResult) CveSummary() string { - var high, medium, low, unknown int - for _, vInfo := range r.ScannedCves { - score := vInfo.CveContents.MaxCvss2Score().Value.Score - if score < 0.1 { - score = vInfo.CveContents.MaxCvss3Score().Value.Score - } - switch { - case 7.0 <= score: - high++ - case 4.0 <= score: - medium++ - case 0 < score: - low++ - default: - unknown++ - } - } - - if config.Conf.IgnoreUnscoredCves { - return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d)", - high+medium+low, high, medium, low) - } - return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d ?:%d)", - high+medium+low+unknown, high, medium, low, unknown) -} - // FormatTextReportHeadedr returns header of text report func (r ScanResult) FormatTextReportHeadedr() string { serverInfo := r.ServerInfo() @@ -253,7 +224,7 @@ func (r ScanResult) FormatTextReportHeadedr() string { return fmt.Sprintf("%s\n%s\n%s\t%s\n", r.ServerInfo(), buf.String(), - r.CveSummary(), + r.ScannedCves.FormatCveSummary(), r.Packages.FormatUpdatablePacksSummary(), ) } diff --git a/models/vulninfos.go b/models/vulninfos.go index 043d1176..402c2456 100644 --- a/models/vulninfos.go +++ b/models/vulninfos.go @@ -22,6 +22,8 @@ import ( "sort" "strings" "time" + + "github.com/future-architect/vuls/config" ) // VulnInfos is VulnInfo list, getter/setter, sortable methods. @@ -65,7 +67,42 @@ func (v VulnInfos) ToSortedSlice() (sorted []VulnInfo) { return } -// VulnInfo holds a vulnerability information and unsecure packages +// CountGroupBySeverity summarize the number of CVEs group by CVSSv2 Severity +func (v VulnInfos) CountGroupBySeverity() map[string]int { + m := map[string]int{} + for _, vInfo := range v { + score := vInfo.CveContents.MaxCvss2Score().Value.Score + if score < 0.1 { + score = vInfo.CveContents.MaxCvss3Score().Value.Score + } + switch { + case 7.0 <= score: + m["High"]++ + case 4.0 <= score: + m["Medium"]++ + case 0 < score: + m["Low"]++ + default: + m["Unknown"]++ + } + } + return m +} + +// FormatCveSummary summarize the number of CVEs group by CVSSv2 Severity +func (v VulnInfos) FormatCveSummary() string { + m := v.CountGroupBySeverity() + + if config.Conf.IgnoreUnscoredCves { + return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d)", + m["High"]+m["Medium"]+m["Low"], m["High"], m["Medium"], m["Low"]) + } + return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d ?:%d)", + m["High"]+m["Medium"]+m["Low"]+m["Unknown"], + m["High"], m["Medium"], m["Low"], m["Unknown"]) +} + +// VulnInfo has a vulnerability information and unsecure packages type VulnInfo struct { CveID string Confidence Confidence diff --git a/models/vulninfos_test.go b/models/vulninfos_test.go index 1e796a3a..7b4f8f40 100644 --- a/models/vulninfos_test.go +++ b/models/vulninfos_test.go @@ -21,6 +21,67 @@ import ( "testing" ) +func TestCountGroupBySeverity(t *testing.T) { + var tests = []struct { + in VulnInfos + out map[string]int + }{ + { + in: VulnInfos{ + "CVE-2017-0002": { + CveID: "CVE-2017-0002", + CveContents: CveContents{ + NVD: { + Type: NVD, + Cvss2Score: 6.0, + }, + RedHat: { + Type: RedHat, + Cvss2Score: 7.0, + }, + }, + }, + "CVE-2017-0003": { + CveID: "CVE-2017-0003", + CveContents: CveContents{ + NVD: { + Type: NVD, + Cvss2Score: 2.0, + }, + }, + }, + "CVE-2017-0004": { + CveID: "CVE-2017-0004", + CveContents: CveContents{ + NVD: { + Type: NVD, + Cvss2Score: 5.0, + }, + }, + }, + "CVE-2017-0005": { + CveID: "CVE-2017-0005", + }, + }, + out: map[string]int{ + "High": 1, + "Medium": 1, + "Low": 1, + "Unknown": 1, + }, + }, + } + for _, tt := range tests { + actual := tt.in.CountGroupBySeverity() + for k := range tt.out { + if tt.out[k] != actual[k] { + t.Errorf("\nexpected %s: %d\n actual %d\n", + k, tt.out[k], actual[k]) + } + } + } +} + func TestToSortedSlice(t *testing.T) { var tests = []struct { in VulnInfos @@ -33,11 +94,11 @@ func TestToSortedSlice(t *testing.T) { CveContents: CveContents{ NVD: { Type: NVD, - Cvss3Score: 6.0, + Cvss2Score: 6.0, }, RedHat: { Type: RedHat, - Cvss2Score: 7.0, + Cvss3Score: 7.0, }, }, }, @@ -46,11 +107,11 @@ func TestToSortedSlice(t *testing.T) { CveContents: CveContents{ NVD: { Type: NVD, - Cvss3Score: 7.0, + Cvss2Score: 7.0, }, RedHat: { Type: RedHat, - Cvss2Score: 8.0, + Cvss3Score: 8.0, }, }, }, @@ -61,11 +122,11 @@ func TestToSortedSlice(t *testing.T) { CveContents: CveContents{ NVD: { Type: NVD, - Cvss3Score: 7.0, + Cvss2Score: 7.0, }, RedHat: { Type: RedHat, - Cvss2Score: 8.0, + Cvss3Score: 8.0, }, }, }, @@ -74,11 +135,11 @@ func TestToSortedSlice(t *testing.T) { CveContents: CveContents{ NVD: { Type: NVD, - Cvss3Score: 6.0, + Cvss2Score: 6.0, }, RedHat: { Type: RedHat, - Cvss2Score: 7.0, + Cvss3Score: 7.0, }, }, }, @@ -92,11 +153,11 @@ func TestToSortedSlice(t *testing.T) { CveContents: CveContents{ NVD: { Type: NVD, - Cvss3Score: 6.0, + Cvss2Score: 6.0, }, RedHat: { Type: RedHat, - Cvss2Score: 7.0, + Cvss3Score: 7.0, }, }, }, @@ -125,11 +186,11 @@ func TestToSortedSlice(t *testing.T) { CveContents: CveContents{ NVD: { Type: NVD, - Cvss3Score: 6.0, + Cvss2Score: 6.0, }, RedHat: { Type: RedHat, - Cvss2Score: 7.0, + Cvss3Score: 7.0, }, }, }, diff --git a/report/email.go b/report/email.go index f20194c4..d3c1717d 100644 --- a/report/email.go +++ b/report/email.go @@ -35,12 +35,18 @@ type EMailWriter struct{} func (w EMailWriter) Write(rs ...models.ScanResult) (err error) { conf := config.Conf var message string - var totalResult models.ScanResult sender := NewEMailSender() + m := map[string]int{} for _, r := range rs { if conf.FormatOneEMail { message += formatFullPlainText(r) + "\r\n\r\n" + + mm := r.ScannedCves.CountGroupBySeverity() + keys := []string{"High", "Medium", "Low", "Unknown"} + for _, k := range keys { + m[k] += mm[k] + } } else { var subject string if len(r.Errors) != 0 { @@ -50,7 +56,7 @@ func (w EMailWriter) Write(rs ...models.ScanResult) (err error) { subject = fmt.Sprintf("%s%s %s", conf.EMail.SubjectPrefix, r.ServerInfo(), - r.CveSummary()) + r.ScannedCves.FormatCveSummary()) } message = formatFullPlainText(r) if err := sender.Send(subject, message); err != nil { @@ -59,6 +65,15 @@ func (w EMailWriter) Write(rs ...models.ScanResult) (err error) { } } + summary := "" + if config.Conf.IgnoreUnscoredCves { + summary = fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d)", + m["High"]+m["Medium"]+m["Low"], m["High"], m["Medium"], m["Low"]) + } + summary = fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d ?:%d)", + m["High"]+m["Medium"]+m["Low"]+m["Unknown"], + m["High"], m["Medium"], m["Low"], m["Unknown"]) + if conf.FormatOneEMail { message = fmt.Sprintf( ` @@ -71,9 +86,7 @@ One Line Summary formatOneLineSummary(rs...), message) subject := fmt.Sprintf("%s %s", - conf.EMail.SubjectPrefix, - totalResult.CveSummary(), - ) + conf.EMail.SubjectPrefix, summary) return sender.Send(subject, message) } return nil diff --git a/report/slack.go b/report/slack.go index 2b1e143e..0094184e 100644 --- a/report/slack.go +++ b/report/slack.go @@ -159,7 +159,7 @@ func msgText(r models.ScanResult) string { return fmt.Sprintf("%s\n%s\n>%s", notifyUsers, serverInfo, - r.CveSummary()) + r.ScannedCves.FormatCveSummary()) } func toSlackAttachments(scanResult models.ScanResult) (attaches []*attachment) { diff --git a/report/util.go b/report/util.go index d5a68d2e..6f27aa2e 100644 --- a/report/util.go +++ b/report/util.go @@ -72,7 +72,7 @@ func formatOneLineSummary(rs ...models.ScanResult) string { if len(r.Errors) == 0 { cols = []interface{}{ r.FormatServerName(), - r.CveSummary(), + r.ScannedCves.FormatCveSummary(), r.Packages.FormatUpdatablePacksSummary(), } } else { From a14810bbd45836ee0a71a05f4b0d444e8539cdc8 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Mon, 29 May 2017 10:50:39 +0900 Subject: [PATCH 045/113] Fix -to-slack --- models/cvecontents.go | 129 ++++++++++++-------- models/cvecontents_test.go | 95 ++++++++++----- models/packages.go | 25 ++-- models/vulninfos.go | 4 +- report/slack.go | 240 ++++++++++++++++++------------------- report/slack_test.go | 36 +++--- report/util.go | 4 +- 7 files changed, 295 insertions(+), 238 deletions(-) diff --git a/models/cvecontents.go b/models/cvecontents.go index 1af139c1..69126f5d 100644 --- a/models/cvecontents.go +++ b/models/cvecontents.go @@ -59,22 +59,40 @@ func (v CveContents) Except(exceptCtypes ...CveContentType) (values CveContents) return } -// CveContentCvss2 has CveContentType and Cvss2 -type CveContentCvss2 struct { +// CveContentCvss has CveContentType and Cvss2 +type CveContentCvss struct { Type CveContentType - Value Cvss2 + Value Cvss } -// Cvss2 has CVSS v2 -type Cvss2 struct { +// CvssType Represent the type of CVSS +type CvssType string + +const ( + // CVSS2 means CVSS vesion2 + CVSS2 CvssType = "2" + + // CVSS3 means CVSS vesion3 + CVSS3 CvssType = "3" +) + +// Cvss has CVSS Score +type Cvss struct { + Type CvssType Score float64 Vector string Severity string } // Format CVSS Score and Vector -func (c Cvss2) Format() string { - return fmt.Sprintf("%3.1f/%s", c.Score, c.Vector) +func (c Cvss) Format() string { + switch c.Type { + case CVSS2: + return fmt.Sprintf("%3.1f/%s", c.Score, c.Vector) + case CVSS3: + return fmt.Sprintf("%3.1f/CVSS:3.0/%s", c.Score, c.Vector) + } + return "" } func cvss2ScoreToSeverity(score float64) string { @@ -87,7 +105,7 @@ func cvss2ScoreToSeverity(score float64) string { } // Cvss2Scores returns CVSS V2 Scores -func (v CveContents) Cvss2Scores() (values []CveContentCvss2) { +func (v CveContents) Cvss2Scores() (values []CveContentCvss) { order := []CveContentType{NVD, RedHat, JVN} for _, ctype := range order { if cont, found := v[ctype]; found && 0 < cont.Cvss2Score { @@ -96,9 +114,10 @@ func (v CveContents) Cvss2Scores() (values []CveContentCvss2) { if ctype == NVD { sev = cvss2ScoreToSeverity(cont.Cvss2Score) } - values = append(values, CveContentCvss2{ + values = append(values, CveContentCvss{ Type: ctype, - Value: Cvss2{ + Value: Cvss{ + Type: CVSS2, Score: cont.Cvss2Score, Vector: cont.Cvss2Vector, Severity: sev, @@ -111,13 +130,13 @@ func (v CveContents) Cvss2Scores() (values []CveContentCvss2) { } // MaxCvss2Score returns Max CVSS V2 Score -func (v CveContents) MaxCvss2Score() CveContentCvss2 { +func (v CveContents) MaxCvss2Score() CveContentCvss { //TODO Severity Ubuntu, Debian... order := []CveContentType{NVD, RedHat, JVN} max := 0.0 - value := CveContentCvss2{ + value := CveContentCvss{ Type: Unknown, - Value: Cvss2{}, + Value: Cvss{Type: CVSS2}, } for _, ctype := range order { if cont, found := v[ctype]; found && max < cont.Cvss2Score { @@ -126,9 +145,10 @@ func (v CveContents) MaxCvss2Score() CveContentCvss2 { if ctype == NVD { sev = cvss2ScoreToSeverity(cont.Cvss2Score) } - value = CveContentCvss2{ + value = CveContentCvss{ Type: ctype, - Value: Cvss2{ + Value: Cvss{ + Type: CVSS2, Score: cont.Cvss2Score, Vector: cont.Cvss2Vector, Severity: sev, @@ -155,9 +175,10 @@ func (v CveContents) MaxCvss2Score() CveContentCvss2 { score = severityToScoreForRedHat(cont.Severity) } if max < score { - value = CveContentCvss2{ + value = CveContentCvss{ Type: ctype, - Value: Cvss2{ + Value: Cvss{ + Type: CVSS2, Score: score, Vector: cont.Cvss2Vector, Severity: cont.Severity, @@ -201,45 +222,46 @@ func severityToScoreForRedHat(severity string) float64 { } // CveContentCvss3 has CveContentType and Cvss3 -type CveContentCvss3 struct { - Type CveContentType - Value Cvss3 -} +// type CveContentCvss3 struct { +// Type CveContentType +// Value Cvss3 +// } // Cvss3 has CVSS v3 Score, Vector and Severity -type Cvss3 struct { - Score float64 - Vector string - Severity string -} +// 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 (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" - } else if 7.0 <= score { - return "HIGH" - } else if 4.0 <= score { - return "MEDIUM" - } - return "LOW" -} +// 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) { +func (v CveContents) Cvss3Scores() (values []CveContentCvss) { // TODO implement NVD 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 - values = append(values, CveContentCvss3{ + values = append(values, CveContentCvss{ Type: ctype, - Value: Cvss3{ + Value: Cvss{ + Type: CVSS3, Score: cont.Cvss3Score, Vector: cont.Cvss3Vector, Severity: sev, @@ -251,21 +273,22 @@ func (v CveContents) Cvss3Scores() (values []CveContentCvss3) { } // MaxCvss3Score returns Max CVSS V3 Score -func (v CveContents) MaxCvss3Score() CveContentCvss3 { +func (v CveContents) MaxCvss3Score() CveContentCvss { // TODO implement NVD order := []CveContentType{RedHat} max := 0.0 - value := CveContentCvss3{ + value := CveContentCvss{ Type: Unknown, - Value: Cvss3{}, + Value: Cvss{Type: CVSS3}, } for _, ctype := range order { if cont, found := v[ctype]; found && max < cont.Cvss3Score { // https://nvd.nist.gov/vuln-metrics/cvss sev := cont.Severity - value = CveContentCvss3{ + value = CveContentCvss{ Type: ctype, - Value: Cvss3{ + Value: Cvss{ + Type: CVSS3, Score: cont.Cvss3Score, Vector: cont.Cvss3Vector, Severity: sev, @@ -279,12 +302,12 @@ func (v CveContents) MaxCvss3Score() CveContentCvss3 { // MaxCvssScore returns max CVSS Score // If there is no CVSS Score, return Severity as a numerical value. -func (v CveContents) MaxCvssScore() float64 { +func (v CveContents) MaxCvssScore() CveContentCvss { v3Max := v.MaxCvss3Score() v2Max := v.MaxCvss2Score() - max := v3Max.Value.Score - if max < v2Max.Value.Score { - max = v2Max.Value.Score + max := v3Max + if max.Value.Score < v2Max.Value.Score { + max = v2Max } return max } @@ -396,13 +419,13 @@ func (v CveContents) SourceLinks(lang, myFamily, cveID string) (values []CveCont } // Severities returns Severities -// func (v CveContents) Severities(myFamily string) (values []CveContentValue) { +// func (v CveContents) Severities(myFamily string) (values []CveContentStr) { // 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{ +// values = append(values, CveContentStr{ // Type: ctype, // Value: cont.Severity, // }) diff --git a/models/cvecontents_test.go b/models/cvecontents_test.go index 5eabfb10..eca860c2 100644 --- a/models/cvecontents_test.go +++ b/models/cvecontents_test.go @@ -47,7 +47,7 @@ func TestExcept(t *testing.T) { func TestCvss2Scores(t *testing.T) { var tests = []struct { in CveContents - out []CveContentCvss2 + out []CveContentCvss }{ { in: CveContents{ @@ -70,10 +70,11 @@ func TestCvss2Scores(t *testing.T) { // Severity is NIOT included in NVD }, }, - out: []CveContentCvss2{ + out: []CveContentCvss{ { Type: NVD, - Value: Cvss2{ + Value: Cvss{ + Type: CVSS2, Score: 8.1, Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", Severity: "HIGH", @@ -81,7 +82,8 @@ func TestCvss2Scores(t *testing.T) { }, { Type: RedHat, - Value: Cvss2{ + Value: Cvss{ + Type: CVSS2, Score: 8.0, Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", Severity: "HIGH", @@ -89,7 +91,8 @@ func TestCvss2Scores(t *testing.T) { }, { Type: JVN, - Value: Cvss2{ + Value: Cvss{ + Type: CVSS2, Score: 8.2, Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", Severity: "HIGH", @@ -114,7 +117,7 @@ func TestCvss2Scores(t *testing.T) { func TestMaxCvss2Scores(t *testing.T) { var tests = []struct { in CveContents - out CveContentCvss2 + out CveContentCvss }{ { in: CveContents{ @@ -137,9 +140,10 @@ func TestMaxCvss2Scores(t *testing.T) { // Severity is NIOT included in NVD }, }, - out: CveContentCvss2{ + out: CveContentCvss{ Type: JVN, - Value: Cvss2{ + Value: Cvss{ + Type: CVSS2, Score: 8.2, Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", Severity: "HIGH", @@ -154,9 +158,10 @@ func TestMaxCvss2Scores(t *testing.T) { Severity: "HIGH", }, }, - out: CveContentCvss2{ + out: CveContentCvss{ Type: Ubuntu, - Value: Cvss2{ + Value: Cvss{ + Type: CVSS2, Score: 10, Severity: "HIGH", }, @@ -165,9 +170,10 @@ func TestMaxCvss2Scores(t *testing.T) { // Empty { in: CveContents{}, - out: CveContentCvss2{ + out: CveContentCvss{ Type: Unknown, - Value: Cvss2{ + Value: Cvss{ + Type: CVSS2, Score: 0.0, Vector: "", Severity: "", @@ -186,7 +192,7 @@ func TestMaxCvss2Scores(t *testing.T) { func TestCvss3Scores(t *testing.T) { var tests = []struct { in CveContents - out []CveContentCvss3 + out []CveContentCvss }{ { in: CveContents{ @@ -203,10 +209,11 @@ func TestCvss3Scores(t *testing.T) { // Severity is NIOT included in NVD }, }, - out: []CveContentCvss3{ + out: []CveContentCvss{ { Type: RedHat, - Value: Cvss3{ + Value: Cvss{ + Type: CVSS3, Score: 8.0, Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", Severity: "HIGH", @@ -231,7 +238,7 @@ func TestCvss3Scores(t *testing.T) { func TestMaxCvss3Scores(t *testing.T) { var tests = []struct { in CveContents - out CveContentCvss3 + out CveContentCvss }{ { in: CveContents{ @@ -242,9 +249,10 @@ func TestMaxCvss3Scores(t *testing.T) { Cvss3Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", }, }, - out: CveContentCvss3{ + out: CveContentCvss{ Type: RedHat, - Value: Cvss3{ + Value: Cvss{ + Type: CVSS3, Score: 8.0, Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", Severity: "HIGH", @@ -254,9 +262,10 @@ func TestMaxCvss3Scores(t *testing.T) { // Empty { in: CveContents{}, - out: CveContentCvss3{ + out: CveContentCvss{ Type: Unknown, - Value: Cvss3{ + Value: Cvss{ + Type: CVSS3, Score: 0.0, Vector: "", Severity: "", @@ -275,7 +284,7 @@ func TestMaxCvss3Scores(t *testing.T) { func TestMaxCvssScores(t *testing.T) { var tests = []struct { in CveContents - out float64 + out CveContentCvss }{ { in: CveContents{ @@ -288,7 +297,13 @@ func TestMaxCvssScores(t *testing.T) { Cvss2Score: 8.0, }, }, - out: 8.0, + out: CveContentCvss{ + Type: RedHat, + Value: Cvss{ + Type: CVSS2, + Score: 8.0, + }, + }, }, { in: CveContents{ @@ -297,7 +312,13 @@ func TestMaxCvssScores(t *testing.T) { Cvss3Score: 8.0, }, }, - out: 8.0, + out: CveContentCvss{ + Type: RedHat, + Value: Cvss{ + Type: CVSS3, + Score: 8.0, + }, + }, }, { in: CveContents{ @@ -306,7 +327,14 @@ func TestMaxCvssScores(t *testing.T) { Severity: "HIGH", }, }, - out: 10.0, + out: CveContentCvss{ + Type: Ubuntu, + Value: Cvss{ + Type: CVSS2, + Score: 10.0, + Severity: "HIGH", + }, + }, }, { in: CveContents{ @@ -319,12 +347,25 @@ func TestMaxCvssScores(t *testing.T) { Cvss2Score: 7.0, }, }, - out: 7.0, + out: CveContentCvss{ + Type: NVD, + Value: Cvss{ + Type: CVSS2, + Score: 7.0, + Severity: "HIGH", + }, + }, }, // Empty { - in: CveContents{}, - out: 0, + in: CveContents{}, + out: CveContentCvss{ + Type: Unknown, + Value: Cvss{ + Type: CVSS3, + Score: 0, + }, + }, }, } for i, tt := range tests { diff --git a/models/packages.go b/models/packages.go index 056c4f83..8439f90a 100644 --- a/models/packages.go +++ b/models/packages.go @@ -90,33 +90,28 @@ type Package struct { NotFixedYet bool // Ubuntu OVAL Only } -// FormatVer returns package name-version-release +// FormatVer returns package version-release func (p Package) FormatVer() string { - str := p.Name - if 0 < len(p.Version) { - str = fmt.Sprintf("%s-%s", str, p.Version) - } + ver := p.Version if 0 < len(p.Release) { - str = fmt.Sprintf("%s-%s", str, p.Release) + ver = fmt.Sprintf("%s-%s", ver, p.Release) } - return str + return ver } -// FormatNewVer returns package name-version-release +// FormatNewVer returns package version-release func (p Package) FormatNewVer() string { - str := p.Name - if 0 < len(p.NewVersion) { - str = fmt.Sprintf("%s-%s", str, p.NewVersion) - } + ver := p.NewVersion if 0 < len(p.NewRelease) { - str = fmt.Sprintf("%s-%s", str, p.NewRelease) + ver = fmt.Sprintf("%s-%s", ver, p.NewRelease) } - return str + return ver } // FormatVersionFromTo formats installed and new package version func (p Package) FormatVersionFromTo() string { - return fmt.Sprintf("%s -> %s", p.FormatVer(), p.FormatNewVer()) + return fmt.Sprintf("%s-%s -> %s", + p.Name, p.FormatVer(), p.FormatNewVer()) } // Changelog has contents of changelog and how to get it. diff --git a/models/vulninfos.go b/models/vulninfos.go index 402c2456..13813ea1 100644 --- a/models/vulninfos.go +++ b/models/vulninfos.go @@ -59,8 +59,8 @@ func (v VulnInfos) ToSortedSlice() (sorted []VulnInfo) { sort.Slice(sorted, func(i, j int) bool { maxI := sorted[i].CveContents.MaxCvssScore() maxJ := sorted[j].CveContents.MaxCvssScore() - if maxI != maxJ { - return maxJ < maxI + if maxI.Value.Score != maxJ.Value.Score { + return maxJ.Value.Score < maxI.Value.Score } return sorted[i].CveID < sorted[j].CveID }) diff --git a/report/slack.go b/report/slack.go index 0094184e..3ffd6f83 100644 --- a/report/slack.go +++ b/report/slack.go @@ -21,6 +21,7 @@ import ( "encoding/json" "fmt" "sort" + "strings" "time" log "github.com/Sirupsen/logrus" @@ -44,6 +45,7 @@ type attachment struct { Color string `json:"color"` Fields []*field `json:"fields"` MrkdwnIn []string `json:"mrkdwn_in"` + Footer string `json:"footer"` } type message struct { Text string `json:"text"` @@ -66,12 +68,12 @@ func (w SlackWriter) Write(rs ...models.ScanResult) error { } if 0 < len(r.Errors) { - //TODO - // serverInfo := fmt.Sprintf("*%s*", r.ServerInfo()) - // notifyUsers := getNotifyUsers(config.Conf.Slack.NotifyUsers) - // txt := fmt.Sprintf("%s\n%s\nError: %s", notifyUsers, serverInfo, r.Errors) + serverInfo := fmt.Sprintf("*%s*", r.ServerInfo()) + notifyUsers := getNotifyUsers(config.Conf.Slack.NotifyUsers) + txt := fmt.Sprintf("%s\n%s\nError: %s", + notifyUsers, serverInfo, r.Errors) msg := message{ - // Text: txt, + Text: txt, Username: conf.AuthUser, IconEmoji: conf.IconEmoji, Channel: channel, @@ -152,9 +154,9 @@ func send(msg message) error { func msgText(r models.ScanResult) string { notifyUsers := "" - // if 0 < len(r.KnownCves) || 0 < len(r.UnknownCves) { - // notifyUsers = getNotifyUsers(config.Conf.Slack.NotifyUsers) - // } + if 0 < len(r.ScannedCves) { + notifyUsers = getNotifyUsers(config.Conf.Slack.NotifyUsers) + } serverInfo := fmt.Sprintf("*%s*", r.ServerInfo()) return fmt.Sprintf("%s\n%s\n>%s", notifyUsers, @@ -162,50 +164,62 @@ func msgText(r models.ScanResult) string { r.ScannedCves.FormatCveSummary()) } -func toSlackAttachments(scanResult models.ScanResult) (attaches []*attachment) { - // cves := scanResult.KnownCves - // if !config.Conf.IgnoreUnscoredCves { - // cves = append(cves, scanResult.UnknownCves...) - // } +func toSlackAttachments(r models.ScanResult) (attaches []*attachment) { + var vinfos []models.VulnInfo + if config.Conf.IgnoreUnscoredCves { + vinfos = r.ScannedCves.FindScoredVulns().ToSortedSlice() + } else { + vinfos = r.ScannedCves.ToSortedSlice() + } - // for _, cveInfo := range cves { - // cveID := cveInfo.VulnInfo.CveID + for _, vinfo := range vinfos { + curent := []string{} + for _, name := range vinfo.PackageNames { + if p, ok := r.Packages[name]; ok { + curent = append(curent, + fmt.Sprintf("%s-%s", p.Name, p.FormatVer())) + } else { + curent = append(curent, name) + } + } + for _, n := range vinfo.CpeNames { + curent = append(curent, n) + } - // curentPackages := []string{} - // for _, p := range cveInfo.Packages { - // curentPackages = append(curentPackages, p.FormatCurrentVer()) - // } - // for _, n := range cveInfo.CpeNames { - // curentPackages = append(curentPackages, n) - // } + new := []string{} + for _, name := range vinfo.PackageNames { + if p, ok := r.Packages[name]; ok { + new = append(new, p.FormatNewVer()) + } else { + new = append(new, "?") + } + } + for range vinfo.CpeNames { + new = append(new, "?") + } - // newPackages := []string{} - // for _, p := range cveInfo.Packages { - // newPackages = append(newPackages, p.FormatNewVer()) - // } - - // a := attachment{ - // Title: cveID, - // TitleLink: fmt.Sprintf("%s/%s", nvdBaseURL, cveID), - // Text: attachmentText(cveInfo, scanResult.Family), - // MrkdwnIn: []string{"text", "pretext"}, - // Fields: []*field{ - // { - // // Title: "Current Package/CPE", - // Title: "Installed", - // Value: strings.Join(curentPackages, "\n"), - // Short: true, - // }, - // { - // Title: "Candidate", - // Value: strings.Join(newPackages, "\n"), - // Short: true, - // }, - // }, - // Color: color(cveInfo.CvssV2Score()), - // } - // attaches = append(attaches, &a) - // } + a := attachment{ + Title: vinfo.CveID, + TitleLink: "https://nvd.nist.gov/vuln/detail/" + vinfo.CveID, + Text: attachmentText(vinfo, r.Family), + MrkdwnIn: []string{"text", "pretext"}, + Fields: []*field{ + { + // Title: "Current Package/CPE", + Title: "Installed", + Value: strings.Join(curent, "\n"), + Short: true, + }, + { + Title: "Candidate", + Value: strings.Join(new, "\n"), + Short: true, + }, + }, + Color: color(vinfo.CveContents.MaxCvssScore().Value.Score), + } + attaches = append(attaches, &a) + } return } @@ -223,80 +237,62 @@ func color(cvssScore float64) string { } } -// func attachmentText(cveInfo models.CveInfo, osFamily string) string { -// linkText := links(cveInfo, osFamily) -//TODO -// return "" -// switch { -// case config.Conf.Lang == "ja" && -// 0 < cveInfo.CveDetail.Jvn.CvssScore(): +func attachmentText(vinfo models.VulnInfo, osFamily string) string { + maxCvss := vinfo.CveContents.MaxCvssScore() + vectors := []string{} + for _, cvss := range vinfo.CveContents.Cvss2Scores() { + calcURL := "" + switch cvss.Value.Type { + case models.CVSS2: + calcURL = fmt.Sprintf( + "https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?vector=%s", + cvss.Value.Vector) + case models.CVSS3: + calcURL = fmt.Sprintf( + "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=%s", + cvss.Value.Vector) + } + v := fmt.Sprintf("<%s|%s> (<%s|%s>)", + calcURL, + cvss.Value.Format(), + vinfo.CveContents[cvss.Type].SourceLink, + cvss.Type) + vectors = append(vectors, v) + } -// jvn := cveInfo.CveDetail.Jvn -// return fmt.Sprintf("*%4.1f (%s)* <%s|%s>\n%s\n%s\n*Confidence:* %v", -// cveInfo.CveDetail.CvssScore(config.Conf.Lang), -// jvn.CvssSeverity(), -// fmt.Sprintf(cvssV2CalcBaseURL, cveInfo.CveDetail.CveID), -// jvn.CvssVector(), -// jvn.CveTitle(), -// linkText, -// cveInfo.VulnInfo.Confidence, -// ) -// case 0 < cveInfo.CveDetail.CvssScore("en"): -// nvd := cveInfo.CveDetail.Nvd -// return fmt.Sprintf("*%4.1f (%s)* <%s|%s>\n%s\n%s\n*Confidence:* %v", -// cveInfo.CveDetail.CvssScore(config.Conf.Lang), -// nvd.CvssSeverity(), -// fmt.Sprintf(cvssV2CalcBaseURL, cveInfo.CveDetail.CveID), -// nvd.CvssVector(), -// nvd.CveSummary(), -// linkText, -// cveInfo.VulnInfo.Confidence, -// ) -// default: -// nvd := cveInfo.CveDetail.Nvd -// return fmt.Sprintf("?\n%s\n%s\n*Confidence:* %v", -// nvd.CveSummary(), linkText, cveInfo.VulnInfo.Confidence) -// } -// } + severity := strings.ToUpper(maxCvss.Value.Severity) + if severity == "" { + severity = "?" + } -// func links(cveInfo models.CveInfo, osFamily string) string { -// links := []string{} + return fmt.Sprintf("*%4.1f (%s)* %s\n%s\n```%s```", + maxCvss.Value.Score, + severity, + cweIDs(vinfo, osFamily), + strings.Join(vectors, "\n"), + vinfo.CveContents.Summaries(config.Conf.Lang, osFamily)[0].Value, + ) +} -// //TODO -// // cweID := cveInfo.CveDetail.CweID() -// // if 0 < len(cweID) { -// // links = append(links, fmt.Sprintf("<%s|%s>", -// // cweURL(cweID), cweID)) -// // if config.Conf.Lang == "ja" { -// // links = append(links, fmt.Sprintf("<%s|%s(JVN)>", -// // cweJvnURL(cweID), cweID)) -// // } -// // } +func cweIDs(vinfo models.VulnInfo, osFamily string) string { + links := []string{} + for _, cwe := range vinfo.CveContents.CweIDs(osFamily) { + if config.Conf.Lang == "ja" { + links = append(links, fmt.Sprintf("<%s|%s>", + cweJvnURL(cwe.Value), cwe.Value)) + } else { + links = append(links, fmt.Sprintf("<%s|%s>", + cweURL(cwe.Value), cwe.Value)) + } + } + return strings.Join(links, " / ") +} -// cveID := cveInfo.VulnInfo.CveID -// //TODO -// // if config.Conf.Lang == "ja" && 0 < len(cveInfo.CveDetail.Jvn.Link()) { -// // jvn := fmt.Sprintf("<%s|JVN>", cveInfo.CveDetail.Jvn.Link()) -// // links = append(links, jvn) -// // } -// dlinks := distroLinks(cveInfo, osFamily) -// for _, link := range dlinks { -// links = append(links, -// fmt.Sprintf("<%s|%s>", link.url, link.title)) -// } -// links = append(links, fmt.Sprintf("<%s|MITRE>", -// fmt.Sprintf("%s%s", mitreBaseURL, cveID))) -// links = append(links, fmt.Sprintf("<%s|CVEDetails>", -// fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID))) - -// return strings.Join(links, " / ") -// } - -// // See testcase -// func getNotifyUsers(notifyUsers []string) string { -// slackStyleTexts := []string{} -// for _, username := range notifyUsers { -// slackStyleTexts = append(slackStyleTexts, fmt.Sprintf("<%s>", username)) -// } -// return strings.Join(slackStyleTexts, " ") -// } +// See testcase +func getNotifyUsers(notifyUsers []string) string { + slackStyleTexts := []string{} + for _, username := range notifyUsers { + slackStyleTexts = append(slackStyleTexts, fmt.Sprintf("<%s>", username)) + } + return strings.Join(slackStyleTexts, " ") +} diff --git a/report/slack_test.go b/report/slack_test.go index 66e77d98..0eae9031 100644 --- a/report/slack_test.go +++ b/report/slack_test.go @@ -1,21 +1,23 @@ package report -// func TestGetNotifyUsers(t *testing.T) { -// var tests = []struct { -// in []string -// expected string -// }{ -// { -// []string{"@user1", "@user2"}, -// "<@user1> <@user2>", -// }, -// } +import "testing" -// for _, tt := range tests { -// actual := getNotifyUsers(tt.in) -// if tt.expected != actual { -// t.Errorf("expected %s, actual %s", tt.expected, actual) -// } -// } +func TestGetNotifyUsers(t *testing.T) { + var tests = []struct { + in []string + expected string + }{ + { + []string{"@user1", "@user2"}, + "<@user1> <@user2>", + }, + } -// } + for _, tt := range tests { + actual := getNotifyUsers(tt.in) + if tt.expected != actual { + t.Errorf("expected %s, actual %s", tt.expected, actual) + } + } + +} diff --git a/report/util.go b/report/util.go index 6f27aa2e..db909bcd 100644 --- a/report/util.go +++ b/report/util.go @@ -493,8 +493,8 @@ func formatOneChangelog(p models.Package) string { return "" } - packVer := fmt.Sprintf("%s -> %s", - p.FormatVer(), p.FormatNewVer()) + packVer := fmt.Sprintf("%s-%s -> %s", + p.Name, p.FormatVer(), p.FormatNewVer()) var delim bytes.Buffer for i := 0; i < len(packVer); i++ { delim.WriteString("-") From c6ad9ea57a0e224ebc1e47326a47f7e5ca12da6f Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Wed, 31 May 2017 20:42:20 +0900 Subject: [PATCH 046/113] Fix tui --- commands/tui.go | 53 +++---- models/cvecontents.go | 8 -- models/packages.go | 32 ++++- report/report.go | 13 +- report/tui.go | 324 ++++++++++++++++++------------------------ report/util.go | 174 +---------------------- util/util.go | 12 ++ 7 files changed, 210 insertions(+), 406 deletions(-) diff --git a/commands/tui.go b/commands/tui.go index cd661a50..ca2f002a 100644 --- a/commands/tui.go +++ b/commands/tui.go @@ -24,6 +24,8 @@ import ( "path/filepath" c "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/models" + "github.com/future-architect/vuls/report" "github.com/future-architect/vuls/util" "github.com/google/subcommands" ) @@ -144,41 +146,22 @@ func (p *TuiCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s } c.Conf.Pipe = p.pipe - // jsonDir, err := report.JSONDir(f.Args()) - // if err != nil { - // log.Errorf("Failed to read json dir under results: %s", err) - // return subcommands.ExitFailure - // } - // results, err := report.LoadScanResults(jsonDir) - // if err != nil { - // log.Errorf("Failed to read from JSON: %s", err) - // return subcommands.ExitFailure - // } + dir, err := report.JSONDir(f.Args()) + if err != nil { + util.Log.Errorf("Failed to read from JSON: %s", err) + return subcommands.ExitFailure + } + var res models.ScanResults + if res, err = report.LoadScanResults(dir); err != nil { + util.Log.Error(err) + return subcommands.ExitFailure + } + util.Log.Infof("Loaded: %s", dir) - // var filledResults []models.ScanResult - // for _, r := range results { - // if p.refreshCve || needToRefreshCve(r) { - // if c.Conf.CveDBType == "sqlite3" { - // if _, err := os.Stat(c.Conf.CveDBPath); os.IsNotExist(err) { - // log.Errorf("SQLite3 DB(CVE-Dictionary) is not exist: %s", - // c.Conf.CveDBPath) - // return subcommands.ExitFailure - // } - // } - - // if err := fillCveInfoFromCveDB(&r); err != nil { - // log.Errorf("Failed to fill CVE information: %s", err) - // return subcommands.ExitFailure - // } - - // if err := overwriteJSONFile(jsonDir, r); err != nil { - // log.Errorf("Failed to write JSON: %s", err) - // return subcommands.ExitFailure - // } - // } - // filledResults = append(filledResults, r) - // } - // return report.RunTui(filledResults) - return subcommands.ExitFailure + if res, err = report.FillCveInfos(res, dir); err != nil { + util.Log.Error(err) + return subcommands.ExitFailure + } + return report.RunTui(res) } diff --git a/models/cvecontents.go b/models/cvecontents.go index 69126f5d..573a666e 100644 --- a/models/cvecontents.go +++ b/models/cvecontents.go @@ -596,14 +596,6 @@ type Cpe struct { // 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 { Source string diff --git a/models/packages.go b/models/packages.go index 8439f90a..19ed3c5f 100644 --- a/models/packages.go +++ b/models/packages.go @@ -18,6 +18,7 @@ along with this program. If not, see . package models import ( + "bytes" "fmt" "strings" ) @@ -110,10 +111,39 @@ func (p Package) FormatNewVer() string { // FormatVersionFromTo formats installed and new package version func (p Package) FormatVersionFromTo() string { - return fmt.Sprintf("%s-%s -> %s", + return fmt.Sprintf("%s-%s - %s", p.Name, p.FormatVer(), p.FormatNewVer()) } +// FormatChangelog formats the changelog +func (p Package) FormatChangelog() string { + buf := []string{} + if p.NewVersion == "" { + return "" + } + + packVer := fmt.Sprintf("%s-%s -> %s", + p.Name, p.FormatVer(), p.FormatNewVer()) + var delim bytes.Buffer + for i := 0; i < len(packVer); i++ { + delim.WriteString("-") + } + + clog := p.Changelog.Contents + if lines := strings.Split(clog, "\n"); len(lines) != 0 { + clog = strings.Join(lines[0:len(lines)-1], "\n") + } + + switch p.Changelog.Method { + case FailedToGetChangelog: + clog = "No changelogs" + case FailedToFindVersionInChangelog: + clog = "Failed to parse changelogs. For detials, check yourself" + } + buf = append(buf, packVer, delim.String(), clog) + return strings.Join(buf, "\n") +} + // Changelog has contents of changelog and how to get it. // Method: modesl.detectionMethodStr type Changelog struct { diff --git a/report/report.go b/report/report.go index 17619a2b..1b05770c 100644 --- a/report/report.go +++ b/report/report.go @@ -25,7 +25,6 @@ import ( "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/oval" "github.com/future-architect/vuls/util" - "github.com/k0kubun/pp" ) const ( @@ -72,9 +71,9 @@ func FillCveInfos(rs []models.ScanResult, dir string) ([]models.ScanResult, erro } //TODO remove debug code - for _, r := range filled { - pp.Printf("filled: %d\n", len(r.ScannedCves)) - } + // for _, r := range filled { + // pp.Printf("filled: %d\n", len(r.ScannedCves)) + // } filtered := []models.ScanResult{} for _, r := range filled { @@ -82,9 +81,9 @@ func FillCveInfos(rs []models.ScanResult, dir string) ([]models.ScanResult, erro } //TODO remove debug code - for _, r := range filtered { - pp.Printf("filtered: %d\n", len(r.ScannedCves)) - } + // for _, r := range filtered { + // pp.Printf("filtered: %d\n", len(r.ScannedCves)) + // } return filtered, nil } diff --git a/report/tui.go b/report/tui.go index 2a3c8fdc..afe638ca 100644 --- a/report/tui.go +++ b/report/tui.go @@ -20,21 +20,25 @@ package report import ( "bytes" "fmt" + "html/template" "os" + "sort" "strings" "time" log "github.com/Sirupsen/logrus" + "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" + "github.com/future-architect/vuls/util" "github.com/google/subcommands" "github.com/gosuri/uitable" "github.com/jroimartin/gocui" - cve "github.com/kotakanbe/go-cve-dictionary/models" ) var scanResults models.ScanResults var currentScanResult models.ScanResult -var currentCveInfo int +var vinfos []models.VulnInfo +var currentVinfo int var currentDetailLimitY int // RunTui execute main logic @@ -220,8 +224,7 @@ func movable(v *gocui.View, nextY int) (ok bool, yLimit int) { } return true, yLimit case "summary": - //TODO - // yLimit = len(currentScanResult.AllCves()) - 1 + yLimit = len(currentScanResult.ScannedCves) - 1 if yLimit < nextY { return false, yLimit } @@ -279,7 +282,7 @@ func cursorDown(g *gocui.Gui, v *gocui.View) error { // ok, := movable(v, oy+cy+1) // _, maxY := v.Size() ok, _ := movable(v, oy+cy+1) - // log.Info(cy, oy, maxY, yLimit) + // log.Info(cy, oy) if !ok { return nil } @@ -290,6 +293,10 @@ func cursorDown(g *gocui.Gui, v *gocui.View) error { } onMovingCursorRedrawView(g, v) } + + cx, cy := v.Cursor() + ox, oy := v.Origin() + debug(g, fmt.Sprintf("%v, %v, %v, %v", cx, cy, ox, oy)) return nil } @@ -441,6 +448,7 @@ func changeHost(g *gocui.Gui, v *gocui.View) error { for _, r := range scanResults { if serverName == strings.TrimSpace(r.ServerInfoTui()) { currentScanResult = r + vinfos = r.ScannedCves.ToSortedSlice() break } } @@ -509,7 +517,8 @@ func showMsg(g *gocui.Gui, v *gocui.View) error { // maxX, maxY := v.Size() _, maxY := v.Size() - l := fmt.Sprintf("cy: %d, oy: %d, maxY: %d, yLimit: %d, curCve %d, ok: %v", cy, oy, maxY, yLimit, currentCveInfo, ok) + l := fmt.Sprintf("cy: %d, oy: %d, maxY: %d, yLimit: %d, curCve %d, ok: %v", + cy, oy, maxY, yLimit, currentVinfo, ok) // if v, err := g.SetView("msg", maxX/2-30, maxY/2, maxX/2+30, maxY/2+2); err != nil { if v, err := g.SetView("msg", 10, maxY/2, 10+50, maxY/2+2); err != nil { if err != gocui.ErrUnknownView { @@ -550,6 +559,20 @@ func layout(g *gocui.Gui) error { if err := setChangelogLayout(g); err != nil { return err } + + return nil +} + +func debug(g *gocui.Gui, str string) error { + if config.Conf.Debug { + maxX, maxY := g.Size() + if _, err := g.View("debug"); err != gocui.ErrUnknownView { + g.DeleteView("debug") + } + if v, err := g.SetView("debug", maxX/2-7, maxY/2, maxX/2+7, maxY/2+2); err != nil { + fmt.Fprintf(v, str) + } + } return nil } @@ -568,6 +591,7 @@ func setSideLayout(g *gocui.Gui) error { return fmt.Errorf("No scan results") } currentScanResult = scanResults[0] + vinfos = scanResults[0].ScannedCves.ToSortedSlice() if _, err := g.SetCurrentView("side"); err != nil { return err } @@ -601,72 +625,35 @@ func summaryLines() string { return "Error: Scan with --debug to view the details" } - //TODO - // indexFormat := "" - // if len(currentScanResult.AllCves()) < 10 { - // indexFormat = "[%1d]" - // } else if len(currentScanResult.AllCves()) < 100 { - // indexFormat = "[%2d]" - // } else { - // indexFormat = "[%3d]" - // } + indexFormat := "" + if len(currentScanResult.ScannedCves) < 10 { + indexFormat = "[%1d]" + } else if len(currentScanResult.ScannedCves) < 100 { + indexFormat = "[%2d]" + } else { + indexFormat = "[%3d]" + } - // for i, d := range currentScanResult.AllCves() { - // var cols []string - // //TODO - // var summary string - // if cont, found := d.Get(models.NVD); found { - // summary = cont.Summary - // } - // var cvssScore string - // if d.CvssV2Score() <= 0 { - // cvssScore = "| ?" - // } else { - // cvssScore = fmt.Sprintf("| %4.1f", d.CvssV2Score()) - // } - // cols = []string{ - // fmt.Sprintf(indexFormat, i+1), - // d.VulnInfo.CveID, - // cvssScore, - // fmt.Sprintf("| %3d |", d.VulnInfo.Confidence.Score), - // summary, - // } - // // if config.Conf.Lang == "ja" && 0 < d.CveDetail.Jvn.CvssScore() { - // // summary := d.CveDetail.Jvn.CveTitle() - // // cols = []string{ - // // fmt.Sprintf(indexFormat, i+1), - // // d.CveDetail.CveID, - // // fmt.Sprintf("| %4.1f", - // // d.CveDetail.CvssScore(config.Conf.Lang)), - // // fmt.Sprintf("| %3d |", d.VulnInfo.Confidence.Score), - // // summary, - // // } - // // } else { - // // summary := d.CveDetail.Nvd.CveSummary() + for i, vinfo := range vinfos { + summary := vinfo.CveContents.Summaries( + config.Conf.Lang, currentScanResult.Family)[0].Value + cvssScore := fmt.Sprintf("| %4.1f", + vinfo.CveContents.MaxCvssScore().Value.Score) - // // var cvssScore string - // // if d.CveDetail.CvssScore("en") <= 0 { - // // cvssScore = "| ?" - // // } else { - // // cvssScore = fmt.Sprintf("| %4.1f", - // // d.CveDetail.CvssScore(config.Conf.Lang)) - // // } - - // // cols = []string{ - // // fmt.Sprintf(indexFormat, i+1), - // // d.CveDetail.CveID, - // // cvssScore, - // // fmt.Sprintf("| %3d |", d.VulnInfo.Confidence.Score), - // // summary, - // // } - // // } - - // icols := make([]interface{}, len(cols)) - // for j := range cols { - // icols[j] = cols[j] - // } - // stable.AddRow(icols...) - // } + var cols []string + cols = []string{ + fmt.Sprintf(indexFormat, i+1), + vinfo.CveID, + cvssScore, + fmt.Sprintf("| %3d |", vinfo.Confidence.Score), + summary, + } + icols := make([]interface{}, len(cols)) + for j := range cols { + icols[j] = cols[j] + } + stable.AddRow(icols...) + } return fmt.Sprintf("%s", stable) } @@ -679,7 +666,7 @@ func setDetailLayout(g *gocui.Gui) error { } _, cy := summaryView.Cursor() _, oy := summaryView.Origin() - currentCveInfo = cy + oy + currentVinfo = cy + oy if v, err := g.SetView("detail", -1, int(float64(maxY)*0.2), int(float64(maxX)*0.5), maxY); err != nil { if err != gocui.ErrUnknownView { @@ -707,27 +694,26 @@ func setChangelogLayout(g *gocui.Gui) error { } _, cy := summaryView.Cursor() _, oy := summaryView.Origin() - currentCveInfo = cy + oy + currentVinfo = cy + oy if v, err := g.SetView("changelog", int(float64(maxX)*0.5), int(float64(maxY)*0.2), maxX, maxY); err != nil { if err != gocui.ErrUnknownView { return err } - //TODO - // if len(currentScanResult.Errors) != 0 || len(currentScanResult.AllCves()) == 0 { - // return nil - // } + if len(currentScanResult.Errors) != 0 || len(currentScanResult.ScannedCves) == 0 { + return nil + } lines := []string{} - //TODO - // cveInfo := currentScanResult.AllCves()[currentCveInfo] - // for _, pack := range cveInfo.Packages { - // for _, p := range currentScanResult.Packages { - // if pack.Name == p.Name { - // lines = append(lines, formatOneChangelog(p), "\n") - // } - // } - // } + vinfo := vinfos[currentVinfo] + for _, name := range vinfo.PackageNames { + pack := currentScanResult.Packages[name] + for _, p := range currentScanResult.Packages { + if pack.Name == p.Name { + lines = append(lines, p.FormatChangelog(), "\n") + } + } + } text := strings.Join(lines, "\n") fmt.Fprint(v, text) v.Editable = false @@ -740,14 +726,12 @@ func setChangelogLayout(g *gocui.Gui) error { type dataForTmpl struct { CveID string - CvssScore string - CvssVector string - CvssSeverity string + Cvsses []models.CveContentCvss Summary string Confidence models.Confidence - CweURL string - VulnSiteLinks []string - References []cve.Reference + Cwes []models.CveContentStr + Links []string + References []models.Reference Packages []string CpeNames []string PublishedDate time.Time @@ -755,123 +739,99 @@ type dataForTmpl struct { } func detailLines() (string, error) { - if len(currentScanResult.Errors) != 0 { + r := currentScanResult + if len(r.Errors) != 0 { return "", nil } - //TODO - // if len(currentScanResult.AllCves()) == 0 { - // return "No vulnerable packages", nil - // } - // cveInfo := currentScanResult.AllCves()[currentCveInfo] - // cveID := cveInfo.VulnInfo.CveID - - // tmpl, err := template.New("detail").Parse(detailTemplate()) - // if err != nil { - // return "", err - // } - - // var cvssSeverity, cvssVector, summary string - // var refs []cve.Reference - switch { - //TODO - // case config.Conf.Lang == "ja" && - // 0 < cveInfo.CveDetail.Jvn.CvssScore(): - // jvn := cveInfo.CveDetail.Jvn - // cvssSeverity = jvn.CvssSeverity() - // cvssVector = jvn.CvssVector() - // summary = fmt.Sprintf("%s\n%s", jvn.CveTitle(), jvn.CveSummary()) - // refs = jvn.VulnSiteReferences() - default: - // var nvd *models.CveContent - //TODO - // if cont, found := cveInfo.Get(models.NVD); found { - // nvd = cont - // } - // cvssSeverity = nvd.CvssSeverity() - // cvssVector = nvd.CvssVector() - // summary = nvd.Summary - // refs = nvd.VulnSiteReferences() + if len(r.ScannedCves) == 0 { + return "No vulnerable packages", nil } - //TODO - // cweURL := cweURL(cveInfo.CveDetail.CweID()) - // links := []string{ - // fmt.Sprintf("[NVD]( %s )", fmt.Sprintf("%s/%s", nvdBaseURL, cveID)), - // fmt.Sprintf("[MITRE]( %s )", fmt.Sprintf("%s%s", mitreBaseURL, cveID)), - // fmt.Sprintf("[CveDetais]( %s )", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)), - // fmt.Sprintf("[CVSSv2 Calc]( %s )", fmt.Sprintf(cvssV2CalcBaseURL, cveID)), - // fmt.Sprintf("[CVSSv3 Calc]( %s )", fmt.Sprintf(cvssV3CalcBaseURL, cveID)), - // } - // dlinks := distroLinks(cveInfo, currentScanResult.Family) - // for _, link := range dlinks { - // links = append(links, fmt.Sprintf("[%s]( %s )", link.title, link.url)) - // } + tmpl, err := template.New("detail").Parse(mdTemplate) + if err != nil { + return "", err + } - //TODO - // var cvssScore string - // if cveInfo.CvssV2Score() == -1 { - // cvssScore = "?" - // // } else { - // // cvssScore = fmt.Sprintf("%4.1f", cveInfo.CveDetail.CvssScore(config.Conf.Lang)) - // } + vinfo := vinfos[currentVinfo] - // packages := []string{} - // for _, pack := range cveInfo.Packages { - // packages = append(packages, - // fmt.Sprintf( - // "%s -> %s", - // pack.FormatCurrentVer(), - // pack.FormatNewVer())) - // } + packsVer := []string{} + sort.Strings(vinfo.PackageNames) + for _, name := range vinfo.PackageNames { + // packages detected by OVAL may not be actually installed + if pack, ok := r.Packages[name]; ok { + packsVer = append(packsVer, pack.FormatVersionFromTo()) + } + } + sort.Strings(vinfo.CpeNames) + for _, name := range vinfo.CpeNames { + packsVer = append(packsVer, name) + } - // data := dataForTmpl{ - // CveID: cveID, - // CvssScore: cvssScore, - // CvssSeverity: cvssSeverity, - // CvssVector: cvssVector, - // Summary: summary, - // Confidence: cveInfo.VulnInfo.Confidence, - // //TODO - // // CweURL: cweURL, - // VulnSiteLinks: links, - // References: refs, - // Packages: packages, - // CpeNames: cveInfo.CpeNames, - // } + links := []string{vinfo.CveContents.SourceLinks( + config.Conf.Lang, r.Family, vinfo.CveID)[0].Value, + vinfo.Cvss2CalcURL(), + vinfo.Cvss3CalcURL()} + for _, url := range vinfo.VendorLinks(r.Family) { + links = append(links, url) + } + // links = util.Distinct(links) + + refs := []models.Reference{} + for _, rr := range vinfo.CveContents.References(r.Family) { + for _, ref := range rr.Value { + refs = append(refs, ref) + } + } + + data := dataForTmpl{ + CveID: vinfo.CveID, + Cvsses: append(vinfo.CveContents.Cvss3Scores(), vinfo.CveContents.MaxCvss2Score()), + Summary: vinfo.CveContents.Summaries(r.Lang, r.Family)[0].Value, + Confidence: vinfo.Confidence, + Cwes: vinfo.CveContents.CweIDs(r.Family), + Links: util.Distinct(links), + Packages: packsVer, + References: refs, + } buf := bytes.NewBuffer(nil) // create empty buffer - // if err := tmpl.Execute(buf, data); err != nil { - // return "", err - // } + if err := tmpl.Execute(buf, data); err != nil { + return "", err + } return string(buf.Bytes()), nil } -func detailTemplate() string { - return ` +const mdTemplate = ` {{.CveID}} ============== -CVSS Score +CVSS Scores -------------- -{{.CvssScore}} ({{.CvssSeverity}}) {{.CvssVector}} +{{range .Cvsses -}} +* {{.Value.Format}} ({{.Type}}) +{{end}} Summary -------------- {{.Summary }} -Confidence +Links -------------- - {{.Confidence }} +{{range $link := .Links -}} +* {{$link}} +{{end}} CWE -------------- - {{.CweURL }} +{{range .Cwes -}} +* {{.Value}} ({{.Type}}) +{{end}} Package/CPE -------------- @@ -882,12 +842,13 @@ Package/CPE {{range $name := .CpeNames -}} * {{$name}} {{end}} -Links + +Confidence -------------- -{{range $link := .VulnSiteLinks -}} -* {{$link}} -{{end}} + {{.Confidence }} + + References -------------- @@ -896,4 +857,3 @@ References {{end}} ` -} diff --git a/report/util.go b/report/util.go index db909bcd..7f898f84 100644 --- a/report/util.go +++ b/report/util.go @@ -18,7 +18,6 @@ along with this program. If not, see . package report import ( - "bytes" "encoding/json" "fmt" "io/ioutil" @@ -238,149 +237,6 @@ func formatFullPlainText(r models.ScanResult) string { return fmt.Sprintf("%s\n%s", header, table) } -//TODO -func formatPlainTextDetails(r models.ScanResult, osFamily string) (scoredReport, unscoredReport []string) { - // for _, cve := range r.KnownCves { - // switch config.Conf.Lang { - // case "en": - // if 0 < cve.CveDetail.Nvd.CvssScore() { - // scoredReport = append( - // scoredReport, formatPlainTextDetailsLangEn(cve, osFamily)) - // } else { - // scoredReport = append( - // scoredReport, formatPlainTextUnknownCve(cve, osFamily)) - // } - // case "ja": - // if 0 < cve.CveDetail.Jvn.CvssScore() { - // scoredReport = append( - // scoredReport, formatPlainTextDetailsLangJa(cve, osFamily)) - // } else if 0 < cve.CveDetail.Nvd.CvssScore() { - // scoredReport = append( - // scoredReport, formatPlainTextDetailsLangEn(cve, osFamily)) - // } else { - // scoredReport = append( - // scoredReport, formatPlainTextUnknownCve(cve, osFamily)) - // } - // } - // } - // for _, cve := range r.UnknownCves { - // unscoredReport = append( - // unscoredReport, formatPlainTextUnknownCve(cve, osFamily)) - // } - return -} - -// func formatPlainTextUnknownCve(cveInfo models.CveInfo, osFamily string) string { -// cveID := cveInfo.VulnInfo.CveID -// dtable := uitable.New() -// dtable.MaxColWidth = maxColWidth -// dtable.Wrap = true -// dtable.AddRow(cveID) -// dtable.AddRow("-------------") -// dtable.AddRow("Score", "?") -// dtable.AddRow("NVD", fmt.Sprintf("%s/%s", nvdBaseURL, cveID)) -// dlinks := distroLinks(cveInfo, osFamily) -// for _, link := range dlinks { -// dtable.AddRow(link.title, link.url) -// } -// dtable.AddRow("CVE Details", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)) -// dtable = addPackageInfos(dtable, cveInfo.Packages) -// dtable = addCpeNames(dtable, cveInfo.CpeNames) -// dtable.AddRow("Confidence", cveInfo.VulnInfo.Confidence) - -// return fmt.Sprintf("%s", dtable) -// } - -//TODO -// func formatPlainTextDetailsLangJa(cveInfo models.CveInfo, osFamily string) string { -// return "TODO" -// cveDetail := cveInfo.CveDetail -// cveID := cveDetail.CveID -// jvn := cveDetail.Jvn - -// dtable := uitable.New() -// dtable.MaxColWidth = maxColWidth -// 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(cveDetail.CweID(), cweURL(cveDetail.CweID())) -// dtable.AddRow(cveDetail.CweID()+"(JVN)", cweJvnURL(cveDetail.CweID())) - -// dtable.AddRow("JVN", jvn.Link()) -// dtable.AddRow("NVD", fmt.Sprintf("%s/%s", nvdBaseURL, cveID)) -// dtable.AddRow("MITRE", fmt.Sprintf("%s%s", mitreBaseURL, cveID)) -// dtable.AddRow("CVE Details", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)) -// dtable.AddRow("CVSSv2 Clac", fmt.Sprintf(cvssV2CalcBaseURL, cveID)) -// dtable.AddRow("CVSSv3 Clac", fmt.Sprintf(cvssV3CalcBaseURL, cveID)) - -// dlinks := distroLinks(cveInfo, osFamily) -// for _, link := range dlinks { -// dtable.AddRow(link.title, link.url) -// } - -// dtable = addPackageInfos(dtable, cveInfo.Packages) -// dtable = addCpeNames(dtable, cveInfo.CpeNames) -// dtable.AddRow("Confidence", cveInfo.VulnInfo.Confidence) - -// return fmt.Sprintf("%s", dtable) -// } - -//TODO -// func formatPlainTextDetailsLangEn(d models.CveInfo, osFamily string) string { -// return "" -// cveDetail := d.CveDetail -// cveID := cveDetail.CveID -// nvd := cveDetail.Nvd - -// dtable := uitable.New() -// dtable.MaxColWidth = maxColWidth -// 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("CWE", cweURL(cveDetail.CweID())) - -// dtable.AddRow("NVD", fmt.Sprintf("%s/%s", nvdBaseURL, cveID)) -// dtable.AddRow("MITRE", fmt.Sprintf("%s%s", mitreBaseURL, cveID)) -// dtable.AddRow("CVE Details", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)) -// dtable.AddRow("CVSSv2 Clac", fmt.Sprintf(cvssV2CalcBaseURL, cveID)) -// dtable.AddRow("CVSSv3 Clac", fmt.Sprintf(cvssV3CalcBaseURL, cveID)) - -// links := distroLinks(d, osFamily) -// for _, link := range links { -// dtable.AddRow(link.title, link.url) -// } -// dtable = addPackageInfos(dtable, d.Packages) -// dtable = addCpeNames(dtable, d.CpeNames) -// dtable.AddRow("Confidence", d.VulnInfo.Confidence) - -// return fmt.Sprintf("%s\n", dtable) -// } - // type distroLink struct { // title string // url string @@ -481,40 +337,12 @@ func formatChangelogs(r models.ScanResult) string { if p.NewVersion == "" { continue } - clog := formatOneChangelog(p) + clog := p.FormatChangelog() buf = append(buf, clog, "\n\n") } return strings.Join(buf, "\n") } -func formatOneChangelog(p models.Package) string { - buf := []string{} - if p.NewVersion == "" { - return "" - } - - packVer := fmt.Sprintf("%s-%s -> %s", - p.Name, p.FormatVer(), p.FormatNewVer()) - var delim bytes.Buffer - for i := 0; i < len(packVer); i++ { - delim.WriteString("-") - } - - clog := p.Changelog.Contents - if lines := strings.Split(clog, "\n"); len(lines) != 0 { - clog = strings.Join(lines[0:len(lines)-1], "\n") - } - - switch p.Changelog.Method { - case models.FailedToGetChangelog: - clog = "No changelogs" - case models.FailedToFindVersionInChangelog: - clog = "Failed to parse changelogs. For detials, check yourself" - } - buf = append(buf, packVer, delim.String(), clog) - return strings.Join(buf, "\n") -} - func needToRefreshCve(r models.ScanResult) bool { if r.Lang != config.Conf.Lang { return true diff --git a/util/util.go b/util/util.go index 31f27d9e..b5707bac 100644 --- a/util/util.go +++ b/util/util.go @@ -136,6 +136,18 @@ func Truncate(str string, length int) string { return str } +// Distinct a slice +func Distinct(ss []string) (distincted []string) { + m := map[string]bool{} + for _, s := range ss { + if _, found := m[s]; !found { + m[s] = true + distincted = append(distincted, s) + } + } + return +} + // VendorLink returns a URL of the given OS family and CVEID //TODO // func VendorLink(family, cveID string) string { From a7951b727c02a534a4c2e049773f48bc30b38809 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Sat, 3 Jun 2017 14:08:11 +0900 Subject: [PATCH 047/113] Remove commented out code --- .../owasp-dependency-check/parser/parser.go | 2 - models/cvecontents.go | 1 - report/cve_client.go | 7 -- report/report.go | 7 -- report/util.go | 85 ------------------- util/util.go | 18 ---- 6 files changed, 120 deletions(-) diff --git a/contrib/owasp-dependency-check/parser/parser.go b/contrib/owasp-dependency-check/parser/parser.go index b39dec94..90857a09 100644 --- a/contrib/owasp-dependency-check/parser/parser.go +++ b/contrib/owasp-dependency-check/parser/parser.go @@ -58,7 +58,5 @@ func Parse(path string) ([]string, error) { } } } - //TODO remove - // sort.Strings(cpes) return cpes, nil } diff --git a/models/cvecontents.go b/models/cvecontents.go index 573a666e..f86607d2 100644 --- a/models/cvecontents.go +++ b/models/cvecontents.go @@ -131,7 +131,6 @@ func (v CveContents) Cvss2Scores() (values []CveContentCvss) { // MaxCvss2Score returns Max CVSS V2 Score func (v CveContents) MaxCvss2Score() CveContentCvss { - //TODO Severity Ubuntu, Debian... order := []CveContentType{NVD, RedHat, JVN} max := 0.0 value := CveContentCvss{ diff --git a/report/cve_client.go b/report/cve_client.go index fad79470..3178636e 100644 --- a/report/cve_client.go +++ b/report/cve_client.go @@ -127,9 +127,6 @@ func (api cvedictClient) FetchCveDetails(cveIDs []string) (cveDetails cve.CveDet return []cve.CveDetail{}, fmt.Errorf("Failed to fetch CVE. err: %v", errs) } - - //TODO - // sort.Sort(cveDetails) return } @@ -156,10 +153,6 @@ func (api cvedictClient) FetchCveDetailsFromCveDB(cveIDs []string) (cveDetails c cveDetails = append(cveDetails, cveDetail) } } - - //TODO - // order by CVE ID desc - // sort.Sort(cveDetails) return } diff --git a/report/report.go b/report/report.go index 1b05770c..7cdefdf9 100644 --- a/report/report.go +++ b/report/report.go @@ -136,13 +136,6 @@ func fillCveDetail(r *models.ScanResult) error { } } } - //TODO Remove - // sort.Slice(r.ScannedCves, func(i, j int) bool { - // if r.ScannedCves[j].CveContents.CvssV2Score() == r.ScannedCves[i].CveContents.CvssV2Score() { - // return r.ScannedCves[j].CveContents.CvssV2Score() < r.ScannedCves[i].CveContents.CvssV2Score() - // } - // return r.ScannedCves[j].CveContents.CvssV2Score() < r.ScannedCves[i].CveContents.CvssV2Score() - // }) return nil } diff --git a/report/util.go b/report/util.go index 7f898f84..e0bf2399 100644 --- a/report/util.go +++ b/report/util.go @@ -237,91 +237,6 @@ func formatFullPlainText(r models.ScanResult) string { return fmt.Sprintf("%s\n%s", header, table) } -// type distroLink struct { -// title string -// url string -// } - -// distroLinks add Vendor URL of the CVE to table -// func distroLinks(cveInfo models.CveInfo, osFamily string) []distroLink { -// cveID := cveInfo.VulnInfo.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 "oraclelinux": -// links := []distroLink{ -// { -// "Oracle-CVE", -// fmt.Sprintf(oracleSecurityBaseURL, cveID), -// }, -// } -// for _, advisory := range cveInfo.DistroAdvisories { -// links = append(links, distroLink{ -// // "Oracle-ELSA" -// advisory.AdvisoryID, -// fmt.Sprintf(oracleELSABaseBaseURL, advisory.AdvisoryID), -// }) -// } -// 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{} -// } -// } - func cweURL(cweID string) string { return fmt.Sprintf("https://cwe.mitre.org/data/definitions/%s.html", strings.TrimPrefix(cweID, "CWE-")) diff --git a/util/util.go b/util/util.go index b5707bac..da888b2c 100644 --- a/util/util.go +++ b/util/util.go @@ -147,21 +147,3 @@ func Distinct(ss []string) (distincted []string) { } return } - -// 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 "" -// } From e9df2bfa0184eac92aeb782146f71c348f8b5bfd Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Sat, 3 Jun 2017 14:44:04 +0900 Subject: [PATCH 048/113] Convert null to empty in JSON --- models/vulninfos.go | 37 ++++++++++++++++++++++--------------- report/report.go | 7 +++++-- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/models/vulninfos.go b/models/vulninfos.go index 13813ea1..195a5f05 100644 --- a/models/vulninfos.go +++ b/models/vulninfos.go @@ -162,22 +162,29 @@ func (v VulnInfo) VendorLinks(family string) map[string]string { return links } -// 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() -// } -// } +func (v *VulnInfo) NilToEmpty() *VulnInfo { + 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() + } + for key := range v.CveContents { + if v.CveContents[key].Cpes == nil { + cont := v.CveContents[key] + cont.Cpes = []Cpe{} + v.CveContents[key] = cont + } + } + return v +} // DistroAdvisory has Amazon Linux, RHEL, FreeBSD Security Advisory information. type DistroAdvisory struct { diff --git a/report/report.go b/report/report.go index 7cdefdf9..c63397a1 100644 --- a/report/report.go +++ b/report/report.go @@ -104,6 +104,11 @@ func fillCveInfo(r *models.ScanResult) error { if err := fillCveInfoFromCveDB(r); err != nil { return fmt.Errorf("Failed to fill CVE information: %s", err) } + + for cveID := range r.ScannedCves { + vinfo := r.ScannedCves[cveID] + r.ScannedCves[cveID] = *vinfo.NilToEmpty() + } return nil } @@ -192,8 +197,6 @@ func fillVulnByCpeNames(cpeNames []string, scannedVulns models.VulnInfos) error CpeNames: []string{name}, Confidence: models.CpeNameMatch, } - //TODO - // v.NilToEmpty() scannedVulns[detail.CveID] = v } } From a662b038dc4fd32474f687b5c0c2532ea33216c9 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Sat, 3 Jun 2017 15:06:16 +0900 Subject: [PATCH 049/113] Fix CVSS2 in TUI --- report/tui.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/report/tui.go b/report/tui.go index afe638ca..fbabf36e 100644 --- a/report/tui.go +++ b/report/tui.go @@ -775,7 +775,6 @@ func detailLines() (string, error) { for _, url := range vinfo.VendorLinks(r.Family) { links = append(links, url) } - // links = util.Distinct(links) refs := []models.Reference{} for _, rr := range vinfo.CveContents.References(r.Family) { @@ -786,7 +785,7 @@ func detailLines() (string, error) { data := dataForTmpl{ CveID: vinfo.CveID, - Cvsses: append(vinfo.CveContents.Cvss3Scores(), vinfo.CveContents.MaxCvss2Score()), + Cvsses: append(vinfo.CveContents.Cvss3Scores(), vinfo.CveContents.Cvss2Scores()...), Summary: vinfo.CveContents.Summaries(r.Lang, r.Family)[0].Value, Confidence: vinfo.Confidence, Cwes: vinfo.CveContents.CweIDs(r.Family), From 63394a240043d559c6973f17c47ebe8ac6f50ccf Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Sun, 4 Jun 2017 10:48:37 +0900 Subject: [PATCH 050/113] Fix error handling while loading JSON in reporting --- report/util.go | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/report/util.go b/report/util.go index e0bf2399..52a34597 100644 --- a/report/util.go +++ b/report/util.go @@ -293,13 +293,13 @@ func loadPrevious(current models.ScanResults) (previous models.ScanResults, err for _, result := range current { for _, dir := range dirs[1:] { - var r models.ScanResult + var r *models.ScanResult path := filepath.Join(dir, result.ServerName+".json") if r, err = loadOneServerScanResult(path); err != nil { continue } if r.Family == result.Family && r.Release == result.Release { - previous = append(previous, r) + previous = append(previous, *r) util.Log.Infof("Privious json found: %s", path) break } @@ -484,13 +484,12 @@ func LoadScanResults(jsonDir string) (results models.ScanResults, err error) { continue } - var r models.ScanResult + var r *models.ScanResult path := filepath.Join(jsonDir, f.Name()) if r, err = loadOneServerScanResult(path); err != nil { return nil, err } - - results = append(results, r) + results = append(results, *r) } if len(results) == 0 { return nil, fmt.Errorf("There is no json file under %s", jsonDir) @@ -499,14 +498,17 @@ func LoadScanResults(jsonDir string) (results models.ScanResults, err error) { } // loadOneServerScanResult read JSON data of one server -func loadOneServerScanResult(jsonFile string) (result models.ScanResult, err error) { - var data []byte +func loadOneServerScanResult(jsonFile string) (*models.ScanResult, error) { + var ( + data []byte + err error + ) if data, err = ioutil.ReadFile(jsonFile); err != nil { - err = fmt.Errorf("Failed to read %s: %s", jsonFile, err) - return + return nil, fmt.Errorf("Failed to read %s: %s", jsonFile, err) } - if json.Unmarshal(data, &result) != nil { - err = fmt.Errorf("Failed to parse %s: %s", jsonFile, err) + result := &models.ScanResult{} + if err := json.Unmarshal(data, result); err != nil { + return nil, fmt.Errorf("Failed to parse %s: %s", jsonFile, err) } - return + return result, nil } From 997dd6022f80ac50ef3cfbc3c883798d15fb1bc7 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Sun, 4 Jun 2017 11:18:52 +0900 Subject: [PATCH 051/113] Kind error message when SSH connection fails --- report/tui.go | 7 ------- scan/debian.go | 3 +-- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/report/tui.go b/report/tui.go index fbabf36e..ef3a32fb 100644 --- a/report/tui.go +++ b/report/tui.go @@ -808,33 +808,28 @@ const mdTemplate = ` CVSS Scores -------------- - {{range .Cvsses -}} * {{.Value.Format}} ({{.Type}}) {{end}} Summary -------------- - {{.Summary }} Links -------------- - {{range $link := .Links -}} * {{$link}} {{end}} CWE -------------- - {{range .Cwes -}} * {{.Value}} ({{.Type}}) {{end}} Package/CPE -------------- - {{range $pack := .Packages -}} * {{$pack}} {{end -}} @@ -844,13 +839,11 @@ Package/CPE Confidence -------------- - {{.Confidence }} References -------------- - {{range .References -}} * [{{.Source}}]( {{.Link}} ) {{end}} diff --git a/scan/debian.go b/scan/debian.go index 4ce0b356..19c8804c 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -60,8 +60,7 @@ func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface, err err return false, deb, nil } if r.ExitStatus == 255 { - return false, deb, fmt.Errorf( - "Unable to connect via SSH. Check SSH settings. %s", r) + return false, deb, fmt.Errorf("Unable to connect via SSH. Check SSH settings. If you have never SSH to the host to be scanned, SSH to the host before scanning in order to add a HostKey. %s", r) } util.Log.Debugf("Not Debian like Linux. %s", r) return false, deb, nil From 1883da3b2a7cfbfda9619c78c25fd6bc45c8c01e Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Mon, 5 Jun 2017 17:37:02 +0900 Subject: [PATCH 052/113] Implement HTTP access to oval-dictionary --- commands/report.go | 45 ++++++++++------- commands/tui.go | 36 ++++++++++++-- config/config.go | 10 ++-- oval/debian.go | 41 +++++++++------ oval/oval.go | 121 ++++++++++++++++++++++++++++++++++++++++++++- oval/redhat.go | 60 +++++++++++++++------- report/report.go | 29 +++++++---- report/tui.go | 4 +- 8 files changed, 277 insertions(+), 69 deletions(-) diff --git a/commands/report.go b/commands/report.go index ee2ea724..d19ab81a 100644 --- a/commands/report.go +++ b/commands/report.go @@ -45,12 +45,13 @@ type ReportCmd struct { ignoreUnscoredCves bool httpProxy string - cvedbtype string - cvedbpath string - cvedbURL string + cveDBType string + cveDBPath string + cveDBURL string - ovaldbtype string - ovaldbpath string + ovalDBType string + ovalDBPath string + ovalDBURL string toSlack bool toEMail bool @@ -98,6 +99,9 @@ func (*ReportCmd) Usage() string { [-cvedb-type=sqlite3|mysql|postgres] [-cvedb-path=/path/to/cve.sqlite3] [-cvedb-url=http://127.0.0.1:1323 or DB connection string] + [-ovaldb-type=sqlite3|mysql] + [-ovaldb-path=/path/to/oval.sqlite3] + [-ovaldb-url=http://127.0.0.1:1324 or DB connection string] [-cvss-over=7] [-diff] [-ignore-unscored-cves] @@ -152,36 +156,42 @@ func (p *ReportCmd) SetFlags(f *flag.FlagSet) { "Refresh CVE information in JSON file under results dir") f.StringVar( - &p.cvedbtype, + &p.cveDBType, "cvedb-type", "sqlite3", "DB type for fetching CVE dictionary (sqlite3, mysql or postgres)") defaultCveDBPath := filepath.Join(wd, "cve.sqlite3") f.StringVar( - &p.cvedbpath, + &p.cveDBPath, "cvedb-path", defaultCveDBPath, "/path/to/sqlite3 (For get cve detail from cve.sqlite3)") f.StringVar( - &p.ovaldbtype, + &p.cveDBURL, + "cvedb-url", + "", + "http://cve-dictionary.com:1323 or mysql connection string") + + f.StringVar( + &p.ovalDBType, "ovaldb-type", "sqlite3", "DB type for fetching OVAL dictionary (sqlite3 or mysql)") defaultOvalDBPath := filepath.Join(wd, "oval.sqlite3") f.StringVar( - &p.ovaldbpath, + &p.ovalDBPath, "ovaldb-path", defaultOvalDBPath, "/path/to/sqlite3 (For get oval detail from oval.sqlite3)") f.StringVar( - &p.cvedbURL, - "cvedb-url", + &p.ovalDBURL, + "ovaldb-url", "", - "http://cve-dictionary.com:8080 or DB connection string") + "http://goval-dictionary.com:1324 or mysql connection string") f.Float64Var( &p.cvssScoreOver, @@ -290,11 +300,12 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} c.Conf.ResultsDir = p.resultsDir c.Conf.RefreshCve = p.refreshCve c.Conf.Diff = p.diff - c.Conf.CveDBType = p.cvedbtype - c.Conf.CveDBPath = p.cvedbpath - c.Conf.CveDBURL = p.cvedbURL - c.Conf.OvalDBType = p.ovaldbtype - c.Conf.OvalDBPath = p.ovaldbpath + c.Conf.CveDBType = p.cveDBType + c.Conf.CveDBPath = p.cveDBPath + c.Conf.CveDBURL = p.cveDBURL + c.Conf.OvalDBType = p.ovalDBType + c.Conf.OvalDBPath = p.ovalDBPath + c.Conf.OvalDBURL = p.ovalDBURL c.Conf.CvssScoreOver = p.cvssScoreOver c.Conf.IgnoreUnscoredCves = p.ignoreUnscoredCves c.Conf.HTTPProxy = p.httpProxy diff --git a/commands/tui.go b/commands/tui.go index ca2f002a..f40d03f7 100644 --- a/commands/tui.go +++ b/commands/tui.go @@ -38,12 +38,17 @@ type TuiCmd struct { configPath string logDir string - resultsDir string - refreshCve bool + resultsDir string + refreshCve bool + cvedbtype string cvedbpath string cveDictionaryURL string + ovalDBType string + ovalDBPath string + ovalDBURL string + pipe bool } @@ -61,6 +66,9 @@ func (*TuiCmd) Usage() string { [-cvedb-type=sqlite3|mysql|postgres] [-cvedb-path=/path/to/cve.sqlite3] [-cvedb-url=http://127.0.0.1:1323 or DB connection string] + [-ovaldb-type=sqlite3|mysql] + [-ovaldb-path=/path/to/oval.sqlite3] + [-ovaldb-url=http://127.0.0.1:1324 or DB connection string] [-refresh-cve] [-results-dir=/path/to/results] [-log-dir=/path/to/log] @@ -110,7 +118,26 @@ func (p *TuiCmd) SetFlags(f *flag.FlagSet) { &p.cveDictionaryURL, "cvedb-url", "", - "http://cve-dictionary.com:8080 or DB connection string") + "http://cve-dictionary.com:1323 or mysql connection string") + + f.StringVar( + &p.ovalDBType, + "ovaldb-type", + "sqlite3", + "DB type for fetching OVAL dictionary (sqlite3 or mysql)") + + defaultOvalDBPath := filepath.Join(wd, "oval.sqlite3") + f.StringVar( + &p.ovalDBPath, + "ovaldb-path", + defaultOvalDBPath, + "/path/to/sqlite3 (For get oval detail from oval.sqlite3)") + + f.StringVar( + &p.ovalDBURL, + "ovaldb-url", + "", + "http://goval-dictionary.com:1324 or mysql connection string") f.BoolVar( &p.pipe, @@ -139,6 +166,9 @@ func (p *TuiCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s c.Conf.CveDBType = p.cvedbtype c.Conf.CveDBPath = p.cvedbpath c.Conf.CveDBURL = p.cveDictionaryURL + c.Conf.OvalDBType = p.ovalDBType + c.Conf.OvalDBPath = p.ovalDBPath + c.Conf.OvalDBURL = p.ovalDBURL log.Info("Validating config...") if !c.Conf.ValidateOnTui() { diff --git a/config/config.go b/config/config.go index 243efbe9..0ea115ad 100644 --- a/config/config.go +++ b/config/config.go @@ -53,13 +53,15 @@ type Config struct { LogDir string ResultsDir string - CveDBType string - CveDBPath string - CveDBURL string - CacheDBPath string + CveDBType string + CveDBPath string + CveDBURL string OvalDBType string OvalDBPath string + OvalDBURL string + + CacheDBPath string RefreshCve bool diff --git a/oval/debian.go b/oval/debian.go index 15d48ead..80f87f87 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -15,8 +15,8 @@ import ( // DebianBase is the base struct of Debian and Ubuntu type DebianBase struct{} -// fillCveInfoFromOvalDB returns scan result after updating CVE info by OVAL -func (o DebianBase) fillCveInfoFromOvalDB(r *models.ScanResult) error { +// fillFromOvalDB returns scan result after updating CVE info by OVAL +func (o DebianBase) fillFromOvalDB(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", @@ -40,7 +40,7 @@ func (o DebianBase) fillCveInfoFromOvalDB(r *models.ScanResult) error { } affected, _ := ver.NewVersion(p.Version) if current.LessThan(affected) { - o.fillOvalInfo(r, &def) + o.update(r, &def) } } } @@ -48,7 +48,7 @@ func (o DebianBase) fillCveInfoFromOvalDB(r *models.ScanResult) error { return nil } -func (o DebianBase) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Definition) { +func (o DebianBase) update(r *models.ScanResult, definition *ovalmodels.Definition) { ovalContent := *o.convertToModel(definition) ovalContent.Type = models.NewCveContentType(r.Family) vinfo, ok := r.ScannedCves[definition.Debian.CveID] @@ -103,15 +103,26 @@ type Debian struct { } // NewDebian creates OVAL client for Debian -func NewDebian() *Debian { - return &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 +// FillWithOval returns scan result after updating CVE info by OVAL +func (o Debian) FillWithOval(r *models.ScanResult) error { + if config.Conf.OvalDBURL != "" { + defs, err := getDefsByPackNameViaHTTP(r) + if err != nil { + return err + } + for _, def := range defs { + o.update(r, &def) + } + } else { + if err := o.fillFromOvalDB(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 @@ -127,13 +138,13 @@ type Ubuntu struct { } // NewUbuntu creates OVAL client for Debian -func NewUbuntu() *Ubuntu { - return &Ubuntu{} +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 { +// FillWithOval returns scan result after updating CVE info by OVAL +func (o Ubuntu) FillWithOval(r *models.ScanResult) error { + if err := o.fillFromOvalDB(r); err != nil { return err } for _, vuln := range r.ScannedCves { diff --git a/oval/oval.go b/oval/oval.go index e222e4b5..947a57f3 100644 --- a/oval/oval.go +++ b/oval/oval.go @@ -1,13 +1,132 @@ package oval import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/cenkalti/backoff" + "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" + "github.com/future-architect/vuls/util" + ver "github.com/knqyf263/go-deb-version" ovalmodels "github.com/kotakanbe/goval-dictionary/models" + "github.com/parnurzeal/gorequest" ) // Client is the interface of OVAL client. type Client interface { - FillCveInfoFromOvalDB(r *models.ScanResult) error + FillWithOval(r *models.ScanResult) error +} + +type request struct { + pack models.Package +} + +type response struct { + pack *models.Package + defs []ovalmodels.Definition +} + +// getDefsByPackNameViaHTTP fetches OVAL information via HTTP +func getDefsByPackNameViaHTTP(r *models.ScanResult) ( + relatedDefs []ovalmodels.Definition, err error) { + + //TODO Health Check + reqChan := make(chan request, len(r.Packages)) + resChan := make(chan response, len(r.Packages)) + errChan := make(chan error, len(r.Packages)) + defer close(reqChan) + defer close(resChan) + defer close(errChan) + + go func() { + for _, pack := range r.Packages { + reqChan <- request{ + pack: pack, + } + } + }() + + concurrency := 10 + tasks := util.GenWorkers(concurrency) + for range r.Packages { + tasks <- func() { + select { + case req := <-reqChan: + url, err := util.URLPathJoin( + config.Conf.OvalDBURL, + "packs", + r.Family, + r.Release, + req.pack.Name, + ) + if err != nil { + errChan <- err + } else { + util.Log.Debugf("HTTP Request to %s", url) + httpGet(url, &req.pack, resChan, errChan) + } + } + } + } + + timeout := time.After(2 * 60 * time.Second) + var errs []error + for range r.Packages { + select { + case res := <-resChan: + current, _ := ver.NewVersion(fmt.Sprintf("%s-%s", + res.pack.Version, res.pack.Release)) + for _, def := range res.defs { + for _, p := range def.AffectedPacks { + affected, _ := ver.NewVersion(p.Version) + if res.pack.Name != p.Name || !current.LessThan(affected) { + continue + } + relatedDefs = append(relatedDefs, def) + } + } + case err := <-errChan: + errs = append(errs, err) + case <-timeout: + return nil, fmt.Errorf("Timeout Fetching OVAL") + } + } + if len(errs) != 0 { + return nil, fmt.Errorf("Failed to fetch OVAL. err: %v", errs) + } + return +} + +func httpGet(url string, pack *models.Package, resChan chan<- response, errChan chan<- error) { + var body string + var errs []error + var resp *http.Response + f := func() (err error) { + // resp, body, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End() + resp, body, errs = gorequest.New().Get(url).End() + if 0 < len(errs) || resp == nil || resp.StatusCode != 200 { + return fmt.Errorf("HTTP GET error: %v, url: %s, resp: %v", errs, url, resp) + } + return nil + } + notify := func(err error, t time.Duration) { + util.Log.Warnf("Failed to HTTP GET. retrying in %s seconds. err: %s", t, err) + } + err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify) + if err != nil { + errChan <- fmt.Errorf("HTTP Error %s", err) + } + defs := []ovalmodels.Definition{} + if err := json.Unmarshal([]byte(body), &defs); err != nil { + errChan <- fmt.Errorf("Failed to Unmarshall. body: %s, err: %s", body, err) + } + resChan <- response{ + pack: pack, + defs: defs, + } } func getPackages(r *models.ScanResult, d *ovalmodels.Definition) (names []string) { diff --git a/oval/redhat.go b/oval/redhat.go index edc4b3ee..e11d392d 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -17,11 +17,22 @@ import ( // RedHatBase is the base struct for RedHat and CentOS type RedHatBase struct{} -// 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 +// FillWithOval returns scan result after updating CVE info by OVAL +func (o RedHatBase) FillWithOval(r *models.ScanResult) error { + if config.Conf.OvalDBURL != "" { + defs, err := getDefsByPackNameViaHTTP(r) + if err != nil { + return err + } + for _, def := range defs { + o.update(r, &def) + } + } else { + if err := o.fillFromOvalDB(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 @@ -30,8 +41,21 @@ func (o RedHatBase) FillCveInfoFromOvalDB(r *models.ScanResult) error { return nil } -// FillCveInfoFromOvalDB returns scan result after updating CVE info by OVAL -func (o RedHatBase) fillCveInfoFromOvalDB(r *models.ScanResult) error { +// fillFromOvalDB returns scan result after updating CVE info by OVAL +func (o RedHatBase) fillFromOvalDB(r *models.ScanResult) error { + defs, err := o.getDefsByPackNameFromOvalDB(r.Release, r.Packages) + if err != nil { + return err + } + for _, def := range defs { + o.update(r, &def) + } + return nil +} + +func (o RedHatBase) getDefsByPackNameFromOvalDB(osRelease string, + packs models.Packages) (relatedDefs []ovalmodels.Definition, err error) { + ovalconf.Conf.DBType = config.Conf.OvalDBType ovalconf.Conf.DBPath = config.Conf.OvalDBPath util.Log.Infof("open oval-dictionary db (%s): %s", @@ -39,28 +63,26 @@ func (o RedHatBase) fillCveInfoFromOvalDB(r *models.ScanResult) error { d := db.NewRedHat() defer d.Close() - for _, pack := range r.Packages { - definitions, err := d.GetByPackName(r.Release, pack.Name) + for _, pack := range packs { + definitions, err := d.GetByPackName(osRelease, pack.Name) if err != nil { - return fmt.Errorf("Failed to get RedHat OVAL info by package name: %v", err) + return nil, fmt.Errorf("Failed to get RedHat OVAL info by package name: %v", err) } - for _, definition := range definitions { + for _, def := range definitions { current, _ := ver.NewVersion(fmt.Sprintf("%s-%s", pack.Version, pack.Release)) - for _, p := range definition.AffectedPacks { - if pack.Name != p.Name { + for _, p := range def.AffectedPacks { + affected, _ := ver.NewVersion(p.Version) + if pack.Name != p.Name || !current.LessThan(affected) { continue } - affected, _ := ver.NewVersion(p.Version) - if current.LessThan(affected) { - o.fillOvalInfo(r, &definition) - } + relatedDefs = append(relatedDefs, def) } } } - return nil + return } -func (o RedHatBase) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Definition) { +func (o RedHatBase) update(r *models.ScanResult, definition *ovalmodels.Definition) { for _, cve := range definition.Advisory.Cves { ovalContent := *o.convertToModel(cve.CveID, definition) vinfo, ok := r.ScannedCves[cve.CveID] @@ -77,7 +99,7 @@ func (o RedHatBase) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.De if _, ok := vinfo.CveContents[models.RedHat]; ok { util.Log.Infof("%s will be updated by OVAL", cve.CveID) } else { - util.Log.Infof("%s is also detected by OVAL", cve.CveID) + util.Log.Infof("%s also detected by OVAL", cve.CveID) cveContents = models.CveContents{} } diff --git a/report/report.go b/report/report.go index c63397a1..5926b779 100644 --- a/report/report.go +++ b/report/report.go @@ -90,18 +90,29 @@ func FillCveInfos(rs []models.ScanResult, dir string) ([]models.ScanResult, erro func fillCveInfo(r *models.ScanResult) error { util.Log.Debugf("need to refresh") - if c.Conf.CveDBType == "sqlite3" && c.Conf.CveDBURL == "" { - if _, err := os.Stat(c.Conf.CveDBPath); os.IsNotExist(err) { - return fmt.Errorf("SQLite3 DB(CVE-Dictionary) is not exist: %s", - c.Conf.CveDBPath) + if c.Conf.CveDBType == "sqlite3" { + if c.Conf.CveDBURL == "" { + if _, err := os.Stat(c.Conf.CveDBPath); os.IsNotExist(err) { + return fmt.Errorf("SQLite3 DB(CVE-Dictionary) is not exist: %s", + c.Conf.CveDBPath) + } + } + if c.Conf.OvalDBURL == "" { + if _, err := os.Stat(c.Conf.OvalDBPath); os.IsNotExist(err) { + //TODO Warning + return fmt.Errorf("SQLite3 DB(OVAL-Dictionary) is not exist: %s", + c.Conf.OvalDBPath) + } } } - if err := fillCveInfoFromOvalDB(r); err != nil { + util.Log.Debugf("Fill CVE detailed information with OVAL") + if err := fillWithOvalDB(r); err != nil { return fmt.Errorf("Failed to fill OVAL information: %s", err) } - if err := fillCveInfoFromCveDB(r); err != nil { + util.Log.Debugf("Fill CVE detailed information with CVE-DB") + if err := fillWithCveDB(r); err != nil { return fmt.Errorf("Failed to fill CVE information: %s", err) } @@ -144,7 +155,7 @@ func fillCveDetail(r *models.ScanResult) error { return nil } -func fillCveInfoFromCveDB(r *models.ScanResult) error { +func fillWithCveDB(r *models.ScanResult) error { sInfo := c.Conf.Servers[r.ServerName] if err := fillVulnByCpeNames(sInfo.CpeNames, r.ScannedCves); err != nil { return err @@ -155,7 +166,7 @@ func fillCveInfoFromCveDB(r *models.ScanResult) error { return nil } -func fillCveInfoFromOvalDB(r *models.ScanResult) error { +func fillWithOvalDB(r *models.ScanResult) error { var ovalClient oval.Client switch r.Family { case "debian": @@ -172,7 +183,7 @@ func fillCveInfoFromOvalDB(r *models.ScanResult) error { default: return fmt.Errorf("Oval %s is not implemented yet", r.Family) } - if err := ovalClient.FillCveInfoFromOvalDB(r); err != nil { + if err := ovalClient.FillWithOval(r); err != nil { return err } return nil diff --git a/report/tui.go b/report/tui.go index ef3a32fb..e2951a76 100644 --- a/report/tui.go +++ b/report/tui.go @@ -783,10 +783,12 @@ func detailLines() (string, error) { } } + summary := vinfo.CveContents.Summaries(r.Lang, r.Family)[0] + data := dataForTmpl{ CveID: vinfo.CveID, Cvsses: append(vinfo.CveContents.Cvss3Scores(), vinfo.CveContents.Cvss2Scores()...), - Summary: vinfo.CveContents.Summaries(r.Lang, r.Family)[0].Value, + Summary: fmt.Sprintf("%s (%s)", summary.Value, summary.Type), Confidence: vinfo.Confidence, Cwes: vinfo.CveContents.CweIDs(r.Family), Links: util.Distinct(links), From f7aa85746d962852476e74dc571bf316f4848b7f Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Fri, 16 Jun 2017 00:09:44 +0900 Subject: [PATCH 053/113] Add retry-max to HTTP access --- oval/oval.go | 18 ++++++++++++++++-- report/cve_client.go | 11 ++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/oval/oval.go b/oval/oval.go index 947a57f3..891580ce 100644 --- a/oval/oval.go +++ b/oval/oval.go @@ -104,11 +104,17 @@ func httpGet(url string, pack *models.Package, resChan chan<- response, errChan var body string var errs []error var resp *http.Response + count, retryMax := 0, 3 f := func() (err error) { // resp, body, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End() resp, body, errs = gorequest.New().Get(url).End() if 0 < len(errs) || resp == nil || resp.StatusCode != 200 { - return fmt.Errorf("HTTP GET error: %v, url: %s, resp: %v", errs, url, resp) + count++ + if count == retryMax { + return nil + } + return fmt.Errorf("HTTP GET error: %v, url: %s, resp: %v", + errs, url, resp) } return nil } @@ -118,10 +124,18 @@ func httpGet(url string, pack *models.Package, resChan chan<- response, errChan err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify) if err != nil { errChan <- fmt.Errorf("HTTP Error %s", err) + return } + if count == retryMax { + errChan <- fmt.Errorf("HRetry count exceeded") + return + } + defs := []ovalmodels.Definition{} if err := json.Unmarshal([]byte(body), &defs); err != nil { - errChan <- fmt.Errorf("Failed to Unmarshall. body: %s, err: %s", body, err) + errChan <- fmt.Errorf("Failed to Unmarshall. body: %s, err: %s", + body, err) + return } resChan <- response{ pack: pack, diff --git a/report/cve_client.go b/report/cve_client.go index 3178636e..cde90e19 100644 --- a/report/cve_client.go +++ b/report/cve_client.go @@ -164,20 +164,25 @@ func (api cvedictClient) httpGet(key, url string, resChan chan<- response, errCh // resp, body, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End() resp, body, errs = gorequest.New().Get(url).End() if 0 < len(errs) || resp == nil || resp.StatusCode != 200 { - return fmt.Errorf("HTTP GET error: %v, url: %s, resp: %v", errs, url, resp) + return fmt.Errorf("HTTP GET error: %v, url: %s, resp: %v", + errs, url, resp) } return nil } notify := func(err error, t time.Duration) { - util.Log.Warnf("Failed to HTTP GET. retrying in %s seconds. err: %s", t, err) + util.Log.Warnf("Failed to HTTP GET. retrying in %s seconds. err: %s", + t, err) } err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify) if err != nil { errChan <- fmt.Errorf("HTTP Error %s", err) + return } cveDetail := cve.CveDetail{} if err := json.Unmarshal([]byte(body), &cveDetail); err != nil { - errChan <- fmt.Errorf("Failed to Unmarshall. body: %s, err: %s", body, err) + errChan <- fmt.Errorf("Failed to Unmarshall. body: %s, err: %s", + body, err) + return } resChan <- response{ key, From c442a433b0a8ce5873cc6314f690386e30c98d1f Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Fri, 16 Jun 2017 16:40:33 +0900 Subject: [PATCH 054/113] Add OVAL HTTP health check --- commands/report.go | 12 ++++++++++- oval/debian.go | 19 ++++++++++++++---- oval/oval.go | 32 ++++++++++++++++++++++++++++- oval/redhat.go | 4 ++-- report/cve_client.go | 48 ++++++++++++++++++++++++++------------------ report/report.go | 37 +++++++++++----------------------- scan/debian.go | 2 +- 7 files changed, 100 insertions(+), 54 deletions(-) diff --git a/commands/report.go b/commands/report.go index d19ab81a..8fcf43db 100644 --- a/commands/report.go +++ b/commands/report.go @@ -26,6 +26,7 @@ import ( c "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" + "github.com/future-architect/vuls/oval" "github.com/future-architect/vuls/report" "github.com/future-architect/vuls/util" "github.com/google/subcommands" @@ -395,7 +396,7 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} if !c.Conf.ValidateOnReport() { return subcommands.ExitUsageError } - if ok, err := report.CveClient.CheckHealth(); !ok { + if err := report.CveClient.CheckHealth(); err != nil { util.Log.Errorf("CVE HTTP server is not running. err: %s", err) util.Log.Errorf("Run go-cve-dictionary as server mode before reporting or run with --cvedb-path option") return subcommands.ExitFailure @@ -408,6 +409,15 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} } } + if c.Conf.OvalDBURL != "" { + err := oval.Base{}.CheckHealth() + if err != nil { + util.Log.Errorf("OVAL HTTP server is not running. err: %s", err) + util.Log.Errorf("Run goval-dictionary as server mode before reporting or run with --ovaldb-path option") + return subcommands.ExitFailure + } + } + var res models.ScanResults if res, err = report.LoadScanResults(dir); err != nil { util.Log.Error(err) diff --git a/oval/debian.go b/oval/debian.go index 80f87f87..a28784a0 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -13,7 +13,7 @@ import ( ) // DebianBase is the base struct of Debian and Ubuntu -type DebianBase struct{} +type DebianBase struct{ Base } // fillFromOvalDB returns scan result after updating CVE info by OVAL func (o DebianBase) fillFromOvalDB(r *models.ScanResult) error { @@ -109,7 +109,7 @@ func NewDebian() Debian { // FillWithOval returns scan result after updating CVE info by OVAL func (o Debian) FillWithOval(r *models.ScanResult) error { - if config.Conf.OvalDBURL != "" { + if o.isFetchViaHTTP() { defs, err := getDefsByPackNameViaHTTP(r) if err != nil { return err @@ -144,9 +144,20 @@ func NewUbuntu() Ubuntu { // FillWithOval returns scan result after updating CVE info by OVAL func (o Ubuntu) FillWithOval(r *models.ScanResult) error { - if err := o.fillFromOvalDB(r); err != nil { - return err + if o.isFetchViaHTTP() { + defs, err := getDefsByPackNameViaHTTP(r) + if err != nil { + return err + } + for _, def := range defs { + o.update(r, &def) + } + } else { + if err := o.fillFromOvalDB(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 diff --git a/oval/oval.go b/oval/oval.go index 891580ce..174ba371 100644 --- a/oval/oval.go +++ b/oval/oval.go @@ -17,9 +17,40 @@ import ( // Client is the interface of OVAL client. type Client interface { + CheckHealth() error FillWithOval(r *models.ScanResult) error } +// Base is a base struct +type Base struct{} + +// CheckHealth do health check +func (b Base) CheckHealth() error { + if !b.isFetchViaHTTP() { + return nil + } + + url := fmt.Sprintf("%s/health", config.Conf.OvalDBURL) + var errs []error + var resp *http.Response + resp, _, errs = gorequest.New().Get(url).End() + // resp, _, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End() + // resp, _, errs = gorequest.New().Proxy(api.httpProxy).Get(url).End() + if 0 < len(errs) || resp == nil || resp.StatusCode != 200 { + return fmt.Errorf("Failed to request to OVAL server. url: %s, errs: %v", + url, errs) + } + return nil +} + +func (b Base) isFetchViaHTTP() bool { + // Default value of OvalDBType is sqlite3 + if config.Conf.OvalDBURL != "" && config.Conf.OvalDBType == "sqlite3" { + return true + } + return false +} + type request struct { pack models.Package } @@ -33,7 +64,6 @@ type response struct { func getDefsByPackNameViaHTTP(r *models.ScanResult) ( relatedDefs []ovalmodels.Definition, err error) { - //TODO Health Check reqChan := make(chan request, len(r.Packages)) resChan := make(chan response, len(r.Packages)) errChan := make(chan error, len(r.Packages)) diff --git a/oval/redhat.go b/oval/redhat.go index e11d392d..4492b533 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -15,11 +15,11 @@ import ( ) // RedHatBase is the base struct for RedHat and CentOS -type RedHatBase struct{} +type RedHatBase struct{ Base } // FillWithOval returns scan result after updating CVE info by OVAL func (o RedHatBase) FillWithOval(r *models.ScanResult) error { - if config.Conf.OvalDBURL != "" { + if o.isFetchViaHTTP() { defs, err := getDefsByPackNameViaHTTP(r) if err != nil { return err diff --git a/report/cve_client.go b/report/cve_client.go index cde90e19..5f8808ca 100644 --- a/report/cve_client.go +++ b/report/cve_client.go @@ -45,10 +45,10 @@ func (api *cvedictClient) initialize() { api.baseURL = config.Conf.CveDBURL } -func (api cvedictClient) CheckHealth() (ok bool, err error) { - if config.Conf.CveDBURL == "" || config.Conf.CveDBType == "mysql" || config.Conf.CveDBType == "postgres" { +func (api cvedictClient) CheckHealth() error { + if !api.isFetchViaHTTP() { util.Log.Debugf("get cve-dictionary from %s", config.Conf.CveDBType) - return true, nil + return nil } api.initialize() @@ -58,9 +58,10 @@ func (api cvedictClient) CheckHealth() (ok bool, err error) { resp, _, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End() // resp, _, errs = gorequest.New().Proxy(api.httpProxy).Get(url).End() if 0 < len(errs) || resp == nil || resp.StatusCode != 200 { - return false, fmt.Errorf("Failed to request to CVE server. url: %s, errs: %v", url, errs) + return fmt.Errorf("Failed to request to CVE server. url: %s, errs: %v", + url, errs) } - return true, nil + return nil } type response struct { @@ -69,8 +70,7 @@ type response struct { } func (api cvedictClient) FetchCveDetails(cveIDs []string) (cveDetails cve.CveDetails, err error) { - switch config.Conf.CveDBType { - case "sqlite3", "mysql", "postgres": + if !api.isFetchViaHTTP() { return api.FetchCveDetailsFromCveDB(cveIDs) } @@ -195,21 +195,28 @@ type responseGetCveDetailByCpeName struct { CveDetails []cve.CveDetail } +func (api cvedictClient) isFetchViaHTTP() bool { + // Default value of CveDBType is sqlite3 + if config.Conf.CveDBURL != "" && config.Conf.CveDBType == "sqlite3" { + return true + } + return false +} + func (api cvedictClient) FetchCveDetailsByCpeName(cpeName string) ([]cve.CveDetail, error) { - switch config.Conf.CveDBType { - case "sqlite3", "mysql", "postgres": - return api.FetchCveDetailsByCpeNameFromDB(cpeName) + if api.isFetchViaHTTP() { + api.baseURL = config.Conf.CveDBURL + url, err := util.URLPathJoin(api.baseURL, "cpes") + if err != nil { + return []cve.CveDetail{}, err + } + + query := map[string]string{"name": cpeName} + util.Log.Debugf("HTTP Request to %s, query: %#v", url, query) + return api.httpPost(cpeName, url, query) } - api.baseURL = config.Conf.CveDBURL - url, err := util.URLPathJoin(api.baseURL, "cpes") - if err != nil { - return []cve.CveDetail{}, err - } - - query := map[string]string{"name": cpeName} - util.Log.Debugf("HTTP Request to %s, query: %#v", url, query) - return api.httpPost(cpeName, url, query) + return api.FetchCveDetailsByCpeNameFromDB(cpeName) } func (api cvedictClient) httpPost(key, url string, query map[string]string) ([]cve.CveDetail, error) { @@ -217,7 +224,8 @@ func (api cvedictClient) httpPost(key, url string, query map[string]string) ([]c var errs []error var resp *http.Response f := func() (err error) { - req := gorequest.New().SetDebug(config.Conf.Debug).Post(url) + // req := gorequest.New().SetDebug(config.Conf.Debug).Post(url) + req := gorequest.New().Post(url) for key := range query { req = req.Send(fmt.Sprintf("%s=%s", key, query[key])).Type("json") } diff --git a/report/report.go b/report/report.go index 5926b779..bbcb725f 100644 --- a/report/report.go +++ b/report/report.go @@ -70,44 +70,31 @@ func FillCveInfos(rs []models.ScanResult, dir string) ([]models.ScanResult, erro } } - //TODO remove debug code - // for _, r := range filled { - // pp.Printf("filled: %d\n", len(r.ScannedCves)) - // } - filtered := []models.ScanResult{} for _, r := range filled { filtered = append(filtered, r.FilterByCvssOver(c.Conf.CvssScoreOver)) } - - //TODO remove debug code - // for _, r := range filtered { - // pp.Printf("filtered: %d\n", len(r.ScannedCves)) - // } - return filtered, nil } func fillCveInfo(r *models.ScanResult) error { util.Log.Debugf("need to refresh") - if c.Conf.CveDBType == "sqlite3" { - if c.Conf.CveDBURL == "" { - if _, err := os.Stat(c.Conf.CveDBPath); os.IsNotExist(err) { - return fmt.Errorf("SQLite3 DB(CVE-Dictionary) is not exist: %s", - c.Conf.CveDBPath) - } + if c.Conf.CveDBType == "sqlite3" && c.Conf.CveDBURL == "" { + if _, err := os.Stat(c.Conf.CveDBPath); os.IsNotExist(err) { + return fmt.Errorf("SQLite3 DB(CVE-Dictionary) is not exist: %s", + c.Conf.CveDBPath) } - if c.Conf.OvalDBURL == "" { - if _, err := os.Stat(c.Conf.OvalDBPath); os.IsNotExist(err) { - //TODO Warning - return fmt.Errorf("SQLite3 DB(OVAL-Dictionary) is not exist: %s", - c.Conf.OvalDBPath) - } + } + if c.Conf.OvalDBType == "sqlite3" && c.Conf.OvalDBURL == "" { + if _, err := os.Stat(c.Conf.OvalDBPath); os.IsNotExist(err) { + // TODO Warning?? + return fmt.Errorf("SQLite3 DB(OVAL-Dictionary) is not exist: %s", + c.Conf.OvalDBPath) } } util.Log.Debugf("Fill CVE detailed information with OVAL") - if err := fillWithOvalDB(r); err != nil { + if err := fillWithOval(r); err != nil { return fmt.Errorf("Failed to fill OVAL information: %s", err) } @@ -166,7 +153,7 @@ func fillWithCveDB(r *models.ScanResult) error { return nil } -func fillWithOvalDB(r *models.ScanResult) error { +func fillWithOval(r *models.ScanResult) error { var ovalClient oval.Client switch r.Family { case "debian": diff --git a/scan/debian.go b/scan/debian.go index 19c8804c..42cd3687 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -60,7 +60,7 @@ func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface, err err return false, deb, nil } if r.ExitStatus == 255 { - return false, deb, fmt.Errorf("Unable to connect via SSH. Check SSH settings. If you have never SSH to the host to be scanned, SSH to the host before scanning in order to add a HostKey. %s", r) + return false, deb, fmt.Errorf("Unable to connect via SSH. Check SSH settings. If you have never SSH to the host to be scanned, SSH to the host before scanning in order to add the HostKey. %s@%s port: %s\n%s", c.User, c.Host, c.Port, r) } util.Log.Debugf("Not Debian like Linux. %s", r) return false, deb, nil From 7778783dd8dbcd1abc97f3ba92fdb5beb4a28d87 Mon Sep 17 00:00:00 2001 From: sadayuki-matsuno Date: Tue, 27 Jun 2017 18:10:09 +0900 Subject: [PATCH 055/113] add db backend redis (#445) --- Gopkg.lock | 46 ++++++++++----- Gopkg.toml | 22 ++++---- README.ja.md | 21 +++++-- README.md | 15 ++++- config/config.go | 113 +++++++++++++++++++++++++++---------- models/cvecontents_test.go | 2 +- models/vulninfos.go | 12 ++-- oval/debian.go | 21 ++++++- oval/redhat.go | 25 ++++++-- report/cve_client.go | 65 ++++++++++++++------- report/report.go | 24 ++------ scan/debian.go | 20 +++---- scan/executil.go | 2 +- scan/freebsd.go | 6 +- scan/redhat.go | 38 ++++++------- scan/serverapi.go | 2 +- 16 files changed, 282 insertions(+), 152 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 72d4d193..7dff1ef5 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,4 +1,5 @@ -memo = "e59ec63c1c329674a0e5e4236131c787e5b81bab37529104fdc02ed8fdf29283" +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + [[projects]] branch = "master" @@ -13,10 +14,10 @@ memo = "e59ec63c1c329674a0e5e4236131c787e5b81bab37529104fdc02ed8fdf29283" version = "v8.0.0" [[projects]] + branch = "master" name = "github.com/BurntSushi/toml" packages = ["."] - revision = "b26d9c308763d68093482582cea63d69be07a0f0" - version = "v0.3.0" + revision = "8b58b6030fce084b58a61e2bc3fdf183d5881ab4" [[projects]] branch = "master" @@ -65,6 +66,12 @@ memo = "e59ec63c1c329674a0e5e4236131c787e5b81bab37529104fdc02ed8fdf29283" revision = "e7fea39b01aea8d5671f6858f0532f56e8bff3a5" version = "v1.27.0" +[[projects]] + name = "github.com/go-redis/redis" + packages = [".","internal","internal/consistenthash","internal/hashtag","internal/pool","internal/proto"] + revision = "e14976b254c5bc5f399dd0ae9314b1d02a176897" + version = "v6.5.0" + [[projects]] name = "github.com/go-sql-driver/mysql" packages = ["."] @@ -126,10 +133,9 @@ memo = "e59ec63c1c329674a0e5e4236131c787e5b81bab37529104fdc02ed8fdf29283" revision = "9865fe14d09b1c729188ac810466dde90f897ee3" [[projects]] - branch = "master" name = "github.com/kotakanbe/go-cve-dictionary" packages = ["config","db","jvn","log","models","nvd","util"] - revision = "d47709be4cc24d2c77a7be9096dcfcf211ba1d57" + revision = "c57d73c89e4d1a71f417ffcef6e13978a5add7ac" [[projects]] name = "github.com/kotakanbe/go-pingscanner" @@ -138,10 +144,9 @@ memo = "e59ec63c1c329674a0e5e4236131c787e5b81bab37529104fdc02ed8fdf29283" version = "v0.1.0" [[projects]] - branch = "improve-db" name = "github.com/kotakanbe/goval-dictionary" - packages = ["config","db","log","models"] - revision = "5f7aa97d45d565eaccc70c0c365e21624a9c6e3f" + packages = ["config","db","db/rdb","log","models"] + revision = "233459d2cc9ae85d8fcfb6a3d1412fdba6b0ea65" [[projects]] branch = "master" @@ -149,18 +154,18 @@ memo = "e59ec63c1c329674a0e5e4236131c787e5b81bab37529104fdc02ed8fdf29283" packages = ["."] revision = "e7519b8c80ba008a3bfc57ffa31232bf2a77f455" -[[projects]] - branch = "master" - name = "github.com/lib/pq" - packages = [".","hstore","oid"] - revision = "2704adc878c21e1329f46f6e56a1c387d788ff94" - [[projects]] name = "github.com/labstack/gommon" packages = ["color","log"] revision = "1121fd3e243c202482226a7afe4dcd07ffc4139a" version = "v0.2.1" +[[projects]] + branch = "master" + name = "github.com/lib/pq" + packages = [".","hstore","oid"] + revision = "8837942c3e09574accbc5f150e2c5e057189cace" + [[projects]] name = "github.com/mattn/go-colorable" packages = ["."] @@ -245,6 +250,12 @@ memo = "e59ec63c1c329674a0e5e4236131c787e5b81bab37529104fdc02ed8fdf29283" packages = ["oval"] revision = "003ac9af5fffac6c97ab1def025d2cb73e88469a" +[[projects]] + branch = "master" + name = "go4.org" + packages = ["syncutil"] + revision = "034d17a462f7b2dcd1a4a73553ec5357ff6e6c6e" + [[projects]] branch = "master" name = "golang.org/x/crypto" @@ -268,3 +279,10 @@ memo = "e59ec63c1c329674a0e5e4236131c787e5b81bab37529104fdc02ed8fdf29283" name = "golang.org/x/text" packages = ["internal/gen","internal/triegen","internal/ucd","secure/bidirule","transform","unicode/bidi","unicode/cldr","unicode/norm","unicode/rangetable"] revision = "19e51611da83d6be54ddafce4a4af510cb3e9ea4" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "2f4b3e869b7567e51d9bff86e52b3960d8c8304164b40d69f03a9e690032c9f5" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 1a77d819..5e7a3863 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -1,36 +1,36 @@ -[[dependencies]] +[[constraint]] branch = "master" name = "github.com/Azure/azure-storage-go" -[[dependencies]] +[[constraint]] branch = "master" name = "github.com/BurntSushi/toml" -[[dependencies]] +[[constraint]] branch = "master" name = "github.com/Sirupsen/logrus" -[[dependencies]] +[[constraint]] name = "github.com/aws/aws-sdk-go" revision = "5b341290c488aa6bd76b335d819b4a68516ec3ab" -[[dependencies]] +[[constraint]] branch = "master" name = "github.com/jroimartin/gocui" -[[dependencies]] +[[constraint]] branch = "master" name = "github.com/k0kubun/pp" -[[dependencies]] - branch = "master" +[[constraint]] name = "github.com/kotakanbe/go-cve-dictionary" + revision = "c57d73c89e4d1a71f417ffcef6e13978a5add7ac" -[[dependencies]] - branch = "improve-db" +[[constraint]] + revision = "233459d2cc9ae85d8fcfb6a3d1412fdba6b0ea65" name = "github.com/kotakanbe/goval-dictionary" -[[dependencies]] +[[constraint]] branch = "master" name = "github.com/kotakanbe/logrus-prefixed-formatter" diff --git a/README.ja.md b/README.ja.md index 0e9c3473..a66ca4e4 100644 --- a/README.ja.md +++ b/README.ja.md @@ -82,6 +82,7 @@ Slackチームは[こちらから](http://goo.gl/forms/xm5KFo35tu)参加でき * [Example: Add optional key-value pairs to JSON](#example-add-optional-key-value-pairs-to-json) * [Example: Use MySQL as a DB storage back-end](#example-use-mysql-as-a-db-storage-back-end) * [Example: Use PostgreSQL as a DB storage back-end](#example-use-postgresql-as-a-db-storage-back-end) + * [Example: Use Redis as a DB storage back-end](#example-use-redis-as-a-db-storage-back-end) - [Usage: Scan vulnerability of non-OS package](#usage-scan-vulnerability-of-non-os-package) - [Usage: Integrate with OWASP Dependency Check to Automatic update when the libraries are updated (Experimental)](#usage-integrate-with-owasp-dependency-check-to-automatic-update-when-the-libraries-are-updated-experimental) - [Usage: TUI](#usage-tui) @@ -194,7 +195,7 @@ Hello Vulsチュートリアルでは手動でのセットアップ方法で説 Vulsセットアップに必要な以下のソフトウェアをインストールする。 -- SQLite3 or MySQL +- SQLite3, MySQL, PostgreSQL or Redis - git - gcc - GNU Make @@ -1041,7 +1042,7 @@ report: [-results-dir=/path/to/results] [-log-dir=/path/to/log] [-refresh-cve] - [-cvedb-type=sqlite3|mysql|postgres] + [-cvedb-type=sqlite3|mysql|postgres|redis] [-cvedb-path=/path/to/cve.sqlite3] [-cvedb-url=http://127.0.0.1:1323 or DB connection string] [-cvss-over=7] @@ -1088,7 +1089,7 @@ report: -cvedb-path string /path/to/sqlite3 (For get cve detail from cve.sqlite3) -cvedb-type string - DB type for fetching CVE dictionary (sqlite3, mysql or postgres) (default "sqlite3") + DB type for fetching CVE dictionary (sqlite3, mysql, postgres or redis) (default "sqlite3") -cvedb-url string http://cve-dictionary.com:8080 or DB connection string -cvss-over float @@ -1436,6 +1437,14 @@ $ vuls report \ -cvedb-url=""host=myhost user=user dbname=dbname sslmode=disable password=password"" ``` +## Example: Use Redis as a DB storage back-end + +``` +$ vuls report \ + -cvedb-type=redis -cvedb-url="redis://localhost/0" + -ovaldb-type=redis -ovaldb-url="redis://localhost/1" +``` + ---- # Usage: Scan vulnerability of non-OS package @@ -1496,7 +1505,7 @@ VulsとDependency Checkを連携すると以下の利点がある ``` tui: tui - [-cvedb-type=sqlite3|mysql|postgres] + [-cvedb-type=sqlite3|mysql|postgres|redis] [-cvedb-path=/path/to/cve.sqlite3] [-cvedb-url=http://127.0.0.1:1323 DB connection string] [-refresh-cve] @@ -1509,7 +1518,7 @@ tui: -cvedb-path string /path/to/sqlite3 (For get cve detail from cve.sqlite3) -cvedb-type string - DB type for fetching CVE dictionary (sqlite3, mysql or postgres) (default "sqlite3") + DB type for fetching CVE dictionary (sqlite3, mysql, postgres or redis) (default "sqlite3") -cvedb-url string http://cve-dictionary.com:8080 or DB connection string -debug @@ -1621,7 +1630,7 @@ slack, emailは日本語対応済み TUIは日本語表示未対応 # Update Vuls With Glide - Update go-cve-dictionary -If the DB schema was changed, please specify new SQLite3 or MySQL DB file. +If the DB schema was changed, please specify new SQLite3, MySQL, PostgreSQL or Redis DB file. ``` $ cd $GOPATH/src/github.com/kotakanbe/go-cve-dictionary $ git pull diff --git a/README.md b/README.md index 6912dcb7..e9f66dc6 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ We have a slack team. [Join slack team](http://goo.gl/forms/xm5KFo35tu) * [Example: Add optional key-value pairs to JSON](#example-add-optional-key-value-pairs-to-json) * [Example: Use MySQL as a DB storage back-end](#example-use-mysql-as-a-db-storage-back-end) * [Example: Use PostgreSQL as a DB storage back-end](#example-use-postgresql-as-a-db-storage-back-end) + * [Example: Use Redis as a DB storage back-end](#example-use-redis-as-a-db-storage-back-end) - [Usage: Scan vulnerabilites of non-OS packages](#usage-scan-vulnerabilites-of-non-os-packages) - [Usage: Integrate with OWASP Dependency Check to Automatic update when the libraries are updated (Experimental)](#usage-integrate-with-owasp-dependency-check-to-automatic-update-when-the-libraries-are-updated-experimental) - [Usage: TUI](#usage-tui) @@ -199,7 +200,7 @@ This can be done in the following steps. Vuls requires the following packages. -- SQLite3 or MySQL +- SQLite3, MySQL, PostgreSQL, Redis - git - gcc - GNU Make @@ -504,7 +505,7 @@ On the aggregation server, you can refer to the scanning result of each scan tar [Details](#example-scan-via-shell-instead-of-ssh) ## [go-cve-dictionary](https://github.com/kotakanbe/go-cve-dictionary) -- Fetch vulnerability information from NVD and JVN(Japanese), then insert into SQLite3 or MySQL. +- Fetch vulnerability information from NVD and JVN(Japanese), then insert into SQLite3, MySQL, PostgreSQL or Redis. ## Scanning Flow ![Vuls-Scan-Flow](img/vuls-scan-flow.png) @@ -1438,6 +1439,14 @@ $ vuls report \ -cvedb-url=""host=myhost user=user dbname=dbname sslmode=disable password=password"" ``` +## Example: Use Redis as a DB storage back-end + +``` +$ vuls report \ + -cvedb-type=redis -cvedb-url="redis://localhost/0" + -ovaldb-type=redis -ovaldb-url="redis://localhost/1" +``` + ---- # Usage: Scan vulnerabilites of non-OS packages @@ -1583,7 +1592,7 @@ see [go-cve-dictionary#usage-fetch-nvd-data](https://github.com/kotakanbe/go-cve # How to Update - Update go-cve-dictionary -If the DB schema was changed, please specify new SQLite3 or MySQL DB file. +If the DB schema was changed, please specify new SQLite3, MySQL, PostgreSQL or Redis DB file. ``` $ cd $GOPATH/src/github.com/kotakanbe/go-cve-dictionary $ git pull diff --git a/config/config.go b/config/config.go index 0ea115ad..d97a389d 100644 --- a/config/config.go +++ b/config/config.go @@ -19,6 +19,7 @@ package config import ( "fmt" + "os" "runtime" "strconv" "strings" @@ -30,6 +31,35 @@ import ( // Conf has Configuration var Conf Config +const ( + // RedHat is + RedHat = "redhat" + + // Debian is + Debian = "debian" + + // Ubuntu is + Ubuntu = "ubuntu" + + // CentOS is + CentOS = "centos" + + // Fedora is + Fedora = "fedora" + + // Amazon is + Amazon = "amazon" + + // Oracle is + Oracle = "oracle" + + // FreeBSD is + FreeBSD = "freebsd" + + // Raspbian is + Raspbian = "raspbian" +) + //Config is struct of Configuration type Config struct { Debug bool @@ -163,26 +193,12 @@ func (c Config) ValidateOnReport() bool { } } - switch c.CveDBType { - case "sqlite3": - if ok, _ := valid.IsFilePath(c.CveDBPath); !ok { - errs = append(errs, fmt.Errorf( - "SQLite3 DB(CVE-Dictionary) path must be a *Absolute* file path. -cvedb-path: %s", - c.CveDBPath)) - } - case "mysql": - if c.CveDBURL == "" { - errs = append(errs, fmt.Errorf( - `MySQL connection string is needed. -cvedb-url="user:pass@tcp(localhost:3306)/dbname"`)) - } - case "postgres": - if c.CveDBURL == "" { - errs = append(errs, fmt.Errorf( - `PostgreSQL connection string is needed. -cvedb-url=""host=myhost user=user dbname=dbname sslmode=disable password=password""`)) - } - default: - errs = append(errs, fmt.Errorf( - "CVE DB type must be either 'sqlite3', 'mysql' or 'postgres'. -cvedb-type: %s", c.CveDBType)) + if err := validateDB("cvedb", c.CveDBType, c.CveDBPath, c.CveDBURL); err != nil { + errs = append(errs, err) + } + + if err := validateDB("ovaldb", c.OvalDBType, c.OvalDBPath, c.OvalDBURL); err != nil { + errs = append(errs, err) } _, err := valid.ValidateStruct(c) @@ -216,16 +232,8 @@ func (c Config) ValidateOnTui() bool { } } - if c.CveDBType != "sqlite3" && c.CveDBType != "mysql" && c.CveDBType != "postgres" { - errs = append(errs, fmt.Errorf( - "CVE DB type must be either 'sqlite3', 'mysql' or 'postgres'. -cve-dictionary-dbtype: %s", c.CveDBType)) - } - - if c.CveDBType == "sqlite3" { - if ok, _ := valid.IsFilePath(c.CveDBPath); !ok { - errs = append(errs, fmt.Errorf( - "SQLite3 DB(CVE-Dictionary) path must be a *Absolute* file path. -cve-dictionary-dbpath: %s", c.CveDBPath)) - } + if err := validateDB("cvedb", c.CveDBType, c.CveDBPath, c.CveDBURL); err != nil { + errs = append(errs, err) } for _, err := range errs { @@ -235,6 +243,51 @@ func (c Config) ValidateOnTui() bool { return len(errs) == 0 } +// validateDB validates configuration +// dictionaryDB name is 'cvedb' or 'ovaldb' +func validateDB(dictionaryDBName, dbType, dbPath, dbURL string) error { + switch dbType { + case "sqlite3": + if ok, _ := valid.IsFilePath(dbPath); !ok { + return fmt.Errorf( + "SQLite3 DB path (%s) must be a *Absolute* file path. -%s-path: %s", + dictionaryDBName, + dictionaryDBName, + dbPath) + } + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + return fmt.Errorf("SQLite3 DB path (%s) is not exist: %s", + dictionaryDBName, + dbPath) + } + case "mysql": + if dbURL == "" { + return fmt.Errorf( + `MySQL connection string is needed. -%s-url="user:pass@tcp(localhost:3306)/dbname"`, + dictionaryDBName) + } + case "postgres": + if dbURL == "" { + return fmt.Errorf( + `PostgreSQL connection string is needed. -%s-url="host=myhost user=user dbname=dbname sslmode=disable password=password"`, + dictionaryDBName) + } + case "redis": + if dbURL == "" { + return fmt.Errorf( + `Redis connection string is needed. -%s-url="redis://localhost/0"`, + dictionaryDBName) + } + default: + return fmt.Errorf( + "%s type must be either 'sqlite3', 'mysql', 'postgres' or 'redis'. -%s-type: %s", + dictionaryDBName, + dictionaryDBName, + dbType) + } + return nil +} + // SMTPConf is smtp config type SMTPConf struct { SMTPAddr string diff --git a/models/cvecontents_test.go b/models/cvecontents_test.go index eca860c2..f7e942ac 100644 --- a/models/cvecontents_test.go +++ b/models/cvecontents_test.go @@ -736,7 +736,7 @@ func TestVendorLink(t *testing.T) { }{ { in: in{ - family: "rhel", + family: "redhat", vinfo: VulnInfo{ CveID: "CVE-2017-6074", CveContents: CveContents{ diff --git a/models/vulninfos.go b/models/vulninfos.go index 195a5f05..2c31e8b7 100644 --- a/models/vulninfos.go +++ b/models/vulninfos.go @@ -126,33 +126,33 @@ func (v VulnInfo) Cvss3CalcURL() string { func (v VulnInfo) VendorLinks(family string) map[string]string { links := map[string]string{} switch family { - case "rhel", "centos": + case config.RedHat, config.CentOS: links["RHEL-CVE"] = "https://access.redhat.com/security/cve/" + v.CveID for _, advisory := range v.DistroAdvisories { aidURL := strings.Replace(advisory.AdvisoryID, ":", "-", -1) links[advisory.AdvisoryID] = fmt.Sprintf("https://rhn.redhat.com/errata/%s.html", aidURL) } return links - case "oraclelinux": + case config.Oracle: links["Oracle-CVE"] = fmt.Sprintf("https://linux.oracle.com/cve/%s.html", v.CveID) for _, advisory := range v.DistroAdvisories { links[advisory.AdvisoryID] = fmt.Sprintf("https://linux.oracle.com/errata/%s.html", advisory.AdvisoryID) } return links - case "amazon": + case config.Amazon: links["RHEL-CVE"] = "https://access.redhat.com/security/cve/" + v.CveID for _, advisory := range v.DistroAdvisories { links[advisory.AdvisoryID] = fmt.Sprintf("https://alas.aws.amazon.com/%s.html", advisory.AdvisoryID) } return links - case "ubuntu": + case config.Ubuntu: links["Ubuntu-CVE"] = "http://people.ubuntu.com/~ubuntu-security/cve/" + v.CveID return links - case "debian": + case config.Debian: links["Debian-CVE"] = "https://security-tracker.debian.org/tracker/" + v.CveID - case "FreeBSD": + case config.FreeBSD: for _, advisory := range v.DistroAdvisories { links["FreeBSD-VuXML"] = fmt.Sprintf("https://vuxml.freebsd.org/freebsd/%s.html", advisory.AdvisoryID) diff --git a/oval/debian.go b/oval/debian.go index a28784a0..ab5e4d78 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -9,6 +9,7 @@ import ( ver "github.com/knqyf263/go-deb-version" ovalconf "github.com/kotakanbe/goval-dictionary/config" db "github.com/kotakanbe/goval-dictionary/db" + ovallog "github.com/kotakanbe/goval-dictionary/log" ovalmodels "github.com/kotakanbe/goval-dictionary/models" ) @@ -19,13 +20,27 @@ type DebianBase struct{ Base } func (o DebianBase) fillFromOvalDB(r *models.ScanResult) error { ovalconf.Conf.DBType = config.Conf.OvalDBType ovalconf.Conf.DBPath = config.Conf.OvalDBPath + if ovalconf.Conf.DBType == "sqlite3" { + ovalconf.Conf.DBPath = config.Conf.OvalDBPath + } else { + ovalconf.Conf.DBPath = config.Conf.OvalDBURL + } util.Log.Infof("open oval-dictionary db (%s): %s", - config.Conf.OvalDBType, config.Conf.OvalDBPath) + ovalconf.Conf.DBType, ovalconf.Conf.DBPath) - ovaldb, err := db.NewDB(r.Family) - if err != nil { + ovallog.Initialize(config.Conf.LogDir) + + var err error + var ovaldb db.DB + if ovaldb, err = db.NewDB( + ovalconf.Debian, + ovalconf.Conf.DBType, + ovalconf.Conf.DBPath, + ovalconf.Conf.DebugSQL, + ); err != nil { return err } + defer ovaldb.CloseDB() for _, pack := range r.Packages { definitions, err := ovaldb.GetByPackName(r.Release, pack.Name) diff --git a/oval/redhat.go b/oval/redhat.go index 4492b533..d52c9c9b 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -11,6 +11,7 @@ import ( ver "github.com/knqyf263/go-deb-version" ovalconf "github.com/kotakanbe/goval-dictionary/config" db "github.com/kotakanbe/goval-dictionary/db" + ovallog "github.com/kotakanbe/goval-dictionary/log" ovalmodels "github.com/kotakanbe/goval-dictionary/models" ) @@ -57,14 +58,28 @@ func (o RedHatBase) getDefsByPackNameFromOvalDB(osRelease string, packs models.Packages) (relatedDefs []ovalmodels.Definition, err error) { ovalconf.Conf.DBType = config.Conf.OvalDBType - ovalconf.Conf.DBPath = config.Conf.OvalDBPath + if ovalconf.Conf.DBType == "sqlite3" { + ovalconf.Conf.DBPath = config.Conf.OvalDBPath + } else { + ovalconf.Conf.DBPath = config.Conf.OvalDBURL + } util.Log.Infof("open oval-dictionary db (%s): %s", - config.Conf.OvalDBType, config.Conf.OvalDBPath) + ovalconf.Conf.DBType, ovalconf.Conf.DBPath) - d := db.NewRedHat() - defer d.Close() + ovallog.Initialize(config.Conf.LogDir) + + var ovaldb db.DB + if ovaldb, err = db.NewDB( + ovalconf.RedHat, + ovalconf.Conf.DBType, + ovalconf.Conf.DBPath, + ovalconf.Conf.DebugSQL, + ); err != nil { + return + } + defer ovaldb.CloseDB() for _, pack := range packs { - definitions, err := d.GetByPackName(osRelease, pack.Name) + definitions, err := ovaldb.GetByPackName(osRelease, pack.Name) if err != nil { return nil, fmt.Errorf("Failed to get RedHat OVAL info by package name: %v", err) } diff --git a/report/cve_client.go b/report/cve_client.go index 5f8808ca..e57cfd4f 100644 --- a/report/cve_client.go +++ b/report/cve_client.go @@ -26,6 +26,7 @@ import ( "github.com/cenkalti/backoff" "github.com/parnurzeal/gorequest" + log "github.com/Sirupsen/logrus" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/util" cveconfig "github.com/kotakanbe/go-cve-dictionary/config" @@ -69,7 +70,7 @@ type response struct { CveDetail cve.CveDetail } -func (api cvedictClient) FetchCveDetails(cveIDs []string) (cveDetails cve.CveDetails, err error) { +func (api cvedictClient) FetchCveDetails(cveIDs []string) (cveDetails []*cve.CveDetail, err error) { if !api.isFetchViaHTTP() { return api.FetchCveDetailsFromCveDB(cveIDs) } @@ -111,26 +112,26 @@ func (api cvedictClient) FetchCveDetails(cveIDs []string) (cveDetails cve.CveDet select { case res := <-resChan: if len(res.CveDetail.CveID) == 0 { - cveDetails = append(cveDetails, cve.CveDetail{ + cveDetails = append(cveDetails, &cve.CveDetail{ CveID: res.Key, }) } else { - cveDetails = append(cveDetails, res.CveDetail) + cveDetails = append(cveDetails, &res.CveDetail) } case err := <-errChan: errs = append(errs, err) case <-timeout: - return []cve.CveDetail{}, fmt.Errorf("Timeout Fetching CVE") + return []*cve.CveDetail{}, fmt.Errorf("Timeout Fetching CVE") } } if len(errs) != 0 { - return []cve.CveDetail{}, + return []*cve.CveDetail{}, fmt.Errorf("Failed to fetch CVE. err: %v", errs) } return } -func (api cvedictClient) FetchCveDetailsFromCveDB(cveIDs []string) (cveDetails cve.CveDetails, err error) { +func (api cvedictClient) FetchCveDetailsFromCveDB(cveIDs []string) (cveDetails []*cve.CveDetail, err error) { util.Log.Debugf("open cve-dictionary db (%s)", config.Conf.CveDBType) cveconfig.Conf.DBType = config.Conf.CveDBType if config.Conf.CveDBType == "sqlite3" { @@ -139,14 +140,27 @@ func (api cvedictClient) FetchCveDetailsFromCveDB(cveIDs []string) (cveDetails c cveconfig.Conf.DBPath = config.Conf.CveDBURL } cveconfig.Conf.DebugSQL = config.Conf.DebugSQL - if err := cvedb.OpenDB(); err != nil { - return []cve.CveDetail{}, + + var driver cvedb.DB + if driver, err = cvedb.NewDB(cveconfig.Conf.DBType); err != nil { + log.Error(err) + return []*cve.CveDetail{}, fmt.Errorf("Failed to New DB. err: %s", err) + } + + log.Infof("Opening DB (%s).", driver.Name()) + if err := driver.OpenDB( + cveconfig.Conf.DBType, + cveconfig.Conf.DBPath, + cveconfig.Conf.DebugSQL, + ); err != nil { + return []*cve.CveDetail{}, fmt.Errorf("Failed to open DB. err: %s", err) } + for _, cveID := range cveIDs { - cveDetail := cvedb.Get(cveID) + cveDetail := driver.Get(cveID) if len(cveDetail.CveID) == 0 { - cveDetails = append(cveDetails, cve.CveDetail{ + cveDetails = append(cveDetails, &cve.CveDetail{ CveID: cveID, }) } else { @@ -203,12 +217,12 @@ func (api cvedictClient) isFetchViaHTTP() bool { return false } -func (api cvedictClient) FetchCveDetailsByCpeName(cpeName string) ([]cve.CveDetail, error) { +func (api cvedictClient) FetchCveDetailsByCpeName(cpeName string) ([]*cve.CveDetail, error) { if api.isFetchViaHTTP() { api.baseURL = config.Conf.CveDBURL url, err := util.URLPathJoin(api.baseURL, "cpes") if err != nil { - return []cve.CveDetail{}, err + return []*cve.CveDetail{}, err } query := map[string]string{"name": cpeName} @@ -219,7 +233,7 @@ func (api cvedictClient) FetchCveDetailsByCpeName(cpeName string) ([]cve.CveDeta return api.FetchCveDetailsByCpeNameFromDB(cpeName) } -func (api cvedictClient) httpPost(key, url string, query map[string]string) ([]cve.CveDetail, error) { +func (api cvedictClient) httpPost(key, url string, query map[string]string) ([]*cve.CveDetail, error) { var body string var errs []error var resp *http.Response @@ -240,18 +254,18 @@ func (api cvedictClient) httpPost(key, url string, query map[string]string) ([]c } err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify) if err != nil { - return []cve.CveDetail{}, fmt.Errorf("HTTP Error %s", err) + return []*cve.CveDetail{}, fmt.Errorf("HTTP Error %s", err) } - cveDetails := []cve.CveDetail{} + cveDetails := []*cve.CveDetail{} if err := json.Unmarshal([]byte(body), &cveDetails); err != nil { - return []cve.CveDetail{}, + return []*cve.CveDetail{}, fmt.Errorf("Failed to Unmarshall. body: %s, err: %s", body, err) } return cveDetails, nil } -func (api cvedictClient) FetchCveDetailsByCpeNameFromDB(cpeName string) ([]cve.CveDetail, error) { +func (api cvedictClient) FetchCveDetailsByCpeNameFromDB(cpeName string) (cveDetails []*cve.CveDetail, err error) { util.Log.Debugf("open cve-dictionary db (%s)", config.Conf.CveDBType) cveconfig.Conf.DBType = config.Conf.CveDBType if config.Conf.CveDBType == "sqlite3" { @@ -261,9 +275,20 @@ func (api cvedictClient) FetchCveDetailsByCpeNameFromDB(cpeName string) ([]cve.C } cveconfig.Conf.DebugSQL = config.Conf.DebugSQL - if err := cvedb.OpenDB(); err != nil { - return []cve.CveDetail{}, + var driver cvedb.DB + if driver, err = cvedb.NewDB(cveconfig.Conf.DBType); err != nil { + log.Error(err) + return []*cve.CveDetail{}, fmt.Errorf("Failed to New DB. err: %s", err) + } + + log.Infof("Opening DB (%s).", driver.Name()) + if err = driver.OpenDB( + cveconfig.Conf.DBType, + cveconfig.Conf.DBPath, + cveconfig.Conf.DebugSQL, + ); err != nil { + return []*cve.CveDetail{}, fmt.Errorf("Failed to open DB. err: %s", err) } - return cvedb.GetByCpeName(cpeName), nil + return driver.GetByCpeName(cpeName), nil } diff --git a/report/report.go b/report/report.go index bbcb725f..dbf90310 100644 --- a/report/report.go +++ b/report/report.go @@ -19,7 +19,6 @@ package report import ( "fmt" - "os" c "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" @@ -79,19 +78,6 @@ func FillCveInfos(rs []models.ScanResult, dir string) ([]models.ScanResult, erro func fillCveInfo(r *models.ScanResult) error { util.Log.Debugf("need to refresh") - if c.Conf.CveDBType == "sqlite3" && c.Conf.CveDBURL == "" { - if _, err := os.Stat(c.Conf.CveDBPath); os.IsNotExist(err) { - return fmt.Errorf("SQLite3 DB(CVE-Dictionary) is not exist: %s", - c.Conf.CveDBPath) - } - } - if c.Conf.OvalDBType == "sqlite3" && c.Conf.OvalDBURL == "" { - if _, err := os.Stat(c.Conf.OvalDBPath); os.IsNotExist(err) { - // TODO Warning?? - return fmt.Errorf("SQLite3 DB(OVAL-Dictionary) is not exist: %s", - c.Conf.OvalDBPath) - } - } util.Log.Debugf("Fill CVE detailed information with OVAL") if err := fillWithOval(r); err != nil { @@ -156,15 +142,15 @@ func fillWithCveDB(r *models.ScanResult) error { func fillWithOval(r *models.ScanResult) error { var ovalClient oval.Client switch r.Family { - case "debian": + case c.Debian: ovalClient = oval.NewDebian() - case "ubuntu": + case c.Ubuntu: ovalClient = oval.NewUbuntu() - case "rhel": + case c.RedHat: ovalClient = oval.NewRedhat() - case "centos": + case c.CentOS: ovalClient = oval.NewCentOS() - case "amazon", "oraclelinux", "Raspbian", "FreeBSD": + case c.Amazon, c.Oracle, c.Raspbian, c.FreeBSD: //TODO implement OracleLinux return nil default: diff --git a/scan/debian.go b/scan/debian.go index 42cd3687..17559ded 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -73,7 +73,7 @@ func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface, err err // e.g. // Raspbian GNU/Linux 7 \n \l result := strings.Fields(r.Stdout) - if len(result) > 2 && result[0] == "Raspbian" { + if len(result) > 2 && result[0] == config.Raspbian { distro := strings.ToLower(trim(result[0])) deb.setDistro(distro, trim(result[2])) return true, deb, nil @@ -121,7 +121,7 @@ func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface, err err // Debian cmd := "cat /etc/debian_version" if r := exec(c, cmd, noSudo); r.isSuccess() { - deb.setDistro("debian", trim(r.Stdout)) + deb.setDistro(config.Debian, trim(r.Stdout)) return true, deb, nil } @@ -147,10 +147,10 @@ func (o *debian) checkIfSudoNoPasswd() error { func (o *debian) checkDependencies() error { switch o.Distro.Family { - case "ubuntu", "raspbian": + case config.Ubuntu, config.Raspbian: return nil - case "debian": + case config.Debian: // Debian needs aptitude to get changelogs. // Because unable to get changelogs via apt-get changelog on Debian. if r := o.exec("test -f /usr/bin/aptitude", noSudo); !r.isSuccess() { @@ -539,9 +539,9 @@ func (o *debian) getChangelogCache(meta *cache.Meta, pack models.Package) string func (o *debian) scanPackageCveIDs(pack models.Package) ([]DetectedCveID, *models.Package, error) { cmd := "" switch o.Distro.Family { - case "ubuntu", "raspbian": + case config.Ubuntu, config.Raspbian: cmd = fmt.Sprintf(`PAGER=cat apt-get -q=2 changelog %s`, pack.Name) - case "debian": + case config.Debian: cmd = fmt.Sprintf(`PAGER=cat aptitude -q=2 changelog %s`, pack.Name) } cmd = util.PrependProxyEnv(cmd) @@ -592,10 +592,10 @@ func (o *debian) getCveIDsFromChangelog( delim := []string{"+", "~", "build"} switch o.Distro.Family { - case "ubuntu": - delim = append(delim, "ubuntu") - case "debian": - case "Raspbian": + case config.Ubuntu: + delim = append(delim, config.Ubuntu) + case config.Debian: + case config.Raspbian: } for _, d := range delim { diff --git a/scan/executil.go b/scan/executil.go index e8215272..6cfcfe1c 100644 --- a/scan/executil.go +++ b/scan/executil.go @@ -167,7 +167,7 @@ func exec(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (resul func localExec(c conf.ServerInfo, cmdstr string, sudo bool) (result execResult) { cmdstr = decorateCmd(c, cmdstr, sudo) var cmd *ex.Cmd - if c.Distro.Family == "FreeBSD" { + if c.Distro.Family == conf.FreeBSD { cmd = ex.Command("/bin/sh", "-c", cmdstr) } else { cmd = ex.Command("/bin/bash", "-c", cmdstr) diff --git a/scan/freebsd.go b/scan/freebsd.go index 03fb08d7..2ddec98e 100644 --- a/scan/freebsd.go +++ b/scan/freebsd.go @@ -51,13 +51,13 @@ func detectFreebsd(c config.ServerInfo) (itsMe bool, bsd osTypeInterface) { bsd = newBsd(c) // Prevent from adding `set -o pipefail` option - c.Distro = config.Distro{Family: "FreeBSD"} + c.Distro = config.Distro{Family: config.FreeBSD} if r := exec(c, "uname", noSudo); r.isSuccess() { - if strings.Contains(r.Stdout, "FreeBSD") == true { + if strings.Contains(r.Stdout, config.FreeBSD) == true { if b := exec(c, "freebsd-version", noSudo); b.isSuccess() { rel := strings.TrimSpace(b.Stdout) - bsd.setDistro("FreeBSD", rel) + bsd.setDistro(config.FreeBSD, rel) return true, bsd } } diff --git a/scan/redhat.go b/scan/redhat.go index 5a6ffd4f..2a8e28d6 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -55,7 +55,7 @@ func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) { red = newRedhat(c) if r := exec(c, "ls /etc/fedora-release", noSudo); r.isSuccess() { - red.setDistro("fedora", "unknown") + red.setDistro(config.Fedora, "unknown") util.Log.Warn("Fedora not tested yet: %s", r) return true, red } @@ -72,7 +72,7 @@ func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) { } release := result[2] - red.setDistro("oraclelinux", release) + red.setDistro(config.Oracle, release) return true, red } } @@ -93,9 +93,9 @@ func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) { release := result[2] switch strings.ToLower(result[1]) { case "centos", "centos linux": - red.setDistro("centos", release) + red.setDistro(config.CentOS, release) default: - red.setDistro("rhel", release) + red.setDistro(config.RedHat, release) } return true, red } @@ -103,7 +103,7 @@ func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) { } if r := exec(c, "ls /etc/system-release", noSudo); r.isSuccess() { - family := "amazon" + family := config.Amazon release := "unknown" if r := exec(c, "cat /etc/system-release", noSudo); r.isSuccess() { fields := strings.Fields(r.Stdout) @@ -133,12 +133,12 @@ func (o *redhat) checkIfSudoNoPasswd() error { var zero = []int{0} switch o.Distro.Family { - case "centos": + case config.CentOS: cmds = []cmd{ {"yum --changelog --assumeno update yum", []int{0, 1}}, } - case "rhel", "oraclelinux": + case config.RedHat, config.Oracle: majorVersion, err := o.Distro.MajorVersion() if err != nil { return fmt.Errorf("Not implemented yet: %s, err: %s", o.Distro, err) @@ -180,7 +180,7 @@ func (o *redhat) checkIfSudoNoPasswd() error { // Amazon ... - func (o *redhat) checkDependencies() error { var packName string - if o.Distro.Family == "amazon" { + if o.Distro.Family == config.Amazon { return nil } @@ -191,7 +191,7 @@ func (o *redhat) checkDependencies() error { return fmt.Errorf(msg) } - if o.Distro.Family == "centos" { + if o.Distro.Family == config.CentOS { if majorVersion < 6 { msg := fmt.Sprintf("CentOS %s is not supported", o.Distro.Release) o.log.Errorf(msg) @@ -208,9 +208,9 @@ func (o *redhat) checkDependencies() error { } switch o.Distro.Family { - case "centos": + case config.CentOS: packName = "yum-plugin-changelog" - case "rhel", "oraclelinux": + case config.RedHat, config.Oracle: if majorVersion < 6 { packName = "yum-security" } else { @@ -293,7 +293,7 @@ func (o *redhat) parseScannedPackagesLine(line string) (models.Package, error) { } func (o *redhat) scanVulnInfos() (models.VulnInfos, error) { - if o.Distro.Family != "centos" { + if o.Distro.Family != config.CentOS { // Amazon, RHEL, Oracle Linux has yum updateinfo as default // yum updateinfo can collenct vendor advisory information. return o.scanUnsecurePackagesUsingYumPluginSecurity() @@ -535,7 +535,7 @@ func (o *redhat) getChangelogCVELines(rpm2changelog map[string]*string, pack mod func (o *redhat) divideChangelogByPackage(allChangelog string) (map[string]*string, error) { var majorVersion int var err error - if o.Distro.Family == "centos" { + if o.Distro.Family == config.CentOS { majorVersion, err = o.Distro.MajorVersion() if err != nil { return nil, fmt.Errorf("Not implemented yet: %s, err: %s", o.Distro, err) @@ -659,7 +659,7 @@ type distroAdvisoryCveIDs struct { // Scaning unsecure packages using yum-plugin-security. // Amazon, RHEL, Oracle Linux func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, error) { - if o.Distro.Family == "centos" { + if o.Distro.Family == config.CentOS { // CentOS has no security channel. // So use yum check-update && parse changelog return nil, fmt.Errorf( @@ -678,7 +678,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, return nil, fmt.Errorf("Not implemented yet: %s, err: %s", o.Distro, err) } - if (o.Distro.Family == "rhel" || o.Distro.Family == "oraclelinux") && major == 5 { + if (o.Distro.Family == config.RedHat || o.Distro.Family == config.Oracle) && major == 5 { cmd = "yum --color=never list-security --security" } else { cmd = "yum --color=never --security updateinfo list updates" @@ -721,7 +721,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, } // get advisoryID(RHSA, ALAS, ELSA) - CVE IDs - if (o.Distro.Family == "rhel" || o.Distro.Family == "oraclelinux") && major == 5 { + if (o.Distro.Family == config.RedHat || o.Distro.Family == config.Oracle) && major == 5 { cmd = "yum --color=never info-security" } else { cmd = "yum --color=never --security updateinfo updates" @@ -817,12 +817,12 @@ func (o *redhat) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveID switch sectionState { case Header: switch o.Distro.Family { - case "centos": + case config.CentOS: // CentOS has no security channel. // So use yum check-update && parse changelog return result, fmt.Errorf( "yum updateinfo is not suppported on CentOS") - case "rhel", "amazon", "oraclelinux": + case config.RedHat, config.Amazon, config.Oracle: // nop } @@ -1032,7 +1032,7 @@ func (o *redhat) clone() osTypeInterface { func (o *redhat) sudo() bool { switch o.Distro.Family { - case "amazon": + case config.Amazon: return false default: return true diff --git a/scan/serverapi.go b/scan/serverapi.go index 1ebf37ae..06b10bb3 100644 --- a/scan/serverapi.go +++ b/scan/serverapi.go @@ -421,7 +421,7 @@ func setupChangelogCache() error { needToSetupCache := false for _, s := range servers { switch s.getDistro().Family { - case "ubuntu", "debian", "raspbian": + case config.Ubuntu, config.Debian, config.Raspbian: needToSetupCache = true break } From 738e9fb1197588b1114b4dbe5689fb222605db5a Mon Sep 17 00:00:00 2001 From: sadayuki-matsuno Date: Wed, 28 Jun 2017 16:10:39 +0900 Subject: [PATCH 056/113] change logrus package to lowercase and update other packages (#446) --- Gopkg.lock | 50 +++++++++++++++++++++++--------------------- Gopkg.toml | 6 +++--- cache/bolt.go | 2 +- cache/bolt_test.go | 2 +- commands/discover.go | 2 +- config/config.go | 2 +- config/tomlloader.go | 2 +- report/cve_client.go | 2 +- report/slack.go | 2 +- report/tui.go | 2 +- scan/base.go | 2 +- scan/debian_test.go | 2 +- scan/executil.go | 2 +- util/logutil.go | 2 +- 14 files changed, 41 insertions(+), 39 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 7dff1ef5..0587848c 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -10,20 +10,14 @@ [[projects]] name = "github.com/Azure/go-autorest" packages = ["autorest","autorest/adal","autorest/azure","autorest/date"] - revision = "58f6f26e200fa5dfb40c9cd1c83f3e2c860d779d" - version = "v8.0.0" + revision = "10791a4516e77c53ab10f198f144804cca3d5b43" + version = "v8.1.0" [[projects]] branch = "master" name = "github.com/BurntSushi/toml" packages = ["."] - revision = "8b58b6030fce084b58a61e2bc3fdf183d5881ab4" - -[[projects]] - branch = "master" - name = "github.com/Sirupsen/logrus" - packages = ["."] - revision = "acfabf31db8f45a9174f54a0d48ea4d15627af4d" + revision = "a368813c5e648fee92e5f6c30e3944ff9d5e8895" [[projects]] name = "github.com/asaskevich/govalidator" @@ -63,8 +57,8 @@ [[projects]] name = "github.com/go-ini/ini" packages = ["."] - revision = "e7fea39b01aea8d5671f6858f0532f56e8bff3a5" - version = "v1.27.0" + revision = "d3de07a94d22b4a0972deb4b96d790c2c0ce8333" + version = "v1.28.0" [[projects]] name = "github.com/go-redis/redis" @@ -133,9 +127,10 @@ revision = "9865fe14d09b1c729188ac810466dde90f897ee3" [[projects]] + branch = "master" name = "github.com/kotakanbe/go-cve-dictionary" packages = ["config","db","jvn","log","models","nvd","util"] - revision = "c57d73c89e4d1a71f417ffcef6e13978a5add7ac" + revision = "89e381b4e7e5a31097bbd5779cbb555f5bd3fe87" [[projects]] name = "github.com/kotakanbe/go-pingscanner" @@ -144,15 +139,16 @@ version = "v0.1.0" [[projects]] + branch = "master" name = "github.com/kotakanbe/goval-dictionary" packages = ["config","db","db/rdb","log","models"] - revision = "233459d2cc9ae85d8fcfb6a3d1412fdba6b0ea65" + revision = "adf0b39cd7fea8f4493f7b65e7316179634be95d" [[projects]] branch = "master" name = "github.com/kotakanbe/logrus-prefixed-formatter" packages = ["."] - revision = "e7519b8c80ba008a3bfc57ffa31232bf2a77f455" + revision = "5ea278a9a3f980f7cdf1dc787dc4a85ac72502d9" [[projects]] name = "github.com/labstack/gommon" @@ -169,8 +165,8 @@ [[projects]] name = "github.com/mattn/go-colorable" packages = ["."] - revision = "d228849504861217f796da67fae4f6e347643f15" - version = "v0.0.7" + revision = "941b50ebc6efddf4c41c8e4537a5f68a4e686b24" + version = "v0.0.8" [[projects]] name = "github.com/mattn/go-isatty" @@ -206,7 +202,7 @@ branch = "master" name = "github.com/nsf/termbox-go" packages = ["."] - revision = "7994c181db7761ca3c67a217068cf31826113f5f" + revision = "72800b73ab9a3c78df350738298b0361354772ff" [[projects]] name = "github.com/parnurzeal/gorequest" @@ -223,8 +219,8 @@ [[projects]] name = "github.com/rifflock/lfshook" packages = ["."] - revision = "2adb3e0c4ddd8778c4adde609d2dfd4fbe6096ea" - version = "1.6" + revision = "6844c808343cb8fa357d7f141b1b990e05d24e41" + version = "1.7" [[projects]] name = "github.com/satori/uuid" @@ -232,6 +228,12 @@ revision = "879c5887cd475cd7864858769793b2ceb0d44feb" version = "v1.1.0" +[[projects]] + branch = "master" + name = "github.com/sirupsen/logrus" + packages = ["."] + revision = "3d4380f53a34dcdc95f0c1db702615992b38d9a4" + [[projects]] branch = "master" name = "github.com/valyala/bytebufferpool" @@ -260,29 +262,29 @@ branch = "master" name = "golang.org/x/crypto" packages = ["curve25519","ed25519","ed25519/internal/edwards25519","ssh","ssh/agent","ssh/terminal"] - revision = "ab89591268e0c8b748cbe4047b00197516011af5" + revision = "adbae1b6b6fb4b02448a0fc0dbbc9ba2b95b294d" [[projects]] branch = "master" name = "golang.org/x/net" packages = ["context","idna","publicsuffix"] - revision = "84f0e6f92b10139f986b1756e149a7d9de270cdc" + revision = "455220fa52c866a8aa14ff5e8cc68cde16b8395e" [[projects]] branch = "master" name = "golang.org/x/sys" packages = ["unix"] - revision = "1e99a4f9d247b28c670884b9a8d6801f39a47b77" + revision = "90796e5a05ce440b41c768bd9af257005e470461" [[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 = "19e51611da83d6be54ddafce4a4af510cb3e9ea4" + revision = "6353ef0f924300eea566d3438817aa4d3374817e" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "2f4b3e869b7567e51d9bff86e52b3960d8c8304164b40d69f03a9e690032c9f5" + inputs-digest = "a6b387c74e75e1f971ee643c8904f6fd4e3dfdb7fa36119ab7bc28d9cfd66427" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 5e7a3863..9e0d6fb4 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -9,7 +9,7 @@ [[constraint]] branch = "master" - name = "github.com/Sirupsen/logrus" + name = "github.com/sirupsen/logrus" [[constraint]] name = "github.com/aws/aws-sdk-go" @@ -24,11 +24,11 @@ name = "github.com/k0kubun/pp" [[constraint]] + branch = "master" name = "github.com/kotakanbe/go-cve-dictionary" - revision = "c57d73c89e4d1a71f417ffcef6e13978a5add7ac" [[constraint]] - revision = "233459d2cc9ae85d8fcfb6a3d1412fdba6b0ea65" + branch = "master" name = "github.com/kotakanbe/goval-dictionary" [[constraint]] diff --git a/cache/bolt.go b/cache/bolt.go index 9edaf3b4..b2803c0d 100644 --- a/cache/bolt.go +++ b/cache/bolt.go @@ -22,9 +22,9 @@ import ( "fmt" "time" - "github.com/Sirupsen/logrus" "github.com/boltdb/bolt" "github.com/future-architect/vuls/util" + "github.com/sirupsen/logrus" ) // Bolt holds a pointer of bolt.DB diff --git a/cache/bolt_test.go b/cache/bolt_test.go index 95a9815a..ed2bd38b 100644 --- a/cache/bolt_test.go +++ b/cache/bolt_test.go @@ -22,10 +22,10 @@ import ( "reflect" "testing" - "github.com/Sirupsen/logrus" "github.com/boltdb/bolt" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" + "github.com/sirupsen/logrus" ) const path = "/tmp/vuls-test-cache-11111111.db" diff --git a/commands/discover.go b/commands/discover.go index 36aeb77e..12a516db 100644 --- a/commands/discover.go +++ b/commands/discover.go @@ -27,8 +27,8 @@ import ( "github.com/google/subcommands" - "github.com/Sirupsen/logrus" ps "github.com/kotakanbe/go-pingscanner" + "github.com/sirupsen/logrus" ) // DiscoverCmd is Subcommand of host discovery mode diff --git a/config/config.go b/config/config.go index d97a389d..2ad9c073 100644 --- a/config/config.go +++ b/config/config.go @@ -24,8 +24,8 @@ import ( "strconv" "strings" - log "github.com/Sirupsen/logrus" valid "github.com/asaskevich/govalidator" + log "github.com/sirupsen/logrus" ) // Conf has Configuration diff --git a/config/tomlloader.go b/config/tomlloader.go index 6ed77023..7030dd0b 100644 --- a/config/tomlloader.go +++ b/config/tomlloader.go @@ -23,8 +23,8 @@ import ( "strings" "github.com/BurntSushi/toml" - log "github.com/Sirupsen/logrus" "github.com/future-architect/vuls/contrib/owasp-dependency-check/parser" + log "github.com/sirupsen/logrus" ) // TOMLLoader loads config diff --git a/report/cve_client.go b/report/cve_client.go index e57cfd4f..52a2cfd2 100644 --- a/report/cve_client.go +++ b/report/cve_client.go @@ -26,12 +26,12 @@ import ( "github.com/cenkalti/backoff" "github.com/parnurzeal/gorequest" - log "github.com/Sirupsen/logrus" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/util" cveconfig "github.com/kotakanbe/go-cve-dictionary/config" cvedb "github.com/kotakanbe/go-cve-dictionary/db" cve "github.com/kotakanbe/go-cve-dictionary/models" + log "github.com/sirupsen/logrus" ) // CveClient is api client of CVE disctionary service. diff --git a/report/slack.go b/report/slack.go index 3ffd6f83..38ddf1b3 100644 --- a/report/slack.go +++ b/report/slack.go @@ -24,11 +24,11 @@ import ( "strings" "time" - log "github.com/Sirupsen/logrus" "github.com/cenkalti/backoff" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" "github.com/parnurzeal/gorequest" + log "github.com/sirupsen/logrus" ) type field struct { diff --git a/report/tui.go b/report/tui.go index e2951a76..83b786f2 100644 --- a/report/tui.go +++ b/report/tui.go @@ -26,13 +26,13 @@ import ( "strings" "time" - log "github.com/Sirupsen/logrus" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/util" "github.com/google/subcommands" "github.com/gosuri/uitable" "github.com/jroimartin/gocui" + log "github.com/sirupsen/logrus" ) var scanResults models.ScanResults diff --git a/scan/base.go b/scan/base.go index 0ddac031..0f13b63e 100644 --- a/scan/base.go +++ b/scan/base.go @@ -23,9 +23,9 @@ import ( "strings" "time" - "github.com/Sirupsen/logrus" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" + "github.com/sirupsen/logrus" ) type base struct { diff --git a/scan/debian_test.go b/scan/debian_test.go index 0fb48040..11492477 100644 --- a/scan/debian_test.go +++ b/scan/debian_test.go @@ -22,11 +22,11 @@ import ( "reflect" "testing" - "github.com/Sirupsen/logrus" "github.com/future-architect/vuls/cache" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" "github.com/k0kubun/pp" + "github.com/sirupsen/logrus" ) func TestParseScannedPackagesLineDebian(t *testing.T) { diff --git a/scan/executil.go b/scan/executil.go index 6cfcfe1c..d5aead1f 100644 --- a/scan/executil.go +++ b/scan/executil.go @@ -33,10 +33,10 @@ import ( "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" - "github.com/Sirupsen/logrus" "github.com/cenkalti/backoff" conf "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/util" + "github.com/sirupsen/logrus" ) type execResult struct { diff --git a/util/logutil.go b/util/logutil.go index 021462ea..653fdf85 100644 --- a/util/logutil.go +++ b/util/logutil.go @@ -22,8 +22,8 @@ import ( "path/filepath" "runtime" - "github.com/Sirupsen/logrus" "github.com/rifflock/lfshook" + "github.com/sirupsen/logrus" "github.com/future-architect/vuls/config" formatter "github.com/kotakanbe/logrus-prefixed-formatter" From a9ebac3818128c3c806383114b1e7178baad3e3f Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Tue, 18 Jul 2017 15:54:25 +0900 Subject: [PATCH 057/113] nosudo on CentOS and Fetch Changelogs on Amazon, RHEL (#448) * Use repoquery for no sudo and avoid unintended line feed of yum or rpm. #444 * Change data type of enablerepo in config.toml. string to array * Fetch yum changelogs at once then grep CVE-IDs * Fix changelog parse logic and Update Gopkg --- .travis.yml | 1 - Gopkg.lock | 61 +-- Gopkg.toml | 74 +++- README.md | 12 +- commands/scan.go | 1 + config/config.go | 2 +- config/tomlloader.go | 3 +- models/models.go | 2 +- models/packages.go | 16 +- models/scanresults.go | 2 +- models/vulninfos.go | 10 +- report/azureblob.go | 2 +- report/tui.go | 50 ++- scan/debian.go | 56 +-- scan/redhat.go | 713 ++++++++++++++++------------------- scan/redhat_test.go | 855 ++++++++++++++++++++++-------------------- 16 files changed, 944 insertions(+), 916 deletions(-) diff --git a/.travis.yml b/.travis.yml index c4552c6f..2875f639 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: go go: - - 1.7 - 1.8 diff --git a/Gopkg.lock b/Gopkg.lock index 0587848c..bc967282 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,10 +2,10 @@ [[projects]] - branch = "master" - name = "github.com/Azure/azure-storage-go" - packages = ["."] - revision = "32cfbe17a139c17f84be16bdf8f9c45c840a046b" + name = "github.com/Azure/azure-sdk-for-go" + packages = ["storage"] + revision = "59c277f1b488b81b1a5f944212f25b69bea8ece3" + version = "v10.1.0-beta" [[projects]] name = "github.com/Azure/go-autorest" @@ -14,10 +14,10 @@ version = "v8.1.0" [[projects]] - branch = "master" name = "github.com/BurntSushi/toml" packages = ["."] - revision = "a368813c5e648fee92e5f6c30e3944ff9d5e8895" + revision = "b26d9c308763d68093482582cea63d69be07a0f0" + version = "v0.3.0" [[projects]] name = "github.com/asaskevich/govalidator" @@ -27,14 +27,15 @@ [[projects]] name = "github.com/aws/aws-sdk-go" - packages = ["aws","aws/awserr","aws/awsutil","aws/client","aws/client/metadata","aws/corehandlers","aws/credentials","aws/credentials/ec2rolecreds","aws/credentials/endpointcreds","aws/credentials/stscreds","aws/defaults","aws/ec2metadata","aws/request","aws/session","aws/signer/v4","private/endpoints","private/protocol","private/protocol/query","private/protocol/query/queryutil","private/protocol/rest","private/protocol/restxml","private/protocol/xml/xmlutil","private/waiter","service/s3","service/sts"] - revision = "5b341290c488aa6bd76b335d819b4a68516ec3ab" + packages = ["aws","aws/awserr","aws/awsutil","aws/client","aws/client/metadata","aws/corehandlers","aws/credentials","aws/credentials/ec2rolecreds","aws/credentials/endpointcreds","aws/credentials/stscreds","aws/defaults","aws/ec2metadata","aws/endpoints","aws/request","aws/session","aws/signer/v4","internal/shareddefaults","private/protocol","private/protocol/query","private/protocol/query/queryutil","private/protocol/rest","private/protocol/restxml","private/protocol/xml/xmlutil","service/s3","service/sts"] + revision = "6d7fc1a00fcae6bbb53550f4a0b98324fd7aa250" + version = "v1.10.12" [[projects]] name = "github.com/boltdb/bolt" packages = ["."] - revision = "583e8937c61f1af6513608ccc75c97b6abdf4ff9" - version = "v1.3.0" + revision = "2f1ce7a837dcb8da3ec595b1dac9d0632f0f99e8" + version = "v1.3.1" [[projects]] name = "github.com/cenkalti/backoff" @@ -63,8 +64,8 @@ [[projects]] name = "github.com/go-redis/redis" packages = [".","internal","internal/consistenthash","internal/hashtag","internal/pool","internal/proto"] - revision = "e14976b254c5bc5f399dd0ae9314b1d02a176897" - version = "v6.5.0" + revision = "da63fe7def48e378caf9539abf64b9b1e37bc01e" + version = "v6.5.3" [[projects]] name = "github.com/go-sql-driver/mysql" @@ -109,10 +110,10 @@ version = "0.2.2" [[projects]] - branch = "master" name = "github.com/jroimartin/gocui" packages = ["."] - revision = "612b0b2987ec1a6af46d7008cef1efd4b3898346" + revision = "4e9ce9a8e26f2ef33dfe297dbdfca148733b6b9b" + version = "v0.3.0" [[projects]] branch = "master" @@ -128,9 +129,15 @@ [[projects]] branch = "master" + name = "github.com/knqyf263/go-rpm-version" + packages = ["."] + revision = "74609b86c936dff800c69ec89fcf4bc52d5f13a4" + +[[projects]] name = "github.com/kotakanbe/go-cve-dictionary" packages = ["config","db","jvn","log","models","nvd","util"] revision = "89e381b4e7e5a31097bbd5779cbb555f5bd3fe87" + version = "v0.1.1" [[projects]] name = "github.com/kotakanbe/go-pingscanner" @@ -153,14 +160,14 @@ [[projects]] name = "github.com/labstack/gommon" packages = ["color","log"] - revision = "1121fd3e243c202482226a7afe4dcd07ffc4139a" - version = "v0.2.1" + revision = "779b8a8b9850a97acba6a3fe20feb628c39e17c1" + version = "0.2.2" [[projects]] branch = "master" name = "github.com/lib/pq" packages = [".","hstore","oid"] - revision = "8837942c3e09574accbc5f150e2c5e057189cace" + revision = "dd1fe2071026ce53f36a39112e645b4d4f5793a4" [[projects]] name = "github.com/mattn/go-colorable" @@ -202,7 +209,7 @@ branch = "master" name = "github.com/nsf/termbox-go" packages = ["."] - revision = "72800b73ab9a3c78df350738298b0361354772ff" + revision = "4ed959e0540971545eddb8c75514973d670cf739" [[projects]] name = "github.com/parnurzeal/gorequest" @@ -232,7 +239,7 @@ branch = "master" name = "github.com/sirupsen/logrus" packages = ["."] - revision = "3d4380f53a34dcdc95f0c1db702615992b38d9a4" + revision = "5ff5dd844dfeb4e23e27528f79f1f845bc8bb78f" [[projects]] branch = "master" @@ -252,39 +259,33 @@ packages = ["oval"] revision = "003ac9af5fffac6c97ab1def025d2cb73e88469a" -[[projects]] - branch = "master" - name = "go4.org" - packages = ["syncutil"] - revision = "034d17a462f7b2dcd1a4a73553ec5357ff6e6c6e" - [[projects]] branch = "master" name = "golang.org/x/crypto" packages = ["curve25519","ed25519","ed25519/internal/edwards25519","ssh","ssh/agent","ssh/terminal"] - revision = "adbae1b6b6fb4b02448a0fc0dbbc9ba2b95b294d" + revision = "7f7c0c2d75ebb4e32a21396ce36e87b6dadc91c9" [[projects]] branch = "master" name = "golang.org/x/net" packages = ["context","idna","publicsuffix"] - revision = "455220fa52c866a8aa14ff5e8cc68cde16b8395e" + revision = "b3756b4b77d7b13260a0a2ec658753cf48922eac" [[projects]] branch = "master" name = "golang.org/x/sys" packages = ["unix"] - revision = "90796e5a05ce440b41c768bd9af257005e470461" + revision = "4cd6d1a821c7175768725b55ca82f14683a29ea4" [[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 = "6353ef0f924300eea566d3438817aa4d3374817e" + revision = "836efe42bb4aa16aaa17b9c155d8813d336ed720" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "a6b387c74e75e1f971ee643c8904f6fd4e3dfdb7fa36119ab7bc28d9cfd66427" + inputs-digest = "269ff02f8e4540ba049a340068dac0ff4f0495df9f8eeb21d4a545ea5dedf2dd" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 9e0d6fb4..da91d653 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -1,23 +1,57 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" + + [[constraint]] - branch = "master" - name = "github.com/Azure/azure-storage-go" - -[[constraint]] - branch = "master" name = "github.com/BurntSushi/toml" + version = "0.3.0" + +[[constraint]] + name = "github.com/asaskevich/govalidator" + version = "6.0.0" + +[[constraint]] + name = "github.com/boltdb/bolt" + version = "1.3.1" + +[[constraint]] + name = "github.com/cenkalti/backoff" + version = "1.0.0" [[constraint]] branch = "master" - name = "github.com/sirupsen/logrus" - -[[constraint]] - name = "github.com/aws/aws-sdk-go" - revision = "5b341290c488aa6bd76b335d819b4a68516ec3ab" + name = "github.com/google/subcommands" [[constraint]] branch = "master" + name = "github.com/gosuri/uitable" + +[[constraint]] + branch = "master" + name = "github.com/howeyc/gopass" + +[[constraint]] name = "github.com/jroimartin/gocui" + version = "0.3.0" [[constraint]] branch = "master" @@ -25,12 +59,28 @@ [[constraint]] branch = "master" - name = "github.com/kotakanbe/go-cve-dictionary" + name = "github.com/knqyf263/go-deb-version" [[constraint]] branch = "master" - name = "github.com/kotakanbe/goval-dictionary" + name = "github.com/knqyf263/go-rpm-version" + +[[constraint]] + name = "github.com/kotakanbe/go-pingscanner" + version = "0.1.0" [[constraint]] branch = "master" name = "github.com/kotakanbe/logrus-prefixed-formatter" + +[[constraint]] + name = "github.com/parnurzeal/gorequest" + version = "0.2.15" + +[[constraint]] + name = "github.com/rifflock/lfshook" + version = "1.7.0" + +[[constraint]] + branch = "master" + name = "github.com/sirupsen/logrus" diff --git a/README.md b/README.md index e9f66dc6..4dc7cf46 100644 --- a/README.md +++ b/README.md @@ -803,12 +803,12 @@ In order to scan, the following dependencies are required, so you need to instal |:-------------|-------------------:|:-------------| | Ubuntu | 12, 14, 16| - | | Debian | 7, 8| aptitude | -| CentOS | 6, 7| yum-plugin-changelog | -| Amazon | All | - | -| RHEL | 5 | yum-security | -| RHEL | 6, 7 | - | -| Oracle Linux | 5 | yum-security | -| Oracle Linux | 6, 7 | - | +| CentOS | 6, 7| yum-plugin-changelog, yum-utils | +| Amazon | All | - | TODO yum-utils?, yum-plugin-changelog +| RHEL | 5 | yum-security | TODO yum-utils? +| RHEL | 6, 7 | - | TODO yum-utils? +| Oracle Linux | 5 | yum-security | TODO yum-utils? +| Oracle Linux | 6, 7 | - |TODO yum-utils? | FreeBSD | 10 | - | | Raspbian | Wheezy, Jessie | - | diff --git a/commands/scan.go b/commands/scan.go index 5359be60..0ce52481 100644 --- a/commands/scan.go +++ b/commands/scan.go @@ -67,6 +67,7 @@ func (*ScanCmd) Usage() string { [-cachedb-path=/path/to/cache.db] [-ssh-native-insecure] [-containers-only] + [-package-list-only] [-skip-broken] [-http-proxy=http://192.168.0.1:8080] [-ask-key-password] diff --git a/config/config.go b/config/config.go index 2ad9c073..edbaa433 100644 --- a/config/config.go +++ b/config/config.go @@ -418,7 +418,7 @@ type ServerInfo struct { Optional [][]interface{} // For CentOS, RHEL, Amazon - Enablerepo string + Enablerepo []string // used internal LogMsgAnsiColor string // DebugLog Color diff --git a/config/tomlloader.go b/config/tomlloader.go index 7030dd0b..1cadd8cc 100644 --- a/config/tomlloader.go +++ b/config/tomlloader.go @@ -20,7 +20,6 @@ package config import ( "fmt" "os" - "strings" "github.com/BurntSushi/toml" "github.com/future-architect/vuls/contrib/owasp-dependency-check/parser" @@ -164,7 +163,7 @@ func (c TOMLLoader) Load(pathToToml, keyPass string) error { s.Enablerepo = d.Enablerepo } if len(s.Enablerepo) != 0 { - for _, repo := range strings.Split(s.Enablerepo, ",") { + for _, repo := range s.Enablerepo { switch repo { case "base", "updates": // nop diff --git a/models/models.go b/models/models.go index bd7ab57f..96850f24 100644 --- a/models/models.go +++ b/models/models.go @@ -18,4 +18,4 @@ along with this program. If not, see . package models // JSONVersion is JSON Version -const JSONVersion = "0.3.0" +const JSONVersion = 2 diff --git a/models/packages.go b/models/packages.go index 19ed3c5f..827ca103 100644 --- a/models/packages.go +++ b/models/packages.go @@ -42,6 +42,7 @@ func (ps Packages) MergeNewVersion(as Packages) { if pack, ok := ps[a.Name]; ok { pack.NewVersion = a.NewVersion pack.NewRelease = a.NewRelease + pack.Repository = a.Repository ps[a.Name] = pack } } @@ -79,6 +80,16 @@ func (ps Packages) FormatUpdatablePacksSummary() string { return fmt.Sprintf("%d updatable packages", nUpdatable) } +// FindOne search a element by name-newver-newrel-arch +func (ps Packages) FindOne(f func(Package) bool) (string, Package, bool) { + for key, p := range ps { + if f(p) { + return key, p, true + } + } + return "", Package{}, false +} + // Package has installed packages. type Package struct { Name string @@ -86,6 +97,7 @@ type Package struct { Release string NewVersion string NewRelease string + Arch string Repository string Changelog Changelog NotFixedYet bool // Ubuntu OVAL Only @@ -145,8 +157,8 @@ func (p Package) FormatChangelog() string { } // Changelog has contents of changelog and how to get it. -// Method: modesl.detectionMethodStr +// Method: models.detectionMethodStr type Changelog struct { Contents string - Method string + Method DetectionMethod } diff --git a/models/scanresults.go b/models/scanresults.go index aa538249..284724fb 100644 --- a/models/scanresults.go +++ b/models/scanresults.go @@ -32,7 +32,7 @@ type ScanResults []ScanResult // ScanResult has the result of scanned CVE information. type ScanResult struct { ScannedAt time.Time - JSONVersion string + JSONVersion int Lang string ServerName string // TOML Section key Family string diff --git a/models/vulninfos.go b/models/vulninfos.go index 2c31e8b7..f0e92b82 100644 --- a/models/vulninfos.go +++ b/models/vulninfos.go @@ -26,7 +26,8 @@ import ( "github.com/future-architect/vuls/config" ) -// VulnInfos is VulnInfo list, getter/setter, sortable methods. +// VulnInfos has a map of VulnInfo +// Key: CveID type VulnInfos map[string]VulnInfo // Find elements that matches the function passed in argument @@ -198,13 +199,18 @@ type DistroAdvisory struct { // Score: 0 - 100 type Confidence struct { Score int - DetectionMethod string + DetectionMethod DetectionMethod } func (c Confidence) String() string { return fmt.Sprintf("%d / %s", c.Score, c.DetectionMethod) } +// DetectionMethod indicates +// - How to detect the CveID +// - How to get the changelog difference between installed and candidate version +type DetectionMethod string + const ( // CpeNameMatchStr is a String representation of CpeNameMatch CpeNameMatchStr = "CpeNameMatch" diff --git a/report/azureblob.go b/report/azureblob.go index 2220e9c1..f065accd 100644 --- a/report/azureblob.go +++ b/report/azureblob.go @@ -24,7 +24,7 @@ import ( "fmt" "time" - "github.com/Azure/azure-storage-go" + storage "github.com/Azure/azure-sdk-for-go/storage" c "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" diff --git a/report/tui.go b/report/tui.go index 83b786f2..9f762f15 100644 --- a/report/tui.go +++ b/report/tui.go @@ -45,14 +45,12 @@ var currentDetailLimitY int func RunTui(results models.ScanResults) subcommands.ExitStatus { scanResults = results - g, err := gocui.NewGui(gocui.OutputNormal) - if err != nil { - log.Errorf("%s", err) - return subcommands.ExitFailure - } + // g, err := gocui.NewGui(gocui.OutputNormal) + g := gocui.NewGui() defer g.Close() - g.SetManagerFunc(layout) + g.SetLayout(layout) + // g.SetManagerFunc(layout) if err := keybindings(g); err != nil { log.Errorf("%s", err) return subcommands.ExitFailure @@ -177,19 +175,19 @@ func nextView(g *gocui.Gui, v *gocui.View) error { var err error if v == nil { - _, err = g.SetCurrentView("side") + err = g.SetCurrentView("side") } switch v.Name() { case "side": - _, err = g.SetCurrentView("summary") + err = g.SetCurrentView("summary") case "summary": - _, err = g.SetCurrentView("detail") + err = g.SetCurrentView("detail") case "detail": - _, err = g.SetCurrentView("changelog") + err = g.SetCurrentView("changelog") case "changelog": - _, err = g.SetCurrentView("side") + err = g.SetCurrentView("side") default: - _, err = g.SetCurrentView("summary") + err = g.SetCurrentView("summary") } return err } @@ -198,19 +196,19 @@ func previousView(g *gocui.Gui, v *gocui.View) error { var err error if v == nil { - _, err = g.SetCurrentView("side") + err = g.SetCurrentView("side") } switch v.Name() { case "side": - _, err = g.SetCurrentView("side") + err = g.SetCurrentView("side") case "summary": - _, err = g.SetCurrentView("side") + err = g.SetCurrentView("side") case "detail": - _, err = g.SetCurrentView("summary") + err = g.SetCurrentView("summary") case "changelog": - _, err = g.SetCurrentView("detail") + err = g.SetCurrentView("detail") default: - _, err = g.SetCurrentView("side") + err = g.SetCurrentView("side") } return err } @@ -393,7 +391,7 @@ func cursorPageUp(g *gocui.Gui, v *gocui.View) error { func previousSummary(g *gocui.Gui, v *gocui.View) error { if v != nil { // cursor to summary - if _, err := g.SetCurrentView("summary"); err != nil { + if err := g.SetCurrentView("summary"); err != nil { return err } // move next line @@ -401,7 +399,7 @@ func previousSummary(g *gocui.Gui, v *gocui.View) error { return err } // cursor to detail - if _, err := g.SetCurrentView("detail"); err != nil { + if err := g.SetCurrentView("detail"); err != nil { return err } } @@ -411,7 +409,7 @@ func previousSummary(g *gocui.Gui, v *gocui.View) error { func nextSummary(g *gocui.Gui, v *gocui.View) error { if v != nil { // cursor to summary - if _, err := g.SetCurrentView("summary"); err != nil { + if err := g.SetCurrentView("summary"); err != nil { return err } // move next line @@ -419,7 +417,7 @@ func nextSummary(g *gocui.Gui, v *gocui.View) error { return err } // cursor to detail - if _, err := g.SetCurrentView("detail"); err != nil { + if err := g.SetCurrentView("detail"); err != nil { return err } } @@ -502,7 +500,7 @@ func getLine(g *gocui.Gui, v *gocui.View) error { return err } fmt.Fprintln(v, l) - if _, err := g.SetCurrentView("msg"); err != nil { + if err := g.SetCurrentView("msg"); err != nil { return err } } @@ -525,7 +523,7 @@ func showMsg(g *gocui.Gui, v *gocui.View) error { return err } fmt.Fprintln(v, l) - if _, err := g.SetCurrentView("msg"); err != nil { + if err := g.SetCurrentView("msg"); err != nil { return err } } @@ -536,7 +534,7 @@ func delMsg(g *gocui.Gui, v *gocui.View) error { if err := g.DeleteView("msg"); err != nil { return err } - if _, err := g.SetCurrentView("summary"); err != nil { + if err := g.SetCurrentView("summary"); err != nil { return err } return nil @@ -592,7 +590,7 @@ func setSideLayout(g *gocui.Gui) error { } currentScanResult = scanResults[0] vinfos = scanResults[0].ScannedCves.ToSortedSlice() - if _, err := g.SetCurrentView("side"); err != nil { + if err := g.SetCurrentView("side"); err != nil { return err } } diff --git a/scan/debian.go b/scan/debian.go index 17559ded..a7ecffb3 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -167,7 +167,7 @@ func (o *debian) checkDependencies() error { } func (o *debian) scanPackages() error { - installed, upgradable, err := o.scanInstalledPackages() + installed, updatable, err := o.scanInstalledPackages() if err != nil { o.log.Errorf("Failed to scan installed packages") return err @@ -178,7 +178,7 @@ func (o *debian) scanPackages() error { return nil } - unsecure, err := o.scanUnsecurePackages(upgradable) + unsecure, err := o.scanUnsecurePackages(updatable) if err != nil { o.log.Errorf("Failed to scan vulnerable packages") return err @@ -189,7 +189,7 @@ func (o *debian) scanPackages() error { func (o *debian) scanInstalledPackages() (models.Packages, models.Packages, error) { installed := models.Packages{} - upgradable := models.Packages{} + updatable := models.Packages{} r := o.exec("dpkg-query -W", noSudo) if !r.isSuccess() { @@ -214,27 +214,27 @@ func (o *debian) scanInstalledPackages() (models.Packages, models.Packages, erro } } - upgradableNames, err := o.GetUpgradablePackNames() + updatableNames, err := o.getUpdatablePackNames() if err != nil { return nil, nil, err } - for _, name := range upgradableNames { + for _, name := range updatableNames { for _, pack := range installed { if pack.Name == name { - upgradable[name] = pack + updatable[name] = pack break } } } // Fill the candidate versions of upgradable packages - err = o.fillCandidateVersion(upgradable) + err = o.fillCandidateVersion(updatable) if err != nil { return nil, nil, fmt.Errorf("Failed to fill candidate versions. err: %s", err) } - installed.MergeNewVersion(upgradable) + installed.MergeNewVersion(updatable) - return installed, upgradable, nil + return installed, updatable, nil } var packageLinePattern = regexp.MustCompile(`^([^\t']+)\t(.+)$`) @@ -263,14 +263,14 @@ func (o *debian) aptGetUpdate() error { return nil } -func (o *debian) scanUnsecurePackages(upgradable models.Packages) (models.VulnInfos, error) { +func (o *debian) scanUnsecurePackages(updatable models.Packages) (models.VulnInfos, error) { o.aptGetUpdate() // Setup changelog cache current := cache.Meta{ Name: o.getServerInfo().GetServerName(), Distro: o.getServerInfo().Distro, - Packs: upgradable, + Packs: updatable, } o.log.Debugf("Ensure changelog cache: %s", current.Name) @@ -280,7 +280,7 @@ func (o *debian) scanUnsecurePackages(upgradable models.Packages) (models.VulnIn } // Collect CVE information of upgradable packages - vulnInfos, err := o.scanVulnInfos(upgradable, meta) + vulnInfos, err := o.scanVulnInfos(updatable, meta) if err != nil { return nil, fmt.Errorf("Failed to scan unsecure packages. err: %s", err) } @@ -349,7 +349,7 @@ func (o *debian) fillCandidateVersion(packages models.Packages) (err error) { return } -func (o *debian) GetUpgradablePackNames() (packNames []string, err error) { +func (o *debian) getUpdatablePackNames() (packNames []string, err error) { cmd := util.PrependProxyEnv("LANGUAGE=en_US.UTF-8 apt-get upgrade --dry-run") r := o.exec(cmd, noSudo) if r.isSuccess(0, 1) { @@ -360,7 +360,7 @@ func (o *debian) GetUpgradablePackNames() (packNames []string, err error) { cmd, r.ExitStatus, r.Stdout, r.Stderr) } -func (o *debian) parseAptGetUpgrade(stdout string) (upgradableNames []string, err error) { +func (o *debian) parseAptGetUpgrade(stdout string) (updatableNames []string, err error) { startRe := regexp.MustCompile(`The following packages will be upgraded:`) stopRe := regexp.MustCompile(`^(\d+) upgraded.*`) startLineFound, stopLineFound := false, false @@ -375,21 +375,21 @@ func (o *debian) parseAptGetUpgrade(stdout string) (upgradableNames []string, er } result := stopRe.FindStringSubmatch(line) if len(result) == 2 { - numUpgradablePacks, err := strconv.Atoi(result[1]) + nUpdatable, err := strconv.Atoi(result[1]) if err != nil { return nil, fmt.Errorf( "Failed to scan upgradable packages number. line: %s", line) } - if numUpgradablePacks != len(upgradableNames) { + if nUpdatable != len(updatableNames) { return nil, fmt.Errorf( "Failed to scan upgradable packages, expected: %s, detected: %d", - result[1], len(upgradableNames)) + result[1], len(updatableNames)) } stopLineFound = true o.log.Debugf("Found the stop line. line: %s", line) break } - upgradableNames = append(upgradableNames, strings.Fields(line)...) + updatableNames = append(updatableNames, strings.Fields(line)...) } if !startLineFound { // no upgrades @@ -410,20 +410,20 @@ type DetectedCveID struct { Confidence models.Confidence } -func (o *debian) scanVulnInfos(upgradablePacks models.Packages, meta *cache.Meta) (models.VulnInfos, error) { +func (o *debian) scanVulnInfos(updatablePacks models.Packages, meta *cache.Meta) (models.VulnInfos, error) { type response struct { pack *models.Package DetectedCveIDs []DetectedCveID } - resChan := make(chan response, len(upgradablePacks)) - errChan := make(chan error, len(upgradablePacks)) - reqChan := make(chan models.Package, len(upgradablePacks)) + resChan := make(chan response, len(updatablePacks)) + errChan := make(chan error, len(updatablePacks)) + reqChan := make(chan models.Package, len(updatablePacks)) defer close(resChan) defer close(errChan) defer close(reqChan) go func() { - for _, pack := range upgradablePacks { + for _, pack := range updatablePacks { reqChan <- pack } }() @@ -431,7 +431,7 @@ func (o *debian) scanVulnInfos(upgradablePacks models.Packages, meta *cache.Meta timeout := time.After(30 * 60 * time.Second) concurrency := 10 tasks := util.GenWorkers(concurrency) - for range upgradablePacks { + for range updatablePacks { tasks <- func() { select { case pack := <-reqChan: @@ -459,7 +459,7 @@ func (o *debian) scanVulnInfos(upgradablePacks models.Packages, meta *cache.Meta // { DetectedCveID{} : [package] } cvePackages := make(map[DetectedCveID][]string) errs := []error{} - for i := 0; i < len(upgradablePacks); i++ { + for i := 0; i < len(updatablePacks); i++ { select { case response := <-resChan: o.Packages[response.pack.Name] = *response.pack @@ -474,7 +474,7 @@ func (o *debian) scanVulnInfos(upgradablePacks models.Packages, meta *cache.Meta cvePackages[cve] = packNames } o.log.Infof("(%d/%d) Scanned %s: %s", - i+1, len(upgradablePacks), response.pack.Name, cves) + i+1, len(updatablePacks), response.pack.Name, cves) case err := <-errChan: errs = append(errs, err) case <-timeout: @@ -500,7 +500,7 @@ func (o *debian) scanVulnInfos(upgradablePacks models.Packages, meta *cache.Meta } // Update meta package information of changelog cache to the latest one. - meta.Packs = upgradablePacks + meta.Packs = updatablePacks if err := cache.DB.RefreshMeta(*meta); err != nil { return nil, err } @@ -664,7 +664,7 @@ func (o *debian) parseChangelog(changelog, name, ver string, confidence models.C clog := models.Changelog{ Contents: strings.Join(buf, "\n"), - Method: string(confidence.DetectionMethod), + Method: confidence.DetectionMethod, } pack := o.Packages[name] pack.Changelog = clog diff --git a/scan/redhat.go b/scan/redhat.go index 2a8e28d6..c1f12708 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -18,6 +18,7 @@ along with this program. If not, see . package scan import ( + "bufio" "fmt" "regexp" "strings" @@ -27,7 +28,7 @@ import ( "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/util" - "github.com/k0kubun/pp" + ver "github.com/knqyf263/go-rpm-version" ) // inherit OsTypeInterface @@ -147,14 +148,14 @@ func (o *redhat) checkIfSudoNoPasswd() error { if majorVersion < 6 { cmds = []cmd{ {"yum --color=never repolist", zero}, - {"yum --color=never check-update", []int{0, 100}}, + // {"yum --color=never check-update", []int{0, 100}}, {"yum --color=never list-security --security", zero}, {"yum --color=never info-security", zero}, } } else { cmds = []cmd{ {"yum --color=never repolist", zero}, - {"yum --color=never check-update", []int{0, 100}}, + // {"yum --color=never check-update", []int{0, 100}}, {"yum --color=never --security updateinfo list updates", zero}, {"yum --color=never --security updateinfo updates", zero}, } @@ -174,12 +175,11 @@ func (o *redhat) checkIfSudoNoPasswd() error { return nil } -// CentOS 6, 7 ... yum-plugin-changelog +// CentOS 6, 7 ... yum-plugin-changelog, yum-utils // RHEL 5 ... yum-security // RHEL 6, 7 ... - // Amazon ... - func (o *redhat) checkDependencies() error { - var packName string if o.Distro.Family == config.Amazon { return nil } @@ -207,12 +207,14 @@ func (o *redhat) checkDependencies() error { } } + //TODO Check if yum-plugin-changelog is installed when scan with --changelog option on Amazon,RHEL, Oracle + var packNames []string switch o.Distro.Family { case config.CentOS: - packName = "yum-plugin-changelog" + packNames = []string{"yum-plugin-changelog", "yum-utils"} case config.RedHat, config.Oracle: if majorVersion < 6 { - packName = "yum-security" + packNames = []string{"yum-security"} } else { // yum-plugin-security is installed by default on RHEL6, 7 return nil @@ -221,27 +223,45 @@ func (o *redhat) checkDependencies() error { return fmt.Errorf("Not implemented yet: %s", o.Distro) } - cmd := "rpm -q " + packName - if r := o.exec(cmd, noSudo); !r.isSuccess() { - msg := fmt.Sprintf("%s is not installed", packName) - o.log.Errorf(msg) - return fmt.Errorf(msg) + for _, name := range packNames { + cmd := "rpm -q " + name + if r := o.exec(cmd, noSudo); !r.isSuccess() { + msg := fmt.Sprintf("%s is not installed", name) + o.log.Errorf(msg) + return fmt.Errorf(msg) + } } - o.log.Infof("Dependencies... Pass") + o.log.Infof("Dependencies ... Pass") return nil } func (o *redhat) scanPackages() error { - var err error - var packs []models.Package - if packs, err = o.scanInstalledPackages(); err != nil { + installed, err := o.scanInstalledPackages() + if err != nil { o.log.Errorf("Failed to scan installed packages") return err } - o.setPackages(models.NewPackages(packs...)) + + updatable, err := o.scanUpdatablePackages() + if err != nil { + o.log.Errorf("Failed to scan installed packages") + return err + } + installed.MergeNewVersion(updatable) + o.setPackages(installed) + + if config.Conf.PackageListOnly { + return nil + } + + //TODO Cache changelogs to bolt + //TODO --with-changelog + if err := o.fillChangelogs(updatable); err != nil { + return nil + } var vinfos models.VulnInfos - if vinfos, err = o.scanVulnInfos(); err != nil { + if vinfos, err = o.scanUnsecurePackages(updatable); err != nil { o.log.Errorf("Failed to scan vulnerable packages") return err } @@ -249,406 +269,360 @@ func (o *redhat) scanPackages() error { return nil } -func (o *redhat) scanInstalledPackages() (installed []models.Package, err error) { - cmd := "rpm -qa --queryformat '%{NAME}\t%{EPOCHNUM}\t%{VERSION}\t%{RELEASE}\n'" +func (o *redhat) scanInstalledPackages() (models.Packages, error) { + installed := models.Packages{} + cmd := "rpm -qa --queryformat '%{NAME} %{EPOCHNUM} %{VERSION} %{RELEASE} %{ARCH}\n'" r := o.exec(cmd, noSudo) if r.isSuccess() { - // e.g. - // openssl 1.0.1e 30.el6.11 + // openssl 0 1.0.1e 30.el6.11 x86_64 lines := strings.Split(r.Stdout, "\n") for _, line := range lines { if trimed := strings.TrimSpace(line); len(trimed) != 0 { - var pack models.Package - if pack, err = o.parseScannedPackagesLine(line); err != nil { - return + pack, err := o.parseInstalledPackagesLine(line) + if err != nil { + return nil, err } - installed = append(installed, pack) + installed[pack.Name] = pack } } - return + return installed, nil } - return nil, fmt.Errorf( - "Scan packages failed. status: %d, stdout: %s, stderr: %s", + return nil, fmt.Errorf("Scan packages failed. status: %d, stdout: %s, stderr: %s", r.ExitStatus, r.Stdout, r.Stderr) + } -func (o *redhat) parseScannedPackagesLine(line string) (models.Package, error) { +func (o *redhat) parseInstalledPackagesLine(line string) (models.Package, error) { fields := strings.Fields(line) - if len(fields) != 4 { + if len(fields) != 5 { return models.Package{}, fmt.Errorf("Failed to parse package line: %s", line) } ver := "" - if fields[1] == "0" { + epoch := fields[1] + if epoch == "0" { ver = fields[2] } else { - ver = fmt.Sprintf("%s:%s", fields[1], fields[2]) + ver = fmt.Sprintf("%s:%s", epoch, fields[2]) } + return models.Package{ Name: fields[0], Version: ver, Release: fields[3], + Arch: fields[4], }, nil } -func (o *redhat) scanVulnInfos() (models.VulnInfos, error) { - if o.Distro.Family != config.CentOS { - // Amazon, RHEL, Oracle Linux has yum updateinfo as default - // yum updateinfo can collenct vendor advisory information. - return o.scanUnsecurePackagesUsingYumPluginSecurity() - } - // CentOS does not have security channel... - // So, yum check-update then parse chnagelog. - return o.scanUnsecurePackagesUsingYumCheckUpdate() -} - -// For CentOS -func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, error) { - cmd := "LANGUAGE=en_US.UTF-8 yum --color=never %s check-update" - if o.getServerInfo().Enablerepo != "" { - cmd = fmt.Sprintf(cmd, "--enablerepo="+o.getServerInfo().Enablerepo) - } else { - cmd = fmt.Sprintf(cmd, "") +func (o *redhat) scanUpdatablePackages() (models.Packages, error) { + cmd := "repoquery --all --pkgnarrow=updates --qf='%{NAME} %{EPOCH} %{VERSION} %{RELEASE} %{REPO}'" + for _, repo := range o.getServerInfo().Enablerepo { + cmd += " --enablerepo=" + repo } - r := o.exec(util.PrependProxyEnv(cmd), noSudo) - if !r.isSuccess(0, 100) { - //returns an exit code of 100 if there are available updates. + r := o.exec(util.PrependProxyEnv(cmd), o.sudo()) + if !r.isSuccess() { return nil, fmt.Errorf("Failed to SSH: %s", r) } - // get Updateble package name, installed, candidate version. - packages, err := o.parseYumCheckUpdateLines(r.Stdout) - if err != nil { - return nil, fmt.Errorf("Failed to parse %s. err: %s", cmd, err) - } - o.log.Debugf("%s", pp.Sprintf("%v", packages)) - - // set candidate version info - o.Packages.MergeNewVersion(packages) - - // Collect CVE-IDs in changelog - type PackageCveIDs struct { - Package models.Package - CveIDs []string - } - - allChangelog, err := o.getAllChangelog(packages) - if err != nil { - o.log.Errorf("Failed to getAllchangelog. err: %s", err) - return nil, err - } - - // { packageName: changelog-lines } - var rpm2changelog map[string]*string - rpm2changelog, err = o.divideChangelogByPackage(allChangelog) - if err != nil { - return nil, fmt.Errorf("Failed to parseAllChangelog. err: %s", err) - } - - for name, clog := range rpm2changelog { - for _, p := range o.Packages { - n := fmt.Sprintf("%s-%s-%s", p.Name, p.NewVersion, p.NewRelease) - if name == n { - p.Changelog = models.Changelog{ - Contents: *clog, - Method: models.ChangelogExactMatchStr, - } - o.Packages[p.Name] = p - break - } - } - } - - var results []PackageCveIDs - i := 0 - for name := range packages { - changelog := o.getChangelogCVELines(rpm2changelog, packages[name]) - - // Collect unique set of CVE-ID in each changelog - uniqueCveIDMap := make(map[string]bool) - lines := strings.Split(changelog, "\n") - for _, line := range lines { - cveIDs := o.parseYumUpdateinfoLineToGetCveIDs(line) - for _, c := range cveIDs { - uniqueCveIDMap[c] = true - } - } - - // keys - var cveIDs []string - for k := range uniqueCveIDMap { - cveIDs = append(cveIDs, k) - } - p := PackageCveIDs{ - Package: packages[name], - CveIDs: cveIDs, - } - results = append(results, p) - - o.log.Infof("(%d/%d) Scanned %s-%s-%s -> %s-%s : %s", - i+1, - len(packages), - p.Package.Name, - p.Package.Version, - p.Package.Release, - p.Package.NewVersion, - p.Package.NewRelease, - p.CveIDs) - i++ - } - - // transform datastructure - // - From - // [ - // { - // Pack: models.Packages, - // CveIDs: []string, - // }, - // ] - // - To - // map { - // CveID: models.Packages{} - // } - cveIDPackages := make(map[string]models.Packages) - for _, res := range results { - for _, cveID := range res.CveIDs { - if packages, ok := cveIDPackages[cveID]; ok { - packages[res.Package.Name] = res.Package - cveIDPackages[cveID] = packages - } else { - cveIDPackages[cveID] = models.NewPackages(res.Package) - } - } - } - - vinfos := models.VulnInfos{} - for cveID, packs := range cveIDPackages { - names := []string{} - for name := range packs { - names = append(names, name) - } - - // Amazon, RHEL do not use this method, so VendorAdvisory do not set. - vinfos[cveID] = models.VulnInfo{ - CveID: cveID, - PackageNames: names, - Confidence: models.ChangelogExactMatch, - } - } - return vinfos, nil + // Collect Updateble packages, installed, candidate version and repository. + return o.parseUpdatablePacksLines(r.Stdout) } -// parseYumCheckUpdateLines parse yum check-update to get package name, candidate version -func (o *redhat) parseYumCheckUpdateLines(stdout string) (models.Packages, error) { - results := models.Packages{} - needToParse := false +// parseUpdatablePacksLines parse the stdout of repoquery to get package name, candidate version +func (o *redhat) parseUpdatablePacksLines(stdout string) (models.Packages, error) { + updatable := models.Packages{} lines := strings.Split(stdout, "\n") for _, line := range lines { - // update information of packages begin after blank line. - if trimed := strings.TrimSpace(line); len(trimed) == 0 { - needToParse = true + // TODO remove + // if strings.HasPrefix(line, "Obsoleting") || + // strings.HasPrefix(line, "Security:") { + // // see https://github.com/future-architect/vuls/issues/165 + // continue + // } + if len(strings.TrimSpace(line)) == 0 { continue } - if needToParse { - if strings.HasPrefix(line, "Obsoleting") || - strings.HasPrefix(line, "Security:") { - // see https://github.com/future-architect/vuls/issues/165 - continue - } - candidate, err := o.parseYumCheckUpdateLine(line) - if err != nil { - return results, err - } - - installed, found := o.Packages[candidate.Name] - if !found { - o.log.Warnf("Not found the package in rpm -qa. candidate: %s-%s-%s", - candidate.Name, candidate.Version, candidate.Release) - results[candidate.Name] = candidate - continue - } - installed.NewVersion = candidate.NewVersion - installed.NewRelease = candidate.NewRelease - installed.Repository = candidate.Repository - results[installed.Name] = installed + pack, err := o.parseUpdatablePacksLine(line) + if err != nil { + return updatable, err } + updatable[pack.Name] = pack } - return results, nil + return updatable, nil } -func (o *redhat) parseYumCheckUpdateLine(line string) (models.Package, error) { +func (o *redhat) parseUpdatablePacksLine(line string) (models.Package, error) { fields := strings.Fields(line) - if len(fields) < 3 { - return models.Package{}, fmt.Errorf("Unknown format: %s", line) + if len(fields) < 5 { + return models.Package{}, fmt.Errorf("Unknown format: %s, fields: %s", line, fields) } - splitted := strings.Split(fields[0], ".") - packName := "" - if len(splitted) == 1 { - packName = fields[0] + + ver := "" + epoch := fields[1] + if epoch == "0" { + ver = fields[2] } else { - packName = strings.Join(strings.Split(fields[0], ".")[0:(len(splitted)-1)], ".") + ver = fmt.Sprintf("%s:%s", epoch, fields[2]) } - verfields := strings.Split(fields[1], "-") - if len(verfields) != 2 { - return models.Package{}, fmt.Errorf("Unknown format: %s", line) - } - release := verfields[1] - repos := strings.Join(fields[2:len(fields)], " ") + repos := strings.Join(fields[4:len(fields)], " ") - return models.Package{ - Name: packName, - NewVersion: verfields[0], - NewRelease: release, + p := models.Package{ + Name: fields[0], + NewVersion: ver, + NewRelease: fields[3], Repository: repos, - }, nil -} - -func (o *redhat) mkPstring() *string { - str := "" - return &str -} - -func (o *redhat) regexpReplace(src string, pat string, rep string) string { - re := regexp.MustCompile(pat) - return re.ReplaceAllString(src, rep) -} - -var changeLogCVEPattern = regexp.MustCompile(`CVE-[0-9]+-[0-9]+`) - -func (o *redhat) getChangelogCVELines(rpm2changelog map[string]*string, pack models.Package) string { - rpm := fmt.Sprintf("%s-%s-%s", pack.Name, pack.NewVersion, pack.NewRelease) - retLine := "" - if rpm2changelog[rpm] != nil { - lines := strings.Split(*rpm2changelog[rpm], "\n") - for _, line := range lines { - if changeLogCVEPattern.MatchString(line) { - retLine += fmt.Sprintf("%s\n", line) - } - } } - return retLine + return p, nil } -func (o *redhat) divideChangelogByPackage(allChangelog string) (map[string]*string, error) { - var majorVersion int - var err error - if o.Distro.Family == config.CentOS { - majorVersion, err = o.Distro.MajorVersion() - if err != nil { - return nil, fmt.Errorf("Not implemented yet: %s, err: %s", o.Distro, err) +func (o *redhat) scanUnsecurePackages(updatable models.Packages) (models.VulnInfos, error) { + if o.Distro.Family != config.CentOS { + // Amazon, RHEL, Oracle Linux has yum updateinfo as default + // yum updateinfo can collenct vendor advisory information. + return o.scanCveIDsByCommands(updatable) + } + + // Parse chnagelog because CentOS does not have security channel... + return o.scanCveIDsInChangelog(updatable) +} + +func (o *redhat) fillChangelogs(updatables models.Packages) error { + names := []string{} + for name := range updatables { + names = append(names, name) + } + + if err := o.fillDiffChangelogs(names); err != nil { + return err + } + + emptyChangelogPackNames := []string{} + for _, pack := range o.Packages { + if pack.NewVersion != "" && pack.Changelog.Contents == "" { + emptyChangelogPackNames = append(emptyChangelogPackNames, pack.Name) } } - orglines := strings.Split(allChangelog, "\n") - tmpline := "" - var lines []string - var prev, now bool - for i := range orglines { - if majorVersion == 5 { - /* for CentOS5 (yum-util < 1.1.20) */ - prev = false - now = false - if 0 < i { - prev, err = o.isRpmPackageNameLine(orglines[i-1]) - if err != nil { - return nil, err - } - } - now, err = o.isRpmPackageNameLine(orglines[i]) - if err != nil { - return nil, err - } - if prev && now { - tmpline = fmt.Sprintf("%s, %s", tmpline, orglines[i]) - continue - } - if !prev && now { - tmpline = fmt.Sprintf("%s%s", tmpline, orglines[i]) - continue - } - if tmpline != "" { - lines = append(lines, fmt.Sprintf("%s", tmpline)) - tmpline = "" - } - lines = append(lines, fmt.Sprintf("%s", orglines[i])) - } else { - /* for CentOS6,7 (yum-util >= 1.1.20) */ - line := orglines[i] - line = o.regexpReplace(line, `^ChangeLog for: `, "") - line = o.regexpReplace(line, `^\*\*\sNo\sChangeLog\sfor:.*`, "") - lines = append(lines, line) + i := 0 + for _, name := range emptyChangelogPackNames { + i++ + o.log.Infof("(%d/%d) Fetched Changelogs %s", i, len(emptyChangelogPackNames), name) + if err := o.fillDiffChangelogs([]string{name}); err != nil { + return err } } - rpm2changelog := make(map[string]*string) - writePointer := o.mkPstring() - for _, line := range lines { - match, err := o.isRpmPackageNameLine(line) - if err != nil { - return nil, err - } - if match { - rpms := strings.Split(line, ",") - pNewString := o.mkPstring() - writePointer = pNewString - for _, rpm := range rpms { - rpm = strings.TrimSpace(rpm) - rpm = o.regexpReplace(rpm, `\.(i386|i486|i586|i686|k6|athlon|x86_64|noarch|ppc|alpha|sparc)$`, "") - if ss := strings.Split(rpm, ":"); 1 < len(ss) { - epoch := ss[0] - packVersion := strings.Join(ss[1:len(ss)], ":") - if sss := strings.Split(packVersion, "-"); 2 < len(sss) { - version := strings.Join(sss[len(sss)-2:len(sss)], "-") - name := strings.Join(sss[0:len(sss)-2], "-") - rpm = fmt.Sprintf("%s-%s:%s", name, epoch, version) - } - } - - rpm2changelog[rpm] = pNewString - } - } else { - if strings.HasPrefix(line, "Dependencies Resolved") { - return rpm2changelog, nil - } - *writePointer += fmt.Sprintf("%s\n", line) - } - } - return rpm2changelog, nil + return nil } -// CentOS -func (o *redhat) getAllChangelog(packages models.Packages) (stdout string, err error) { - packageNames := "" - for _, pack := range packages { - packageNames += fmt.Sprintf("%s ", pack.Name) - } - - command := "" - if 0 < len(config.Conf.HTTPProxy) { - command += util.ProxyEnv() - } - +func (o *redhat) getAvailableChangelogs(packNames []string) (map[string]string, error) { yumopts := "" - if o.getServerInfo().Enablerepo != "" { - yumopts = " --enablerepo=" + o.getServerInfo().Enablerepo + if 0 < len(o.getServerInfo().Enablerepo) { + yumopts = " --enablerepo=" + strings.Join(o.getServerInfo().Enablerepo, ",") } if config.Conf.SkipBroken { yumopts += " --skip-broken" } + cmd := `yum --color=never %s changelog all %s | grep -A 10000 '==================== Available Packages ===================='` + cmd = fmt.Sprintf(cmd, yumopts, strings.Join(packNames, " ")) - // yum update --changelog doesn't have --color option. - command += fmt.Sprintf(" LANGUAGE=en_US.UTF-8 yum --changelog --assumeno update %s ", yumopts) + packageNames - - r := o.exec(command, sudo) + r := o.exec(util.PrependProxyEnv(cmd), noSudo) if !r.isSuccess(0, 1) { - return "", fmt.Errorf( - "Failed to get changelog. status: %d, stdout: %s, stderr: %s", - r.ExitStatus, r.Stdout, r.Stderr) + return nil, fmt.Errorf("Failed to SSH: %s", r) } - return strings.Replace(r.Stdout, "\r", "", -1), nil + + return o.divideChangelogsIntoEachPackages(r.Stdout), nil +} + +// Divide available change logs of all updatable packages into each package's changelog +func (o *redhat) divideChangelogsIntoEachPackages(stdout string) map[string]string { + changelogs := make(map[string]string) + scanner := bufio.NewScanner(strings.NewReader(stdout)) + + crlf, newBlock := false, true + packNameVer, contents := "", []string{} + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "==================== Available Packages ====================") { + continue + } + if newBlock { + left := strings.Fields(line)[0] + // ss := strings.Split(left, ".") + // packNameVer = strings.Join(ss[0:len(ss)-1], ".") + packNameVer = left + newBlock = false + continue + } + if len(strings.TrimSpace(line)) == 0 { + if crlf { + changelogs[packNameVer] = strings.Join(contents, "\n") + packNameVer = "" + contents = []string{} + newBlock = true + crlf = false + } else { + contents = append(contents, line) + crlf = true + } + } else { + contents = append(contents, line) + crlf = false + } + } + if 0 < len(contents) { + changelogs[packNameVer] = strings.Join(contents, "\n") + } + return changelogs +} + +func (o *redhat) fillDiffChangelogs(packNames []string) error { + changelogs, err := o.getAvailableChangelogs(packNames) + if err != nil { + return err + } + + for s := range changelogs { + // name, pack, found := o.Packages.FindOne(func(p models.Package) bool { + name, pack, found := o.Packages.FindOne(func(p models.Package) bool { + var epochNameVerRel string + if index := strings.Index(p.NewVersion, ":"); 0 < index { + epoch := p.NewVersion[0:index] + ver := p.NewVersion[index+1 : len(p.NewVersion)] + epochNameVerRel = fmt.Sprintf("%s:%s-%s", + epoch, p.Name, ver) + } else { + epochNameVerRel = fmt.Sprintf("%s-%s", + p.Name, p.NewVersion) + } + return strings.HasPrefix(s, epochNameVerRel) + }) + + if found { + diff, err := o.getDiffChangelog(pack, changelogs[s]) + detectionMethod := models.ChangelogExactMatchStr + + if err != nil { + o.log.Debug(err) + // Try without epoch + if index := strings.Index(pack.Version, ":"); 0 < index { + pack.Version = pack.Version[index+1 : len(pack.Version)] + o.log.Debug("Try without epoch", pack) + diff, err = o.getDiffChangelog(pack, changelogs[s]) + if err != nil { + o.log.Debugf("Failed to find the version in changelog: %s-%s-%s", + pack.Name, pack.Version, pack.Release) + detectionMethod = models.FailedToFindVersionInChangelog + } else { + o.log.Debugf("Found the version in changelog without epoch: %s-%s-%s", + pack.Name, pack.Version, pack.Release) + detectionMethod = models.ChangelogLenientMatchStr + } + } + } + + pack = o.Packages[name] + pack.Changelog = models.Changelog{ + Contents: diff, + Method: models.DetectionMethod(detectionMethod), + } + o.Packages[name] = pack + } + } + return nil +} + +func (o *redhat) getDiffChangelog(pack models.Package, availableChangelog string) (string, error) { + installedVer := ver.NewVersion(fmt.Sprintf("%s-%s", pack.Version, pack.Release)) + scanner := bufio.NewScanner(strings.NewReader(availableChangelog)) + diff := []string{} + found := false + for scanner.Scan() { + line := scanner.Text() + if !strings.HasPrefix(line, "* ") { + diff = append(diff, line) + continue + } + + // openssh on RHEL + // openssh-server-6.6.1p1-35.el7_3.x86_64 rhui-rhel-7-server-rhui-rpms + // Wed Mar 1 21:00:00 2017 Jakub Jelen - 6.6.1p1-35 + 0.9.3-9 + ss := strings.Split(line, " + ") + if 1 < len(ss) { + line = ss[0] + } + + ss = strings.Split(line, " ") + if len(ss) < 2 { + diff = append(diff, line) + continue + } + v := ss[len(ss)-1] + v = strings.TrimPrefix(v, "-") + v = strings.TrimPrefix(v, "[") + v = strings.TrimSuffix(v, "]") + version := ver.NewVersion(v) + if installedVer.Equal(version) || installedVer.GreaterThan(version) { + found = true + break + } + diff = append(diff, line) + } + + if len(diff) == 0 || !found { + return availableChangelog, + fmt.Errorf("Failed to find the version in changelog: %s-%s-%s", + pack.Name, pack.Version, pack.Release) + } + return strings.TrimSpace(strings.Join(diff, "\n")), nil +} + +func (o *redhat) scanCveIDsInChangelog(updatable models.Packages) (models.VulnInfos, error) { + packCveIDs := make(map[string][]string) + for name := range updatable { + cveIDs := []string{} + pack := o.Packages[name] + if pack.Changelog.Method == models.FailedToFindVersionInChangelog { + continue + } + scanner := bufio.NewScanner(strings.NewReader(pack.Changelog.Contents)) + for scanner.Scan() { + if matches := cveRe.FindAllString(scanner.Text(), -1); 0 < len(matches) { + for _, m := range matches { + cveIDs = util.AppendIfMissing(cveIDs, m) + } + } + } + packCveIDs[name] = cveIDs + } + + // transform datastructure + // - From + // "packname": []{"CVE-2017-1111", ".../ + // + // - To + // map { + // "CVE-2017-1111": "packname", + // } + vinfos := models.VulnInfos{} + for name, cveIDs := range packCveIDs { + for _, cid := range cveIDs { + if v, ok := vinfos[cid]; ok { + v.PackageNames = append(v.PackageNames, name) + vinfos[cid] = v + } else { + vinfos[cid] = models.VulnInfo{ + CveID: cid, + PackageNames: []string{name}, + Confidence: models.ChangelogExactMatch, + } + } + } + } + return vinfos, nil } type distroAdvisoryCveIDs struct { @@ -658,7 +632,7 @@ type distroAdvisoryCveIDs struct { // Scaning unsecure packages using yum-plugin-security. // Amazon, RHEL, Oracle Linux -func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, error) { +func (o *redhat) scanCveIDsByCommands(updatable models.Packages) (models.VulnInfos, error) { if o.Distro.Family == config.CentOS { // CentOS has no security channel. // So use yum check-update && parse changelog @@ -689,22 +663,6 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, } advIDPackNamesList, err := o.parseYumUpdateinfoListAvailable(r.Stdout) - // get package name, version, rel to be upgrade. - cmd = "LANGUAGE=en_US.UTF-8 yum --color=never check-update" - r = o.exec(util.PrependProxyEnv(cmd), o.sudo()) - if !r.isSuccess(0, 100) { - //returns an exit code of 100 if there are available updates. - return nil, fmt.Errorf("Failed to SSH: %s", r) - } - updatable, err := o.parseYumCheckUpdateLines(r.Stdout) - if err != nil { - return nil, fmt.Errorf("Failed to parse %s. err: %s", cmd, err) - } - o.log.Debugf("%s", pp.Sprintf("%v", updatable)) - - // set candidate version info - o.Packages.MergeNewVersion(updatable) - dict := make(map[string]models.Packages) for _, advIDPackNames := range advIDPackNamesList { packages := models.Packages{} @@ -796,8 +754,6 @@ func (o *redhat) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveID for cveID := range cveIDsSetInThisSection { foundCveIDs = append(foundCveIDs, cveID) } - //TODO remove - // sort.Strings(foundCveIDs) result = append(result, distroAdvisoryCveIDs{ DistroAdvisory: advisory, @@ -883,24 +839,6 @@ func (o *redhat) changeSectionState(state int) (newState int) { return newState } -var rpmPackageArchPattern = regexp.MustCompile( - `^[^ ]+\.(i386|i486|i586|i686|k6|athlon|x86_64|noarch|ppc|alpha|sparc)$`) - -func (o *redhat) isRpmPackageNameLine(line string) (bool, error) { - s := strings.TrimPrefix(line, "ChangeLog for: ") - ss := strings.Split(s, ", ") - if len(ss) == 0 { - return false, nil - } - for _, s := range ss { - s = strings.TrimRight(s, " \r\n") - if !rpmPackageArchPattern.MatchString(s) { - return false, nil - } - } - return true, nil -} - var yumCveIDPattern = regexp.MustCompile(`(CVE-\d{4}-\d{4,})`) func (o *redhat) parseYumUpdateinfoLineToGetCveIDs(line string) []string { @@ -1032,9 +970,10 @@ func (o *redhat) clone() osTypeInterface { func (o *redhat) sudo() bool { switch o.Distro.Family { - case config.Amazon: + case config.Amazon, config.CentOS: return false default: + // RHEL return true } } diff --git a/scan/redhat_test.go b/scan/redhat_test.go index f3bf219a..e5414fe7 100644 --- a/scan/redhat_test.go +++ b/scan/redhat_test.go @@ -6,9 +6,7 @@ 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 +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 @@ -20,6 +18,7 @@ package scan import ( "reflect" "sort" + "strings" "testing" "time" @@ -42,7 +41,7 @@ func TestParseScanedPackagesLineRedhat(t *testing.T) { pack models.Package }{ { - "openssl 0 1.0.1e 30.el6.11", + "openssl 0 1.0.1e 30.el6.11 x86_64", models.Package{ Name: "openssl", Version: "1.0.1e", @@ -50,7 +49,7 @@ func TestParseScanedPackagesLineRedhat(t *testing.T) { }, }, { - "Percona-Server-shared-56 1 5.6.19 rel67.0.el6", + "Percona-Server-shared-56 1 5.6.19 rel67.0.el6 x84_64", models.Package{ Name: "Percona-Server-shared-56", Version: "1:5.6.19", @@ -60,7 +59,7 @@ func TestParseScanedPackagesLineRedhat(t *testing.T) { } for _, tt := range packagetests { - p, _ := r.parseScannedPackagesLine(tt.in) + p, _ := r.parseInstalledPackagesLine(tt.in) if p.Name != tt.pack.Name { t.Errorf("name: expected %s, actual %s", tt.pack.Name, p.Name) } @@ -262,54 +261,6 @@ func TestIsDescriptionLine(t *testing.T) { } } -func TestIsRpmPackageNameLine(t *testing.T) { - r := newRedhat(config.ServerInfo{}) - var tests = []struct { - in string - found bool - }{ - { - "stunnel-4.15-2.el5.2.i386", - true, - }, - { - "iproute-2.6.18-15.el5.i386", - true, - }, - { - "1:yum-updatesd-0.9-6.el5_10.noarch", - true, - }, - { - "glibc-2.12-1.192.el6.x86_64", - true, - }, - { - " glibc-2.12-1.192.el6.x86_64", - false, - }, - { - "glibc-2.12-1.192.el6.x86_64, iproute-2.6.18-15.el5.i386", - true, - }, - { - "k6 hoge.i386", - false, - }, - { - "triathlon", - false, - }, - } - - for i, tt := range tests { - found, err := r.isRpmPackageNameLine(tt.in) - if tt.found != found { - t.Errorf("[%d] line: %s, expected %t, actual %t, err %v", i, tt.in, tt.found, found, err) - } - } -} - func TestParseYumUpdateinfoToGetSeverity(t *testing.T) { r := newRedhat(config.ServerInfo{}) var tests = []struct { @@ -672,57 +623,64 @@ Description : Package updates are available for Amazon Linux AMI that fix the } } +func TestParseYumCheckUpdateLine(t *testing.T) { + r := newRedhat(config.ServerInfo{}) + r.Distro = config.Distro{Family: "centos"} + var tests = []struct { + in string + out models.Package + }{ + { + "zlib 0 1.2.7 17.el7 rhui-REGION-rhel-server-releases", + models.Package{ + Name: "zlib", + NewVersion: "1.2.7", + NewRelease: "17.el7", + Repository: "rhui-REGION-rhel-server-releases", + }, + }, + { + "shadow-utils 2 4.1.5.1 24.el7 rhui-REGION-rhel-server-releases", + models.Package{ + Name: "shadow-utils", + NewVersion: "2:4.1.5.1", + NewRelease: "24.el7", + Repository: "rhui-REGION-rhel-server-releases", + }, + }, + } + + for _, tt := range tests { + aPack, err := r.parseUpdatablePacksLine(tt.in) + if err != nil { + t.Errorf("Error has occurred, err: %s\ntt.in: %v", err, tt.in) + return + } + if !reflect.DeepEqual(tt.out, aPack) { + e := pp.Sprintf("%v", tt.out) + a := pp.Sprintf("%v", aPack) + t.Errorf("expected %s, actual %s", e, a) + } + } +} + func TestParseYumCheckUpdateLines(t *testing.T) { r := newRedhat(config.ServerInfo{}) r.Distro = config.Distro{Family: "centos"} - stdout := `Loaded plugins: changelog, fastestmirror, keys, protect-packages, protectbase, security -Loading mirror speeds from cached hostfile - * base: mirror.fairway.ne.jp - * epel: epel.mirror.srv.co.ge - * extras: mirror.fairway.ne.jp - * updates: mirror.fairway.ne.jp -0 packages excluded due to repository protections - -audit-libs.x86_64 2.3.7-5.el6 base -bash.x86_64 4.1.2-33.el6_7.1 updates -Obsoleting Packages -python-libs.i686 2.6.6-64.el6 rhui-REGION-rhel-server-releases - python-ordereddict.noarch 1.1-3.el6ev installed -bind-utils.x86_64 30:9.3.6-25.P1.el5_11.8 updates -pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5 -` + stdout := `audit-libs 0 2.3.7 5.el6 base +bash 0 4.1.2 33.el6_7.1 updates +python-libs 0 2.6.6 64.el6 rhui-REGION-rhel-server-releases +python-ordereddict 0 1.1 3.el6ev installed +bind-utils 30 9.3.6 25.P1.el5_11.8 updates +pytalloc 0 2.0.7 2.el6 @CentOS 6.5/6.5` r.setPackages(models.NewPackages( - models.Package{ - Name: "audit-libs", - Version: "2.3.6", - Release: "4.el6", - }, - models.Package{ - Name: "bash", - Version: "4.1.1", - Release: "33", - }, - models.Package{ - Name: "python-libs", - Version: "2.6.0", - Release: "1.1-0", - }, - models.Package{ - Name: "python-ordereddict", - Version: "1.0", - Release: "1", - }, - models.Package{ - Name: "bind-utils", - Version: "1.0", - Release: "1", - }, - models.Package{ - Name: "pytalloc", - Version: "2.0.1", - Release: "0", - }, + models.Package{Name: "audit-libs"}, + models.Package{Name: "bash"}, + models.Package{Name: "python-libs"}, + models.Package{Name: "python-ordereddict"}, + models.Package{Name: "bind-utils"}, + models.Package{Name: "pytalloc"}, )) var tests = []struct { in string @@ -733,48 +691,36 @@ pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5 models.NewPackages( models.Package{ Name: "audit-libs", - Version: "2.3.6", - Release: "4.el6", NewVersion: "2.3.7", NewRelease: "5.el6", Repository: "base", }, models.Package{ Name: "bash", - Version: "4.1.1", - Release: "33", NewVersion: "4.1.2", NewRelease: "33.el6_7.1", Repository: "updates", }, models.Package{ Name: "python-libs", - Version: "2.6.0", - Release: "1.1-0", NewVersion: "2.6.6", NewRelease: "64.el6", Repository: "rhui-REGION-rhel-server-releases", }, models.Package{ Name: "python-ordereddict", - Version: "1.0", - Release: "1", NewVersion: "1.1", NewRelease: "3.el6ev", Repository: "installed", }, models.Package{ Name: "bind-utils", - Version: "1.0", - Release: "1", NewVersion: "30:9.3.6", NewRelease: "25.P1.el5_11.8", Repository: "updates", }, models.Package{ Name: "pytalloc", - Version: "2.0.1", - Release: "0", NewVersion: "2.0.7", NewRelease: "2.el6", Repository: "@CentOS 6.5/6.5", @@ -784,7 +730,7 @@ pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5 } for _, tt := range tests { - packages, err := r.parseYumCheckUpdateLines(tt.in) + packages, err := r.parseUpdatablePacksLines(tt.in) if err != nil { t.Errorf("Error has occurred, err: %s\ntt.in: %v", err, tt.in) return @@ -802,29 +748,13 @@ pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5 func TestParseYumCheckUpdateLinesAmazon(t *testing.T) { r := newRedhat(config.ServerInfo{}) r.Distro = config.Distro{Family: "amazon"} - stdout := `Loaded plugins: priorities, update-motd, upgrade-helper -34 package(s) needed for security, out of 71 available - -bind-libs.x86_64 32:9.8.2-0.37.rc1.45.amzn1 amzn-main -java-1.7.0-openjdk.x86_64 1.7.0.95-2.6.4.0.65.amzn1 amzn-main -if-not-architecture 100-200 amzn-main -` + stdout := `bind-libs 32 9.8.2 0.37.rc1.45.amzn1 amzn-main +java-1.7.0-openjdk 0 1.7.0.95 2.6.4.0.65.amzn1 amzn-main +if-not-architecture 0 100 200 amzn-main` r.Packages = models.NewPackages( - models.Package{ - Name: "bind-libs", - Version: "9.8.0", - Release: "0.33.rc1.45.amzn1", - }, - models.Package{ - Name: "java-1.7.0-openjdk", - Version: "1.7.0.0", - Release: "2.6.4.0.0.amzn1", - }, - models.Package{ - Name: "if-not-architecture", - Version: "10", - Release: "20", - }, + models.Package{Name: "bind-libs"}, + models.Package{Name: "java-1.7.0-openjdk"}, + models.Package{Name: "if-not-architecture"}, ) var tests = []struct { in string @@ -835,24 +765,18 @@ if-not-architecture 100-200 amzn-main models.NewPackages( models.Package{ Name: "bind-libs", - Version: "9.8.0", - Release: "0.33.rc1.45.amzn1", NewVersion: "32:9.8.2", NewRelease: "0.37.rc1.45.amzn1", Repository: "amzn-main", }, models.Package{ Name: "java-1.7.0-openjdk", - Version: "1.7.0.0", - Release: "2.6.4.0.0.amzn1", NewVersion: "1.7.0.95", NewRelease: "2.6.4.0.65.amzn1", Repository: "amzn-main", }, models.Package{ Name: "if-not-architecture", - Version: "10", - Release: "20", NewVersion: "100", NewRelease: "200", Repository: "amzn-main", @@ -862,7 +786,7 @@ if-not-architecture 100-200 amzn-main } for _, tt := range tests { - packages, err := r.parseYumCheckUpdateLines(tt.in) + packages, err := r.parseUpdatablePacksLines(tt.in) if err != nil { t.Errorf("Error has occurred, err: %s\ntt.in: %v", err, tt.in) return @@ -976,308 +900,407 @@ func TestExtractPackNameVerRel(t *testing.T) { } -const ( - /* for CentOS6,7 (yum-util >= 1.1.20) */ - stdoutCentos6 = `---> Package libaio.x86_64 0:0.3.107-10.el6 will be installed ---> Finished Dependency Resolution - -Changes in packages about to be updated: - -ChangeLog for: binutils-2.20.51.0.2-5.44.el6.x86_64 -* Mon Dec 7 21:00:00 2015 Nick Clifton - 2.20.51.0.2-5.44 -- Backport upstream RELRO fixes. (#1227839) - -** No ChangeLog for: chkconfig-1.3.49.5-1.el6.x86_64 - -ChangeLog for: coreutils-8.4-43.el6.x86_64, coreutils-libs-8.4-43.el6.x86_64 -* Wed Feb 10 21:00:00 2016 Ondrej Vasik - 8.4-43 -- sed should actually be /bin/sed (related #1222140) - -* Wed Jan 6 21:00:00 2016 Ondrej Vasik - 8.4-41 -- colorls.sh,colorls.csh - call utilities with complete path (#1222140) -- mkdir, mkfifo, mknod - respect default umask/acls when - COREUTILS_CHILD_DEFAULT_ACLS envvar is set (to match rhel 7 behaviour, - -ChangeLog for: centos-release-6-8.el6.centos.12.3.x86_64 -* Wed May 18 21:00:00 2016 Johnny Hughes 6-8.el6.centos.12.3 -- CentOS-6.8 Released -- TESTSTRING CVE-0000-0000 - -ChangeLog for: 12:dhclient-4.1.1-51.P1.el6.centos.x86_64, 12:dhcp-common-4.1.1-51.P1.el6.centos.x86_64 -* Tue May 10 21:00:00 2016 Johnny Hughes - 12:4.1.1-51.P1 -- created patch 1000 for CentOS Branding -- replaced vvendor variable with CentOS in the SPEC file -- TESTSTRING CVE-1111-1111 - -* Mon Jan 11 21:00:00 2016 Jiri Popelka - 12:4.1.1-51.P1 -- send unicast request/release via correct interface (#1297445) - -* Thu Dec 3 21:00:00 2015 Jiri Popelka - 12:4.1.1-50.P1 -- Lease table overflow crash. (#1133917) -- Add ignore-client-uids option. (#1196768) -- dhclient-script: it's OK if the arping reply comes from our system. (#1204095) -- VLAN ID is only bottom 12-bits of TCI. (#1259552) -- dhclient: Make sure link-local address is ready in stateless mode. (#1263466) -- dhclient-script: make_resolv_conf(): Keep old nameservers - if server sends domain-name/search, but no nameservers. (#1269595) - -ChangeLog for: file-5.04-30.el6.x86_64, file-libs-5.04-30.el6.x86_64 -* Tue Feb 16 21:00:00 2016 Jan Kaluza 5.04-30 -- fix CVE-2014-3538 (unrestricted regular expression matching) - -* Tue Jan 5 21:00:00 2016 Jan Kaluza 5.04-29 -- fix #1284826 - try to read ELF header to detect corrupted one - -* Wed Dec 16 21:00:00 2015 Jan Kaluza 5.04-28 -- fix #1263987 - fix bugs found by coverity in the patch - -* Thu Nov 26 21:00:00 2015 Jan Kaluza 5.04-27 -- fix CVE-2014-3587 (incomplete fix for CVE-2012-1571) -- fix CVE-2014-3710 (out-of-bounds read in elf note headers) -- fix CVE-2014-8116 (multiple DoS issues (resource consumption)) -- fix CVE-2014-8117 (denial of service issue (resource consumption)) -- fix CVE-2014-9620 (limit the number of ELF notes processed) -- fix CVE-2014-9653 (malformed elf file causes access to uninitialized memory) - - -Dependencies Resolved - -` - /* for CentOS5 (yum-util < 1.1.20) */ - stdoutCentos5 = `---> Package portmap.i386 0:4.0-65.2.2.1 set to be updated ---> Finished Dependency Resolution - -Changes in packages about to be updated: - -libuser-0.54.7-3.el5.i386 -nss_db-2.2-38.el5_11.i386 -* Thu Nov 20 23:00:00 2014 Nalin Dahyabhai - 2.2-38 -- build without strict aliasing (internal build tooling) - -* Sat Nov 15 23:00:00 2014 Nalin Dahyabhai - 2.2-37 -- pull in fix for a memory leak in nss_db (#1163493) - -acpid-1.0.4-12.el5.i386 -* Thu Oct 6 00:00:00 2011 Jiri Skala - 1.0.4-12 -- Resolves: #729769 - acpid dumping useless info to log - -nash-5.1.19.6-82.el5.i386, mkinitrd-5.1.19.6-82.el5.i386 -* Tue Apr 15 00:00:00 2014 Brian C. Lane 5.1.19.6-82 -- Use ! instead of / when searching sysfs for ccis device - Resolves: rhbz#988020 -- Always include ahci module (except on s390) (bcl) - Resolves: rhbz#978245 -- Prompt to recreate default initrd (karsten) - Resolves: rhbz#472764 - -util-linux-2.13-0.59.el5_8.i386 -* Wed Oct 17 00:00:00 2012 Karel Zak 2.13-0.59.el5_8 -- fix #865791 - fdisk fails to partition disk not in use - -* Wed Dec 21 23:00:00 2011 Karel Zak 2.13-0.59 -- fix #768382 - CVE-2011-1675 CVE-2011-1677 util-linux various flaws - -* Wed Oct 26 00:00:00 2011 Karel Zak 2.13-0.58 -- fix #677452 - util-linux fails to build with gettext-0.17 - -30:bind-utils-9.3.6-25.P1.el5_11.8.i386, 30:bind-libs-9.3.6-25.P1.el5_11.8.i386 -* Mon Mar 14 23:00:00 2016 Tomas Hozza - 30:9.3.6-25.P1.8 -- Fix issue with patch for CVE-2016-1285 and CVE-2016-1286 found by test suite - -* Wed Mar 9 23:00:00 2016 Tomas Hozza - 30:9.3.6-25.P1.7 -- Fix CVE-2016-1285 and CVE-2016-1286 - -* Mon Jan 18 23:00:00 2016 Tomas Hozza - 30:9.3.6-25.P1.6 -- Fix CVE-2015-8704 - -* Thu Sep 3 00:00:00 2015 Tomas Hozza - 30:9.3.6-25.P1.5 -- Fix CVE-2015-8000 - - -Dependencies Resolved - -` -) - -func TestGetChangelogCVELines(t *testing.T) { - var testsCentos6 = []struct { - in models.Package - out string - }{ - { - models.Package{ - Name: "binutils", - NewVersion: "2.20.51.0.2", - NewRelease: "5.44.el6", - }, - "", - }, - { - models.Package{ - Name: "centos-release", - NewVersion: "6", - NewRelease: "8.el6.centos.12.3", - }, - `- TESTSTRING CVE-0000-0000 -`, - }, - { - models.Package{ - Name: "dhclient", - NewVersion: "12:4.1.1", - NewRelease: "51.P1.el6.centos", - }, - `- TESTSTRING CVE-1111-1111 -`, - }, - { - models.Package{ - Name: "dhcp-common", - NewVersion: "12:4.1.1", - NewRelease: "51.P1.el6.centos", - }, - `- TESTSTRING CVE-1111-1111 -`, - }, - { - models.Package{ - Name: "coreutils-libs", - NewVersion: "8.4", - NewRelease: "43.el6", - }, - "", - }, - { - models.Package{ - Name: "file", - NewVersion: "5.04", - NewRelease: "30.el6", - }, - `- fix CVE-2014-3538 (unrestricted regular expression matching) -- fix CVE-2014-3587 (incomplete fix for CVE-2012-1571) -- fix CVE-2014-3710 (out-of-bounds read in elf note headers) -- fix CVE-2014-8116 (multiple DoS issues (resource consumption)) -- fix CVE-2014-8117 (denial of service issue (resource consumption)) -- fix CVE-2014-9620 (limit the number of ELF notes processed) -- fix CVE-2014-9653 (malformed elf file causes access to uninitialized memory) -`, - }, - { - models.Package{ - Name: "file-libs", - NewVersion: "5.04", - NewRelease: "30.el6", - }, - `- fix CVE-2014-3538 (unrestricted regular expression matching) -- fix CVE-2014-3587 (incomplete fix for CVE-2012-1571) -- fix CVE-2014-3710 (out-of-bounds read in elf note headers) -- fix CVE-2014-8116 (multiple DoS issues (resource consumption)) -- fix CVE-2014-8117 (denial of service issue (resource consumption)) -- fix CVE-2014-9620 (limit the number of ELF notes processed) -- fix CVE-2014-9653 (malformed elf file causes access to uninitialized memory) -`, - }, - } - +func TestGetDiffChangelog(t *testing.T) { r := newRedhat(config.ServerInfo{}) - r.Distro = config.Distro{ - Family: "centos", - Release: "6.7", - } - for _, tt := range testsCentos6 { - rpm2changelog, err := r.divideChangelogByPackage(stdoutCentos6) - if err != nil { - t.Errorf("err: %s", err) - } - changelog := r.getChangelogCVELines(rpm2changelog, tt.in) - if tt.out != changelog { - t.Errorf("line: expected %s, actual %s, tt: %#v", tt.out, changelog, tt) - } + type in struct { + pack models.Package + changelog string } - var testsCentos5 = []struct { - in models.Package + var tests = []struct { + in in out string }{ + // 0 { - models.Package{ - Name: "libuser", - NewVersion: "0.54.7", - NewRelease: "3.el5", + in: in{ + pack: models.Package{ + Version: "2017a", + Release: "1", + }, + changelog: `* Mon Mar 20 12:00:00 2017 Patsy Franklin - 2017b-1 +- Rebase to tzdata-2017b. + - Haiti resumed DST on March 12, 2017. + +* Thu Mar 2 12:00:00 2017 Patsy Franklin - 2017a-1 +- Rebase to tzdata-2017a + - Mongolia no longer observes DST. (BZ #1425222) + - Add upstream patch to fix over-runing of POSIX limit on zone abbreviations. +- Add zone1970.tab file to the install list. (BZ #1427694) + +* Wed Nov 23 12:00:00 2016 Patsy Franklin - 2016j-1 +- Rebase to tzdata-2016ij + - Saratov region of Russia is moving from +03 offset to +04 offset + on 2016-12-04.`, }, - "", + out: `* Mon Mar 20 12:00:00 2017 Patsy Franklin - 2017b-1 +- Rebase to tzdata-2017b. + - Haiti resumed DST on March 12, 2017.`, }, + // 1 { - models.Package{ - Name: "nss_db", - NewVersion: "2.2", - NewRelease: "38.el5_11", + in: in{ + pack: models.Package{ + Version: "2004e", + Release: "2", + }, + changelog: `* Mon Mar 20 12:00:00 2017 Patsy Franklin - 2017b-1 +- Rebase to tzdata-2017b. + - Haiti resumed DST on March 12, 2017. + +* Wed Nov 23 12:00:00 2016 Patsy Franklin - 2016j-1 +- Rebase to tzdata-2016ij + - Saratov region of Russia is moving from +03 offset to +04 offset + on 2016-12-04. + +* Mon Nov 29 12:00:00 2004 Jakub Jelinek 2004g-1 +- 2004g (#141107) +- updates for Cuba + +* Mon Oct 11 12:00:00 2004 Jakub Jelinek 2004e-2 +- 2004e (#135194) +- updates for Brazil, Uruguay and Argentina`, }, - "", + out: `* Mon Mar 20 12:00:00 2017 Patsy Franklin - 2017b-1 +- Rebase to tzdata-2017b. + - Haiti resumed DST on March 12, 2017. + +* Wed Nov 23 12:00:00 2016 Patsy Franklin - 2016j-1 +- Rebase to tzdata-2016ij + - Saratov region of Russia is moving from +03 offset to +04 offset + on 2016-12-04. + +* Mon Nov 29 12:00:00 2004 Jakub Jelinek 2004g-1 +- 2004g (#141107) +- updates for Cuba`, }, + // 2 { - models.Package{ - Name: "acpid", - NewVersion: "1.0.4", - NewRelease: "82.el5", + in: in{ + pack: models.Package{ + Version: "2016j", + Release: "1", + }, + changelog: `* Mon Mar 20 12:00:00 2017 Patsy Franklin -2017b-1 +- Rebase to tzdata-2017b. + - Haiti resumed DST on March 12, 2017. + +* Wed Nov 23 12:00:00 2016 Patsy Franklin -2016j-1 +- Rebase to tzdata-2016ij + - Saratov region of Russia is moving from +03 offset to +04 offset + on 2016-12-04.`, }, - "", + out: `* Mon Mar 20 12:00:00 2017 Patsy Franklin -2017b-1 +- Rebase to tzdata-2017b. + - Haiti resumed DST on March 12, 2017.`, }, + // 3 { - models.Package{ - Name: "mkinitrd", - NewVersion: "5.1.19.6", - NewRelease: "82.el5", + in: in{ + pack: models.Package{ + Version: "3.10.0", + Release: "327.22.1.el7", + }, + changelog: `* Thu Jun 9 21:00:00 2016 Alexander Gordeev [3.10.0-327.22.2.el7] +- [infiniband] security: Restrict use of the write() interface (Don Dutile) [1332553 1316685] {CVE-2016-4565} + +* Mon May 16 21:00:00 2016 Alexander Gordeev [3.10.0-327.22.1.el7] +- [mm] mmu_notifier: fix memory corruption (Jerome Glisse) [1335727 1307042] +- [misc] cxl: Increase timeout for detection of AFU mmio hang (Steve Best) [1335419 1329682] +- [misc] cxl: Configure the PSL for two CAPI ports on POWER8NVL (Steve Best) [1336389 1278793]`, }, - "", + out: `* Thu Jun 9 21:00:00 2016 Alexander Gordeev [3.10.0-327.22.2.el7] +- [infiniband] security: Restrict use of the write() interface (Don Dutile) [1332553 1316685] {CVE-2016-4565}`, }, + // 4 { - models.Package{ - Name: "util-linux", - NewVersion: "2.13", - NewRelease: "0.59.el5_8", + in: in{ + pack: models.Package{ + Version: "6.6.1p1", + Release: "34", + }, + + changelog: `* Wed Mar 1 21:00:00 2017 Jakub Jelen - 6.6.1p1-35 + 0.9.3-9 +- Do not send SD_NOTIFY from forked childern (#1381997) + +* Fri Feb 24 21:00:00 2017 Jakub Jelen - 6.6.1p1-34 + 0.9.3-9 +- Add SD_NOTIFY code to help systemd to track running service (#1381997)`, }, - `- fix #768382 - CVE-2011-1675 CVE-2011-1677 util-linux various flaws -`, + out: `* Wed Mar 1 21:00:00 2017 Jakub Jelen - 6.6.1p1-35 +- Do not send SD_NOTIFY from forked childern (#1381997)`, }, + // 5 { - models.Package{ - Name: "bind-libs", - NewVersion: "30:9.3.6", - NewRelease: "25.P1.el5_11.8", + in: in{ + pack: models.Package{ + Version: "2.1.23", + Release: "15.el6", + }, + changelog: `* Fri Feb 27 12:00:00 2015 Jakub Jelen 2.1.23-15.2 +- Support AIX SASL GSSAPI (#1174315) + +* Tue Nov 18 12:00:00 2014 Petr Lautrbach 2.1.23-15.1 +- check a context value in sasl_gss_encode() (#1087221) + +* Mon Jun 23 12:00:00 2014 Petr Lautrbach 2.1.23-15 +- don't use " for saslauth user's description (#1081445) +- backport the ad_compat option (#994242) +- fixed a memory leak in the client side DIGEST-MD5 code (#838628)`, }, - `- Fix issue with patch for CVE-2016-1285 and CVE-2016-1286 found by test suite -- Fix CVE-2016-1285 and CVE-2016-1286 -- Fix CVE-2015-8704 -- Fix CVE-2015-8000 -`, + out: `* Fri Feb 27 12:00:00 2015 Jakub Jelen 2.1.23-15.2 +- Support AIX SASL GSSAPI (#1174315) + +* Tue Nov 18 12:00:00 2014 Petr Lautrbach 2.1.23-15.1 +- check a context value in sasl_gss_encode() (#1087221)`, }, + // 6 { - models.Package{ - Name: "bind-utils", - NewVersion: "30:9.3.6", - NewRelease: "25.P1.el5_11.8", + in: in{ + pack: models.Package{ + Version: "3.6.20", + Release: "1.el6", + }, + changelog: `* Wed Jul 29 12:00:00 2015 Jan Stanek - 3.6.20-1.2 +- Add patch for compiler warnings highlighted by rpmdiff. + Related: rhbz#1244727 + +* Wed Jul 22 12:00:00 2015 Viktor Jancik - 3.6.20-1.el6_7.1 +- fix for CVE-2015-3416 + Resolves: #1244727 + +* Tue Nov 17 12:00:00 2009 Panu Matilainen - 3.6.20-1 +- update to 3.6.20 (http://www.sqlite.org/releaselog/3_6_20.html) + +* Tue Oct 6 12:00:00 2009 Panu Matilainen - 3.6.18-1 +- update to 3.6.18 (http://www.sqlite.org/releaselog/3_6_18.html) +- drop no longer needed test-disabler patches`, }, - `- Fix issue with patch for CVE-2016-1285 and CVE-2016-1286 found by test suite -- Fix CVE-2016-1285 and CVE-2016-1286 -- Fix CVE-2015-8704 -- Fix CVE-2015-8000 -`, + out: `* Wed Jul 29 12:00:00 2015 Jan Stanek - 3.6.20-1.2 +- Add patch for compiler warnings highlighted by rpmdiff. + Related: rhbz#1244727 + +* Wed Jul 22 12:00:00 2015 Viktor Jancik - 3.6.20-1.el6_7.1 +- fix for CVE-2015-3416 + Resolves: #1244727`, + }, + /* + // 7 + { + in: in{ + pack: models.Package{ + Version: "2:7.4.160", + Release: "1.el7", + }, + changelog: `* Mon Dec 12 21:00:00 2016 Karsten Hopp 7.4.160-1.1 + - add fix for CVE-2016-1248 + + * Wed Jan 29 21:00:00 2014 Karsten Hopp 7.4.160-1 + - patchlevel 160 + - Resolves: rhbz#1059321`, + }, + out: `* Mon Dec 12 21:00:00 2016 Karsten Hopp 7.4.160-1.1 + - add fix for CVE-2016-1248`, + }, + // 8 + { + in: in{ + pack: models.Package{ + Version: "2:1.26", + Release: "29.el7", + }, + changelog: `* Mon Jun 20 21:00:00 2016 Pavel Raiskup - 1.26-31 + - avoid double free in selinux code (rhbz#1347396) + + * Thu Jun 4 21:00:00 2015 Pavel Raiskup - 1.26-30 + - don't mistakenly set default ACLs (#1220890) + + * Fri Jan 24 21:00:00 2014 Daniel Mach - 2:1.26-29 + - Mass rebuild 2014-01-24`, + }, + out: `* Mon Jun 20 21:00:00 2016 Pavel Raiskup - 1.26-31 + - avoid double free in selinux code (rhbz#1347396) + + * Thu Jun 4 21:00:00 2015 Pavel Raiskup - 1.26-30 + - don't mistakenly set default ACLs (#1220890)`, + }, + // 9 + { + in: in{ + pack: models.Package{ + Version: "1:1.0.1e", + Release: "51.el7_2.5", + }, + changelog: `* Mon Feb 6 21:00:00 2017 Tomáš Mráz 1.0.1e-60.1 + - fix CVE-2017-3731 - DoS via truncated packets with RC4-MD5 cipher + - fix CVE-2016-8610 - DoS of single-threaded servers via excessive alerts + + * Fri Dec 4 21:00:00 2015 Tomáš Mráz 1.0.1e-52 + - fix CVE-2015-3194 - certificate verify crash with missing PSS parameter + - fix CVE-2015-3195 - X509_ATTRIBUTE memory leak + - fix CVE-2015-3196 - race condition when handling PSK identity hint + + * Tue Jun 23 21:00:00 2015 Tomáš Mráz 1.0.1e-51 + - fix the CVE-2015-1791 fix (broken server side renegotiation)`, + }, + out: `* Mon Feb 6 21:00:00 2017 Tomáš Mráz 1.0.1e-60.1 + - fix CVE-2017-3731 - DoS via truncated packets with RC4-MD5 cipher + - fix CVE-2016-8610 - DoS of single-threaded servers via excessive alerts + + * Fri Dec 4 21:00:00 2015 Tomáš Mráz 1.0.1e-52 + - fix CVE-2015-3194 - certificate verify crash with missing PSS parameter + - fix CVE-2015-3195 - X509_ATTRIBUTE memory leak + - fix CVE-2015-3196 - race condition when handling PSK identity hint`, + }, + // 10 + { + in: in{ + pack: models.Package{ + Version: "1:5.5.47", + Release: "1.el7_2", + }, + changelog: `* Wed Sep 21 21:00:00 2016 Honza Horak - 5.5.52-1 + - Rebase to 5.5.52, that also include fix for CVE-2016-6662 + Resolves: #1377974 + + * Thu Feb 18 21:00:00 2016 Jakub Dorňák - 1:5.5.47-2 + - Add warning to /usr/lib/tmpfiles.d/mariadb.conf + Resolves: #1241623 + + * Wed Feb 3 21:00:00 2016 Jakub Dorňák - 1:5.5.47-1 + - Rebase to 5.5.47 + Also fixes: CVE-2015-4792 CVE-2015-4802 CVE-2015-4815 CVE-2015-4816 + CVE-2015-4819 CVE-2015-4826 CVE-2015-4830 CVE-2015-4836 CVE-2015-4858 + CVE-2015-4861 CVE-2015-4870 CVE-2015-4879 CVE-2015-4913 CVE-2015-7744 + CVE-2016-0505 CVE-2016-0546 CVE-2016-0596 CVE-2016-0597 CVE-2016-0598 + CVE-2016-0600 CVE-2016-0606 CVE-2016-0608 CVE-2016-0609 CVE-2016-0616 + CVE-2016-2047 + Resolves: #1300621`, + }, + out: `* Wed Sep 21 21:00:00 2016 Honza Horak - 5.5.52-1 + - Rebase to 5.5.52, that also include fix for CVE-2016-6662 + Resolves: #1377974 + + * Thu Feb 18 21:00:00 2016 Jakub Dorňák - 1:5.5.47-2 + - Add warning to /usr/lib/tmpfiles.d/mariadb.conf + Resolves: #1241623`, + }, + */ + // 11 + { + in: in{ + pack: models.Package{ + Version: "0.252", + Release: "8.1.el7", + }, + changelog: `* Thu Sep 29 21:00:00 2016 Vitezslav Crhonek - 0.252-8.4 +- Remove wrong entry from usb ids. + Resolves: #1380159 + +* Mon Sep 26 21:00:00 2016 Vitezslav Crhonek - 0.252-8.3 +- Updated pci, usb and vendor ids. +- Resolves: rhbz#1292382 + +* Tue Jun 28 21:00:00 2016 Michal Minar 0.252-8.2 +- Updated pci, usb and vendor ids. +- Resolves: rhbz#1292382 +- Resolves: rhbz#1291614 +- Resolves: rhbz#1324198 + +* Fri Oct 23 21:00:00 2015 Michal Minar 0.252-8.1 +- Updated pci, usb and vendor ids.`, + }, + out: `* Thu Sep 29 21:00:00 2016 Vitezslav Crhonek - 0.252-8.4 +- Remove wrong entry from usb ids. + Resolves: #1380159 + +* Mon Sep 26 21:00:00 2016 Vitezslav Crhonek - 0.252-8.3 +- Updated pci, usb and vendor ids. +- Resolves: rhbz#1292382 + +* Tue Jun 28 21:00:00 2016 Michal Minar 0.252-8.2 +- Updated pci, usb and vendor ids. +- Resolves: rhbz#1292382 +- Resolves: rhbz#1291614 +- Resolves: rhbz#1324198`, + }, + // 12 + { + in: in{ + pack: models.Package{ + Version: "1:2.02", + Release: "0.34.el7_2", + }, + changelog: `* Mon Aug 29 21:00:00 2016 Peter Jones - 2.02-0.44 +- Work around tftp servers that don't work with multiple consecutive slashes in + file paths. + Resolves: rhbz#1217243`, + }, + out: `* Mon Aug 29 21:00:00 2016 Peter Jones - 2.02-0.44 +- Work around tftp servers that don't work with multiple consecutive slashes in + file paths. + Resolves: rhbz#1217243`, }, } - r.Distro = config.Distro{ - Family: "centos", - Release: "5.6", - } - for _, tt := range testsCentos5 { - rpm2changelog, err := r.divideChangelogByPackage(stdoutCentos5) - if err != nil { - t.Errorf("err: %s", err) - } - changelog := r.getChangelogCVELines(rpm2changelog, tt.in) - if tt.out != changelog { - t.Errorf("line: expected %s, actual %s, tt: %#v", tt.out, changelog, tt) + for i, tt := range tests { + diff, _ := r.getDiffChangelog(tt.in.pack, tt.in.changelog) + if tt.out != diff { + t.Errorf("[%d] name: expected \n%s\nactual \n%s", i, tt.out, diff) } } + +} + +func TestDivideChangelogsIntoEachPackages(t *testing.T) { + r := newRedhat(config.ServerInfo{}) + type in struct { + pack models.Package + changelog string + } + + var tests = []struct { + in string + out map[string]string + }{ + { + in: `==================== Available Packages ==================== +1:NetworkManager-1.4.0-20.el7_3.x86_64 rhui-rhel-7-server-rhui-rpms +* Mon Apr 24 21:00:00 2017 Beniamino Galvani - 1:1.4.0-20 +- vlan: use parent interface mtu as default (rh#1414186) + +* Wed Mar 29 21:00:00 2017 Beniamino Galvani - 1:1.4.0-19 +- core: alyways force a sync of the default route (rh#1431268) + + +1:NetworkManager-0.9.9.1-25.git20140326. rhui-rhel-7-server-rhui-optional-rpms +* Tue Jul 1 21:00:00 2014 Jiří Klimeš - 1:0.9.9.1-25.git20140326 +- core: fix MTU handling while merging/subtracting IP configs (rh #1093231) + +* Mon Jun 23 21:00:00 2014 Thomas Haller - 1:0.9.9.1-24.git20140326 +- core: fix crash on failure of reading bridge sysctl values (rh #1112020)`, + out: map[string]string{ + "1:NetworkManager-1.4.0-20.el7_3.x86_64": `* Mon Apr 24 21:00:00 2017 Beniamino Galvani - 1:1.4.0-20 +- vlan: use parent interface mtu as default (rh#1414186) + +* Wed Mar 29 21:00:00 2017 Beniamino Galvani - 1:1.4.0-19 +- core: alyways force a sync of the default route (rh#1431268)`, + + "1:NetworkManager-0.9.9.1-25.git20140326.": `* Tue Jul 1 21:00:00 2014 Jiří Klimeš - 1:0.9.9.1-25.git20140326 +- core: fix MTU handling while merging/subtracting IP configs (rh #1093231) + +* Mon Jun 23 21:00:00 2014 Thomas Haller - 1:0.9.9.1-24.git20140326 +- core: fix crash on failure of reading bridge sysctl values (rh #1112020)`, + }, + }, + } + + for _, tt := range tests { + changelogs := r.divideChangelogsIntoEachPackages(tt.in) + for k, v := range tt.out { + if strings.TrimSpace(v) != strings.TrimSpace(changelogs[k]) { + t.Errorf("expected: %v\nactual: %v", pp.Sprint(tt.out), pp.Sprint(changelogs)) + } + } + } + } From ffbaa0a508beb9b95c6d04ab06cca5952b1d7ec6 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Tue, 18 Jul 2017 21:54:11 +0900 Subject: [PATCH 058/113] Extract Advisory.Description on RHEL, Amazon, Oracle (#450) --- models/vulninfos.go | 9 ++-- scan/redhat.go | 26 ++++++++--- scan/redhat_test.go | 103 +++++++++++++++----------------------------- 3 files changed, 60 insertions(+), 78 deletions(-) diff --git a/models/vulninfos.go b/models/vulninfos.go index f0e92b82..df5ecc40 100644 --- a/models/vulninfos.go +++ b/models/vulninfos.go @@ -189,10 +189,11 @@ func (v *VulnInfo) NilToEmpty() *VulnInfo { // DistroAdvisory has Amazon Linux, RHEL, FreeBSD Security Advisory information. type DistroAdvisory struct { - AdvisoryID string - Severity string - Issued time.Time - Updated time.Time + AdvisoryID string + Severity string + Issued time.Time + Updated time.Time + Description string } // Confidence is a ranking how confident the CVE-ID was deteted correctly diff --git a/scan/redhat.go b/scan/redhat.go index c1f12708..f1960ae7 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -564,6 +564,13 @@ func (o *redhat) getDiffChangelog(pack models.Package, availableChangelog string v = strings.TrimPrefix(v, "-") v = strings.TrimPrefix(v, "[") v = strings.TrimSuffix(v, "]") + + // On Amazon often end with email address. Go to next line + if strings.HasPrefix(v, "<") && strings.HasSuffix(v, ">") { + diff = append(diff, line) + continue + } + version := ver.NewVersion(v) if installedVer.Equal(version) || installedVer.GreaterThan(version) { found = true @@ -746,15 +753,12 @@ func (o *redhat) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveID // find the new section pattern if horizontalRulePattern.MatchString(line) { - // set previous section's result to return-variable if sectionState == Content { - foundCveIDs := []string{} for cveID := range cveIDsSetInThisSection { foundCveIDs = append(foundCveIDs, cveID) } - result = append(result, distroAdvisoryCveIDs{ DistroAdvisory: advisory, CveIDs: foundCveIDs, @@ -763,6 +767,7 @@ func (o *redhat) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveID // reset for next section. cveIDsSetInThisSection = make(map[string]bool) inDesctiption = false + advisory = models.DistroAdvisory{} } // Go to next section @@ -785,16 +790,24 @@ func (o *redhat) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveID case Content: if found := o.isDescriptionLine(line); found { inDesctiption = true + ss := strings.Split(line, ":") + advisory.Description += fmt.Sprintf("%s ", + strings.TrimSpace(strings.Join(ss[1:len(ss)], ":"))) + continue } // severity - severity, found := o.parseYumUpdateinfoToGetSeverity(line) - if found { + if severity, found := o.parseYumUpdateinfoToGetSeverity(line); found { advisory.Severity = severity + continue } // No need to parse in description except severity if inDesctiption { + if ss := strings.Split(line, ":"); 1 < len(ss) { + advisory.Description += fmt.Sprintf("%s ", + strings.TrimSpace(strings.Join(ss[1:len(ss)], ":"))) + } continue } @@ -806,16 +819,19 @@ func (o *redhat) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveID advisoryID, found := o.parseYumUpdateinfoToGetAdvisoryID(line) if found { advisory.AdvisoryID = advisoryID + continue } issued, found := o.parseYumUpdateinfoLineToGetIssued(line) if found { advisory.Issued = issued + continue } updated, found := o.parseYumUpdateinfoLineToGetUpdated(line) if found { advisory.Updated = updated + continue } } } diff --git a/scan/redhat_test.go b/scan/redhat_test.go index e5414fe7..eda7eb6f 100644 --- a/scan/redhat_test.go +++ b/scan/redhat_test.go @@ -307,9 +307,6 @@ func TestParseYumUpdateinfoOL(t *testing.T) { Issued : 2017-02-15 CVEs : CVE-2017-3135 Description : [32:9.9.4-38.2] - : - Fix CVE-2017-3135 (ISC change 4557) - : - Fix and test caching CNAME before DNAME (ISC - : change 4558) Severity : Moderate =============================================================================== @@ -323,10 +320,6 @@ Description : [32:9.9.4-38.2] CVEs : CVE-2016-8610 : CVE-2017-3731 Description : [1.0.1e-48.4] - : - fix CVE-2017-3731 - DoS via truncated packets - : with RC4-MD5 cipher - : - fix CVE-2016-8610 - DoS of single-threaded - : servers via excessive alerts Severity : Moderate =============================================================================== @@ -339,10 +332,6 @@ Description : [1.0.1e-48.4] Issued : 2017-02-15 CVEs : CVE-2017-6074 Description : kernel-uek - : [4.1.12-61.1.28] - : - dccp: fix freeing skb too early for - : IPV6_RECVPKTINFO (Andrey Konovalov) [Orabug: - : 25598257] {CVE-2017-6074} Severity : Important ` @@ -360,17 +349,19 @@ Description : kernel-uek []distroAdvisoryCveIDs{ { DistroAdvisory: models.DistroAdvisory{ - AdvisoryID: "ELSA-2017-0276", - Severity: "Moderate", - Issued: issued, + AdvisoryID: "ELSA-2017-0276", + Severity: "Moderate", + Issued: issued, + Description: "[32:9.9.4-38.2] ", }, CveIDs: []string{"CVE-2017-3135"}, }, { DistroAdvisory: models.DistroAdvisory{ - AdvisoryID: "ELSA-2017-0286", - Severity: "Moderate", - Issued: issued, + AdvisoryID: "ELSA-2017-0286", + Severity: "Moderate", + Issued: issued, + Description: "[1.0.1e-48.4] ", }, CveIDs: []string{ "CVE-2016-8610", @@ -379,9 +370,10 @@ Description : kernel-uek }, { DistroAdvisory: models.DistroAdvisory{ - AdvisoryID: "ELSA-2017-3520", - Severity: "Important", - Issued: issued, + AdvisoryID: "ELSA-2017-3520", + Severity: "Important", + Issued: issued, + Description: "kernel-uek ", }, CveIDs: []string{"CVE-2017-6074"}, }, @@ -418,12 +410,6 @@ func TestParseYumUpdateinfoRHEL(t *testing.T) { Bugs : 1259087 - CVE-2015-5722 bind: malformed DNSSEC key failed assertion denial of service CVEs : CVE-2015-5722 Description : The Berkeley Internet Name Domain (BIND) is an implementation of - : the Domain Name System (DNS) protocols. BIND - : includes a DNS server (named); a resolver library - : (routines for applications to use when interfacing - : with DNS); and tools for verifying that the DNS - : server is operating correctly. - : Severity : Important =============================================================================== @@ -439,12 +425,6 @@ Description : The Berkeley Internet Name Domain (BIND) is an implementation of CVEs : CVE-2015-8000 : CVE-2015-8001 Description : The Berkeley Internet Name Domain (BIND) is an implementation of - : the Domain Name System (DNS) protocols. BIND - : includes a DNS server (named); a resolver library - : (routines for applications to use when interfacing - : with DNS); and tools for verifying that the DNS - : server is operating correctly. - : Severity : Low =============================================================================== @@ -458,12 +438,6 @@ Description : The Berkeley Internet Name Domain (BIND) is an implementation of Bugs : 1299364 - CVE-2015-8704 bind: specific APL data could trigger an INSIST in apl_42.c CVEs : CVE-2015-8704 : CVE-2015-8705 Description : The Berkeley Internet Name Domain (BIND) is an implementation of - : the Domain Name System (DNS) protocols. BIND - : includes a DNS server (named); a resolver library - : (routines for applications to use when interfacing - : with DNS); and tools for verifying that the DNS - : server is operating correctly. - : Severity : Moderate ` @@ -482,18 +456,20 @@ Description : The Berkeley Internet Name Domain (BIND) is an implementation of []distroAdvisoryCveIDs{ { DistroAdvisory: models.DistroAdvisory{ - AdvisoryID: "RHSA-2015:1705", - Severity: "Important", - Issued: issued, + AdvisoryID: "RHSA-2015:1705", + Severity: "Important", + Issued: issued, + Description: "The Berkeley Internet Name Domain (BIND) is an implementation of ", }, CveIDs: []string{"CVE-2015-5722"}, }, { DistroAdvisory: models.DistroAdvisory{ - AdvisoryID: "RHSA-2015:2655", - Severity: "Low", - Issued: issued, - Updated: updated, + AdvisoryID: "RHSA-2015:2655", + Severity: "Low", + Issued: issued, + Updated: updated, + Description: "The Berkeley Internet Name Domain (BIND) is an implementation of ", }, CveIDs: []string{ "CVE-2015-8000", @@ -502,10 +478,10 @@ Description : The Berkeley Internet Name Domain (BIND) is an implementation of }, { DistroAdvisory: models.DistroAdvisory{ - AdvisoryID: "RHSA-2016:0073", - Severity: "Moderate", - Issued: issued, - Updated: updated, + AdvisoryID: "RHSA-2016:0073", + Severity: "Moderate", + Issued: issued, + Description: "The Berkeley Internet Name Domain (BIND) is an implementation of ", }, CveIDs: []string{ "CVE-2015-8704", @@ -553,10 +529,6 @@ func TestParseYumUpdateinfoAmazon(t *testing.T) { Issued : 2015-12-15 13:30 CVEs : CVE-2016-1494 Description : Package updates are available for Amazon Linux AMI that fix the - : following vulnerabilities: CVE-2016-1494: - : 1295869: - : CVE-2016-1494 python-rsa: Signature forgery using - : Bleichenbacher'06 attack Severity : medium =============================================================================== @@ -571,32 +543,25 @@ Description : Package updates are available for Amazon Linux AMI that fix the : CVE-2015-3195 : CVE-2015-3196 Description : Package updates are available for Amazon Linux AMI that fix the - : following vulnerabilities: CVE-2015-3196: - : 1288326: - : CVE-2015-3196 OpenSSL: Race condition handling PSK - : identify hint A race condition flaw, leading to a - : double free, was found in the way OpenSSL handled - : pre-shared keys (PSKs). A remote attacker could - : use this flaw to crash a multi-threaded SSL/TLS - : client. - : Severity : medium`, []distroAdvisoryCveIDs{ { DistroAdvisory: models.DistroAdvisory{ - AdvisoryID: "ALAS-2016-644", - Severity: "medium", - Issued: issued, + AdvisoryID: "ALAS-2016-644", + Severity: "medium", + Issued: issued, + Description: "Package updates are available for Amazon Linux AMI that fix the ", }, CveIDs: []string{"CVE-2016-1494"}, }, { DistroAdvisory: models.DistroAdvisory{ - AdvisoryID: "ALAS-2015-614", - Severity: "medium", - Issued: issued, - Updated: updated, + AdvisoryID: "ALAS-2015-614", + Severity: "medium", + Issued: issued, + Updated: updated, + Description: "Package updates are available for Amazon Linux AMI that fix the ", }, CveIDs: []string{ "CVE-2015-3194", From 26e447f11accae17dd2b8e5934625c1efe93c616 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Tue, 25 Jul 2017 17:25:09 +0900 Subject: [PATCH 059/113] Check existence and last modified time of local OVAL database when reporting --- Gopkg.lock | 22 +++++------ commands/report.go | 2 +- oval/debian.go | 8 ++-- oval/oval.go | 88 ++++++++++++++++++++++++++++++++++++++++++-- oval/redhat.go | 8 ++-- report/cve_client.go | 2 +- report/report.go | 35 ++++++++++++++++-- report/tui.go | 4 ++ scan/redhat.go | 12 +++--- 9 files changed, 147 insertions(+), 34 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index bc967282..a5359ee6 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -4,8 +4,8 @@ [[projects]] name = "github.com/Azure/azure-sdk-for-go" packages = ["storage"] - revision = "59c277f1b488b81b1a5f944212f25b69bea8ece3" - version = "v10.1.0-beta" + revision = "5b6066bbd213e47c49a5fa2be2b29529bbcb6704" + version = "v10.1.1-beta" [[projects]] name = "github.com/Azure/go-autorest" @@ -28,8 +28,8 @@ [[projects]] name = "github.com/aws/aws-sdk-go" packages = ["aws","aws/awserr","aws/awsutil","aws/client","aws/client/metadata","aws/corehandlers","aws/credentials","aws/credentials/ec2rolecreds","aws/credentials/endpointcreds","aws/credentials/stscreds","aws/defaults","aws/ec2metadata","aws/endpoints","aws/request","aws/session","aws/signer/v4","internal/shareddefaults","private/protocol","private/protocol/query","private/protocol/query/queryutil","private/protocol/rest","private/protocol/restxml","private/protocol/xml/xmlutil","service/s3","service/sts"] - revision = "6d7fc1a00fcae6bbb53550f4a0b98324fd7aa250" - version = "v1.10.12" + revision = "6ff7be1a127941b8dc9a408ac9c2dd6eb2167a5d" + version = "v1.10.15" [[projects]] name = "github.com/boltdb/bolt" @@ -64,8 +64,8 @@ [[projects]] name = "github.com/go-redis/redis" packages = [".","internal","internal/consistenthash","internal/hashtag","internal/pool","internal/proto"] - revision = "da63fe7def48e378caf9539abf64b9b1e37bc01e" - version = "v6.5.3" + revision = "a005081ecd2d0d963a1a20efda049223205cf90a" + version = "v6.5.4" [[projects]] name = "github.com/go-sql-driver/mysql" @@ -149,7 +149,7 @@ branch = "master" name = "github.com/kotakanbe/goval-dictionary" packages = ["config","db","db/rdb","log","models"] - revision = "adf0b39cd7fea8f4493f7b65e7316179634be95d" + revision = "2c949ba2967dcd35574f2a78a12551c5326de6a9" [[projects]] branch = "master" @@ -239,7 +239,7 @@ branch = "master" name = "github.com/sirupsen/logrus" packages = ["."] - revision = "5ff5dd844dfeb4e23e27528f79f1f845bc8bb78f" + revision = "3eef8ce63d02f65d2da43214faf7bb19b0b2bb7a" [[projects]] branch = "master" @@ -263,19 +263,19 @@ branch = "master" name = "golang.org/x/crypto" packages = ["curve25519","ed25519","ed25519/internal/edwards25519","ssh","ssh/agent","ssh/terminal"] - revision = "7f7c0c2d75ebb4e32a21396ce36e87b6dadc91c9" + revision = "6914964337150723782436d56b3f21610a74ce7b" [[projects]] branch = "master" name = "golang.org/x/net" packages = ["context","idna","publicsuffix"] - revision = "b3756b4b77d7b13260a0a2ec658753cf48922eac" + revision = "ab5485076ff3407ad2d02db054635913f017b0ed" [[projects]] branch = "master" name = "golang.org/x/sys" packages = ["unix"] - revision = "4cd6d1a821c7175768725b55ca82f14683a29ea4" + revision = "c4489faa6e5ab84c0ef40d6ee878f7a030281f0f" [[projects]] branch = "master" diff --git a/commands/report.go b/commands/report.go index 8fcf43db..7dfca1dd 100644 --- a/commands/report.go +++ b/commands/report.go @@ -410,7 +410,7 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} } if c.Conf.OvalDBURL != "" { - err := oval.Base{}.CheckHealth() + err := oval.Base{}.CheckHTTPHealth() if err != nil { util.Log.Errorf("OVAL HTTP server is not running. err: %s", err) util.Log.Errorf("Run goval-dictionary as server mode before reporting or run with --ovaldb-path option") diff --git a/oval/debian.go b/oval/debian.go index ab5e4d78..ae4a07c3 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -25,7 +25,7 @@ func (o DebianBase) fillFromOvalDB(r *models.ScanResult) error { } else { ovalconf.Conf.DBPath = config.Conf.OvalDBURL } - util.Log.Infof("open oval-dictionary db (%s): %s", + util.Log.Infof("Open oval-dictionary db (%s): %s", ovalconf.Conf.DBType, ovalconf.Conf.DBPath) ovallog.Initialize(config.Conf.LogDir) @@ -68,7 +68,7 @@ func (o DebianBase) update(r *models.ScanResult, definition *ovalmodels.Definiti ovalContent.Type = models.NewCveContentType(r.Family) vinfo, ok := r.ScannedCves[definition.Debian.CveID] if !ok { - util.Log.Infof("%s is newly detected by OVAL", definition.Debian.CveID) + util.Log.Debugf("%s is newly detected by OVAL", definition.Debian.CveID) vinfo = models.VulnInfo{ CveID: definition.Debian.CveID, Confidence: models.OvalMatch, @@ -79,9 +79,9 @@ func (o DebianBase) update(r *models.ScanResult, definition *ovalmodels.Definiti cveContents := vinfo.CveContents ctype := models.NewCveContentType(r.Family) if _, ok := vinfo.CveContents[ctype]; ok { - util.Log.Infof("%s will be updated by OVAL", definition.Debian.CveID) + util.Log.Debugf("%s will be updated by OVAL", definition.Debian.CveID) } else { - util.Log.Infof("%s is also detected by OVAL", definition.Debian.CveID) + util.Log.Debugf("%s is also detected by OVAL", definition.Debian.CveID) cveContents = models.CveContents{} } if vinfo.Confidence.Score < models.OvalMatch.Score { diff --git a/oval/oval.go b/oval/oval.go index 174ba371..3c6f492d 100644 --- a/oval/oval.go +++ b/oval/oval.go @@ -11,21 +11,27 @@ import ( "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/util" ver "github.com/knqyf263/go-deb-version" + "github.com/kotakanbe/goval-dictionary/db" + ovallog "github.com/kotakanbe/goval-dictionary/log" ovalmodels "github.com/kotakanbe/goval-dictionary/models" "github.com/parnurzeal/gorequest" ) // Client is the interface of OVAL client. type Client interface { - CheckHealth() error + CheckHTTPHealth() error FillWithOval(r *models.ScanResult) error + + // CheckIfOvalFetched checks if oval entries are in DB by family, release. + CheckIfOvalFetched(string, string) (bool, error) + CheckIfOvalFresh(string, string) (bool, error) } // Base is a base struct type Base struct{} -// CheckHealth do health check -func (b Base) CheckHealth() error { +// CheckHTTPHealth do health check +func (b Base) CheckHTTPHealth() error { if !b.isFetchViaHTTP() { return nil } @@ -43,6 +49,82 @@ func (b Base) CheckHealth() error { return nil } +// CheckIfOvalFetched checks if oval entries are in DB by family, release. +func (b Base) CheckIfOvalFetched(osFamily, release string) (fetched bool, err error) { + ovallog.Initialize(config.Conf.LogDir) + if !b.isFetchViaHTTP() { + var ovaldb db.DB + if ovaldb, err = db.NewDB( + osFamily, + config.Conf.OvalDBType, + config.Conf.OvalDBPath, + config.Conf.DebugSQL, + ); err != nil { + return false, err + } + defer ovaldb.CloseDB() + count, err := ovaldb.CountDefs(osFamily, release) + if err != nil { + return false, fmt.Errorf("Failed to count OVAL defs: %s, %s, %v", + osFamily, release, err) + } + return 0 < count, nil + } + + url, _ := util.URLPathJoin(config.Conf.OvalDBURL, "count", osFamily, release) + resp, body, errs := gorequest.New().Get(url).End() + if 0 < len(errs) || resp == nil || resp.StatusCode != 200 { + return false, fmt.Errorf("HTTP GET error: %v, url: %s, resp: %v", + errs, url, resp) + } + count := 0 + if err := json.Unmarshal([]byte(body), &count); err != nil { + return false, fmt.Errorf("Failed to Unmarshall. body: %s, err: %s", + body, err) + } + return 0 < count, nil +} + +// CheckIfOvalFresh checks if oval entries are fresh enough +func (b Base) CheckIfOvalFresh(osFamily, release string) (ok bool, err error) { + ovallog.Initialize(config.Conf.LogDir) + var lastModified time.Time + if !b.isFetchViaHTTP() { + var ovaldb db.DB + if ovaldb, err = db.NewDB( + osFamily, + config.Conf.OvalDBType, + config.Conf.OvalDBPath, + config.Conf.DebugSQL, + ); err != nil { + return false, err + } + defer ovaldb.CloseDB() + lastModified = ovaldb.GetLastModified(osFamily, release) + } else { + url, _ := util.URLPathJoin(config.Conf.OvalDBURL, "lastmodified", osFamily, release) + resp, body, errs := gorequest.New().Get(url).End() + if 0 < len(errs) || resp == nil || resp.StatusCode != 200 { + return false, fmt.Errorf("HTTP GET error: %v, url: %s, resp: %v", + errs, url, resp) + } + + if err := json.Unmarshal([]byte(body), &lastModified); err != nil { + return false, fmt.Errorf("Failed to Unmarshall. body: %s, err: %s", + body, err) + } + } + + since := time.Now() + since = since.AddDate(0, 0, -3) + if lastModified.Before(since) { + util.Log.Warnf("%s-%s OVAL is old, last modified is %s. It's recommended to update OVAL to improve scanning accuracy. To update OVAL database, see https://github.com/kotakanbe/goval-dictionary#usage", + osFamily, release, lastModified) + return false, nil + } + return true, nil +} + func (b Base) isFetchViaHTTP() bool { // Default value of OvalDBType is sqlite3 if config.Conf.OvalDBURL != "" && config.Conf.OvalDBType == "sqlite3" { diff --git a/oval/redhat.go b/oval/redhat.go index d52c9c9b..7db9038c 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -8,7 +8,7 @@ import ( "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/util" - ver "github.com/knqyf263/go-deb-version" + ver "github.com/knqyf263/go-rpm-version" ovalconf "github.com/kotakanbe/goval-dictionary/config" db "github.com/kotakanbe/goval-dictionary/db" ovallog "github.com/kotakanbe/goval-dictionary/log" @@ -63,7 +63,7 @@ func (o RedHatBase) getDefsByPackNameFromOvalDB(osRelease string, } else { ovalconf.Conf.DBPath = config.Conf.OvalDBURL } - util.Log.Infof("open oval-dictionary db (%s): %s", + util.Log.Infof("Open oval-dictionary db (%s): %s", ovalconf.Conf.DBType, ovalconf.Conf.DBPath) ovallog.Initialize(config.Conf.LogDir) @@ -84,9 +84,9 @@ func (o RedHatBase) getDefsByPackNameFromOvalDB(osRelease string, return nil, fmt.Errorf("Failed to get RedHat OVAL info by package name: %v", err) } for _, def := range definitions { - current, _ := ver.NewVersion(fmt.Sprintf("%s-%s", pack.Version, pack.Release)) + current := ver.NewVersion(fmt.Sprintf("%s-%s", pack.Version, pack.Release)) for _, p := range def.AffectedPacks { - affected, _ := ver.NewVersion(p.Version) + affected := ver.NewVersion(p.Version) if pack.Name != p.Name || !current.LessThan(affected) { continue } diff --git a/report/cve_client.go b/report/cve_client.go index 52a2cfd2..19b5742f 100644 --- a/report/cve_client.go +++ b/report/cve_client.go @@ -147,7 +147,7 @@ func (api cvedictClient) FetchCveDetailsFromCveDB(cveIDs []string) (cveDetails [ return []*cve.CveDetail{}, fmt.Errorf("Failed to New DB. err: %s", err) } - log.Infof("Opening DB (%s).", driver.Name()) + util.Log.Infof("Opening DB (%s).", driver.Name()) if err := driver.OpenDB( cveconfig.Conf.DBType, cveconfig.Conf.DBPath, diff --git a/report/report.go b/report/report.go index dbf90310..9d268fb3 100644 --- a/report/report.go +++ b/report/report.go @@ -79,12 +79,12 @@ func FillCveInfos(rs []models.ScanResult, dir string) ([]models.ScanResult, erro func fillCveInfo(r *models.ScanResult) error { util.Log.Debugf("need to refresh") - util.Log.Debugf("Fill CVE detailed information with OVAL") + util.Log.Infof("Fill CVE detailed information with OVAL") if err := fillWithOval(r); err != nil { return fmt.Errorf("Failed to fill OVAL information: %s", err) } - util.Log.Debugf("Fill CVE detailed information with CVE-DB") + util.Log.Infof("Fill CVE detailed information with CVE-DB") if err := fillWithCveDB(r); err != nil { return fmt.Errorf("Failed to fill CVE information: %s", err) } @@ -139,23 +139,50 @@ func fillWithCveDB(r *models.ScanResult) error { return nil } -func fillWithOval(r *models.ScanResult) error { +func fillWithOval(r *models.ScanResult) (err error) { var ovalClient oval.Client + var ovalFamily string + + // TODO switch r.Family { case c.Debian: ovalClient = oval.NewDebian() + ovalFamily = c.Debian case c.Ubuntu: ovalClient = oval.NewUbuntu() + ovalFamily = c.Ubuntu case c.RedHat: ovalClient = oval.NewRedhat() + ovalFamily = c.RedHat case c.CentOS: ovalClient = oval.NewCentOS() + //use RedHat's OVAL + ovalFamily = c.RedHat + //TODO implement OracleLinux + // case c.Oracle: + // ovalClient = oval.New() + // ovalFamily = c.Oracle case c.Amazon, c.Oracle, c.Raspbian, c.FreeBSD: - //TODO implement OracleLinux return nil default: return fmt.Errorf("Oval %s is not implemented yet", r.Family) } + + ok, err := ovalClient.CheckIfOvalFetched(ovalFamily, r.Release) + if err != nil { + return err + } + if !ok { + util.Log.Warnf("OVAL is emtpy: %s-%s. It's recommended to use OVAL to improve scanning accuracy. To fetch OVAL database, see https://github.com/kotakanbe/goval-dictionary#usage", r.Family, r.Release) + return nil + } + + _, err = ovalClient.CheckIfOvalFresh(ovalFamily, r.Release) + if err != nil { + return err + } + util.Log.Infof("OVAL is fresh: %s-%s ", r.Family, r.Release) + if err := ovalClient.FillWithOval(r); err != nil { return err } diff --git a/report/tui.go b/report/tui.go index 9f762f15..5c2838dc 100644 --- a/report/tui.go +++ b/report/tui.go @@ -47,6 +47,10 @@ func RunTui(results models.ScanResults) subcommands.ExitStatus { // g, err := gocui.NewGui(gocui.OutputNormal) g := gocui.NewGui() + if err := g.Init(); err != nil { + log.Errorf("%s", err) + return subcommands.ExitFailure + } defer g.Close() g.SetLayout(layout) diff --git a/scan/redhat.go b/scan/redhat.go index f1960ae7..aa4e0fba 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -254,12 +254,6 @@ func (o *redhat) scanPackages() error { return nil } - //TODO Cache changelogs to bolt - //TODO --with-changelog - if err := o.fillChangelogs(updatable); err != nil { - return nil - } - var vinfos models.VulnInfos if vinfos, err = o.scanUnsecurePackages(updatable); err != nil { o.log.Errorf("Failed to scan vulnerable packages") @@ -379,6 +373,12 @@ func (o *redhat) parseUpdatablePacksLine(line string) (models.Package, error) { } func (o *redhat) scanUnsecurePackages(updatable models.Packages) (models.VulnInfos, error) { + //TODO Cache changelogs to bolt + //TODO --with-changelog + if err := o.fillChangelogs(updatable); err != nil { + return nil, err + } + if o.Distro.Family != config.CentOS { // Amazon, RHEL, Oracle Linux has yum updateinfo as default // yum updateinfo can collenct vendor advisory information. From 1aae42594568d205ca78e662f041d8ef46d8acf0 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Tue, 25 Jul 2017 20:55:54 +0900 Subject: [PATCH 060/113] Undisplay the number of CVEs at the end of 'scan --package-list-only' --- Gopkg.lock | 4 ++-- oval/debian.go | 2 +- oval/redhat.go | 8 ++++---- report/cve_client.go | 4 ++-- report/report.go | 2 +- report/util.go | 8 +++++++- 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index a5359ee6..550d3b8c 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -149,7 +149,7 @@ branch = "master" name = "github.com/kotakanbe/goval-dictionary" packages = ["config","db","db/rdb","log","models"] - revision = "2c949ba2967dcd35574f2a78a12551c5326de6a9" + revision = "766b881c46d2037c75833ec0021da1c3da1ad2a1" [[projects]] branch = "master" @@ -239,7 +239,7 @@ branch = "master" name = "github.com/sirupsen/logrus" packages = ["."] - revision = "3eef8ce63d02f65d2da43214faf7bb19b0b2bb7a" + revision = "86bd21e371d71c8885b29e8dfb161c6034dc4abe" [[projects]] branch = "master" diff --git a/oval/debian.go b/oval/debian.go index ae4a07c3..5e077689 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -25,7 +25,7 @@ func (o DebianBase) fillFromOvalDB(r *models.ScanResult) error { } else { ovalconf.Conf.DBPath = config.Conf.OvalDBURL } - util.Log.Infof("Open oval-dictionary db (%s): %s", + util.Log.Debugf("Open oval-dictionary db (%s): %s", ovalconf.Conf.DBType, ovalconf.Conf.DBPath) ovallog.Initialize(config.Conf.LogDir) diff --git a/oval/redhat.go b/oval/redhat.go index 7db9038c..cfc174dd 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -63,7 +63,7 @@ func (o RedHatBase) getDefsByPackNameFromOvalDB(osRelease string, } else { ovalconf.Conf.DBPath = config.Conf.OvalDBURL } - util.Log.Infof("Open oval-dictionary db (%s): %s", + util.Log.Debugf("Open oval-dictionary db (%s): %s", ovalconf.Conf.DBType, ovalconf.Conf.DBPath) ovallog.Initialize(config.Conf.LogDir) @@ -102,7 +102,7 @@ func (o RedHatBase) update(r *models.ScanResult, definition *ovalmodels.Definiti ovalContent := *o.convertToModel(cve.CveID, definition) vinfo, ok := r.ScannedCves[cve.CveID] if !ok { - util.Log.Infof("%s is newly detected by OVAL", cve.CveID) + util.Log.Debugf("%s is newly detected by OVAL", cve.CveID) vinfo = models.VulnInfo{ CveID: cve.CveID, Confidence: models.OvalMatch, @@ -112,9 +112,9 @@ func (o RedHatBase) update(r *models.ScanResult, definition *ovalmodels.Definiti } else { cveContents := vinfo.CveContents if _, ok := vinfo.CveContents[models.RedHat]; ok { - util.Log.Infof("%s will be updated by OVAL", cve.CveID) + util.Log.Debugf("%s will be updated by OVAL", cve.CveID) } else { - util.Log.Infof("%s also detected by OVAL", cve.CveID) + util.Log.Debugf("%s also detected by OVAL", cve.CveID) cveContents = models.CveContents{} } diff --git a/report/cve_client.go b/report/cve_client.go index 19b5742f..62d0036b 100644 --- a/report/cve_client.go +++ b/report/cve_client.go @@ -147,7 +147,7 @@ func (api cvedictClient) FetchCveDetailsFromCveDB(cveIDs []string) (cveDetails [ return []*cve.CveDetail{}, fmt.Errorf("Failed to New DB. err: %s", err) } - util.Log.Infof("Opening DB (%s).", driver.Name()) + util.Log.Debugf("Opening DB (%s).", driver.Name()) if err := driver.OpenDB( cveconfig.Conf.DBType, cveconfig.Conf.DBPath, @@ -281,7 +281,7 @@ func (api cvedictClient) FetchCveDetailsByCpeNameFromDB(cpeName string) (cveDeta return []*cve.CveDetail{}, fmt.Errorf("Failed to New DB. err: %s", err) } - log.Infof("Opening DB (%s).", driver.Name()) + util.Log.Debugf("Opening DB (%s).", driver.Name()) if err = driver.OpenDB( cveconfig.Conf.DBType, cveconfig.Conf.DBPath, diff --git a/report/report.go b/report/report.go index 9d268fb3..594c2035 100644 --- a/report/report.go +++ b/report/report.go @@ -173,7 +173,7 @@ func fillWithOval(r *models.ScanResult) (err error) { return err } if !ok { - util.Log.Warnf("OVAL is emtpy: %s-%s. It's recommended to use OVAL to improve scanning accuracy. To fetch OVAL database, see https://github.com/kotakanbe/goval-dictionary#usage", r.Family, r.Release) + util.Log.Warnf("OVAL entries of %s-%s are not found. It's recommended to use OVAL to improve scanning accuracy. To fetch OVAL, see https://github.com/kotakanbe/goval-dictionary#usage , Then report with --ovaldb-path or --ovaldb-url flag", r.Family, r.Release) return nil } diff --git a/report/util.go b/report/util.go index 52a34597..3122029d 100644 --- a/report/util.go +++ b/report/util.go @@ -43,10 +43,16 @@ func formatScanSummary(rs ...models.ScanResult) string { for _, r := range rs { var cols []interface{} if len(r.Errors) == 0 { + var cves string + if config.Conf.PackageListOnly { + cves = fmt.Sprintf("- CVEs") + } else { + cves = fmt.Sprintf("%d CVEs", len(r.ScannedCves)) + } cols = []interface{}{ r.FormatServerName(), fmt.Sprintf("%s%s", r.Family, r.Release), - fmt.Sprintf("%d CVEs", len(r.ScannedCves)), + cves, r.Packages.FormatUpdatablePacksSummary(), } } else { From ed162d7d6eb086b0765a30872941428ddc59e916 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Wed, 26 Jul 2017 01:03:27 +0900 Subject: [PATCH 061/113] Display the information of yum updateinfo on TUI (for RHEL, Amazon, Oracle) --- models/vulninfos.go | 15 +++++++++++++++ report/tui.go | 4 ++++ scan/redhat.go | 12 ++++++------ scan/redhat_test.go | 18 ++++++++++-------- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/models/vulninfos.go b/models/vulninfos.go index df5ecc40..d14df562 100644 --- a/models/vulninfos.go +++ b/models/vulninfos.go @@ -18,6 +18,7 @@ along with this program. If not, see . package models import ( + "bytes" "fmt" "sort" "strings" @@ -196,6 +197,20 @@ type DistroAdvisory struct { Description string } +// Format the distro advisory information +func (p DistroAdvisory) Format() string { + if p.AdvisoryID == "" { + return "" + } + + var delim bytes.Buffer + for i := 0; i < len(p.AdvisoryID); i++ { + delim.WriteString("-") + } + buf := []string{p.AdvisoryID, delim.String(), p.Description} + return strings.Join(buf, "\n") +} + // Confidence is a ranking how confident the CVE-ID was deteted correctly // Score: 0 - 100 type Confidence struct { diff --git a/report/tui.go b/report/tui.go index 5c2838dc..36a3d635 100644 --- a/report/tui.go +++ b/report/tui.go @@ -708,6 +708,10 @@ func setChangelogLayout(g *gocui.Gui) error { lines := []string{} vinfo := vinfos[currentVinfo] + for _, adv := range vinfo.DistroAdvisories { + lines = append(lines, adv.Format()) + } + for _, name := range vinfo.PackageNames { pack := currentScanResult.Packages[name] for _, p := range currentScanResult.Packages { diff --git a/scan/redhat.go b/scan/redhat.go index aa4e0fba..f1673a86 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -790,9 +790,9 @@ func (o *redhat) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveID case Content: if found := o.isDescriptionLine(line); found { inDesctiption = true - ss := strings.Split(line, ":") - advisory.Description += fmt.Sprintf("%s ", - strings.TrimSpace(strings.Join(ss[1:len(ss)], ":"))) + ss := strings.Split(line, " : ") + advisory.Description += fmt.Sprintf("%s\n", + strings.Join(ss[1:len(ss)], " : ")) continue } @@ -804,9 +804,9 @@ func (o *redhat) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveID // No need to parse in description except severity if inDesctiption { - if ss := strings.Split(line, ":"); 1 < len(ss) { - advisory.Description += fmt.Sprintf("%s ", - strings.TrimSpace(strings.Join(ss[1:len(ss)], ":"))) + if ss := strings.Split(line, ": "); 1 < len(ss) { + advisory.Description += fmt.Sprintf("%s\n", + strings.Join(ss[1:len(ss)], ": ")) } continue } diff --git a/scan/redhat_test.go b/scan/redhat_test.go index eda7eb6f..8c051aac 100644 --- a/scan/redhat_test.go +++ b/scan/redhat_test.go @@ -352,7 +352,7 @@ Description : kernel-uek AdvisoryID: "ELSA-2017-0276", Severity: "Moderate", Issued: issued, - Description: "[32:9.9.4-38.2] ", + Description: "[32:9.9.4-38.2]\n", }, CveIDs: []string{"CVE-2017-3135"}, }, @@ -361,7 +361,7 @@ Description : kernel-uek AdvisoryID: "ELSA-2017-0286", Severity: "Moderate", Issued: issued, - Description: "[1.0.1e-48.4] ", + Description: "[1.0.1e-48.4]\n", }, CveIDs: []string{ "CVE-2016-8610", @@ -373,7 +373,7 @@ Description : kernel-uek AdvisoryID: "ELSA-2017-3520", Severity: "Important", Issued: issued, - Description: "kernel-uek ", + Description: "kernel-uek\n", }, CveIDs: []string{"CVE-2017-6074"}, }, @@ -459,7 +459,7 @@ Description : The Berkeley Internet Name Domain (BIND) is an implementation of AdvisoryID: "RHSA-2015:1705", Severity: "Important", Issued: issued, - Description: "The Berkeley Internet Name Domain (BIND) is an implementation of ", + Description: "The Berkeley Internet Name Domain (BIND) is an implementation of\n", }, CveIDs: []string{"CVE-2015-5722"}, }, @@ -469,7 +469,7 @@ Description : The Berkeley Internet Name Domain (BIND) is an implementation of Severity: "Low", Issued: issued, Updated: updated, - Description: "The Berkeley Internet Name Domain (BIND) is an implementation of ", + Description: "The Berkeley Internet Name Domain (BIND) is an implementation of\n", }, CveIDs: []string{ "CVE-2015-8000", @@ -481,7 +481,7 @@ Description : The Berkeley Internet Name Domain (BIND) is an implementation of AdvisoryID: "RHSA-2016:0073", Severity: "Moderate", Issued: issued, - Description: "The Berkeley Internet Name Domain (BIND) is an implementation of ", + Description: "The Berkeley Internet Name Domain (BIND) is an implementation of\n", }, CveIDs: []string{ "CVE-2015-8704", @@ -543,6 +543,8 @@ Description : Package updates are available for Amazon Linux AMI that fix the : CVE-2015-3195 : CVE-2015-3196 Description : Package updates are available for Amazon Linux AMI that fix the + : foo bar baz + : hoge fuga hega Severity : medium`, []distroAdvisoryCveIDs{ @@ -551,7 +553,7 @@ Description : Package updates are available for Amazon Linux AMI that fix the AdvisoryID: "ALAS-2016-644", Severity: "medium", Issued: issued, - Description: "Package updates are available for Amazon Linux AMI that fix the ", + Description: "Package updates are available for Amazon Linux AMI that fix the\n", }, CveIDs: []string{"CVE-2016-1494"}, }, @@ -561,7 +563,7 @@ Description : Package updates are available for Amazon Linux AMI that fix the Severity: "medium", Issued: issued, Updated: updated, - Description: "Package updates are available for Amazon Linux AMI that fix the ", + Description: "Package updates are available for Amazon Linux AMI that fix the\nfoo bar baz\nhoge fuga hega\n", }, CveIDs: []string{ "CVE-2015-3194", From 9b6d84def65727fae45ad81dc649b3ba974c3365 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Wed, 26 Jul 2017 16:02:09 +0900 Subject: [PATCH 062/113] Fix false positive detection on RHEL, Amazon and Oracle --- report/tui.go | 1 + scan/redhat.go | 29 +++++++++++++++++++++++------ scan/redhat_test.go | 38 ++++++++++++++++++++++++++++++++------ 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/report/tui.go b/report/tui.go index 36a3d635..1a5c554f 100644 --- a/report/tui.go +++ b/report/tui.go @@ -824,6 +824,7 @@ Summary -------------- {{.Summary }} + Links -------------- {{range $link := .Links -}} diff --git a/scan/redhat.go b/scan/redhat.go index f1673a86..944d7b31 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -746,7 +746,7 @@ func (o *redhat) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveID cveIDsSetInThisSection := make(map[string]bool) // use this flag to Collect CVE IDs in CVEs field. - var inDesctiption = false + inDesctiption, inCves := false, false for _, line := range lines { line = strings.TrimSpace(line) @@ -766,7 +766,7 @@ func (o *redhat) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveID // reset for next section. cveIDsSetInThisSection = make(map[string]bool) - inDesctiption = false + inDesctiption, inCves = false, false advisory = models.DistroAdvisory{} } @@ -789,7 +789,7 @@ func (o *redhat) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveID case Content: if found := o.isDescriptionLine(line); found { - inDesctiption = true + inDesctiption, inCves = true, false ss := strings.Split(line, " : ") advisory.Description += fmt.Sprintf("%s\n", strings.Join(ss[1:len(ss)], " : ")) @@ -811,9 +811,22 @@ func (o *redhat) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveID continue } - cveIDs := o.parseYumUpdateinfoLineToGetCveIDs(line) - for _, cveID := range cveIDs { - cveIDsSetInThisSection[cveID] = true + if found := o.isCvesHeaderLine(line); found { + inCves = true + ss := strings.Split(line, "CVEs : ") + line = strings.Join(ss[1:len(ss)], " ") + cveIDs := o.parseYumUpdateinfoLineToGetCveIDs(line) + for _, cveID := range cveIDs { + cveIDsSetInThisSection[cveID] = true + } + continue + } + + if inCves { + cveIDs := o.parseYumUpdateinfoLineToGetCveIDs(line) + for _, cveID := range cveIDs { + cveIDsSetInThisSection[cveID] = true + } } advisoryID, found := o.parseYumUpdateinfoToGetAdvisoryID(line) @@ -855,6 +868,10 @@ func (o *redhat) changeSectionState(state int) (newState int) { return newState } +func (o *redhat) isCvesHeaderLine(line string) bool { + return strings.Contains(line, "CVEs : ") +} + var yumCveIDPattern = regexp.MustCompile(`(CVE-\d{4}-\d{4,})`) func (o *redhat) parseYumUpdateinfoLineToGetCveIDs(line string) []string { diff --git a/scan/redhat_test.go b/scan/redhat_test.go index 8c051aac..2ee03ce1 100644 --- a/scan/redhat_test.go +++ b/scan/redhat_test.go @@ -435,11 +435,25 @@ Description : The Berkeley Internet Name Domain (BIND) is an implementation of Type : security Status : final Issued : 2015-09-03 02:00:00 - Bugs : 1299364 - CVE-2015-8704 bind: specific APL data could trigger an INSIST in apl_42.c CVEs : CVE-2015-8704 + Bugs : 1299364 - CVE-2015-8704 bind: specific APL data could trigger an INSIST in apl_42.c + CVEs : CVE-2015-8704 : CVE-2015-8705 Description : The Berkeley Internet Name Domain (BIND) is an implementation of + : CVE-2015-10000 Severity : Moderate +=============================================================================== + Moderate: sudo security update +=============================================================================== + Update ID : RHSA-2017:1574 + Release : 0 + Type : security + Status : final + Issued : 2015-09-03 02:00:00 + Bugs : 1459152 - CVE-2017-1000368 sudo: Privilege escalation via improper get_process_ttyname() parsing (insufficient fix for CVE-2017-1000367) CVEs : CVE-2017-1000368 +Description : The sudo packages contain the sudo utility which allows system + : administrators to provide certain users with the + Severity : Moderate ` issued, _ := time.Parse("2006-01-02", "2015-09-03") updated, _ := time.Parse("2006-01-02", "2015-09-04") @@ -481,13 +495,24 @@ Description : The Berkeley Internet Name Domain (BIND) is an implementation of AdvisoryID: "RHSA-2016:0073", Severity: "Moderate", Issued: issued, - Description: "The Berkeley Internet Name Domain (BIND) is an implementation of\n", + Description: "The Berkeley Internet Name Domain (BIND) is an implementation of\nCVE-2015-10000\n", }, CveIDs: []string{ "CVE-2015-8704", "CVE-2015-8705", }, }, + { + DistroAdvisory: models.DistroAdvisory{ + AdvisoryID: "RHSA-2017:1574", + Severity: "Moderate", + Issued: issued, + Description: "The sudo packages contain the sudo utility which allows system\nadministrators to provide certain users with the\n", + }, + CveIDs: []string{ + "CVE-2017-1000368", + }, + }, }, }, } @@ -499,7 +524,7 @@ Description : The Berkeley Internet Name Domain (BIND) is an implementation of if !reflect.DeepEqual(tt.out[i], advisoryCveIDs) { e := pp.Sprintf("%v", tt.out[i]) a := pp.Sprintf("%v", advisoryCveIDs) - t.Errorf("[%d] Alas is not same. \nexpected: %s\nactual: %s", + t.Errorf("[%d] not same. \nexpected: %s\nactual: %s", i, e, a) } } @@ -512,7 +537,7 @@ func TestParseYumUpdateinfoAmazon(t *testing.T) { r.Distro = config.Distro{Family: "redhat"} issued, _ := time.Parse("2006-01-02", "2015-12-15") - updated, _ := time.Parse("2006-01-02", "2015-12-16") + // updated, _ := time.Parse("2006-01-02", "2015-12-16") var tests = []struct { in string @@ -529,6 +554,8 @@ func TestParseYumUpdateinfoAmazon(t *testing.T) { Issued : 2015-12-15 13:30 CVEs : CVE-2016-1494 Description : Package updates are available for Amazon Linux AMI that fix the + : CVE-20160-1111 + : hogehoge Severity : medium =============================================================================== @@ -553,7 +580,7 @@ Description : Package updates are available for Amazon Linux AMI that fix the AdvisoryID: "ALAS-2016-644", Severity: "medium", Issued: issued, - Description: "Package updates are available for Amazon Linux AMI that fix the\n", + Description: "Package updates are available for Amazon Linux AMI that fix the\nCVE-20160-1111\nhogehoge\n", }, CveIDs: []string{"CVE-2016-1494"}, }, @@ -562,7 +589,6 @@ Description : Package updates are available for Amazon Linux AMI that fix the AdvisoryID: "ALAS-2015-614", Severity: "medium", Issued: issued, - Updated: updated, Description: "Package updates are available for Amazon Linux AMI that fix the\nfoo bar baz\nhoge fuga hega\n", }, CveIDs: []string{ From b1428b6758692e7b421bfeb37d26afc3e6c0a9dc Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Wed, 26 Jul 2017 18:49:05 +0900 Subject: [PATCH 063/113] Fix a bug of fill oval information of Ubuntu --- oval/debian.go | 3 ++- oval/redhat.go | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/oval/debian.go b/oval/debian.go index 5e077689..9bccf21d 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -18,6 +18,7 @@ type DebianBase struct{ Base } // fillFromOvalDB returns scan result after updating CVE info by OVAL func (o DebianBase) fillFromOvalDB(r *models.ScanResult) error { + ovalconf.Conf.DebugSQL = config.Conf.DebugSQL ovalconf.Conf.DBType = config.Conf.OvalDBType ovalconf.Conf.DBPath = config.Conf.OvalDBPath if ovalconf.Conf.DBType == "sqlite3" { @@ -33,7 +34,7 @@ func (o DebianBase) fillFromOvalDB(r *models.ScanResult) error { var err error var ovaldb db.DB if ovaldb, err = db.NewDB( - ovalconf.Debian, + r.Family, ovalconf.Conf.DBType, ovalconf.Conf.DBPath, ovalconf.Conf.DebugSQL, diff --git a/oval/redhat.go b/oval/redhat.go index cfc174dd..d0ff57e8 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -57,6 +57,7 @@ func (o RedHatBase) fillFromOvalDB(r *models.ScanResult) error { func (o RedHatBase) getDefsByPackNameFromOvalDB(osRelease string, packs models.Packages) (relatedDefs []ovalmodels.Definition, err error) { + ovalconf.Conf.DebugSQL = config.Conf.DebugSQL ovalconf.Conf.DBType = config.Conf.OvalDBType if ovalconf.Conf.DBType == "sqlite3" { ovalconf.Conf.DBPath = config.Conf.OvalDBPath From 17527367144f25435b62517802b9486a90407163 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Fri, 28 Jul 2017 19:59:50 +0900 Subject: [PATCH 064/113] Fix nil pointer --- scan/debian.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scan/debian.go b/scan/debian.go index a7ecffb3..e1609d63 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -462,6 +462,9 @@ func (o *debian) scanVulnInfos(updatablePacks models.Packages, meta *cache.Meta) for i := 0; i < len(updatablePacks); i++ { select { case response := <-resChan: + if response.pack == nil { + continue + } o.Packages[response.pack.Name] = *response.pack cves := response.DetectedCveIDs for _, cve := range cves { @@ -511,7 +514,7 @@ func (o *debian) scanVulnInfos(updatablePacks models.Packages, meta *cache.Meta) func (o *debian) getChangelogCache(meta *cache.Meta, pack models.Package) string { cachedPack, found := meta.Packs[pack.Name] if !found { - o.log.Debugf("Not found: %s", pack.Name) + o.log.Debugf("Not found in cache: %s", pack.Name) return "" } From 56603dcfae7564564c88abd605897f8904042c5f Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Sat, 29 Jul 2017 12:46:17 +0900 Subject: [PATCH 065/113] Fix a bug of lower limit of cursor movement in TUI --- report/tui.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/report/tui.go b/report/tui.go index 1a5c554f..bdf52348 100644 --- a/report/tui.go +++ b/report/tui.go @@ -40,6 +40,7 @@ var currentScanResult models.ScanResult var vinfos []models.VulnInfo var currentVinfo int var currentDetailLimitY int +var currentChangelogLimitY int // RunTui execute main logic func RunTui(results models.ScanResults) subcommands.ExitStatus { @@ -237,10 +238,10 @@ func movable(v *gocui.View, nextY int) (ok bool, yLimit int) { } return true, currentDetailLimitY case "changelog": - if currentDetailLimitY < nextY { - return false, currentDetailLimitY + if currentChangelogLimitY < nextY { + return false, currentChangelogLimitY } - return true, currentDetailLimitY + return true, currentChangelogLimitY default: return true, 0 } @@ -725,7 +726,7 @@ func setChangelogLayout(g *gocui.Gui) error { v.Editable = false v.Wrap = true - currentDetailLimitY = len(strings.Split(text, "\n")) - 1 + currentChangelogLimitY = len(strings.Split(text, "\n")) - 1 } return nil } From 4379b8bacf726cba1dcaf4ae3fcda59fa83f486c Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Sat, 29 Jul 2017 14:02:06 +0900 Subject: [PATCH 066/113] Use version comparison logic when parsing change log (Ubuntu, Debian) --- models/cvecontents.go | 2 +- scan/debian.go | 48 +++++++++++++------ scan/debian_test.go | 107 ++++++++++++++++++++---------------------- 3 files changed, 85 insertions(+), 72 deletions(-) diff --git a/models/cvecontents.go b/models/cvecontents.go index f86607d2..6fa75853 100644 --- a/models/cvecontents.go +++ b/models/cvecontents.go @@ -160,7 +160,7 @@ func (v CveContents) MaxCvss2Score() CveContentCvss { return value } - // If CVSS score isn't on NVD, RedHat and JVN use OVAL's Severity information. + // If CVSS score isn't on NVD, RedHat and JVN, use OVAL's Severity information. // Convert severity to cvss srore, then returns max severity. // Only Ubuntu, RedHat and Oracle OVAL has severity data. order = []CveContentType{Ubuntu, RedHat, Oracle} diff --git a/scan/debian.go b/scan/debian.go index e1609d63..a3601305 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -18,6 +18,7 @@ along with this program. If not, see . package scan import ( + "bufio" "fmt" "regexp" "strconv" @@ -28,6 +29,8 @@ import ( "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/util" + + "github.com/knqyf263/go-deb-version" ) // inherit OsTypeInterface @@ -571,9 +574,6 @@ func (o *debian) scanPackageCveIDs(pack models.Package) ([]DetectedCveID, *model return cveIDs, clogFilledPack, nil } -// Debian Version Numbers -// https://readme.phys.ethz.ch/documentation/debian_version_numbers/ -// TODO Changed to parse and compare versions func (o *debian) getCveIDsFromChangelog( changelog, name, ver string) ([]DetectedCveID, *models.Package) { @@ -636,25 +636,43 @@ func (o *debian) getCveIDsFromChangelog( var cveRe = regexp.MustCompile(`(CVE-\d{4}-\d{4,})`) // Collect CVE-IDs included in the changelog. -// The version which specified in argument(versionOrLater) is excluded. +// The version specified in argument(versionOrLater) is used to compare. func (o *debian) parseChangelog(changelog, name, ver string, confidence models.Confidence) ([]DetectedCveID, *models.Package, error) { + installedVer, err := version.NewVersion(ver) + if err != nil { + return nil, nil, fmt.Errorf("Failed to parse installed version: %s, %s", ver, err) + } buf, cveIDs := []string{}, []string{} - stopRe := regexp.MustCompile(fmt.Sprintf(`\(%s\)`, regexp.QuoteMeta(ver))) - stopLineFound := false - lines := strings.Split(changelog, "\n") - for _, line := range lines { + scanner := bufio.NewScanner(strings.NewReader(changelog)) + found := false + for scanner.Scan() { + line := scanner.Text() buf = append(buf, line) - if match := stopRe.MatchString(line); match { - // o.log.Debugf("Found the stop line: %s", line) - stopLineFound = true - break - } else if matches := cveRe.FindAllString(line, -1); 0 < len(matches) { + if matches := cveRe.FindAllString(line, -1); 0 < len(matches) { for _, m := range matches { cveIDs = util.AppendIfMissing(cveIDs, m) } } + + ss := strings.Fields(line) + if len(ss) < 2 { + continue + } + + if !strings.HasPrefix(ss[1], "(") || !strings.HasSuffix(ss[1], ")") { + continue + } + clogVer, err := version.NewVersion(ss[1][1 : len(ss[1])-1]) + if err != nil { + continue + } + if installedVer.Equal(clogVer) || installedVer.GreaterThan(clogVer) { + found = true + break + } } - if !stopLineFound { + + if !found { pack := o.Packages[name] pack.Changelog = models.Changelog{ Contents: "", @@ -666,7 +684,7 @@ func (o *debian) parseChangelog(changelog, name, ver string, confidence models.C } clog := models.Changelog{ - Contents: strings.Join(buf, "\n"), + Contents: strings.Join(buf[0:len(buf)-1], "\n"), Method: confidence.DetectionMethod, } pack := o.Packages[name] diff --git a/scan/debian_test.go b/scan/debian_test.go index 11492477..269214e9 100644 --- a/scan/debian_test.go +++ b/scan/debian_test.go @@ -66,7 +66,7 @@ func TestGetCveIDsFromChangelog(t *testing.T) { changelog models.Changelog }{ { - // verubuntu1 + //0 verubuntu1 []string{ "systemd", "228-4ubuntu1", @@ -81,9 +81,9 @@ systemd (228-4) unstable; urgency=medium systemd (228-3) unstable; urgency=medium`, }, []DetectedCveID{ - {"CVE-2015-2325", models.ChangelogLenientMatch}, - {"CVE-2015-2326", models.ChangelogLenientMatch}, - {"CVE-2015-3210", models.ChangelogLenientMatch}, + {"CVE-2015-2325", models.ChangelogExactMatch}, + {"CVE-2015-2326", models.ChangelogExactMatch}, + {"CVE-2015-3210", models.ChangelogExactMatch}, }, models.Changelog{ Contents: `systemd (229-2) unstable; urgency=medium @@ -92,13 +92,12 @@ systemd (228-6) unstable; urgency=medium CVE-2015-2325: heap buffer overflow in compile_branch(). (Closes: #781795) CVE-2015-2326: heap buffer overflow in pcre_compile2(). (Closes: #783285) CVE-2015-3210: heap buffer overflow in pcre_compile2() / -systemd (228-5) unstable; urgency=medium -systemd (228-4) unstable; urgency=medium`, - Method: models.ChangelogLenientMatchStr, +systemd (228-5) unstable; urgency=medium`, + Method: models.ChangelogExactMatchStr, }, }, { - // ver + //1 ver []string{ "libpcre3", "2:8.35-7.1ubuntu1", @@ -115,9 +114,9 @@ systemd (228-4) unstable; urgency=medium`, pcre3 (2:8.35-7) unstable; urgency=medium`, }, []DetectedCveID{ - {"CVE-2015-2325", models.ChangelogLenientMatch}, - {"CVE-2015-2326", models.ChangelogLenientMatch}, - {"CVE-2015-3210", models.ChangelogLenientMatch}, + {"CVE-2015-2325", models.ChangelogExactMatch}, + {"CVE-2015-2326", models.ChangelogExactMatch}, + {"CVE-2015-3210", models.ChangelogExactMatch}, }, models.Changelog{ Contents: `pcre3 (2:8.38-2) unstable; urgency=low @@ -128,13 +127,12 @@ systemd (228-4) unstable; urgency=medium`, pcre3 (2:8.35-7.2) unstable; urgency=low CVE-2015-2325: heap buffer overflow in compile_branch(). (Closes: #781795) CVE-2015-2326: heap buffer overflow in pcre_compile2(). (Closes: #783285) - CVE-2015-3210: heap buffer overflow in pcre_compile2() / - pcre3 (2:8.35-7.1) unstable; urgency=medium`, - Method: models.ChangelogLenientMatchStr, + CVE-2015-3210: heap buffer overflow in pcre_compile2() /`, + Method: models.ChangelogExactMatchStr, }, }, { - // ver-ubuntu3 + //2 ver-ubuntu3 []string{ "sysvinit", "2.88dsf-59.2ubuntu3", @@ -168,13 +166,12 @@ systemd (228-4) unstable; urgency=medium`, sysvinit (2.88dsf-59.3) unstable; urgency=medium CVE-2015-2325: heap buffer overflow in compile_branch(). (Closes: #781795) CVE-2015-2326: heap buffer overflow in pcre_compile2(). (Closes: #783285) - CVE-2015-3210: heap buffer overflow in pcre_compile2() / - sysvinit (2.88dsf-59.2ubuntu3) xenial; urgency=medium`, + CVE-2015-3210: heap buffer overflow in pcre_compile2() /`, Method: models.ChangelogExactMatchStr, }, }, { - // 1:ver-ubuntu3 + //3 1:ver-ubuntu3 []string{ "bsdutils", "1:2.27.1-1ubuntu3", @@ -192,25 +189,25 @@ systemd (228-4) unstable; urgency=medium`, util-linux (2.27-3ubuntu1) xenial; urgency=medium`, }, []DetectedCveID{ - {"CVE-2015-2325", models.ChangelogLenientMatch}, - {"CVE-2015-2326", models.ChangelogLenientMatch}, - {"CVE-2015-3210", models.ChangelogLenientMatch}, - {"CVE-2016-1000000", models.ChangelogLenientMatch}, + // {"CVE-2015-2325", models.ChangelogLenientMatch}, + // {"CVE-2015-2326", models.ChangelogLenientMatch}, + // {"CVE-2015-3210", models.ChangelogLenientMatch}, + // {"CVE-2016-1000000", models.ChangelogLenientMatch}, }, models.Changelog{ - Contents: `util-linux (2.27.1-3ubuntu1) xenial; urgency=medium - util-linux (2.27.1-3) unstable; urgency=medium - CVE-2015-2325: heap buffer overflow in compile_branch(). (Closes: #781795) - CVE-2015-2326: heap buffer overflow in pcre_compile2(). (Closes: #783285) - CVE-2015-3210: CVE-2016-1000000heap buffer overflow in pcre_compile2() / - util-linux (2.27.1-2) unstable; urgency=medium - util-linux (2.27.1-1ubuntu4) xenial; urgency=medium - util-linux (2.27.1-1ubuntu3) xenial; urgency=medium`, - Method: models.ChangelogLenientMatchStr, + // Contents: `util-linux (2.27.1-3ubuntu1) xenial; urgency=medium + // util-linux (2.27.1-3) unstable; urgency=medium + // CVE-2015-2325: heap buffer overflow in compile_branch(). (Closes: #781795) + // CVE-2015-2326: heap buffer overflow in pcre_compile2(). (Closes: #783285) + // CVE-2015-3210: CVE-2016-1000000heap buffer overflow in pcre_compile2() / + // util-linux (2.27.1-2) unstable; urgency=medium + // util-linux (2.27.1-1ubuntu4) xenial; urgency=medium + // util-linux (2.27.1-1ubuntu3) xenial; urgency=medium`, + Method: models.ChangelogExactMatchStr, }, }, { - // 1:ver-ubuntu3 + //4 1:ver-ubuntu3 []string{ "bsdutils", "1:2.27-3ubuntu3", @@ -228,29 +225,28 @@ systemd (228-4) unstable; urgency=medium`, util-linux (2.27-3) xenial; urgency=medium`, }, []DetectedCveID{ - {"CVE-2015-2325", models.ChangelogLenientMatch}, - {"CVE-2015-2326", models.ChangelogLenientMatch}, - {"CVE-2015-3210", models.ChangelogLenientMatch}, - {"CVE-2016-1000000", models.ChangelogLenientMatch}, + // {"CVE-2015-2325", models.ChangelogLenientMatch}, + // {"CVE-2015-2326", models.ChangelogLenientMatch}, + // {"CVE-2015-3210", models.ChangelogLenientMatch}, + // {"CVE-2016-1000000", models.ChangelogLenientMatch}, }, models.Changelog{ - Contents: `util-linux (2.27.1-3ubuntu1) xenial; urgency=medium - util-linux (2.27.1-3) unstable; urgency=medium - CVE-2015-2325: heap buffer overflow in compile_branch(). (Closes: #781795) - CVE-2015-2326: heap buffer overflow in pcre_compile2(). (Closes: #783285) - CVE-2015-3210: CVE-2016-1000000heap buffer overflow in pcre_compile2() / - util-linux (2.27.1-2) unstable; urgency=medium - util-linux (2.27.1-1ubuntu4) xenial; urgency=medium - util-linux (2.27.1-1ubuntu3) xenial; urgency=medium - util-linux (2.27.1-1ubuntu2) xenial; urgency=medium - util-linux (2.27.1-1ubuntu1) xenial; urgency=medium - util-linux (2.27.1-1) unstable; urgency=medium - util-linux (2.27-3) xenial; urgency=medium`, - Method: models.ChangelogLenientMatchStr, + // Contents: `util-linux (2.27.1-3ubuntu1) xenial; urgency=medium + // util-linux (2.27.1-3) unstable; urgency=medium + // CVE-2015-2325: heap buffer overflow in compile_branch(). (Closes: #781795) + // CVE-2015-2326: heap buffer overflow in pcre_compile2(). (Closes: #783285) + // CVE-2015-3210: CVE-2016-1000000heap buffer overflow in pcre_compile2() / + // util-linux (2.27.1-2) unstable; urgency=medium + // util-linux (2.27.1-1ubuntu4) xenial; urgency=medium + // util-linux (2.27.1-1ubuntu3) xenial; urgency=medium + // util-linux (2.27.1-1ubuntu2) xenial; urgency=medium + // util-linux (2.27.1-1ubuntu1) xenial; urgency=medium + // util-linux (2.27.1-1) unstable; urgency=medium`, + Method: models.ChangelogExactMatchStr, }, }, { - // https://github.com/future-architect/vuls/pull/350 + //5 https://github.com/future-architect/vuls/pull/350 []string{ "tar", "1.27.1-2+b1", @@ -259,13 +255,12 @@ systemd (228-4) unstable; urgency=medium`, tar (1.27.1-2) unstable; urgency=low`, }, []DetectedCveID{ - {"CVE-2016-6321", models.ChangelogLenientMatch}, + {"CVE-2016-6321", models.ChangelogExactMatch}, }, models.Changelog{ Contents: `tar (1.27.1-2+deb8u1) jessie-security; urgency=high - * CVE-2016-6321: Bypassing the extract path name. - tar (1.27.1-2) unstable; urgency=low`, - Method: models.ChangelogLenientMatchStr, + * CVE-2016-6321: Bypassing the extract path name.`, + Method: models.ChangelogExactMatchStr, }, }, } @@ -286,11 +281,11 @@ systemd (228-4) unstable; urgency=medium`, } if aPack.Changelog.Contents != tt.changelog.Contents { - t.Errorf(pp.Sprintf("expected: %s, actual: %s", tt.changelog.Contents, aPack.Changelog.Contents)) + t.Error(pp.Sprintf("[%d] expected: %s, actual: %s", i, tt.changelog.Contents, aPack.Changelog.Contents)) } if aPack.Changelog.Method != tt.changelog.Method { - t.Errorf(pp.Sprintf("expected: %s, actual: %s", tt.changelog.Method, aPack.Changelog.Method)) + t.Error(pp.Sprintf("[%d] expected: %s, actual: %s", i, tt.changelog.Method, aPack.Changelog.Method)) } } } From 8b6a2831143d69478a463596b46d56ee1a66d4a2 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Sun, 30 Jul 2017 09:41:40 +0900 Subject: [PATCH 067/113] Add a deep flag to scan --- commands/scan.go | 38 +++++++++++++++++++------------------- config/config.go | 8 ++++---- report/util.go | 7 ------- scan/debian.go | 2 +- scan/redhat.go | 11 ++++++----- 5 files changed, 30 insertions(+), 36 deletions(-) diff --git a/commands/scan.go b/commands/scan.go index 0ce52481..623b518d 100644 --- a/commands/scan.go +++ b/commands/scan.go @@ -35,20 +35,20 @@ import ( // ScanCmd is Subcommand of host discovery mode type ScanCmd struct { - debug bool - configPath string - resultsDir string - logDir string - cacheDBPath string - httpProxy string - askKeyPassword bool - containersOnly bool - packageListOnly bool - skipBroken bool - sshNative bool - pipe bool - timeoutSec int - scanTimeoutSec int + debug bool + configPath string + resultsDir string + logDir string + cacheDBPath string + httpProxy string + askKeyPassword bool + containersOnly bool + deep bool + skipBroken bool + sshNative bool + pipe bool + timeoutSec int + scanTimeoutSec int } // Name return subcommand name @@ -61,13 +61,13 @@ func (*ScanCmd) Synopsis() string { return "Scan vulnerabilities" } func (*ScanCmd) Usage() string { return `scan: scan + [-deep] [-config=/path/to/config.toml] [-results-dir=/path/to/results] [-log-dir=/path/to/log] [-cachedb-path=/path/to/cache.db] [-ssh-native-insecure] [-containers-only] - [-package-list-only] [-skip-broken] [-http-proxy=http://192.168.0.1:8080] [-ask-key-password] @@ -135,10 +135,10 @@ func (p *ScanCmd) SetFlags(f *flag.FlagSet) { ) f.BoolVar( - &p.packageListOnly, - "package-list-only", + &p.deep, + "deep", false, - "List all packages without scan") + "Deep scan mode. Scan accuracy improves and information becomes richer. Since analysis of changelog, issue commands requiring sudo, but is slower and heavy") f.BoolVar( &p.pipe, @@ -231,7 +231,7 @@ func (p *ScanCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) c.Conf.SSHNative = p.sshNative c.Conf.HTTPProxy = p.httpProxy c.Conf.ContainersOnly = p.containersOnly - c.Conf.PackageListOnly = p.packageListOnly + c.Conf.Deep = p.deep c.Conf.SkipBroken = p.skipBroken util.Log.Info("Validating config...") diff --git a/config/config.go b/config/config.go index edbaa433..296401fa 100644 --- a/config/config.go +++ b/config/config.go @@ -74,10 +74,10 @@ type Config struct { CvssScoreOver float64 IgnoreUnscoredCves bool - SSHNative bool - ContainersOnly bool - PackageListOnly bool - SkipBroken bool + SSHNative bool + ContainersOnly bool + Deep bool + SkipBroken bool HTTPProxy string `valid:"url"` LogDir string diff --git a/report/util.go b/report/util.go index 3122029d..f52c9f66 100644 --- a/report/util.go +++ b/report/util.go @@ -43,16 +43,9 @@ func formatScanSummary(rs ...models.ScanResult) string { for _, r := range rs { var cols []interface{} if len(r.Errors) == 0 { - var cves string - if config.Conf.PackageListOnly { - cves = fmt.Sprintf("- CVEs") - } else { - cves = fmt.Sprintf("%d CVEs", len(r.ScannedCves)) - } cols = []interface{}{ r.FormatServerName(), fmt.Sprintf("%s%s", r.Family, r.Release), - cves, r.Packages.FormatUpdatablePacksSummary(), } } else { diff --git a/scan/debian.go b/scan/debian.go index a3601305..6cae65cd 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -177,7 +177,7 @@ func (o *debian) scanPackages() error { } o.setPackages(installed) - if config.Conf.PackageListOnly { + if !config.Conf.Deep { return nil } diff --git a/scan/redhat.go b/scan/redhat.go index 944d7b31..f08b96a1 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -250,7 +250,7 @@ func (o *redhat) scanPackages() error { installed.MergeNewVersion(updatable) o.setPackages(installed) - if config.Conf.PackageListOnly { + if !config.Conf.Deep && o.Distro.Family != config.Amazon { return nil } @@ -373,10 +373,11 @@ func (o *redhat) parseUpdatablePacksLine(line string) (models.Package, error) { } func (o *redhat) scanUnsecurePackages(updatable models.Packages) (models.VulnInfos, error) { - //TODO Cache changelogs to bolt - //TODO --with-changelog - if err := o.fillChangelogs(updatable); err != nil { - return nil, err + if config.Conf.Deep { + //TODO Cache changelogs to bolt + if err := o.fillChangelogs(updatable); err != nil { + return nil, err + } } if o.Distro.Family != config.CentOS { From 27724a2fafc4301c803763ac3b88270e298b4ac5 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Sun, 30 Jul 2017 15:21:26 +0900 Subject: [PATCH 068/113] Use CVSS seveirty of distro advisory when no entiry in NVD and OVAL --- models/cvecontents.go | 297 ++------------------------- models/cvecontents_test.go | 386 ----------------------------------- models/scanresults.go | 4 +- models/vulninfos.go | 302 ++++++++++++++++++++++++++- models/vulninfos_test.go | 408 +++++++++++++++++++++++++++++++++++++ report/report.go | 5 +- report/slack.go | 6 +- report/tui.go | 6 +- report/util.go | 18 +- 9 files changed, 740 insertions(+), 692 deletions(-) diff --git a/models/cvecontents.go b/models/cvecontents.go index 6fa75853..fc3566f6 100644 --- a/models/cvecontents.go +++ b/models/cvecontents.go @@ -18,7 +18,6 @@ along with this program. If not, see . package models import ( - "fmt" "strings" "time" ) @@ -59,274 +58,6 @@ func (v CveContents) Except(exceptCtypes ...CveContentType) (values CveContents) return } -// CveContentCvss has CveContentType and Cvss2 -type CveContentCvss struct { - Type CveContentType - Value Cvss -} - -// CvssType Represent the type of CVSS -type CvssType string - -const ( - // CVSS2 means CVSS vesion2 - CVSS2 CvssType = "2" - - // CVSS3 means CVSS vesion3 - CVSS3 CvssType = "3" -) - -// Cvss has CVSS Score -type Cvss struct { - Type CvssType - Score float64 - Vector string - Severity string -} - -// Format CVSS Score and Vector -func (c Cvss) Format() string { - switch c.Type { - case CVSS2: - return fmt.Sprintf("%3.1f/%s", c.Score, c.Vector) - case CVSS3: - return fmt.Sprintf("%3.1f/CVSS:3.0/%s", c.Score, c.Vector) - } - return "" -} - -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 []CveContentCvss) { - 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, CveContentCvss{ - Type: ctype, - Value: Cvss{ - Type: CVSS2, - Score: cont.Cvss2Score, - Vector: cont.Cvss2Vector, - Severity: sev, - }, - }) - } - } - - return -} - -// MaxCvss2Score returns Max CVSS V2 Score -func (v CveContents) MaxCvss2Score() CveContentCvss { - order := []CveContentType{NVD, RedHat, JVN} - max := 0.0 - value := CveContentCvss{ - Type: Unknown, - Value: Cvss{Type: 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 = CveContentCvss{ - Type: ctype, - Value: Cvss{ - Type: CVSS2, - Score: cont.Cvss2Score, - Vector: cont.Cvss2Vector, - Severity: sev, - }, - } - max = cont.Cvss2Score - } - } - if 0 < max { - return value - } - - // If CVSS score isn't on NVD, RedHat and JVN, use OVAL's Severity information. - // Convert severity to cvss srore, then returns max severity. - // Only Ubuntu, RedHat and Oracle OVAL has severity data. - order = []CveContentType{Ubuntu, RedHat, Oracle} - for _, ctype := range order { - if cont, found := v[ctype]; found && 0 < len(cont.Severity) { - score := 0.0 - switch cont.Type { - case Ubuntu: - score = severityToScoreForUbuntu(cont.Severity) - case Oracle, RedHat: - score = severityToScoreForRedHat(cont.Severity) - } - if max < score { - value = CveContentCvss{ - Type: ctype, - Value: Cvss{ - Type: CVSS2, - Score: score, - Vector: cont.Cvss2Vector, - Severity: cont.Severity, - }, - } - } - max = score - } - } - return value -} - -// Convert Severity to Score for Ubuntu OVAL -func severityToScoreForUbuntu(severity string) float64 { - switch strings.ToUpper(severity) { - case "HIGH": - return 10.0 - case "MEDIUM": - return 6.9 - case "LOW": - return 3.9 - } - return 0 -} - -// Convert Severity to Score for RedHat, Oracle OVAL -// https://access.redhat.com/security/updates/classification -// Use the definition of CVSSv3 because the exact definition of severity and score is not described. -func severityToScoreForRedHat(severity string) float64 { - switch strings.ToUpper(severity) { - case "CRITICAL": - return 10.0 - case "IMPORTANT": - return 8.9 - case "MODERATE": - return 6.9 - case "LOW": - return 3.9 - } - return 0 -} - -// CveContentCvss3 has CveContentType and Cvss3 -// type CveContentCvss3 struct { -// Type CveContentType -// Value Cvss3 -// } - -// 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" -// } 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 []CveContentCvss) { - // TODO implement NVD - 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 - values = append(values, CveContentCvss{ - Type: ctype, - Value: Cvss{ - Type: CVSS3, - Score: cont.Cvss3Score, - Vector: cont.Cvss3Vector, - Severity: sev, - }, - }) - } - } - return -} - -// MaxCvss3Score returns Max CVSS V3 Score -func (v CveContents) MaxCvss3Score() CveContentCvss { - // TODO implement NVD - order := []CveContentType{RedHat} - max := 0.0 - value := CveContentCvss{ - Type: Unknown, - Value: Cvss{Type: CVSS3}, - } - for _, ctype := range order { - if cont, found := v[ctype]; found && max < cont.Cvss3Score { - // https://nvd.nist.gov/vuln-metrics/cvss - sev := cont.Severity - value = CveContentCvss{ - Type: ctype, - Value: Cvss{ - Type: CVSS3, - Score: cont.Cvss3Score, - Vector: cont.Cvss3Vector, - Severity: sev, - }, - } - max = cont.Cvss3Score - } - } - return value -} - -// MaxCvssScore returns max CVSS Score -// If there is no CVSS Score, return Severity as a numerical value. -func (v CveContents) MaxCvssScore() CveContentCvss { - v3Max := v.MaxCvss3Score() - v2Max := v.MaxCvss2Score() - max := v3Max - if max.Value.Score < v2Max.Value.Score { - max = v2Max - } - return max -} - -// 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" { @@ -417,21 +148,23 @@ func (v CveContents) SourceLinks(lang, myFamily, cveID string) (values []CveCont return values } +/* // Severities returns Severities -// func (v CveContents) Severities(myFamily string) (values []CveContentStr) { -// order := CveContentTypes{NVD, NewCveContentType(myFamily)} -// order = append(order, AllCveContetTypes.Except(append(order)...)...) +func (v CveContents) Severities(myFamily string) (values []CveContentStr) { + 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, CveContentStr{ -// Type: ctype, -// Value: cont.Severity, -// }) -// } -// } -// return -// } + for _, ctype := range order { + if cont, found := v[ctype]; found && 0 < len(cont.Severity) { + values = append(values, CveContentStr{ + Type: ctype, + Value: cont.Severity, + }) + } + } + return +} +*/ // CveContentCpes has CveContentType and Value type CveContentCpes struct { diff --git a/models/cvecontents_test.go b/models/cvecontents_test.go index f7e942ac..03a186f6 100644 --- a/models/cvecontents_test.go +++ b/models/cvecontents_test.go @@ -44,392 +44,6 @@ func TestExcept(t *testing.T) { } } -func TestCvss2Scores(t *testing.T) { - var tests = []struct { - in CveContents - out []CveContentCvss - }{ - { - in: CveContents{ - JVN: { - Type: JVN, - Severity: "HIGH", - Cvss2Score: 8.2, - Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", - }, - RedHat: { - Type: RedHat, - Severity: "HIGH", - Cvss2Score: 8.0, - Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", - }, - NVD: { - Type: NVD, - Cvss2Score: 8.1, - Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", - // Severity is NIOT included in NVD - }, - }, - out: []CveContentCvss{ - { - Type: NVD, - Value: Cvss{ - Type: CVSS2, - Score: 8.1, - Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", - Severity: "HIGH", - }, - }, - { - Type: RedHat, - Value: Cvss{ - Type: CVSS2, - Score: 8.0, - Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", - Severity: "HIGH", - }, - }, - { - Type: JVN, - Value: Cvss{ - Type: CVSS2, - Score: 8.2, - Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", - Severity: "HIGH", - }, - }, - }, - }, - // Empty - { - in: CveContents{}, - out: nil, - }, - } - for _, tt := range tests { - actual := tt.in.Cvss2Scores() - if !reflect.DeepEqual(tt.out, actual) { - t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) - } - } -} - -func TestMaxCvss2Scores(t *testing.T) { - var tests = []struct { - in CveContents - out CveContentCvss - }{ - { - in: CveContents{ - JVN: { - Type: JVN, - Severity: "HIGH", - Cvss2Score: 8.2, - Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", - }, - RedHat: { - Type: RedHat, - Severity: "HIGH", - Cvss2Score: 8.0, - Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", - }, - NVD: { - Type: NVD, - Cvss2Score: 8.1, - Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", - // Severity is NIOT included in NVD - }, - }, - out: CveContentCvss{ - Type: JVN, - Value: Cvss{ - Type: CVSS2, - Score: 8.2, - Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", - Severity: "HIGH", - }, - }, - }, - // Severity in OVAL - { - in: CveContents{ - Ubuntu: { - Type: Ubuntu, - Severity: "HIGH", - }, - }, - out: CveContentCvss{ - Type: Ubuntu, - Value: Cvss{ - Type: CVSS2, - Score: 10, - Severity: "HIGH", - }, - }, - }, - // Empty - { - in: CveContents{}, - out: CveContentCvss{ - Type: Unknown, - Value: Cvss{ - Type: CVSS2, - Score: 0.0, - Vector: "", - Severity: "", - }, - }, - }, - } - for _, tt := range tests { - actual := tt.in.MaxCvss2Score() - if !reflect.DeepEqual(tt.out, actual) { - t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) - } - } -} - -func TestCvss3Scores(t *testing.T) { - var tests = []struct { - in CveContents - out []CveContentCvss - }{ - { - in: CveContents{ - RedHat: { - Type: RedHat, - Severity: "HIGH", - Cvss3Score: 8.0, - Cvss3Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", - }, - NVD: { - Type: NVD, - Cvss3Score: 8.1, - Cvss3Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", - // Severity is NIOT included in NVD - }, - }, - out: []CveContentCvss{ - { - Type: RedHat, - Value: Cvss{ - Type: CVSS3, - Score: 8.0, - Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", - Severity: "HIGH", - }, - }, - }, - }, - // Empty - { - in: CveContents{}, - out: nil, - }, - } - for _, tt := range tests { - actual := tt.in.Cvss3Scores() - if !reflect.DeepEqual(tt.out, actual) { - t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) - } - } -} - -func TestMaxCvss3Scores(t *testing.T) { - var tests = []struct { - in CveContents - out CveContentCvss - }{ - { - in: CveContents{ - RedHat: { - Type: RedHat, - Severity: "HIGH", - Cvss3Score: 8.0, - Cvss3Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", - }, - }, - out: CveContentCvss{ - Type: RedHat, - Value: Cvss{ - Type: CVSS3, - Score: 8.0, - Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", - Severity: "HIGH", - }, - }, - }, - // Empty - { - in: CveContents{}, - out: CveContentCvss{ - Type: Unknown, - Value: Cvss{ - Type: CVSS3, - Score: 0.0, - Vector: "", - Severity: "", - }, - }, - }, - } - for _, tt := range tests { - actual := tt.in.MaxCvss3Score() - if !reflect.DeepEqual(tt.out, actual) { - t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) - } - } -} - -func TestMaxCvssScores(t *testing.T) { - var tests = []struct { - in CveContents - out CveContentCvss - }{ - { - in: CveContents{ - NVD: { - Type: NVD, - Cvss3Score: 7.0, - }, - RedHat: { - Type: RedHat, - Cvss2Score: 8.0, - }, - }, - out: CveContentCvss{ - Type: RedHat, - Value: Cvss{ - Type: CVSS2, - Score: 8.0, - }, - }, - }, - { - in: CveContents{ - RedHat: { - Type: RedHat, - Cvss3Score: 8.0, - }, - }, - out: CveContentCvss{ - Type: RedHat, - Value: Cvss{ - Type: CVSS3, - Score: 8.0, - }, - }, - }, - { - in: CveContents{ - Ubuntu: { - Type: Ubuntu, - Severity: "HIGH", - }, - }, - out: CveContentCvss{ - Type: Ubuntu, - Value: Cvss{ - Type: CVSS2, - Score: 10.0, - Severity: "HIGH", - }, - }, - }, - { - in: CveContents{ - Ubuntu: { - Type: Ubuntu, - Severity: "MEDIUM", - }, - NVD: { - Type: NVD, - Cvss2Score: 7.0, - }, - }, - out: CveContentCvss{ - Type: NVD, - Value: Cvss{ - Type: CVSS2, - Score: 7.0, - Severity: "HIGH", - }, - }, - }, - // Empty - { - in: CveContents{}, - out: CveContentCvss{ - Type: Unknown, - Value: Cvss{ - Type: CVSS3, - Score: 0, - }, - }, - }, - } - for i, tt := range tests { - actual := tt.in.MaxCvssScore() - if !reflect.DeepEqual(tt.out, actual) { - t.Errorf("\n[%d] expected: %v\n actual: %v\n", i, tt.out, actual) - } - } -} - -func TestFormatMaxCvssScore(t *testing.T) { - var tests = []struct { - in CveContents - out string - }{ - { - in: CveContents{ - JVN: { - Type: JVN, - Severity: "HIGH", - Cvss2Score: 8.3, - }, - RedHat: { - Type: RedHat, - Severity: "HIGH", - Cvss3Score: 8.0, - }, - NVD: { - Type: NVD, - Cvss2Score: 8.1, - // Severity is NIOT included in NVD - }, - }, - out: "8.3 HIGH (jvn)", - }, - { - in: CveContents{ - JVN: { - Type: JVN, - Severity: "HIGH", - Cvss2Score: 8.3, - }, - RedHat: { - Type: RedHat, - Severity: "HIGH", - Cvss2Score: 8.0, - Cvss3Score: 9.9, - }, - NVD: { - Type: NVD, - Cvss2Score: 8.1, - }, - }, - out: "9.9 HIGH (redhat)", - }, - } - for _, tt := range tests { - actual := tt.in.FormatMaxCvssScore() - if !reflect.DeepEqual(tt.out, actual) { - t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) - } - } -} - func TestTitles(t *testing.T) { type in struct { lang string diff --git a/models/scanresults.go b/models/scanresults.go index 284724fb..082ff2f1 100644 --- a/models/scanresults.go +++ b/models/scanresults.go @@ -140,8 +140,8 @@ func (r ScanResult) ConvertJvnToModel(cveID string, jvn cvedict.Jvn) *CveContent // FilterByCvssOver is filter function. func (r ScanResult) FilterByCvssOver(over float64) ScanResult { filtered := r.ScannedCves.Find(func(v VulnInfo) bool { - v2Max := v.CveContents.MaxCvss2Score() - v3Max := v.CveContents.MaxCvss3Score() + v2Max := v.MaxCvss2Score() + v3Max := v.MaxCvss3Score() max := v2Max.Value.Score if max < v3Max.Value.Score { max = v3Max.Value.Score diff --git a/models/vulninfos.go b/models/vulninfos.go index d14df562..5b458cb3 100644 --- a/models/vulninfos.go +++ b/models/vulninfos.go @@ -45,8 +45,8 @@ func (v VulnInfos) Find(f func(VulnInfo) bool) VulnInfos { // FindScoredVulns return scored 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 { + if 0 < vv.MaxCvss2Score().Value.Score || + 0 < vv.MaxCvss3Score().Value.Score { return true } return false @@ -59,8 +59,8 @@ func (v VulnInfos) ToSortedSlice() (sorted []VulnInfo) { sorted = append(sorted, v[k]) } sort.Slice(sorted, func(i, j int) bool { - maxI := sorted[i].CveContents.MaxCvssScore() - maxJ := sorted[j].CveContents.MaxCvssScore() + maxI := sorted[i].MaxCvssScore() + maxJ := sorted[j].MaxCvssScore() if maxI.Value.Score != maxJ.Value.Score { return maxJ.Value.Score < maxI.Value.Score } @@ -73,9 +73,9 @@ func (v VulnInfos) ToSortedSlice() (sorted []VulnInfo) { func (v VulnInfos) CountGroupBySeverity() map[string]int { m := map[string]int{} for _, vInfo := range v { - score := vInfo.CveContents.MaxCvss2Score().Value.Score + score := vInfo.MaxCvss2Score().Value.Score if score < 0.1 { - score = vInfo.CveContents.MaxCvss3Score().Value.Score + score = vInfo.MaxCvss3Score().Value.Score } switch { case 7.0 <= score: @@ -114,6 +114,296 @@ type VulnInfo struct { CveContents CveContents } +// Cvss2Scores returns CVSS V2 Scores +func (v VulnInfo) Cvss2Scores() (values []CveContentCvss) { + order := []CveContentType{NVD, RedHat, JVN} + for _, ctype := range order { + if cont, found := v.CveContents[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, CveContentCvss{ + Type: ctype, + Value: Cvss{ + Type: CVSS2, + Score: cont.Cvss2Score, + Vector: cont.Cvss2Vector, + Severity: strings.ToUpper(sev), + }, + }) + } + } + + for _, adv := range v.DistroAdvisories { + if adv.Severity != "" { + values = append(values, CveContentCvss{ + Type: "Vendor", + Value: Cvss{ + Type: CVSS2, + Score: severityToV2ScoreRoughly(adv.Severity), + Vector: "-", + Severity: strings.ToUpper(adv.Severity), + }, + }) + } + } + + return +} + +// Cvss3Scores returns CVSS V3 Score +func (v VulnInfo) Cvss3Scores() (values []CveContentCvss) { + // TODO implement NVD + order := []CveContentType{RedHat} + for _, ctype := range order { + if cont, found := v.CveContents[ctype]; found && 0 < cont.Cvss3Score { + // https://nvd.nist.gov/vuln-metrics/cvss + sev := cont.Severity + values = append(values, CveContentCvss{ + Type: ctype, + Value: Cvss{ + Type: CVSS3, + Score: cont.Cvss3Score, + Vector: cont.Cvss3Vector, + Severity: sev, + }, + }) + } + } + return +} + +// MaxCvss3Score returns Max CVSS V3 Score +func (v VulnInfo) MaxCvss3Score() CveContentCvss { + // TODO implement NVD + order := []CveContentType{RedHat} + max := 0.0 + value := CveContentCvss{ + Type: Unknown, + Value: Cvss{Type: CVSS3}, + } + for _, ctype := range order { + if cont, found := v.CveContents[ctype]; found && max < cont.Cvss3Score { + // https://nvd.nist.gov/vuln-metrics/cvss + sev := cont.Severity + value = CveContentCvss{ + Type: ctype, + Value: Cvss{ + Type: CVSS3, + Score: cont.Cvss3Score, + Vector: cont.Cvss3Vector, + Severity: sev, + }, + } + max = cont.Cvss3Score + } + } + return value +} + +// MaxCvssScore returns max CVSS Score +// If there is no CVSS Score, return Severity as a numerical value. +func (v VulnInfo) MaxCvssScore() CveContentCvss { + v3Max := v.MaxCvss3Score() + v2Max := v.MaxCvss2Score() + max := v3Max + if max.Value.Score < v2Max.Value.Score { + max = v2Max + } + return max +} + +// MaxCvss2Score returns Max CVSS V2 Score +func (v VulnInfo) MaxCvss2Score() CveContentCvss { + order := []CveContentType{NVD, RedHat, JVN} + max := 0.0 + value := CveContentCvss{ + Type: Unknown, + Value: Cvss{Type: CVSS2}, + } + for _, ctype := range order { + if cont, found := v.CveContents[ctype]; found && max < cont.Cvss2Score { + // https://nvd.nist.gov/vuln-metrics/cvss + sev := cont.Severity + if ctype == NVD { + sev = cvss2ScoreToSeverity(cont.Cvss2Score) + } + value = CveContentCvss{ + Type: ctype, + Value: Cvss{ + Type: CVSS2, + Score: cont.Cvss2Score, + Vector: cont.Cvss2Vector, + Severity: sev, + }, + } + max = cont.Cvss2Score + } + } + if 0 < max { + return value + } + + // If CVSS score isn't on NVD, RedHat and JVN, use OVAL and advisory Severity. + // Convert severity to cvss srore roughly, then returns max severity. + // Only Ubuntu, RedHat and Oracle OVAL has severity data in OVAL. + order = []CveContentType{Ubuntu, RedHat, Oracle} + for _, ctype := range order { + if cont, found := v.CveContents[ctype]; found && 0 < len(cont.Severity) { + score := severityToV2ScoreRoughly(cont.Severity) + if max < score { + value = CveContentCvss{ + Type: ctype, + Value: Cvss{ + Type: CVSS2, + Score: score, + Vector: cont.Cvss2Vector, + Severity: cont.Severity, + }, + } + } + max = score + } + } + + // Only RedHat, Oracle and Amazon has severity data in advisory. + for _, adv := range v.DistroAdvisories { + if adv.Severity != "" { + score := severityToV2ScoreRoughly(adv.Severity) + if max < score { + value = CveContentCvss{ + Type: "Vendor", + Value: Cvss{ + Type: CVSS2, + Score: score, + Vector: "-", + Severity: adv.Severity, + }, + } + } + } + } + return value +} + +// CveContentCvss has CveContentType and Cvss2 +type CveContentCvss struct { + Type CveContentType + Value Cvss +} + +// CvssType Represent the type of CVSS +type CvssType string + +const ( + // CVSS2 means CVSS vesion2 + CVSS2 CvssType = "2" + + // CVSS3 means CVSS vesion3 + CVSS3 CvssType = "3" +) + +// Cvss has CVSS Score +type Cvss struct { + Type CvssType + Score float64 + Vector string + Severity string +} + +// Format CVSS Score and Vector +func (c Cvss) Format() string { + switch c.Type { + case CVSS2: + return fmt.Sprintf("%3.1f/%s", c.Score, c.Vector) + case CVSS3: + return fmt.Sprintf("%3.1f/CVSS:3.0/%s", c.Score, c.Vector) + } + return "" +} + +func cvss2ScoreToSeverity(score float64) string { + if 7.0 <= score { + return "HIGH" + } else if 4.0 <= score { + return "MEDIUM" + } + return "LOW" +} + +// Amazon Linux Security Advisory +// Critical, Important, Medium, Low +// https://alas.aws.amazon.com/ +// +// RedHat, Oracle OVAL +// Critical, Important, Moderate, Low +// https://access.redhat.com/security/updates/classification +// +// Ubuntu OVAL +// Critical, High, Medium, Low +// https://wiki.ubuntu.com/Bugs/Importance +// https://people.canonical.com/~ubuntu-security/cve/priority.html +func severityToV2ScoreRoughly(severity string) float64 { + switch strings.ToUpper(severity) { + case "CRITICAL": + return 10.0 + case "IMPORTANT", "HIGH": + return 8.9 + case "MODERATE", "MEDIUM": + return 6.9 + case "LOW": + return 3.9 + } + return 0 +} + +// CveContentCvss3 has CveContentType and Cvss3 +// type CveContentCvss3 struct { +// Type CveContentType +// Value Cvss3 +// } + +// 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" +// } else if 7.0 <= score { +// return "HIGH" +// } else if 4.0 <= score { +// return "MEDIUM" +// } +// return "LOW" +// } + +// FormatMaxCvssScore returns Max CVSS Score +func (v VulnInfo) 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) +} + // 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 diff --git a/models/vulninfos_test.go b/models/vulninfos_test.go index 7b4f8f40..067b5ed3 100644 --- a/models/vulninfos_test.go +++ b/models/vulninfos_test.go @@ -247,3 +247,411 @@ func TestToSortedSlice(t *testing.T) { } } } + +func TestCvss2Scores(t *testing.T) { + var tests = []struct { + in VulnInfo + out []CveContentCvss + }{ + { + in: VulnInfo{ + CveContents: CveContents{ + JVN: { + Type: JVN, + Severity: "HIGH", + Cvss2Score: 8.2, + Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + }, + RedHat: { + Type: RedHat, + Severity: "HIGH", + Cvss2Score: 8.0, + Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + }, + NVD: { + Type: NVD, + Cvss2Score: 8.1, + Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + // Severity is NIOT included in NVD + }, + }, + }, + out: []CveContentCvss{ + { + Type: NVD, + Value: Cvss{ + Type: CVSS2, + Score: 8.1, + Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + Severity: "HIGH", + }, + }, + { + Type: RedHat, + Value: Cvss{ + Type: CVSS2, + Score: 8.0, + Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + Severity: "HIGH", + }, + }, + { + Type: JVN, + Value: Cvss{ + Type: CVSS2, + Score: 8.2, + Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + Severity: "HIGH", + }, + }, + }, + }, + // Empty + { + in: VulnInfo{}, + out: nil, + }, + } + for _, tt := range tests { + actual := tt.in.Cvss2Scores() + if !reflect.DeepEqual(tt.out, actual) { + t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + } + } +} + +func TestMaxCvss2Scores(t *testing.T) { + var tests = []struct { + in VulnInfo + out CveContentCvss + }{ + { + in: VulnInfo{ + CveContents: CveContents{ + JVN: { + Type: JVN, + Severity: "HIGH", + Cvss2Score: 8.2, + Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + }, + RedHat: { + Type: RedHat, + Severity: "HIGH", + Cvss2Score: 8.0, + Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + }, + NVD: { + Type: NVD, + Cvss2Score: 8.1, + Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + // Severity is NIOT included in NVD + }, + }, + }, + out: CveContentCvss{ + Type: JVN, + Value: Cvss{ + Type: CVSS2, + Score: 8.2, + Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + Severity: "HIGH", + }, + }, + }, + // Severity in OVAL + { + in: VulnInfo{ + CveContents: CveContents{ + Ubuntu: { + Type: Ubuntu, + Severity: "HIGH", + }, + }, + }, + out: CveContentCvss{ + Type: Ubuntu, + Value: Cvss{ + Type: CVSS2, + Score: 8.9, + Severity: "HIGH", + }, + }, + }, + // Empty + { + in: VulnInfo{}, + out: CveContentCvss{ + Type: Unknown, + Value: Cvss{ + Type: CVSS2, + Score: 0.0, + Vector: "", + Severity: "", + }, + }, + }, + } + for _, tt := range tests { + actual := tt.in.MaxCvss2Score() + if !reflect.DeepEqual(tt.out, actual) { + t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + } + } +} + +func TestCvss3Scores(t *testing.T) { + var tests = []struct { + in VulnInfo + out []CveContentCvss + }{ + { + in: VulnInfo{ + CveContents: CveContents{ + RedHat: { + Type: RedHat, + Severity: "HIGH", + Cvss3Score: 8.0, + Cvss3Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + }, + NVD: { + Type: NVD, + Cvss3Score: 8.1, + Cvss3Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + // Severity is NIOT included in NVD + }, + }, + }, + out: []CveContentCvss{ + { + Type: RedHat, + Value: Cvss{ + Type: CVSS3, + Score: 8.0, + Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + Severity: "HIGH", + }, + }, + }, + }, + // Empty + { + in: VulnInfo{}, + out: nil, + }, + } + for _, tt := range tests { + actual := tt.in.Cvss3Scores() + if !reflect.DeepEqual(tt.out, actual) { + t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + } + } +} + +func TestMaxCvss3Scores(t *testing.T) { + var tests = []struct { + in VulnInfo + out CveContentCvss + }{ + { + in: VulnInfo{ + CveContents: CveContents{ + RedHat: { + Type: RedHat, + Severity: "HIGH", + Cvss3Score: 8.0, + Cvss3Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + }, + }, + }, + out: CveContentCvss{ + Type: RedHat, + Value: Cvss{ + Type: CVSS3, + Score: 8.0, + Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + Severity: "HIGH", + }, + }, + }, + // Empty + { + in: VulnInfo{}, + out: CveContentCvss{ + Type: Unknown, + Value: Cvss{ + Type: CVSS3, + Score: 0.0, + Vector: "", + Severity: "", + }, + }, + }, + } + for _, tt := range tests { + actual := tt.in.MaxCvss3Score() + if !reflect.DeepEqual(tt.out, actual) { + t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + } + } +} + +func TestMaxCvssScores(t *testing.T) { + var tests = []struct { + in VulnInfo + out CveContentCvss + }{ + { + in: VulnInfo{ + CveContents: CveContents{ + NVD: { + Type: NVD, + Cvss3Score: 7.0, + }, + RedHat: { + Type: RedHat, + Cvss2Score: 8.0, + }, + }, + }, + out: CveContentCvss{ + Type: RedHat, + Value: Cvss{ + Type: CVSS2, + Score: 8.0, + }, + }, + }, + { + in: VulnInfo{ + CveContents: CveContents{ + RedHat: { + Type: RedHat, + Cvss3Score: 8.0, + }, + }, + }, + out: CveContentCvss{ + Type: RedHat, + Value: Cvss{ + Type: CVSS3, + Score: 8.0, + }, + }, + }, + { + in: VulnInfo{ + CveContents: CveContents{ + Ubuntu: { + Type: Ubuntu, + Severity: "HIGH", + }, + }, + }, + out: CveContentCvss{ + Type: Ubuntu, + Value: Cvss{ + Type: CVSS2, + Score: 8.9, + Severity: "HIGH", + }, + }, + }, + { + in: VulnInfo{ + CveContents: CveContents{ + Ubuntu: { + Type: Ubuntu, + Severity: "MEDIUM", + }, + NVD: { + Type: NVD, + Cvss2Score: 7.0, + }, + }, + }, + out: CveContentCvss{ + Type: NVD, + Value: Cvss{ + Type: CVSS2, + Score: 7.0, + Severity: "HIGH", + }, + }, + }, + // Empty + { + in: VulnInfo{}, + out: CveContentCvss{ + Type: Unknown, + Value: Cvss{ + Type: CVSS3, + Score: 0, + }, + }, + }, + } + for i, tt := range tests { + actual := tt.in.MaxCvssScore() + if !reflect.DeepEqual(tt.out, actual) { + t.Errorf("\n[%d] expected: %v\n actual: %v\n", i, tt.out, actual) + } + } +} + +func TestFormatMaxCvssScore(t *testing.T) { + var tests = []struct { + in VulnInfo + out string + }{ + { + in: VulnInfo{ + CveContents: CveContents{ + JVN: { + Type: JVN, + Severity: "HIGH", + Cvss2Score: 8.3, + }, + RedHat: { + Type: RedHat, + Severity: "HIGH", + Cvss3Score: 8.0, + }, + NVD: { + Type: NVD, + Cvss2Score: 8.1, + // Severity is NIOT included in NVD + }, + }, + }, + out: "8.3 HIGH (jvn)", + }, + { + in: VulnInfo{ + CveContents: CveContents{ + JVN: { + Type: JVN, + Severity: "HIGH", + Cvss2Score: 8.3, + }, + RedHat: { + Type: RedHat, + Severity: "HIGH", + Cvss2Score: 8.0, + Cvss3Score: 9.9, + }, + NVD: { + Type: NVD, + Cvss2Score: 8.1, + }, + }, + }, + out: "9.9 HIGH (redhat)", + }, + } + for _, tt := range tests { + actual := tt.in.FormatMaxCvssScore() + if !reflect.DeepEqual(tt.out, actual) { + t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + } + } +} diff --git a/report/report.go b/report/report.go index 594c2035..1206a900 100644 --- a/report/report.go +++ b/report/report.go @@ -158,10 +158,13 @@ func fillWithOval(r *models.ScanResult) (err error) { ovalClient = oval.NewCentOS() //use RedHat's OVAL ovalFamily = c.RedHat - //TODO implement OracleLinux + //TODO // case c.Oracle: // ovalClient = oval.New() // ovalFamily = c.Oracle + // case c.Suse: + // ovalClient = oval.New() + // ovalFamily = c.Oracle case c.Amazon, c.Oracle, c.Raspbian, c.FreeBSD: return nil default: diff --git a/report/slack.go b/report/slack.go index 38ddf1b3..53af4673 100644 --- a/report/slack.go +++ b/report/slack.go @@ -216,7 +216,7 @@ func toSlackAttachments(r models.ScanResult) (attaches []*attachment) { Short: true, }, }, - Color: color(vinfo.CveContents.MaxCvssScore().Value.Score), + Color: color(vinfo.MaxCvssScore().Value.Score), } attaches = append(attaches, &a) } @@ -238,9 +238,9 @@ func color(cvssScore float64) string { } func attachmentText(vinfo models.VulnInfo, osFamily string) string { - maxCvss := vinfo.CveContents.MaxCvssScore() + maxCvss := vinfo.MaxCvssScore() vectors := []string{} - for _, cvss := range vinfo.CveContents.Cvss2Scores() { + for _, cvss := range vinfo.Cvss2Scores() { calcURL := "" switch cvss.Value.Type { case models.CVSS2: diff --git a/report/tui.go b/report/tui.go index bdf52348..1405e6d1 100644 --- a/report/tui.go +++ b/report/tui.go @@ -641,7 +641,7 @@ func summaryLines() string { summary := vinfo.CveContents.Summaries( config.Conf.Lang, currentScanResult.Family)[0].Value cvssScore := fmt.Sprintf("| %4.1f", - vinfo.CveContents.MaxCvssScore().Value.Score) + vinfo.MaxCvssScore().Value.Score) var cols []string cols = []string{ @@ -794,7 +794,7 @@ func detailLines() (string, error) { data := dataForTmpl{ CveID: vinfo.CveID, - Cvsses: append(vinfo.CveContents.Cvss3Scores(), vinfo.CveContents.Cvss2Scores()...), + Cvsses: append(vinfo.Cvss3Scores(), vinfo.Cvss2Scores()...), Summary: fmt.Sprintf("%s (%s)", summary.Value, summary.Type), Confidence: vinfo.Confidence, Cwes: vinfo.CveContents.CweIDs(r.Family), @@ -818,7 +818,7 @@ const mdTemplate = ` CVSS Scores -------------- {{range .Cvsses -}} -* {{.Value.Format}} ({{.Type}}) +* {{.Value.Severity}} {{.Value.Format}} ({{.Type}}) {{end}} Summary diff --git a/report/util.go b/report/util.go index f52c9f66..30a3cb91 100644 --- a/report/util.go +++ b/report/util.go @@ -120,18 +120,18 @@ func formatShortPlainText(r models.ScanResult) string { } cvsses := "" - for _, cvss := range vuln.CveContents.Cvss2Scores() { + for _, cvss := range vuln.Cvss2Scores() { cvsses += fmt.Sprintf("%s (%s)\n", cvss.Value.Format(), cvss.Type) } cvsses += vuln.Cvss2CalcURL() + "\n" - for _, cvss := range vuln.CveContents.Cvss3Scores() { + for _, cvss := range vuln.Cvss3Scores() { cvsses += fmt.Sprintf("%s (%s)\n", cvss.Value.Format(), cvss.Type) } - if 0 < len(vuln.CveContents.Cvss3Scores()) { + if 0 < len(vuln.Cvss3Scores()) { cvsses += vuln.Cvss3CalcURL() + "\n" } - maxCvss := vuln.CveContents.FormatMaxCvssScore() + maxCvss := vuln.FormatMaxCvssScore() rightCol := fmt.Sprintf(`%s %s --- @@ -186,17 +186,17 @@ func formatFullPlainText(r models.ScanResult) string { for _, vuln := range vulns.ToSortedSlice() { table.AddRow(vuln.CveID) table.AddRow("----------------") - table.AddRow("Max Score", vuln.CveContents.FormatMaxCvssScore()) - for _, cvss := range vuln.CveContents.Cvss2Scores() { + table.AddRow("Max Score", vuln.FormatMaxCvssScore()) + for _, cvss := range vuln.Cvss2Scores() { table.AddRow(cvss.Type, cvss.Value.Format()) } - for _, cvss := range vuln.CveContents.Cvss3Scores() { + for _, cvss := range vuln.Cvss3Scores() { table.AddRow(cvss.Type, cvss.Value.Format()) } - if 0 < len(vuln.CveContents.Cvss2Scores()) { + if 0 < len(vuln.Cvss2Scores()) { table.AddRow("CVSSv2 Calc", vuln.Cvss2CalcURL()) } - if 0 < len(vuln.CveContents.Cvss3Scores()) { + if 0 < len(vuln.Cvss3Scores()) { table.AddRow("CVSSv3 Calc", vuln.Cvss3CalcURL()) } table.AddRow("Summary", vuln.CveContents.Summaries( From 9899cba8162856bdd7c60297e832fd583fe777e0 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Sun, 30 Jul 2017 17:04:25 +0900 Subject: [PATCH 069/113] Display summary of advisory when no entry in NVD, OVAL --- models/cvecontents.go | 66 ----------- models/cvecontents_test.go | 196 -------------------------------- models/vulninfos.go | 77 +++++++++++++ models/vulninfos_test.go | 222 +++++++++++++++++++++++++++++++++++++ report/slack.go | 2 +- report/tui.go | 4 +- report/util.go | 4 +- 7 files changed, 304 insertions(+), 267 deletions(-) diff --git a/models/cvecontents.go b/models/cvecontents.go index fc3566f6..68822888 100644 --- a/models/cvecontents.go +++ b/models/cvecontents.go @@ -58,72 +58,6 @@ func (v CveContents) Except(exceptCtypes ...CveContentType) (values CveContents) return } -// 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 index < len(summary) { - summary = summary[0:index] + "..." - } - values = append(values, CveContentStr{ - Type: ctype, - Value: summary, - }) - } - } - - 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, cveID string) (values []CveContentStr) { if lang == "ja" { diff --git a/models/cvecontents_test.go b/models/cvecontents_test.go index 03a186f6..4c3cadb2 100644 --- a/models/cvecontents_test.go +++ b/models/cvecontents_test.go @@ -44,202 +44,6 @@ func TestExcept(t *testing.T) { } } -func TestTitles(t *testing.T) { - type in struct { - lang string - cont CveContents - } - var tests = []struct { - in in - out []CveContentStr - }{ - // lang: ja - { - in: in{ - lang: "ja", - cont: CveContents{ - JVN: { - Type: JVN, - Title: "Title1", - }, - RedHat: { - Type: RedHat, - Summary: "Summary RedHat", - }, - NVD: { - Type: NVD, - Summary: "Summary NVD", - // Severity is NIOT included in NVD - }, - }, - }, - out: []CveContentStr{ - { - Type: JVN, - Value: "Title1", - }, - { - Type: NVD, - Value: "Summary NVD", - }, - { - Type: RedHat, - Value: "Summary RedHat", - }, - }, - }, - // lang: en - { - in: in{ - lang: "en", - cont: CveContents{ - JVN: { - Type: JVN, - Title: "Title1", - }, - RedHat: { - Type: RedHat, - Summary: "Summary RedHat", - }, - NVD: { - Type: NVD, - Summary: "Summary NVD", - // Severity is NIOT included in NVD - }, - }, - }, - out: []CveContentStr{ - { - Type: NVD, - Value: "Summary NVD", - }, - { - Type: RedHat, - Value: "Summary RedHat", - }, - }, - }, - // lang: empty - { - in: in{ - lang: "en", - cont: CveContents{}, - }, - out: []CveContentStr{ - { - Type: Unknown, - Value: "-", - }, - }, - }, - } - for _, tt := range tests { - actual := tt.in.cont.Titles(tt.in.lang, "redhat") - if !reflect.DeepEqual(tt.out, actual) { - t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) - } - } -} - -func TestSummaries(t *testing.T) { - type in struct { - lang string - cont CveContents - } - var tests = []struct { - in in - out []CveContentStr - }{ - // lang: ja - { - in: in{ - lang: "ja", - cont: CveContents{ - JVN: { - Type: JVN, - Title: "Title JVN", - Summary: "Summary JVN", - }, - RedHat: { - Type: RedHat, - Summary: "Summary RedHat", - }, - NVD: { - Type: NVD, - Summary: "Summary NVD", - // Severity is NIOT included in NVD - }, - }, - }, - out: []CveContentStr{ - { - Type: JVN, - Value: "Title JVN\nSummary JVN", - }, - { - Type: NVD, - Value: "Summary NVD", - }, - { - Type: RedHat, - Value: "Summary RedHat", - }, - }, - }, - // lang: en - { - in: in{ - lang: "en", - cont: CveContents{ - JVN: { - Type: JVN, - Title: "Title JVN", - Summary: "Summary JVN", - }, - RedHat: { - Type: RedHat, - Summary: "Summary RedHat", - }, - NVD: { - Type: NVD, - Summary: "Summary NVD", - // Severity is NIOT included in NVD - }, - }, - }, - out: []CveContentStr{ - { - Type: NVD, - Value: "Summary NVD", - }, - { - Type: RedHat, - Value: "Summary RedHat", - }, - }, - }, - // lang: empty - { - in: in{ - lang: "en", - cont: CveContents{}, - }, - out: []CveContentStr{ - { - Type: Unknown, - Value: "-", - }, - }, - }, - } - for _, tt := range tests { - actual := tt.in.cont.Summaries(tt.in.lang, "redhat") - if !reflect.DeepEqual(tt.out, actual) { - t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) - } - } -} - func TestSourceLinks(t *testing.T) { type in struct { lang string diff --git a/models/vulninfos.go b/models/vulninfos.go index 5b458cb3..01107497 100644 --- a/models/vulninfos.go +++ b/models/vulninfos.go @@ -114,6 +114,83 @@ type VulnInfo struct { CveContents CveContents } +// Titles returns tilte (TUI) +func (v VulnInfo) Titles(lang, myFamily string) (values []CveContentStr) { + if lang == "ja" { + if cont, found := v.CveContents[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.CveContents[ctype]; found && 0 < len(cont.Summary) { + summary := strings.Replace(cont.Summary, "\n", " ", -1) + values = append(values, CveContentStr{ + Type: ctype, + Value: summary, + }) + } + } + + for _, adv := range v.DistroAdvisories { + values = append(values, CveContentStr{ + Type: "Vendor", + Value: strings.Replace(adv.Description, "\n", " ", -1), + }) + } + + if len(values) == 0 { + values = []CveContentStr{{ + Type: Unknown, + Value: "-", + }} + } + return +} + +// Summaries returns summaries +func (v VulnInfo) Summaries(lang, myFamily string) (values []CveContentStr) { + if lang == "ja" { + if cont, found := v.CveContents[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.CveContents[ctype]; found && 0 < len(cont.Summary) { + summary := strings.Replace(cont.Summary, "\n", " ", -1) + values = append(values, CveContentStr{ + Type: ctype, + Value: summary, + }) + } + } + + for _, adv := range v.DistroAdvisories { + values = append(values, CveContentStr{ + Type: "Vendor", + Value: adv.Description, + }) + } + + if len(values) == 0 { + return []CveContentStr{{ + Type: Unknown, + Value: "-", + }} + } + + return +} + // Cvss2Scores returns CVSS V2 Scores func (v VulnInfo) Cvss2Scores() (values []CveContentCvss) { order := []CveContentType{NVD, RedHat, JVN} diff --git a/models/vulninfos_test.go b/models/vulninfos_test.go index 067b5ed3..7502f4ea 100644 --- a/models/vulninfos_test.go +++ b/models/vulninfos_test.go @@ -21,6 +21,210 @@ import ( "testing" ) +func TestTitles(t *testing.T) { + type in struct { + lang string + cont VulnInfo + } + var tests = []struct { + in in + out []CveContentStr + }{ + // lang: ja + { + in: in{ + lang: "ja", + cont: VulnInfo{ + CveContents: CveContents{ + JVN: { + Type: JVN, + Title: "Title1", + }, + RedHat: { + Type: RedHat, + Summary: "Summary RedHat", + }, + NVD: { + Type: NVD, + Summary: "Summary NVD", + // Severity is NIOT included in NVD + }, + }, + }, + }, + out: []CveContentStr{ + { + Type: JVN, + Value: "Title1", + }, + { + Type: NVD, + Value: "Summary NVD", + }, + { + Type: RedHat, + Value: "Summary RedHat", + }, + }, + }, + // lang: en + { + in: in{ + lang: "en", + cont: VulnInfo{ + CveContents: CveContents{ + JVN: { + Type: JVN, + Title: "Title1", + }, + RedHat: { + Type: RedHat, + Summary: "Summary RedHat", + }, + NVD: { + Type: NVD, + Summary: "Summary NVD", + // Severity is NIOT included in NVD + }, + }, + }, + }, + out: []CveContentStr{ + { + Type: NVD, + Value: "Summary NVD", + }, + { + Type: RedHat, + Value: "Summary RedHat", + }, + }, + }, + // lang: empty + { + in: in{ + lang: "en", + cont: VulnInfo{}, + }, + out: []CveContentStr{ + { + Type: Unknown, + Value: "-", + }, + }, + }, + } + for _, tt := range tests { + actual := tt.in.cont.Titles(tt.in.lang, "redhat") + if !reflect.DeepEqual(tt.out, actual) { + t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + } + } +} + +func TestSummaries(t *testing.T) { + type in struct { + lang string + cont VulnInfo + } + var tests = []struct { + in in + out []CveContentStr + }{ + // lang: ja + { + in: in{ + lang: "ja", + cont: VulnInfo{ + CveContents: CveContents{ + JVN: { + Type: JVN, + Title: "Title JVN", + Summary: "Summary JVN", + }, + RedHat: { + Type: RedHat, + Summary: "Summary RedHat", + }, + NVD: { + Type: NVD, + Summary: "Summary NVD", + // Severity is NIOT included in NVD + }, + }, + }, + }, + out: []CveContentStr{ + { + Type: JVN, + Value: "Title JVN\nSummary JVN", + }, + { + Type: NVD, + Value: "Summary NVD", + }, + { + Type: RedHat, + Value: "Summary RedHat", + }, + }, + }, + // lang: en + { + in: in{ + lang: "en", + cont: VulnInfo{ + CveContents: CveContents{ + JVN: { + Type: JVN, + Title: "Title JVN", + Summary: "Summary JVN", + }, + RedHat: { + Type: RedHat, + Summary: "Summary RedHat", + }, + NVD: { + Type: NVD, + Summary: "Summary NVD", + // Severity is NIOT included in NVD + }, + }, + }, + }, + out: []CveContentStr{ + { + Type: NVD, + Value: "Summary NVD", + }, + { + Type: RedHat, + Value: "Summary RedHat", + }, + }, + }, + // lang: empty + { + in: in{ + lang: "en", + cont: VulnInfo{}, + }, + out: []CveContentStr{ + { + Type: Unknown, + Value: "-", + }, + }, + }, + } + for _, tt := range tests { + actual := tt.in.cont.Summaries(tt.in.lang, "redhat") + if !reflect.DeepEqual(tt.out, actual) { + t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + } + } +} + func TestCountGroupBySeverity(t *testing.T) { var tests = []struct { in VulnInfos @@ -578,6 +782,24 @@ func TestMaxCvssScores(t *testing.T) { }, }, }, + { + in: VulnInfo{ + DistroAdvisories: []DistroAdvisory{ + { + Severity: "HIGH", + }, + }, + }, + out: CveContentCvss{ + Type: "Vendor", + Value: Cvss{ + Type: CVSS2, + Score: 8.9, + Vector: "-", + Severity: "HIGH", + }, + }, + }, // Empty { in: VulnInfo{}, diff --git a/report/slack.go b/report/slack.go index 53af4673..7b975e9c 100644 --- a/report/slack.go +++ b/report/slack.go @@ -270,7 +270,7 @@ func attachmentText(vinfo models.VulnInfo, osFamily string) string { severity, cweIDs(vinfo, osFamily), strings.Join(vectors, "\n"), - vinfo.CveContents.Summaries(config.Conf.Lang, osFamily)[0].Value, + vinfo.Summaries(config.Conf.Lang, osFamily)[0].Value, ) } diff --git a/report/tui.go b/report/tui.go index 1405e6d1..1d04de20 100644 --- a/report/tui.go +++ b/report/tui.go @@ -638,7 +638,7 @@ func summaryLines() string { } for i, vinfo := range vinfos { - summary := vinfo.CveContents.Summaries( + summary := vinfo.Titles( config.Conf.Lang, currentScanResult.Family)[0].Value cvssScore := fmt.Sprintf("| %4.1f", vinfo.MaxCvssScore().Value.Score) @@ -790,7 +790,7 @@ func detailLines() (string, error) { } } - summary := vinfo.CveContents.Summaries(r.Lang, r.Family)[0] + summary := vinfo.Summaries(r.Lang, r.Family)[0] data := dataForTmpl{ CveID: vinfo.CveID, diff --git a/report/util.go b/report/util.go index 30a3cb91..b92d1c5f 100644 --- a/report/util.go +++ b/report/util.go @@ -110,7 +110,7 @@ func formatShortPlainText(r models.ScanResult) string { stable.MaxColWidth = maxColWidth stable.Wrap = true for _, vuln := range vulns.ToSortedSlice() { - summaries := vuln.CveContents.Summaries(config.Conf.Lang, r.Family) + summaries := vuln.Summaries(config.Conf.Lang, r.Family) links := vuln.CveContents.SourceLinks( config.Conf.Lang, r.Family, vuln.CveID) @@ -199,7 +199,7 @@ func formatFullPlainText(r models.ScanResult) string { if 0 < len(vuln.Cvss3Scores()) { table.AddRow("CVSSv3 Calc", vuln.Cvss3CalcURL()) } - table.AddRow("Summary", vuln.CveContents.Summaries( + table.AddRow("Summary", vuln.Summaries( config.Conf.Lang, r.Family)[0].Value) links := vuln.CveContents.SourceLinks( From 008da49b8334594dfb90c320b8dc5785f0778e2e Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Mon, 31 Jul 2017 17:17:35 +0900 Subject: [PATCH 070/113] Imlement OVAL scan on Oracle Linux --- models/cvecontents.go | 2 ++ models/vulninfos.go | 4 ++-- oval/debian.go | 2 ++ oval/oval.go | 7 ++++-- oval/redhat.go | 55 ++++++++++++++++++++++++++++++++++--------- report/report.go | 15 ++++++------ report/tui.go | 34 +++++++++++++++++--------- report/util.go | 12 +++++----- scan/redhat.go | 10 ++++++-- 9 files changed, 100 insertions(+), 41 deletions(-) diff --git a/models/cvecontents.go b/models/cvecontents.go index 68822888..170b3553 100644 --- a/models/cvecontents.go +++ b/models/cvecontents.go @@ -199,6 +199,8 @@ func NewCveContentType(name string) CveContentType { return JVN case "redhat", "centos": return RedHat + case "oracle": + return Oracle case "ubuntu": return Ubuntu case "debian": diff --git a/models/vulninfos.go b/models/vulninfos.go index 01107497..edd0d15a 100644 --- a/models/vulninfos.go +++ b/models/vulninfos.go @@ -53,7 +53,7 @@ func (v VulnInfos) FindScoredVulns() VulnInfos { }) } -// ToSortedSlice returns slice of VulnInfos that is sorted by CVE-ID +// ToSortedSlice returns slice of VulnInfos that is sorted by Score, CVE-ID func (v VulnInfos) ToSortedSlice() (sorted []VulnInfo) { for k := range v { sorted = append(sorted, v[k]) @@ -244,7 +244,7 @@ func (v VulnInfo) Cvss3Scores() (values []CveContentCvss) { Type: CVSS3, Score: cont.Cvss3Score, Vector: cont.Cvss3Vector, - Severity: sev, + Severity: strings.ToUpper(sev), }, }) } diff --git a/oval/debian.go b/oval/debian.go index 9bccf21d..2c18ce7a 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -139,6 +139,7 @@ func (o Debian) FillWithOval(r *models.ScanResult) error { } } + // TODO merge to VulnInfo.VendorLinks for _, vuln := range r.ScannedCves { if cont, ok := vuln.CveContents[models.Debian]; ok { cont.SourceLink = "https://security-tracker.debian.org/tracker/" + cont.CveID @@ -174,6 +175,7 @@ func (o Ubuntu) FillWithOval(r *models.ScanResult) error { } } + // TODO merge to VulnInfo.VendorLinks for _, vuln := range r.ScannedCves { if cont, ok := vuln.CveContents[models.Ubuntu]; ok { cont.SourceLink = "http://people.ubuntu.com/~ubuntu-security/cve/" + cont.CveID diff --git a/oval/oval.go b/oval/oval.go index 3c6f492d..a4f15965 100644 --- a/oval/oval.go +++ b/oval/oval.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "strings" "time" "github.com/cenkalti/backoff" @@ -115,13 +116,15 @@ func (b Base) CheckIfOvalFresh(osFamily, release string) (ok bool, err error) { } } + major := strings.Split(release, ".")[0] since := time.Now() since = since.AddDate(0, 0, -3) if lastModified.Before(since) { - util.Log.Warnf("%s-%s OVAL is old, last modified is %s. It's recommended to update OVAL to improve scanning accuracy. To update OVAL database, see https://github.com/kotakanbe/goval-dictionary#usage", - osFamily, release, lastModified) + util.Log.Warnf("OVAL for %s %s is old, last modified is %s. It's recommended to update OVAL to improve scanning accuracy. How to update OVAL database, see https://github.com/kotakanbe/goval-dictionary#usage", + osFamily, major, lastModified) return false, nil } + util.Log.Infof("OVAL is fresh: %s %s ", osFamily, major) return true, nil } diff --git a/oval/redhat.go b/oval/redhat.go index d0ff57e8..9684bf8a 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -16,7 +16,10 @@ import ( ) // RedHatBase is the base struct for RedHat and CentOS -type RedHatBase struct{ Base } +type RedHatBase struct { + Base + family string +} // FillWithOval returns scan result after updating CVE info by OVAL func (o RedHatBase) FillWithOval(r *models.ScanResult) error { @@ -34,9 +37,17 @@ func (o RedHatBase) FillWithOval(r *models.ScanResult) error { } } + // TODO merge to VulnInfo.VendorLinks for _, vuln := range r.ScannedCves { - if cont, ok := vuln.CveContents[models.RedHat]; ok { - cont.SourceLink = "https://access.redhat.com/security/cve/" + cont.CveID + switch models.NewCveContentType(o.family) { + case models.RedHat: + if cont, ok := vuln.CveContents[models.RedHat]; ok { + cont.SourceLink = "https://access.redhat.com/security/cve/" + cont.CveID + } + case models.Oracle: + if cont, ok := vuln.CveContents[models.Oracle]; ok { + cont.SourceLink = fmt.Sprintf("https://linux.oracle.com/cve/%s.html", cont.CveID) + } } } return nil @@ -71,7 +82,7 @@ func (o RedHatBase) getDefsByPackNameFromOvalDB(osRelease string, var ovaldb db.DB if ovaldb, err = db.NewDB( - ovalconf.RedHat, + o.family, ovalconf.Conf.DBType, ovalconf.Conf.DBPath, ovalconf.Conf.DebugSQL, @@ -82,7 +93,7 @@ func (o RedHatBase) getDefsByPackNameFromOvalDB(osRelease string, for _, pack := range packs { definitions, err := ovaldb.GetByPackName(osRelease, pack.Name) if err != nil { - return nil, fmt.Errorf("Failed to get RedHat OVAL info by package name: %v", err) + return nil, fmt.Errorf("Failed to get %s OVAL info by package name: %v", o.family, err) } for _, def := range definitions { current := ver.NewVersion(fmt.Sprintf("%s-%s", pack.Version, pack.Release)) @@ -99,6 +110,7 @@ func (o RedHatBase) getDefsByPackNameFromOvalDB(osRelease string, } func (o RedHatBase) update(r *models.ScanResult, definition *ovalmodels.Definition) { + ctype := models.NewCveContentType(o.family) for _, cve := range definition.Advisory.Cves { ovalContent := *o.convertToModel(cve.CveID, definition) vinfo, ok := r.ScannedCves[cve.CveID] @@ -112,7 +124,7 @@ func (o RedHatBase) update(r *models.ScanResult, definition *ovalmodels.Definiti } } else { cveContents := vinfo.CveContents - if _, ok := vinfo.CveContents[models.RedHat]; ok { + if _, ok := vinfo.CveContents[ctype]; ok { util.Log.Debugf("%s will be updated by OVAL", cve.CveID) } else { util.Log.Debugf("%s also detected by OVAL", cve.CveID) @@ -122,7 +134,7 @@ func (o RedHatBase) update(r *models.ScanResult, definition *ovalmodels.Definiti if vinfo.Confidence.Score < models.OvalMatch.Score { vinfo.Confidence = models.OvalMatch } - cveContents[models.RedHat] = ovalContent + cveContents[ctype] = ovalContent vinfo.CveContents = cveContents } r.ScannedCves[cve.CveID] = vinfo @@ -147,7 +159,7 @@ func (o RedHatBase) convertToModel(cveID string, def *ovalmodels.Definition) *mo score3, vec3 := o.parseCvss3(cve.Cvss3) return &models.CveContent{ - Type: models.RedHat, + Type: models.NewCveContentType(o.family), CveID: cve.CveID, Title: def.Title, Summary: def.Description, @@ -156,7 +168,6 @@ func (o RedHatBase) convertToModel(cveID string, def *ovalmodels.Definition) *mo Cvss2Vector: vec2, Cvss3Score: score3, Cvss3Vector: vec3, - SourceLink: "https://access.redhat.com/security/cve/" + cve.CveID, References: refs, CweID: cve.Cwe, Published: def.Advisory.Issued, @@ -201,7 +212,11 @@ type RedHat struct { // NewRedhat creates OVAL client for Redhat func NewRedhat() RedHat { - return RedHat{} + return RedHat{ + RedHatBase{ + family: config.RedHat, + }, + } } // CentOS is the interface for CentOS OVAL @@ -211,5 +226,23 @@ type CentOS struct { // NewCentOS creates OVAL client for CentOS func NewCentOS() CentOS { - return CentOS{} + return CentOS{ + RedHatBase{ + family: config.CentOS, + }, + } +} + +// Oracle is the interface for CentOS OVAL +type Oracle struct { + RedHatBase +} + +// NewOracle creates OVAL client for Oracle +func NewOracle() Oracle { + return Oracle{ + RedHatBase{ + family: config.Oracle, + }, + } } diff --git a/report/report.go b/report/report.go index 1206a900..d9ed1f0e 100644 --- a/report/report.go +++ b/report/report.go @@ -19,6 +19,7 @@ package report import ( "fmt" + "strings" c "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" @@ -158,17 +159,17 @@ func fillWithOval(r *models.ScanResult) (err error) { ovalClient = oval.NewCentOS() //use RedHat's OVAL ovalFamily = c.RedHat + case c.Oracle: + ovalClient = oval.NewOracle() + ovalFamily = c.Oracle //TODO - // case c.Oracle: - // ovalClient = oval.New() - // ovalFamily = c.Oracle // case c.Suse: // ovalClient = oval.New() // ovalFamily = c.Oracle - case c.Amazon, c.Oracle, c.Raspbian, c.FreeBSD: + case c.Amazon, c.Raspbian, c.FreeBSD: return nil default: - return fmt.Errorf("Oval %s is not implemented yet", r.Family) + return fmt.Errorf("OVAL for %s is not implemented yet", r.Family) } ok, err := ovalClient.CheckIfOvalFetched(ovalFamily, r.Release) @@ -176,7 +177,8 @@ func fillWithOval(r *models.ScanResult) (err error) { return err } if !ok { - util.Log.Warnf("OVAL entries of %s-%s are not found. It's recommended to use OVAL to improve scanning accuracy. To fetch OVAL, see https://github.com/kotakanbe/goval-dictionary#usage , Then report with --ovaldb-path or --ovaldb-url flag", r.Family, r.Release) + major := strings.Split(r.Release, ".")[0] + util.Log.Warnf("OVAL entries of %s %s are not found. It's recommended to use OVAL to improve scanning accuracy. To fetch OVAL, see https://github.com/kotakanbe/goval-dictionary#usage , Then report with --ovaldb-path or --ovaldb-url flag", ovalFamily, major) return nil } @@ -184,7 +186,6 @@ func fillWithOval(r *models.ScanResult) (err error) { if err != nil { return err } - util.Log.Infof("OVAL is fresh: %s-%s ", r.Family, r.Release) if err := ovalClient.FillWithOval(r); err != nil { return err diff --git a/report/tui.go b/report/tui.go index 1d04de20..1364a6b8 100644 --- a/report/tui.go +++ b/report/tui.go @@ -233,14 +233,14 @@ func movable(v *gocui.View, nextY int) (ok bool, yLimit int) { } return true, yLimit case "detail": - if currentDetailLimitY < nextY { - return false, currentDetailLimitY - } + // if currentDetailLimitY < nextY { + // return false, currentDetailLimitY + // } return true, currentDetailLimitY case "changelog": - if currentChangelogLimitY < nextY { - return false, currentChangelogLimitY - } + // if currentChangelogLimitY < nextY { + // return false, currentChangelogLimitY + // } return true, currentChangelogLimitY default: return true, 0 @@ -733,7 +733,7 @@ func setChangelogLayout(g *gocui.Gui) error { type dataForTmpl struct { CveID string - Cvsses []models.CveContentCvss + Cvsses string Summary string Confidence models.Confidence Cwes []models.CveContentStr @@ -792,9 +792,23 @@ func detailLines() (string, error) { summary := vinfo.Summaries(r.Lang, r.Family)[0] + table := uitable.New() + table.MaxColWidth = maxColWidth + table.Wrap = true + scores := append(vinfo.Cvss3Scores(), vinfo.Cvss2Scores()...) + var cols []interface{} + for _, score := range scores { + cols = []interface{}{ + score.Value.Severity, + score.Value.Format(), + score.Type, + } + table.AddRow(cols...) + } + data := dataForTmpl{ CveID: vinfo.CveID, - Cvsses: append(vinfo.Cvss3Scores(), vinfo.Cvss2Scores()...), + Cvsses: fmt.Sprintf("%s\n", table), Summary: fmt.Sprintf("%s (%s)", summary.Value, summary.Type), Confidence: vinfo.Confidence, Cwes: vinfo.CveContents.CweIDs(r.Family), @@ -817,9 +831,7 @@ const mdTemplate = ` CVSS Scores -------------- -{{range .Cvsses -}} -* {{.Value.Severity}} {{.Value.Format}} ({{.Type}}) -{{end}} +{{.Cvsses }} Summary -------------- diff --git a/report/util.go b/report/util.go index b92d1c5f..f9085377 100644 --- a/report/util.go +++ b/report/util.go @@ -100,9 +100,9 @@ func formatShortPlainText(r models.ScanResult) string { if len(vulns) == 0 { return fmt.Sprintf(` - %s - No CVE-IDs are found in updatable packages. - %s +%s +No CVE-IDs are found in updatable packages. +%s `, header, r.Packages.FormatUpdatablePacksSummary()) } @@ -174,9 +174,9 @@ func formatFullPlainText(r models.ScanResult) string { if len(vulns) == 0 { return fmt.Sprintf(` - %s - No CVE-IDs are found in updatable packages. - %s +%s +No CVE-IDs are found in updatable packages. +%s `, header, r.Packages.FormatUpdatablePacksSummary()) } diff --git a/scan/redhat.go b/scan/redhat.go index f08b96a1..13b5cdbc 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -265,7 +265,13 @@ func (o *redhat) scanPackages() error { func (o *redhat) scanInstalledPackages() (models.Packages, error) { installed := models.Packages{} - cmd := "rpm -qa --queryformat '%{NAME} %{EPOCHNUM} %{VERSION} %{RELEASE} %{ARCH}\n'" + var cmd string + majorVersion, _ := o.Distro.MajorVersion() + if majorVersion < 6 { + cmd = "rpm -qa --queryformat '%{NAME} %{EPOCH} %{VERSION} %{RELEASE} %{ARCH}\n'" + } else { + cmd = "rpm -qa --queryformat '%{NAME} %{EPOCHNUM} %{VERSION} %{RELEASE} %{ARCH}\n'" + } r := o.exec(cmd, noSudo) if r.isSuccess() { // openssl 0 1.0.1e 30.el6.11 x86_64 @@ -295,7 +301,7 @@ func (o *redhat) parseInstalledPackagesLine(line string) (models.Package, error) } ver := "" epoch := fields[1] - if epoch == "0" { + if epoch == "0" || epoch == "(none)" { ver = fields[2] } else { ver = fmt.Sprintf("%s:%s", epoch, fields[2]) From 9e0032b25863d7f3e1d9ac10781bb26ec7caa4a9 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Mon, 31 Jul 2017 20:32:27 +0900 Subject: [PATCH 071/113] Fix cvss link in slack notification --- report/slack.go | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/report/slack.go b/report/slack.go index 7b975e9c..ffac39e5 100644 --- a/report/slack.go +++ b/report/slack.go @@ -245,19 +245,37 @@ func attachmentText(vinfo models.VulnInfo, osFamily string) string { switch cvss.Value.Type { case models.CVSS2: calcURL = fmt.Sprintf( - "https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?vector=%s", - cvss.Value.Vector) + "https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?name=%s", + vinfo.CveID) case models.CVSS3: calcURL = fmt.Sprintf( - "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=%s", - cvss.Value.Vector) + "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?name=%s", + vinfo.CveID) + } + + if cont, ok := vinfo.CveContents[cvss.Type]; ok { + v := fmt.Sprintf("<%s|%s> (<%s|%s>)", + calcURL, + cvss.Value.Format(), + cont.SourceLink, + cvss.Type) + vectors = append(vectors, v) + + } else { + if 0 < len(vinfo.DistroAdvisories) { + links := []string{} + for k, v := range vinfo.VendorLinks(osFamily) { + links = append(links, fmt.Sprintf("<%s|%s>", + v, k)) + } + + v := fmt.Sprintf("<%s|%s> (%s)", + calcURL, + cvss.Value.Format(), + strings.Join(links, ", ")) + vectors = append(vectors, v) + } } - v := fmt.Sprintf("<%s|%s> (<%s|%s>)", - calcURL, - cvss.Value.Format(), - vinfo.CveContents[cvss.Type].SourceLink, - cvss.Type) - vectors = append(vectors, v) } severity := strings.ToUpper(maxCvss.Value.Severity) From 5f49e7da8e759424cfb7b884e805d0709148dcd1 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Mon, 31 Jul 2017 20:36:27 +0900 Subject: [PATCH 072/113] Refactoring --- models/scanresults.go | 92 ---------------------------------- models/utils.go | 114 ++++++++++++++++++++++++++++++++++++++++++ report/report.go | 4 +- 3 files changed, 116 insertions(+), 94 deletions(-) create mode 100644 models/utils.go diff --git a/models/scanresults.go b/models/scanresults.go index 082ff2f1..6ca22bcc 100644 --- a/models/scanresults.go +++ b/models/scanresults.go @@ -20,10 +20,7 @@ package models import ( "bytes" "fmt" - "strings" "time" - - cvedict "github.com/kotakanbe/go-cve-dictionary/models" ) // ScanResults is a slide of ScanResult @@ -48,95 +45,6 @@ type ScanResult struct { Optional [][]interface{} } -// ConvertNvdToModel convert NVD to CveContent -func (r ScanResult) ConvertNvdToModel(cveID string, nvd cvedict.Nvd) *CveContent { - var cpes []Cpe - for _, c := range nvd.Cpes { - cpes = append(cpes, Cpe{CpeName: c.CpeName}) - } - - var refs []Reference - for _, r := range nvd.References { - refs = append(refs, Reference{ - Link: r.Link, - Source: r.Source, - }) - } - - validVec := true - for _, v := range []string{ - nvd.AccessVector, - nvd.AccessComplexity, - nvd.Authentication, - nvd.ConfidentialityImpact, - nvd.IntegrityImpact, - nvd.AvailabilityImpact, - } { - if len(v) == 0 { - validVec = false - } - } - - vector := "" - if validVec { - vector = fmt.Sprintf("AV:%s/AC:%s/Au:%s/C:%s/I:%s/A:%s", - string(nvd.AccessVector[0]), - string(nvd.AccessComplexity[0]), - string(nvd.Authentication[0]), - string(nvd.ConfidentialityImpact[0]), - string(nvd.IntegrityImpact[0]), - string(nvd.AvailabilityImpact[0])) - } - - //TODO CVSSv3 - return &CveContent{ - Type: NVD, - CveID: cveID, - 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, - Published: nvd.PublishedDate, - LastModified: nvd.LastModifiedDate, - } -} - -// ConvertJvnToModel convert JVN to CveContent -func (r ScanResult) ConvertJvnToModel(cveID string, jvn cvedict.Jvn) *CveContent { - var cpes []Cpe - for _, c := range jvn.Cpes { - cpes = append(cpes, Cpe{CpeName: c.CpeName}) - } - - refs := []Reference{} - for _, r := range jvn.References { - refs = append(refs, Reference{ - Link: r.Link, - Source: r.Source, - }) - } - - vector := strings.TrimSuffix(strings.TrimPrefix(jvn.Vector, "("), ")") - return &CveContent{ - Type: JVN, - CveID: cveID, - Title: jvn.Title, - Summary: jvn.Summary, - Severity: jvn.Severity, - Cvss2Score: jvn.Score, - Cvss2Vector: vector, - SourceLink: jvn.JvnLink, - Cpes: cpes, - References: refs, - Published: jvn.PublishedDate, - LastModified: jvn.LastModifiedDate, - } -} - // FilterByCvssOver is filter function. func (r ScanResult) FilterByCvssOver(over float64) ScanResult { filtered := r.ScannedCves.Find(func(v VulnInfo) bool { diff --git a/models/utils.go b/models/utils.go new file mode 100644 index 00000000..d1b97308 --- /dev/null +++ b/models/utils.go @@ -0,0 +1,114 @@ +/* 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 models + +import ( + "fmt" + "strings" + + cvedict "github.com/kotakanbe/go-cve-dictionary/models" +) + +// ConvertNvdToModel convert NVD to CveContent +func ConvertNvdToModel(cveID string, nvd cvedict.Nvd) *CveContent { + var cpes []Cpe + for _, c := range nvd.Cpes { + cpes = append(cpes, Cpe{CpeName: c.CpeName}) + } + + var refs []Reference + for _, r := range nvd.References { + refs = append(refs, Reference{ + Link: r.Link, + Source: r.Source, + }) + } + + validVec := true + for _, v := range []string{ + nvd.AccessVector, + nvd.AccessComplexity, + nvd.Authentication, + nvd.ConfidentialityImpact, + nvd.IntegrityImpact, + nvd.AvailabilityImpact, + } { + if len(v) == 0 { + validVec = false + } + } + + vector := "" + if validVec { + vector = fmt.Sprintf("AV:%s/AC:%s/Au:%s/C:%s/I:%s/A:%s", + string(nvd.AccessVector[0]), + string(nvd.AccessComplexity[0]), + string(nvd.Authentication[0]), + string(nvd.ConfidentialityImpact[0]), + string(nvd.IntegrityImpact[0]), + string(nvd.AvailabilityImpact[0])) + } + + //TODO CVSSv3 + return &CveContent{ + Type: NVD, + CveID: cveID, + 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, + Published: nvd.PublishedDate, + LastModified: nvd.LastModifiedDate, + } +} + +// ConvertJvnToModel convert JVN to CveContent +func ConvertJvnToModel(cveID string, jvn cvedict.Jvn) *CveContent { + var cpes []Cpe + for _, c := range jvn.Cpes { + cpes = append(cpes, Cpe{CpeName: c.CpeName}) + } + + refs := []Reference{} + for _, r := range jvn.References { + refs = append(refs, Reference{ + Link: r.Link, + Source: r.Source, + }) + } + + vector := strings.TrimSuffix(strings.TrimPrefix(jvn.Vector, "("), ")") + return &CveContent{ + Type: JVN, + CveID: cveID, + Title: jvn.Title, + Summary: jvn.Summary, + Severity: jvn.Severity, + Cvss2Score: jvn.Score, + Cvss2Vector: vector, + SourceLink: jvn.JvnLink, + Cpes: cpes, + References: refs, + Published: jvn.PublishedDate, + LastModified: jvn.LastModifiedDate, + } +} diff --git a/report/report.go b/report/report.go index d9ed1f0e..f8610ff1 100644 --- a/report/report.go +++ b/report/report.go @@ -109,8 +109,8 @@ func fillCveDetail(r *models.ScanResult) error { return err } for _, d := range ds { - nvd := r.ConvertNvdToModel(d.CveID, d.Nvd) - jvn := r.ConvertJvnToModel(d.CveID, d.Jvn) + nvd := models.ConvertNvdToModel(d.CveID, d.Nvd) + jvn := models.ConvertJvnToModel(d.CveID, d.Jvn) for cveID, vinfo := range r.ScannedCves { if vinfo.CveID == d.CveID { if vinfo.CveContents == nil { From 2887dc0d36c08f79622631aed8d9bc22ff05efeb Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Tue, 1 Aug 2017 12:47:32 +0900 Subject: [PATCH 073/113] Fix configtest to match fast and deep scan mode --- commands/configtest.go | 5 +++++ scan/debian.go | 8 ++++++++ scan/freebsd.go | 1 + scan/redhat.go | 29 ++++++++++++++--------------- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/commands/configtest.go b/commands/configtest.go index 40809613..02a3ca73 100644 --- a/commands/configtest.go +++ b/commands/configtest.go @@ -36,6 +36,7 @@ type ConfigtestCmd struct { logDir string askKeyPassword bool containersOnly bool + deep bool sshNative bool httpProxy string timeoutSec int @@ -53,6 +54,7 @@ func (*ConfigtestCmd) Synopsis() string { return "Test configuration" } func (*ConfigtestCmd) Usage() string { return `configtest: configtest + [-deep] [-config=/path/to/config.toml] [-log-dir=/path/to/log] [-ask-key-password] @@ -86,6 +88,8 @@ func (p *ConfigtestCmd) SetFlags(f *flag.FlagSet) { "Ask ssh privatekey password before scanning", ) + f.BoolVar(&p.deep, "deep", false, "Config test for deep scan mode") + f.StringVar( &p.httpProxy, "http-proxy", @@ -133,6 +137,7 @@ func (p *ConfigtestCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interfa c.Conf.SSHNative = p.sshNative c.Conf.HTTPProxy = p.httpProxy c.Conf.ContainersOnly = p.containersOnly + c.Conf.Deep = p.deep var servernames []string if 0 < len(f.Args()) { diff --git a/scan/debian.go b/scan/debian.go index 6cae65cd..31d4bb23 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -137,6 +137,10 @@ func trim(str string) string { } func (o *debian) checkIfSudoNoPasswd() error { + if !config.Conf.Deep { + o.log.Infof("sudo ... No need") + return nil + } cmd := util.PrependProxyEnv("apt-get update") o.log.Infof("Checking... sudo %s", cmd) r := o.exec(cmd, sudo) @@ -149,6 +153,10 @@ func (o *debian) checkIfSudoNoPasswd() error { } func (o *debian) checkDependencies() error { + if !config.Conf.Deep { + o.log.Infof("Dependencies... No need") + return nil + } switch o.Distro.Family { case config.Ubuntu, config.Raspbian: return nil diff --git a/scan/freebsd.go b/scan/freebsd.go index 2ddec98e..bdcac08c 100644 --- a/scan/freebsd.go +++ b/scan/freebsd.go @@ -73,6 +73,7 @@ func (o *bsd) checkIfSudoNoPasswd() error { } func (o *bsd) checkDependencies() error { + o.log.Infof("Dependencies... No need") return nil } diff --git a/scan/redhat.go b/scan/redhat.go index 13b5cdbc..1fb7b430 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -121,7 +121,7 @@ func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) { } func (o *redhat) checkIfSudoNoPasswd() error { - if !o.sudo() { + if !config.Conf.Deep || !o.sudo() { o.log.Infof("sudo ... No need") return nil } @@ -134,11 +134,6 @@ func (o *redhat) checkIfSudoNoPasswd() error { var zero = []int{0} switch o.Distro.Family { - case config.CentOS: - cmds = []cmd{ - {"yum --changelog --assumeno update yum", []int{0, 1}}, - } - case config.RedHat, config.Oracle: majorVersion, err := o.Distro.MajorVersion() if err != nil { @@ -175,12 +170,17 @@ func (o *redhat) checkIfSudoNoPasswd() error { return nil } -// CentOS 6, 7 ... yum-plugin-changelog, yum-utils -// RHEL 5 ... yum-security -// RHEL 6, 7 ... - -// Amazon ... - +// - Fast scan mode +// No additional dependencies needed +// +// - Deep scan mode +// CentOS 6, 7 ... yum-utils +// RHEL 5 ... yum-security +// RHEL 6, 7 ... yum-utils +// Amazon ... yum-utils func (o *redhat) checkDependencies() error { - if o.Distro.Family == config.Amazon { + if !config.Conf.Deep { + o.log.Infof("Dependencies... No need") return nil } @@ -207,14 +207,13 @@ func (o *redhat) checkDependencies() error { } } - //TODO Check if yum-plugin-changelog is installed when scan with --changelog option on Amazon,RHEL, Oracle var packNames []string switch o.Distro.Family { - case config.CentOS: - packNames = []string{"yum-plugin-changelog", "yum-utils"} + case config.CentOS, config.Amazon: + packNames = []string{"yum-utils"} case config.RedHat, config.Oracle: if majorVersion < 6 { - packNames = []string{"yum-security"} + packNames = []string{"yum-utils", "yum-security"} } else { // yum-plugin-security is installed by default on RHEL6, 7 return nil From d3014025b0eb7746f6495b790429c8dbc84215f1 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Mon, 7 Aug 2017 05:57:09 +0900 Subject: [PATCH 074/113] Update README --- README.ja.md | 545 +++++++++++++++++++++--------------------- README.md | 530 ++++++++++++++++++++-------------------- commands/scan.go | 2 +- img/vuls-abstract.png | Bin 0 -> 125909 bytes 4 files changed, 536 insertions(+), 541 deletions(-) create mode 100644 img/vuls-abstract.png diff --git a/README.ja.md b/README.ja.md index a66ca4e4..bafc30c0 100644 --- a/README.ja.md +++ b/README.ja.md @@ -10,6 +10,8 @@ Vulnerability scanner for Linux/FreeBSD, agentless, written in golang. [README in English](https://github.com/future-architect/vuls/blob/master/README.md) Slackチームは[こちらから](http://goo.gl/forms/xm5KFo35tu)参加できます。(日本語でオッケーです) +![Vuls-Abstract](img/vuls-abstract.png) + [![asciicast](https://asciinema.org/a/bazozlxrw1wtxfu9yojyihick.png)](https://asciinema.org/a/bazozlxrw1wtxfu9yojyihick) ![Vuls-slack](img/vuls-slack-ja.png) @@ -18,90 +20,7 @@ Slackチームは[こちらから](http://goo.gl/forms/xm5KFo35tu)参加でき # TOC -- [Vuls: VULnerability Scanner](#vuls-vulnerability-scanner) -- [TOC](#toc) -- [Abstract](#abstract) -- [Main Features](#main-features) -- [What Vuls Doesn't Do](#what-vuls-doesnt-do) -- [Setup Vuls](#setup-vuls) -- [Tutorial: Local Scan Mode](#tutorial-local-scan-mode) - * [Step1. Launch Amazon Linux](#step1-launch-amazon-linux) - * [Step2. Install requirements](#step2-install-requirements) - * [Step3. Deploy go-cve-dictionary](#step3-deploy-go-cve-dictionary) - * [Step4. Deploy Vuls](#step4-deploy-vuls) - * [Step5. Config](#step5-config) - * [Step6. Check config.toml and settings on the server before scanning](#step6-check-configtoml-and-settings-on-the-server-before-scanning) - * [Step7. Start Scanning](#step7-start-scanning) - * [Step8. Reporting](#step8-reporting) - * [Step9. TUI](#step9-tui) - * [Step10. Web UI](#step10-web-ui) -- [Tutorial: Remote Scan Mode](#tutorial-remote-scan-mode) - * [Step1. Launch Another Amazon Linux](#step1-launch-another-amazon-linux) - * [Step2. Install Dependencies on the Remote Server](#step2-install-dependencies-on-the-remote-server) - * [Step3. Enable to SSH from Localhost](#step3-enable-to-ssh-from-localhost) - * [Step4. Config](#step4-config) - * [Step5. Check config.toml and settings on the server before scanning](#step5-check-configtoml-and-settings-on-the-server-before-scanning) - * [Step6. Start Scanning](#step6-start-scanning) - * [Step7. Reporting](#step7-reporting) -- [Architecture](#architecture) - * [A. Scan via SSH Mode (Remote Scan Mode)](#a-scan-via-ssh-mode-remote-scan-mode) - * [B. Scan without SSH (Local Scan Mode)](#b-scan-without-ssh-local-scan-mode) - * [go-cve-dictionary](#go-cve-dictionary) - * [Vuls](#vuls) -- [Performance Considerations](#performance-considerations) -- [Use Cases](#use-cases) - * [Scan all servers](#scan-all-servers) - * [Scan a single server](#scan-a-single-server) -- [Support OS](#support-os) -- [Usage: Automatic Server Discovery](#usage-automatic-server-discovery) - * [Example](#example) -- [Configuration](#configuration) -- [Usage: Configtest](#usage-configtest) - * [Dependencies on Target Servers](#dependencies-on-target-servers) - * [Check /etc/sudoers](#check-etcsudoers) -- [Usage: Scan](#usage-scan) - * [-ssh-native-insecure option](#-ssh-native-insecure-option) - * [-ask-key-password option](#-ask-key-password-option) - * [Example: Scan all servers defined in config file](#example-scan-all-servers-defined-in-config-file) - * [Example: Scan specific servers](#example-scan-specific-servers) - * [Example: Scan via shell instead of SSH.](#example-scan-via-shell-instead-of-ssh) - + [cronで動かす場合](#cron%E3%81%A7%E5%8B%95%E3%81%8B%E3%81%99%E5%A0%B4%E5%90%88) - * [Example: Scan containers (Docker/LXD)](#example-scan-containers-dockerlxd) - + [Docker](#docker) - + [LXDコンテナをスキャンする場合](#lxd%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%82%92%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%B3%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88) -- [Usage: Report](#usage-report) - * [How to read a report](#how-to-read-a-report) - + [Example](#example-1) - + [Summary part](#summary-part) - + [Detailed Part](#detailed-part) - + [Changelog Part](#changelog-part) - * [Example: Send scan results to Slack](#example-send-scan-results-to-slack) - * [Example: Put results in S3 bucket](#example-put-results-in-s3-bucket) - * [Example: Put results in Azure Blob storage](#example-put-results-in-azure-blob-storage) - * [Example: IgnoreCves](#example-ignorecves) - * [Example: Add optional key-value pairs to JSON](#example-add-optional-key-value-pairs-to-json) - * [Example: Use MySQL as a DB storage back-end](#example-use-mysql-as-a-db-storage-back-end) - * [Example: Use PostgreSQL as a DB storage back-end](#example-use-postgresql-as-a-db-storage-back-end) - * [Example: Use Redis as a DB storage back-end](#example-use-redis-as-a-db-storage-back-end) -- [Usage: Scan vulnerability of non-OS package](#usage-scan-vulnerability-of-non-os-package) -- [Usage: Integrate with OWASP Dependency Check to Automatic update when the libraries are updated (Experimental)](#usage-integrate-with-owasp-dependency-check-to-automatic-update-when-the-libraries-are-updated-experimental) -- [Usage: TUI](#usage-tui) - * [Display the latest scan results](#display-the-latest-scan-results) - * [Display the previous scan results](#display-the-previous-scan-results) -- [Display the previous scan results using peco](#display-the-previous-scan-results-using-peco) -- [Usage: go-cve-dictionary on different server](#usage-go-cve-dictionary-on-different-server) -- [Usage: Update NVD Data](#usage-update-nvd-data) -- [レポートの日本語化](#%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88%E3%81%AE%E6%97%A5%E6%9C%AC%E8%AA%9E%E5%8C%96) - * [fetchnvd, fetchjvnの実行順序の注意](#fetchnvd-fetchjvn%E3%81%AE%E5%AE%9F%E8%A1%8C%E9%A0%86%E5%BA%8F%E3%81%AE%E6%B3%A8%E6%84%8F) - * [スキャン実行](#%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%B3%E5%AE%9F%E8%A1%8C) -- [Update Vuls With Glide](#update-vuls-with-glide) -- [Misc](#misc) -- [Related Projects](#related-projects) -- [Data Source](#data-source) -- [Authors](#authors) -- [Contribute](#contribute) -- [Change Log](#change-log) -- [License](#license) +TODO ---- @@ -130,13 +49,29 @@ Vulsは上に挙げた手動運用での課題を解決するツールであり - Linuxサーバに存在する脆弱性をスキャン - Ubuntu, Debian, CentOS, Amazon Linux, RHEL, Raspbianに対応 - クラウド、オンプレミス、Docker +- 高精度なスキャン + - Vulsは複数の脆弱性データベースを使っている + - OVAL + - RHSA/ALAS/ELSA/FreeBSD-SA + - Changelog +- FastスキャンとDeepスキャン + - Fastスキャン + - root権限必要なし + - スキャン対象サーバの負荷ほぼなし + - Deepスキャン + - Changelogの差分を取得し、そこに含まれる脆弱性を検知 + - スキャン対象サーバに負荷がかかる場合がある +- リモートスキャンとローカルスキャン + - リモートスキャン + - スキャン対象サーバにSSH接続可能なマシン1台にセットアップするだけで動作 + - ローカルスキャン + - もし中央のサーバから各サーバにSSH接続できない環境の場合はローカルスキャンモードでスキャン可能 - OSパッケージ管理対象外のミドルウェアをスキャン - プログラミング言語のライブラリやフレームワーク、ミドルウェアの脆弱性スキャン - CPEに登録されているソフトウェアが対象 -- エージェントレスアーキテクチャ - - スキャン対象サーバにSSH接続可能なマシン1台にセットアップするだけで動作 - 非破壊スキャン(SSHでコマンド発行するだけ) - AWSでの脆弱性/侵入テスト事前申請は必要なし + - 毎日スケジュール実行すれば新規に公開された脆弱性にすぐに気付くことができる - 設定ファイルのテンプレート自動生成 - CIDRを指定してサーバを自動検出、設定ファイルのテンプレートを生成 - EmailやSlackで通知可能(日本語でのレポートも可能) @@ -159,7 +94,19 @@ Vulsのセットアップは以下の2パターンがある see https://github.com/future-architect/vuls/tree/master/setup/docker - 手動でセットアップ -Hello Vulsチュートリアルでは手動でのセットアップ方法で説明する +チュートリアルでは手動でのセットアップ方法で説明する + +---- + +# Tutorial + +1. Tutorial: Local Scan Mode + - Launch CentOS on AWS + - Deploy Vuls + - Scan localhost, Reporting +1. Tutorial: Remote Scan Mode + - Launch Ubuntu Linux on AWS + - このUbuntuを先程セットアップしたVulsからスキャンする ---- @@ -168,9 +115,10 @@ Hello Vulsチュートリアルでは手動でのセットアップ方法で説 本チュートリアルでは、Amazon EC2にVulsをセットアップし、自分に存在する脆弱性をスキャンする方法を説明する。 手順は以下の通り -1. Amazon Linuxを新規作成 +1. CentOSを新規作成 1. 必要なソフトウェアをインストール 1. go-cve-dictionaryをデプロイ +1. goval-dictionaryをデプロイ 1. Vulsをデプロイ 1. 設定 1. 設定ファイルと、スキャン対象サーバの設定のチェック @@ -179,9 +127,9 @@ Hello Vulsチュートリアルでは手動でのセットアップ方法で説 1. TUI(Terminal-Based User Interface)で結果を参照する 1. Web UI([VulsRepo](https://github.com/usiusi360/vulsrepo))で結果を参照する -## Step1. Launch Amazon Linux +## Step1. Launch CentOS7 -- 今回は説明のために、脆弱性を含む古いAMIを使う (amzn-ami-hvm-2015.09.1.x86_64-gp2 - ami-383c1956) +- 今回は説明のために、脆弱性を含む古いAMIを使う - EC2作成時に自動アップデートされるとVulsスキャン結果が0件になってしまうので、cloud-initに以下を指定してEC2を作成する。 ``` @@ -199,14 +147,14 @@ Vulsセットアップに必要な以下のソフトウェアをインストー - git - gcc - GNU Make -- go v1.7.1 or later (The latest version is recommended) +- go v1.8.3 or later (The latest version is recommended) - https://golang.org/doc/install ```bash -$ ssh ec2-user@52.100.100.100 -i ~/.ssh/private.pem -$ sudo yum -y install sqlite git gcc make -$ wget https://storage.googleapis.com/golang/go1.7.1.linux-amd64.tar.gz -$ sudo tar -C /usr/local -xzf go1.7.1.linux-amd64.tar.gz +$ ssh centos@52.100.100.100 -i ~/.ssh/private.pem +$ sudo yum -y install sqlite git gcc make wget +$ wget https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz +$ sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz $ mkdir $HOME/go ``` /etc/profile.d/goenv.sh を作成し、下記を追加する。 @@ -228,7 +176,7 @@ $ source /etc/profile.d/goenv.sh ```bash $ sudo mkdir /var/log/vuls -$ sudo chown ec2-user /var/log/vuls +$ sudo chown centos /var/log/vuls $ sudo chmod 700 /var/log/vuls $ $ mkdir -p $GOPATH/src/github.com/kotakanbe @@ -238,7 +186,7 @@ $ cd go-cve-dictionary $ make install ``` バイナリは、`$GOPATH/bin`以下に生成される - +もしもインストールプロセスが途中で止まる場合は、Out of memory errorが発生している可能性があるので、インスタンスタイプを大きくして再実行してみてください。 NVDから脆弱性データベースを取得する。 環境によって異なるが、AWS上では10分程度かかる。 @@ -251,14 +199,34 @@ $ ls -alh cve.sqlite3 -rw-r--r-- 1 ec2-user ec2-user 7.0M Mar 24 13:20 cve.sqlite3 ``` -日本語化したい場合は、JVNから脆弱性データベースを取得する。 +脆弱性レポートを日本語化したい場合は、JVNから脆弱性データベースを取得する。 ```bash $ cd $HOME $ for i in `seq 1998 $(date +"%Y")`; do go-cve-dictionary fetchjvn -years $i; done ``` -## Step4. Deploy Vuls +## Step4. Deploy goval-dictionary + +[goval-dictionary](https://github.com/kotakanbe/goval-dictionary) + +```bash +$ mkdir -p $GOPATH/src/github.com/kotakanbe +$ cd $GOPATH/src/github.com/kotakanbe +$ git clone https://github.com/kotakanbe/goval-dictionary.git +$ cd goval-dictionary +$ make install +``` +The binary was built under `$GOPATH/bin` +もしもインストールプロセスが途中で止まる場合は、Out of memory errorが発生している可能性があるので、インスタンスタイプを大きくして再実行してみてください。 + +今回はCentOSがスキャン対象なので、RedHatが公開しているOVAL情報を取り込む. [README](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-redhat) + +```bash +$ goval-dictionary fetch-redhat 5 6 7 +``` + +## Step5. Deploy Vuls 新規にターミナルを起動し、先ほど作成したEC2にSSH接続する。 ``` @@ -268,8 +236,10 @@ $ git clone https://github.com/future-architect/vuls.git $ cd vuls $ make install ``` +The binary was built under `$GOPATH/bin` +もしもインストールプロセスが途中で止まる場合は、Out of memory errorが発生している可能性があるので、インスタンスタイプを大きくして再実行してみてください。 -## Step5. Config +## Step6. Config Vulsの設定ファイルを作成する(TOMLフォーマット) @@ -279,104 +249,101 @@ $ cat config.toml [servers] [servers.localhost] -host = "localhost" -port = "local" +host = "localhost" +port = "local" ``` -Root権限が必要なディストリビューションもあるので、スキャン対象サーバの/etc/sudoersを変更する。 -パスワードありのsudoはセキュリティ上の理由からサポートしていないので、スキャンに必要なコマンドは、`NOPASSAWORD`として、remote host上の`etc/sudoers`に定義しておく。 -See [Usage: Configtest#Check /etc/sudoers](#check-etcsudoers) - -## Step6. Check config.toml and settings on the server before scanning +## Step7. Check config.toml and settings on the server before scanning ``` $ vuls configtest ``` 詳細は [Usage: configtest](#usage-configtest) を参照 -## Step7. Start Scanning +## Step8. Start Scanning ``` $ vuls scan + ... snip ... -Scan Summary -============ -localhost amazon 2015.09 94 CVEs 103 updatable packages +One Line Summary +================ +localhost centos7.3.1611 31 updatable packages ``` -## Step8. Reporting +## Step9. Reporting View one-line summary ``` -$ vuls report -format-one-line-text -cvedb-path=$PWD/cve.sqlite3 +$ vuls report -lang=ja -format-one-line-text -cvedb-path=$PWD/cve.sqlite3 -ovaldb-path=$PWD/oval.sqlite3 One Line Summary ================ -localhost Total: 94 (High:19 Medium:54 Low:7 ?:14) 103 updatable packages +localhost Total: 101 (High:35 Medium:50 Low:16 ?:0) 31 updatable packages ``` View short summary. ``` -$ vuls report -format-short-text -cvedb-path=$PWD/cve.sqlite3 --lang=ja +$ vuls report -lang=ja -format-short-text |less -localhost (amazon 2015.09) -=========================== -Total: 94 (High:19 Medium:54 Low:7 ?:14) 103 updatable packages +localhost (centos7.3.1611) +========================== +Total: 101 (High:35 Medium:50 Low:16 ?:0) 31 updatable packages -CVE-2016-5636 10.0 (High) CPython の zipimport.c の get_data 関数における整数オーバーフローの脆弱性 - http://jvndb.jvn.jp/ja/contents/2016/JVNDB-2016-004528.html - https://access.redhat.com/security/cve/CVE-2016-5636 - python27-2.7.10-4.119.amzn1 -> python27-2.7.12-2.120.amzn1 - python27-devel-2.7.10-4.119.amzn1 -> python27-devel-2.7.12-2.120.amzn1 - python27-libs-2.7.10-4.119.amzn1 -> python27-libs-2.7.12-2.120.amzn1 - Confidence: 100 / YumUpdateSecurityMatch +CVE-2017-7895 10.0 HIGH (nvd) + Linux Kernel の NFSv2/NFSv3 + サーバの実装におけるポインタ演算エラーを誘発される脆弱性 + Linux Kernel の NFSv2/NFSv3 + サーバの実装は、バッファの終端に対する特定のチェックが欠落しているため、ポイン... + (pointer-arithmetic error) + を誘発されるなど、不特定の影響を受ける脆弱性が存在します。 + --- + http://jvndb.jvn.jp/ja/contents/2017/JVNDB-2017-003674.html + https://access.redhat.com/security/cve/CVE-2017-7895 (RHEL-CVE) + 10.0/AV:N/AC:L/Au:N/C:C/I:C/A:C (nvd) + 10.0/AV:N/AC:L/Au:N/C:C/I:C/A:C (jvn) + https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?name=CVE-2017-7895 + 6.5/CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N (redhat) + https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?name=CVE-2017-7895 + Confidence: 100 / OvalMatch -... snip ... ```` View full report. ``` -$ vuls report -format-full-text -cvedb-path=$PWD/cve.sqlite3 --lang=ja +$ vuls report -lang=ja -format-full-text |less -localhost (amazon 2015.09) -============================ -Total: 94 (High:19 Medium:54 Low:7 ?:14) 103 updatable packages +localhost (centos7.3.1611) +========================== +Total: 101 (High:35 Medium:50 Low:16 ?:0) 31 updatable packages -CVE-2016-5636 -------------- -Score 10.0 (High) -Vector (AV:N/AC:L/Au:N/C:C/I:C/A:C) -Title CPython の zipimport.c の get_data 関数における整数オーバーフローの脆弱性 -Description CPython (別名 Python) の zipimport.c の get_data - 関数には、整数オーバーフローの脆弱性が存在します。 +CVE-2015-2806 +---------------- +Max Score 10.0 HIGH (nvd) +nvd 10.0/AV:N/AC:L/Au:N/C:C/I:C/A:C +redhat 2.6/AV:N/AC:H/Au:N/C:N/I:N/A:P +redhat 3.3/CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L +CVSSv2 Calc https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?name=CVE-2015-2806 +CVSSv3 Calc https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?name=CVE-2015-2806 +Summary Stack-based buffer overflow in asn1_der_decoding in libtasn1 before 4.4 allows + remote attackers to have unspecified impact via unknown vectors. +Source https://nvd.nist.gov/vuln/detail/CVE-2015-2806 +RHEL-CVE https://access.redhat.com/security/cve/CVE-2015-2806 +CWE-119 (nvd) https://cwe.mitre.org/data/definitions/119.html +Package/CPE libtasn1-3.8-3.el7 - +Confidence 100 / OvalMatch - 補足情報 : CWE による脆弱性タイプは、CWE-190: Integer Overflow or Wraparound - (整数オーバーフローまたはラップアラウンド) と識別されています。 - http://cwe.mitre.org/data/definitions/190.html -CWE-190 https://cwe.mitre.org/data/definitions/190.html -CWE-190(JVN) http://jvndb.jvn.jp/ja/cwe/CWE-190.html -JVN http://jvndb.jvn.jp/ja/contents/2016/JVNDB-2016-004528.html -NVD https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2016-5636 -MITRE https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-5636 -CVE Details http://www.cvedetails.com/cve/CVE-2016-5636 -CVSS Claculator https://nvd.nist.gov/cvss/v2-calculator?name=CVE-2016-5636&vector=(AV:N/AC:L/... -RHEL-CVE https://access.redhat.com/security/cve/CVE-2016-5636 -ALAS-2016-724 https://alas.aws.amazon.com/ALAS-2016-724.html -Package python27-2.7.10-4.119.amzn1 -> python27-2.7.12-2.120.amzn1 - python27-devel-2.7.10-4.119.amzn1 -> python27-devel-2.7.12-2.120.amzn1 - python27-libs-2.7.10-4.119.amzn1 -> python27-libs-2.7.12-2.120.amzn1 -Confidence 100 / YumUpdateSecurityMatch ... snip ... ``` -## Step9. TUI +## Step10. TUI Vulsにはスキャン結果の詳細を参照できるイカしたTUI(Terminal-Based User Interface)が付属している。 @@ -386,7 +353,7 @@ $ vuls tui ![Vuls-TUI](img/hello-vuls-tui.png) -## Step10. Web UI +## Step11. Web UI [VulsRepo](https://github.com/usiusi360/vulsrepo)はスキャン結果をビボットテーブルのように分析可能にするWeb UIである。 [Online Demo](http://usiusi360.github.io/vulsrepo/)があるので試してみて。 @@ -397,33 +364,28 @@ $ vuls tui SSHを用いてリモートのホストをスキャンする方法を説明する。 -1. Amazon Linuxを新規に1台作成(スキャン対象) -1. 必要なソフトウェアをインストール -1. RemoteホストにlocalhostからSSH可能にする -1. 設定 +1. Ubuntu Linuxを新規に1台作成(スキャン対象) +1. スキャン対象のRemoteホストにlocalhostからSSH可能にする +1. config.tomlの設定 1. 設定ファイルと、スキャン対象サーバの設定のチェック 1. Scan 1. Reporting 先程のチュートリアルで作成したVulsサーバ(以下localhostと記述)を用いる。 -## Step1. Launch Another Amazon Linux +## Step1. Launch new Ubuntu Linux (the server to be sacnned) [Tutorial: Local Scan Mode#Step1. Launch Amazon Linux](#step1-launch-amazon-linux)と同じ +[Tutorial: Local Scan Mode#Step1. Launch CentOS7](#step1-launch-centos7)のようにUbuntu Linuxを新規に作成する。 新規にターミナルを開いて今作成したEC2にSSH接続する。 +$HOME/.ssh/known_hostsにリモートホストのHost Keyを追加するために、スキャン前にリモートホストにSSH接続する必要がある。 -## Step2. Install Dependencies on the Remote Server - -ディストリビューションによってはスキャンに必要な依存ソフトウェアをインストールする必要がある。 -これらはリモートサーバ上に手動かAnsibleなどでインストールする。 -依存ソフトウェアの詳細は [Dependencies on Target Servers](#dependencies-on-target-servers) を参照。 - -## Step3. Enable to SSH from Localhost +## Step2. Enable to SSH from localhost VulsはSSHパスワード認証をサポートしてない。SSHの鍵認証の設定をしなければならない。 localhost上でkeypairを作成し、remote host上のauthorized_keysに追加する。 -- Localhost +- localhost ```bash $ ssh-keygen -t rsa ``` @@ -439,47 +401,50 @@ $ vim ~/.ssh/authorized_keys ``` Paste from the clipboard to ~/.ssh/.authorized_keys -パスワードありのsudoはセキュリティ上の理由からサポートしていないので、スキャンに必要なコマンドは、`NOPASSAWORD`として、remote host上の`etc/sudoers`に定義しておく。 -See [Usage: Configtest#Check /etc/sudoers](#check-etcsudoers) +localhostのknown_hostsにremote hostのホストキーが登録されている必要があるので確認すること。 +$HOME/.ssh/known_hostsにリモートホストのHost Keyを追加するために、スキャン前にリモートホストにSSH接続する必要がある。 -また、localhostのknown_hostsにremote hostのホストキーが登録されている必要があるので確認すること。 -## Step4. Config +- localhost +``` +$ ssh ubuntu@172.31.4.82 -i ~/.ssh/id_rsa +``` -- Localhost +## Step3. config.tomlの設定 + +- localhost ``` $ cd $HOME $ cat config.toml [servers] -[servers.172-31-4-82] +[servers.ubuntu] host = "172.31.4.82" port = "22" -user = "ec2-user" -keyPath = "/home/ec2-user/.ssh/id_rsa" +user = "ubuntu" +keyPath = "/home/centos/.ssh/id_rsa" ``` -## Step5. Check config.toml and settings on the server before scanning +## Step4. Check config.toml and settings on the server before scanning ``` -$ vuls configtest +$ vuls configtest ubuntu ``` see [Usage: configtest](#usage-configtest) -## Step6. Start Scanning +## Step5. Start Scanning ``` -$ vuls scan +$ vuls scan ubuntu ... snip ... -Scan Summary -============ -172-31-4-82 amazon 2015.09 94 CVEs 103 updatable packages - +One Line Summary +================ +ubuntu ubuntu16.04 30 updatable packages ``` -## Step7. Reporting +## Step6. Reporting See [Tutorial: Local Scan Mode#Step8. Reporting](#step8-reporting) See [Tutorial: Local Scan Mode#Step9. TUI](#step9-tui) @@ -756,6 +721,7 @@ host = "172.31.4.82" $ vuls configtest --help configtest: configtest + [-deep] [-config=/path/to/config.toml] [-log-dir=/path/to/log] [-ask-key-password] @@ -774,6 +740,8 @@ configtest: Test containers only. Default: Test both of hosts and containers -debug debug mode + -deep + Config test for deep scan mode -http-proxy string http://proxy-url:port (default: empty) -log-dir string @@ -784,30 +752,33 @@ configtest: Timeout(Sec) (default 300) ``` -configtestサブコマンドは以下をチェックする -- config.tomlで定義されたサーバ/コンテナに対してSSH可能かどうか +configtestサブコマンドは、config.tomlで定義されたサーバ/コンテナに対してSSH可能かどうかをチェックする。 + +## Deep Scan Mode + +Deep Scan Modeではスキャン対象サーバ上にいくつかの依存パッケージが必要。 +configtestに--deepをつけて実行するとSSH接続に加えて以下もチェックする。 - スキャン対象のサーバ上に依存パッケーがインストールされているか - /etc/sudoers -## Dependencies on Target Servers +### Dependencies and /etc/sudoers on Target Servers -スキャンするためには、下記のパッケージが必要なので、手動かまたはAnsibleなどのツールで事前にインストールする必要がある。 +Deep Scan Modeでスキャンするためには、下記のパッケージが必要なので、手動かまたはAnsibleなどのツールで事前にインストールする必要がある。 -| Distribution| Release | Requirements | -|:------------|-------------------:|:-------------| -| Ubuntu | 12, 14, 16| - | -| Debian | 7, 8| aptitude | -| CentOS | 6, 7| yum-plugin-changelog | -| Amazon | All | - | -| RHEL | 5 | yum-security | -| RHEL | 6, 7 | - | -| FreeBSD | 10 | - | -| Raspbian | Wheezy, Jessie | - | +| Distribution | Release | Requirements | +|:-------------|-------------------:|:-------------| +| Ubuntu | 12, 14, 16| - | +| Debian | 7, 8| aptitude | +| CentOS | 6, 7| yum-plugin-changelog, yum-utils | +| Amazon | All | yum-plugin-changelog, yum-utils | +| RHEL | 5 | yum-utils, yum-security, yum-changelog | +| RHEL | 6, 7 | yum-utils, yum-plugin-changelog | +| Oracle Linux | 5 | yum-utils, yum-security, yum-changelog | +| Oracle Linux | 6, 7 | yum-utils, yum-plugin-changelog | +| FreeBSD | 10 | - | +| Raspbian | Wheezy, Jessie | - | -## Check /etc/sudoers - -スキャン対象サーバに対してパスワードなしでSUDO可能な状態か確認する。 -また、requirettyも定義されているか確認する。(--ssh-native-insecureオプションでscanする場合はrequirettyは定義しなくても良い) +また、Deep Scan Modeで利用するコマンドの中にはRoot権限が必要なものものある。configtestサブコマンドでは、スキャン対象サーバに対してそのコマンドがパスワードなしでSUDO可能な状態か確認する。また、requirettyも定義されているかも確認する。(--ssh-native-insecureオプションでscanする場合はrequirettyは定義しなくても良い) ``` Defaults:vuls !requiretty ``` @@ -815,37 +786,25 @@ For details, see [-ssh-native-insecure option](#-ssh-native-insecure-option) スキャン対象サーバ上の`/etc/sudoers`のサンプル -- CentOS +- RHEL 5 / Oracle Linux 5 ``` -vuls ALL=(ALL) NOPASSWD:/usr/bin/yum --changelog --assumeno update * +vuls ALL=(ALL) NOPASSWD:/usr/bin/yum --color=never repolist, /usr/bin/yum --color=never list-security --security, /usr/bin/yum --color=never info-security Defaults:vuls env_keep="http_proxy https_proxy HTTP_PROXY HTTPS_PROXY" ``` -- RHEL 5 +- RHEL 6, 7 / Oracle Linux 6, 7 ``` -vuls ALL=(ALL) NOPASSWD:/usr/bin/yum --color=never repolist, /usr/bin/yum --color=never list-security --security, /usr/bin/yum --color=never check-update, /usr/bin/yum --color=never info-security +vuls ALL=(ALL) NOPASSWD:/usr/bin/yum --color=never repolist, /usr/bin/yum --color=never --security updateinfo list updates, /usr/bin/yum --color=never --security updateinfo updates Defaults:vuls env_keep="http_proxy https_proxy HTTP_PROXY HTTPS_PROXY" ``` -- RHEL 6, 7 -``` -vuls ALL=(ALL) NOPASSWD:/usr/bin/yum --color=never repolist, /usr/bin/yum --color=never --security updateinfo list updates, /usr/bin/yum --color=never check-update, /usr/bin/yum --color=never --security updateinfo updates -Defaults:vuls env_keep="http_proxy https_proxy HTTP_PROXY HTTPS_PROXY" -``` - -- Debian +- Debian/Ubuntu/Raspbian ``` vuls ALL=(ALL) NOPASSWD: /usr/bin/apt-get update Defaults:vuls env_keep="http_proxy https_proxy HTTP_PROXY HTTPS_PROXY" ``` -- Ubuntu/Raspbian -``` -vuls ALL=(ALL) NOPASSWD: /usr/bin/apt-get update -Defaults:vuls env_keep="http_proxy https_proxy HTTP_PROXY HTTPS_PROXY" -``` - -- Amazon Linux, FreeBSDは今のところRoot権限なしでスキャン可能 +- CentOS, Amazon Linux, FreeBSDは今のところRoot権限なしでスキャン可能 ---- @@ -855,6 +814,7 @@ Defaults:vuls env_keep="http_proxy https_proxy HTTP_PROXY HTTPS_PROXY" $ vuls scan -help scan: scan + [-deep] [-config=/path/to/config.toml] [-results-dir=/path/to/results] [-log-dir=/path/to/log] @@ -880,6 +840,8 @@ scan: Scan containers only. Default: Scan both of hosts and containers -debug debug mode + -deep + Deep scan mode. Scan accuracy improves and information becomes richer. Since analysis of changelog, issue commands requiring sudo, but it may be slower and high load on the scan tareget server. -http-proxy string http://proxy-url:port (default: empty) -log-dir string @@ -898,6 +860,23 @@ scan: Number of second for scaning vulnerabilities for all servers (default 7200) ``` +## -deep option + +You need to execute `vuls configtest --deep` to check the configuration of the target server before scanning with -deep flag. + +| Distribution | Changelog | +|:-------------|:---------:| +| Ubuntu | yes | +| Debian | yes | +| CentOS | yes | +| Amazon | yes | +| RHEL | yes | +| RHEL | yes | +| Oracle Linux | yes | +| Oracle Linux | yes | +| FreeBSD | no | +| Raspbian | yes | + ## -ssh-native-insecure option Vulsは2種類のSSH接続方法をサポートしている。 @@ -1045,6 +1024,9 @@ report: [-cvedb-type=sqlite3|mysql|postgres|redis] [-cvedb-path=/path/to/cve.sqlite3] [-cvedb-url=http://127.0.0.1:1323 or DB connection string] + [-ovaldb-type=sqlite3|mysql] + [-ovaldb-path=/path/to/oval.sqlite3] + [-ovaldb-url=http://127.0.0.1:1324 or DB connection string] [-cvss-over=7] [-diff] [-ignore-unscored-cves] @@ -1122,6 +1104,12 @@ report: [en|ja] (default "en") -log-dir string /path/to/log (default "/var/log/vuls") + -ovaldb-path string + /path/to/sqlite3 (For get oval detail from oval.sqlite3) (default "/Users/kotakanbe/go/src/github.com/future-architect/vuls/oval.sqlite3") + -ovaldb-type string + DB type for fetching OVAL dictionary (sqlite3 or mysql) (default "sqlite3") + -ovaldb-url string + http://goval-dictionary.com:1324 or mysql connection string -pipe Use stdin via PIPE -refresh-cve @@ -1177,46 +1165,45 @@ Confidence 100 / YumUpdateSecurityMatch ### Summary part ``` -172-31-4-82 (amazon 2015.09) -============================ -Total: 94 (High:19 Medium:54 Low:7 ?:14) 103 updatable packages +cent6 (centos6.6) +================= +Total: 145 (High:23 Medium:101 Low:21 ?:0) 83 updatable packages ``` -- `172-31-4-82` means that it is a scan report of `servers.172-31-4-82` defined in cocnfig.toml. -- `(amazon 2015.09)` means that the version of the OS is Amazon Linux 2015.09. -- `Total: 94 (High:19 Medium:54 Low:7 ?:14)` means that a total of 94 vulnerabilities exist, and the distribution of CVSS Severity is displayed. -- `103 updatable packages` means that there are 103 updateable packages on the target server. +- `cent6` means that it is a scan report of `servers.cent6` defined in cocnfig.toml. +- `(centos6.6)` means that the version of the OS is CentOS6.6. +- `Total: 145 (High:23 Medium:101 Low:21 ?:0)` means that a total of 145 vulnerabilities exist, and the distribution of CVSS Severity is displayed. +- `83 updatable packages` means that there are 83 updateable packages on the target server. ### Detailed Part ``` -CVE-2016-5636 -------------- -Score 10.0 (High) -Vector (AV:N/AC:L/Au:N/C:C/I:C/A:C) -Summary Integer overflow in the get_data function in zipimport.c in CPython (aka Python) - before 2.7.12, 3.x before 3.4.5, and 3.5.x before 3.5.2 allows remote attackers - to have unspecified impact via a negative data size value, which triggers a - heap-based buffer overflow. -CWE https://cwe.mitre.org/data/definitions/190.html -NVD https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2016-5636 -MITRE https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-5636 -CVE Details http://www.cvedetails.com/cve/CVE-2016-5636 -CVSS Claculator https://nvd.nist.gov/cvss/v2-calculator?name=CVE-2016-5636&vector=(AV:N/AC:L/... -RHEL-CVE https://access.redhat.com/security/cve/CVE-2016-5636 -ALAS-2016-724 https://alas.aws.amazon.com/ALAS-2016-724.html -Package python27-2.7.10-4.119.amzn1 -> python27-2.7.12-2.120.amzn1 - python27-devel-2.7.10-4.119.amzn1 -> python27-devel-2.7.12-2.120.amzn1 - python27-libs-2.7.10-4.119.amzn1 -> python27-libs-2.7.12-2.120.amzn1 -Confidence 100 / YumUpdateSecurityMatch +CVE-2016-0702 +---------------- +Max Score 2.6 IMPORTANT (redhat) +nvd 1.9/AV:L/AC:M/Au:N/C:P/I:N/A:N +redhat 2.6/AV:L/AC:H/Au:N/C:P/I:P/A:N +jvn 1.9/AV:L/AC:M/Au:N/C:P/I:N/A:N +CVSSv2 Calc https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?name=CVE-2016-0702 +Summary The MOD_EXP_CTIME_COPY_FROM_PREBUF function in crypto/bn/bn_exp.c in OpenSSL + 1.0.1 before 1.0.1s and 1.0.2 before 1.0.2g does not properly consider + cache-bank access times during modular exponentiation, which makes it easier for + local users to discover RSA keys by running a crafted application on the same + Intel Sandy Bridge CPU core as a victim and leveraging cache-bank conflicts, aka + a "CacheBleed" attack. +Source https://nvd.nist.gov/vuln/detail/CVE-2016-0702 +RHEL-CVE https://access.redhat.com/security/cve/CVE-2016-0702 +CWE-200 (nvd) https://cwe.mitre.org/data/definitions/200.html +Package/CPE openssl-1.0.1e-30.el6 - 1.0.1e-57.el6 +Confidence 100 / OvalMatch ``` -- `Score` means CVSS Score. -- `Vector` means [CVSS Vector](https://nvd.nist.gov/CVSS/Vector-v2.aspx) +- `Max Score` means Max CVSS Score. +- `nvd` shows [CVSS Vector](https://nvd.nist.gov/CVSS/Vector-v2.aspx) of NVD +- `redhat` shows [CVSS Vector](https://nvd.nist.gov/CVSS/Vector-v2.aspx) of RedHat OVAL +- `jvn` shows [CVSS Vector](https://nvd.nist.gov/CVSS/Vector-v2.aspx) of JVN - `Summary` means Summary of the CVE. - `CWE` means [CWE - Common Weakness Enumeration](https://nvd.nist.gov/cwe.cfm) of the CVE. -- `NVD` `MITRE` `CVE Details` `CVSS Caluculator` -- `RHEL-CVE` means the URL of OS distributor support. - `Package` shows the package version information including this vulnerability. - `Confidence` means the reliability of detection. - `100` is highly reliable @@ -1225,34 +1212,14 @@ Confidence 100 / YumUpdateSecurityMatch | Detection Method | Confidence | OS |Description| |:-----------------------|-------------------:|:---------------------------------|:--| - | YumUpdateSecurityMatch | 100 | RHEL, Amazon Linux |Detection using yum-plugin-security| + | OvalMatch | 100 | CentOS, RHEL, Oracle, Ubuntu, Debian |Detection using OVAL | + | YumUpdateSecurityMatch | 100 | RHEL, Amazon, Oracle |Detection using yum-plugin-security| | ChangelogExactMatch | 95 | CentOS, Ubuntu, Debian, Raspbian |Exact version match between changelog and package version| | ChangelogLenientMatch | 50 | Ubuntu, Debian, Raspbian |Lenient version match between changelog and package version| | PkgAuditMatch | 100 | FreeBSD |Detection using pkg audit| | CpeNameMatch | 100 | All |Search for NVD information with CPE name specified in config.toml| -### Changelog Part - -The scan results of Ubuntu, Debian, Raspbian or CentOS are also output Changelog in TUI or report with -format-full-text. -(RHEL, Amazon or FreeBSD will be available in the near future) - -The output change log includes only the difference between the currently installed version and candidate version. - -``` -tar-1.28-2.1 -> tar-1.28-2.1ubuntu0.1 -------------------------------------- -tar (1.28-2.1ubuntu0.1) xenial-security; urgency=medium - - * SECURITY UPDATE: extract pathname bypass - - debian/patches/CVE-2016-6321.patch: skip members whose names contain - ".." in src/extract.c. - - CVE-2016-6321 - - -- Marc Deslauriers Thu, 17 Nov 2016 11:06:07 -0500 -``` - - ## Example: Send scan results to Slack ``` $ vuls report \ @@ -1508,6 +1475,9 @@ tui: [-cvedb-type=sqlite3|mysql|postgres|redis] [-cvedb-path=/path/to/cve.sqlite3] [-cvedb-url=http://127.0.0.1:1323 DB connection string] + [-ovaldb-type=sqlite3|mysql] + [-ovaldb-path=/path/to/oval.sqlite3] + [-ovaldb-url=http://127.0.0.1:1324 or DB connection string] [-refresh-cve] [-results-dir=/path/to/results] [-log-dir=/path/to/log] @@ -1521,6 +1491,12 @@ tui: DB type for fetching CVE dictionary (sqlite3, mysql, postgres or redis) (default "sqlite3") -cvedb-url string http://cve-dictionary.com:8080 or DB connection string + -ovaldb-path string + /path/to/sqlite3 (For get oval detail from oval.sqlite3) (default "/Users/kotakanbe/go/src/github.com/future-architect/vuls/oval.sqlite3") + -ovaldb-type string + DB type for fetching OVAL dictionary (sqlite3 or mysql) (default "sqlite3") + -ovaldb-url string + http://goval-dictionary.com:1324 or mysql connection string -debug debug mode -debug-sql @@ -1584,7 +1560,7 @@ $ go-cve-dictionary server -bind=192.168.10.1 -port=1323 Run Vuls with -cve-dictionary-url option. ``` -$ vuls scan -cve-dictionary-url=http://192.168.0.1:1323 +$ vuls report -cve-dictionary-url=http://192.168.0.1:1323 ``` # Usage: Update NVD Data @@ -1593,6 +1569,27 @@ see [go-cve-dictionary#usage-fetch-nvd-data](https://github.com/kotakanbe/go-cve ---- +# Usage: goval-dictionary on different server + +``` +$ goval-dictionary server -bind=192.168.10.1 -port=1324 +``` + +Run Vuls with -ovaldb-url option. + +``` +$ vuls report -ovaldb-url=http://192.168.0.1:1323 +``` + +# Usage: Update OVAL Data + +- [RedHat, CentOS](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-redhat) +- [Ubuntu](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-ubuntu) +- [Debian](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-debian) +- [Oracle](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-oracle) + +---- + # レポートの日本語化 see [go-cve-dictionary#usage-fetch-jvn-data](https://github.com/kotakanbe/go-cve-dictionary#usage-fetch-jvn-data) diff --git a/README.md b/README.md index 4dc7cf46..f1fb88f6 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,11 @@ Vulnerability scanner for Linux/FreeBSD, agentless, written in golang. We have a slack team. [Join slack team](http://goo.gl/forms/xm5KFo35tu) -[README in Japanese](https://github.com/future-architect/vuls/blob/master/README.ja.md) +[README 日本語](https://github.com/future-architect/vuls/blob/master/README.ja.md) [README in French](https://github.com/future-architect/vuls/blob/master/README.fr.md) +![Vuls-Abstract](img/vuls-abstract.png) + [![asciicast](https://asciinema.org/a/3y9zrf950agiko7klg8abvyck.png)](https://asciinema.org/a/3y9zrf950agiko7klg8abvyck) ![Vuls-slack](img/vuls-slack-en.png) @@ -23,89 +25,7 @@ We have a slack team. [Join slack team](http://goo.gl/forms/xm5KFo35tu) # TOC -- [Vuls: VULnerability Scanner](#vuls-vulnerability-scanner) -- [TOC](#toc) -- [Abstract](#abstract) -- [Main Features](#main-features) -- [What Vuls Doesn't Do](#what-vuls-doesnt-do) -- [Setup Vuls](#setup-vuls) -- [Tutorial: Local Scan Mode](#tutorial-local-scan-mode) - * [Step1. Launch Amazon Linux](#step1-launch-amazon-linux) - * [Step2. Install requirements](#step2-install-requirements) - * [Step3. Deploy go-cve-dictionary](#step3-deploy-go-cve-dictionary) - * [Step4. Deploy Vuls](#step4-deploy-vuls) - * [Step5. Config](#step5-config) - * [Step6. Check config.toml and settings on the server before scanning](#step6-check-configtoml-and-settings-on-the-server-before-scanning) - * [Step7. Start Scanning](#step7-start-scanning) - * [Step8. Reporting](#step8-reporting) - * [Step9. TUI](#step9-tui) - * [Step10. Web UI](#step10-web-ui) -- [Tutorial: Remote Scan Mode](#tutorial-remote-scan-mode) - * [Step1. Launch Another Amazon Linux](#step1-launch-another-amazon-linux) - * [Step2. Install Dependencies on the Remote Server](#step2-install-dependencies-on-the-remote-server) - * [Step3. Enable to SSH from Localhost](#step3-enable-to-ssh-from-localhost) - * [Step4. Config](#step4-config) - * [Step5. Check config.toml and settings on the server before scanning](#step5-check-configtoml-and-settings-on-the-server-before-scanning) - * [Step6. Start Scanning](#step6-start-scanning) - * [Step7. Reporting](#step7-reporting) -- [Setup Vuls in a Docker Container](#setup-vuls-in-a-docker-container) -- [Architecture](#architecture) - * [A. Scan via SSH Mode (Remote Scan Mode)](#a-scan-via-ssh-mode-remote-scan-mode) - * [B. Scan without SSH (Local Scan Mode)](#b-scan-without-ssh-local-scan-mode) - * [go-cve-dictionary](#go-cve-dictionary) - * [Scanning Flow](#scanning-flow) -- [Performance Considerations](#performance-considerations) -- [Use Cases](#use-cases) - * [Scan All Servers](#scan-all-servers) - * [Scan a Single Server](#scan-a-single-server) - * [Scan Staging Environment](#scan-staging-environment) -- [Support OS](#support-os) -- [Usage: Automatic Server Discovery](#usage-automatic-server-discovery) - * [Example](#example) -- [Configuration](#configuration) -- [Usage: Configtest](#usage-configtest) - * [Dependencies on Target Servers](#dependencies-on-target-servers) - * [Check /etc/sudoers](#check-etcsudoers) -- [Usage: Scan](#usage-scan) - * [-ssh-native-insecure option](#-ssh-native-insecure-option) - * [-ask-key-password option](#-ask-key-password-option) - * [Example: Scan all servers defined in config file](#example-scan-all-servers-defined-in-config-file) - * [Example: Scan specific servers](#example-scan-specific-servers) - * [Example: Scan via shell instead of SSH.](#example-scan-via-shell-instead-of-ssh) - + [cron](#cron) - * [Example: Scan containers (Docker/LXD)](#example-scan-containers-dockerlxd) - + [Docker](#docker) - + [LXD](#lxd) -- [Usage: Report](#usage-report) - * [How to read a report](#how-to-read-a-report) - + [Example](#example-1) - + [Summary part](#summary-part) - + [Detailed Part](#detailed-part) - + [Changelog Part](#changelog-part) - * [Example: Send scan results to Slack](#example-send-scan-results-to-slack) - * [Example: Put results in S3 bucket](#example-put-results-in-s3-bucket) - * [Example: Put results in Azure Blob storage](#example-put-results-in-azure-blob-storage) - * [Example: IgnoreCves](#example-ignorecves) - * [Example: Add optional key-value pairs to JSON](#example-add-optional-key-value-pairs-to-json) - * [Example: Use MySQL as a DB storage back-end](#example-use-mysql-as-a-db-storage-back-end) - * [Example: Use PostgreSQL as a DB storage back-end](#example-use-postgresql-as-a-db-storage-back-end) - * [Example: Use Redis as a DB storage back-end](#example-use-redis-as-a-db-storage-back-end) -- [Usage: Scan vulnerabilites of non-OS packages](#usage-scan-vulnerabilites-of-non-os-packages) -- [Usage: Integrate with OWASP Dependency Check to Automatic update when the libraries are updated (Experimental)](#usage-integrate-with-owasp-dependency-check-to-automatic-update-when-the-libraries-are-updated-experimental) -- [Usage: TUI](#usage-tui) - * [Display the latest scan results](#display-the-latest-scan-results) - * [Display the previous scan results](#display-the-previous-scan-results) -- [Display the previous scan results using peco](#display-the-previous-scan-results-using-peco) -- [Usage: go-cve-dictionary on different server](#usage-go-cve-dictionary-on-different-server) -- [Usage: Update NVD Data](#usage-update-nvd-data) -- [How to Update](#how-to-update) -- [Misc](#misc) -- [Related Projects](#related-projects) -- [Data Source](#data-source) -- [Authors](#authors) -- [Contribute](#contribute) -- [Change Log](#change-log) -- [License](#license) +TODO ---- @@ -134,13 +54,33 @@ Vuls is a tool created to solve the problems listed above. It has the following - Scan for any vulnerabilities in Linux/FreeBSD Server - Supports Ubuntu, Debian, CentOS, Amazon Linux, RHEL, Oracle Linux, FreeBSD and Raspbian - Cloud, on-premise, Docker +- High quality scan + - Vuls uses Multiple vulnerability databases + - OVAL + - RHSA/ALAS/ELSA/FreeBSD-SA + - Changelog +- Fast scan and Deep scan + - Fast Scan + - Scan without root privilege + - Almost no load on the scan target server + - Deep Scan + - Scan with root privilege + - Parses the Changelog + Changelog has a history of version changes. When a security issue is fixed, the relevant CVE ID is listed. + By parsing the changelog and analysing the updates between the installed version of software on the server and the newest version of that software + it's possible to create a list of all vulnerabilities that need to be fixed. + - Sometimes load on the scan target server +- Remote scan and Local scan + - Remote Scan + - User is required to only setup one machine that is connected to other target servers via SSH + - Local Scan + - If you don't want the central Vuls server to connect to each server by SSH, you can use Vuls in the Local Scan mode. - Scan middleware that are not included in OS package management - Scan middleware, programming language libraries and framework for vulnerability - Support software registered in CPE -- Agentless architecture - - User is required to only setup one machine that is connected to other target servers via SSH - Nondestructive testing -- Pre-authorization is not necessary before scanning on AWS +- Pre-authorization is *NOT* necessary before scanning on AWS + - Vuls works well with Continuous Integration since tests can be run every day. This allows you to find vulnerabilities very quickly. - Auto generation of configuration file template - Auto detection of servers set using CIDR, generate configuration file template - Email and Slack notification is possible (supports Japanese language) @@ -168,14 +108,29 @@ Tutorial shows how to setup vuls manually. ---- +# Tutorial + +To give you an idea of how easy Vuls is to use. +This tutorial consists of three steps. +1. Tutorial: Local Scan Mode + - Launch CentOS on AWS + - Deploy Vuls + - Scan localhost, Reporting +1. Tutorial: Remote Scan Mode + - Launch Ubuntu Linux on AWS + - Scan this Ubuntu from the Vuls you set up earlier + +---- + # Tutorial: Local Scan Mode This tutorial will let you scan the vulnerabilities on the localhost with Vuls. This can be done in the following steps. -1. Launch Amazon Linux +1. Launch CentOS 1. Install requirements 1. Deploy go-cve-dictionary +1. Deploy goval-dictionary 1. Deploy Vuls 1. Configuration 1. Check config.toml and settings on the server before scanning @@ -184,9 +139,9 @@ This can be done in the following steps. 1. TUI(Terminal-Based User Interface) 1. Web UI ([VulsRepo](https://github.com/usiusi360/vulsrepo)) -## Step1. Launch Amazon Linux +## Step1. Launch CentOS7 -- We are using the old AMI (amzn-ami-hvm-2015.09.1.x86_64-gp2 - ami-383c1956) for this example +- We are using the old AMI for this example - Add the following to the cloud-init, to avoid auto-update at the first launch. ``` @@ -204,14 +159,14 @@ Vuls requires the following packages. - git - gcc - GNU Make -- go v1.7.1 or later (The latest version is recommended) +- go v1.8.3 or later (The latest version is recommended) - https://golang.org/doc/install ```bash -$ ssh ec2-user@52.100.100.100 -i ~/.ssh/private.pem -$ sudo yum -y install sqlite git gcc make -$ wget https://storage.googleapis.com/golang/go1.7.1.linux-amd64.tar.gz -$ sudo tar -C /usr/local -xzf go1.7.1.linux-amd64.tar.gz +$ ssh centos@52.100.100.100 -i ~/.ssh/private.pem +$ sudo yum -y install sqlite git gcc make wget +$ wget https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz +$ sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz $ mkdir $HOME/go ``` Add these lines into /etc/profile.d/goenv.sh @@ -233,7 +188,7 @@ $ source /etc/profile.d/goenv.sh ```bash $ sudo mkdir /var/log/vuls -$ sudo chown ec2-user /var/log/vuls +$ sudo chown centos /var/log/vuls $ sudo chmod 700 /var/log/vuls $ $ mkdir -p $GOPATH/src/github.com/kotakanbe @@ -243,6 +198,8 @@ $ cd go-cve-dictionary $ make install ``` The binary was built under `$GOPATH/bin` +If the installation process stops halfway, try increasing the instance type of EC2. An out of memory error may have occurred. + Fetch vulnerability data from NVD. It takes about 10 minutes (on AWS). @@ -252,10 +209,32 @@ $ cd $HOME $ for i in `seq 2002 $(date +"%Y")`; do go-cve-dictionary fetchnvd -years $i; done ... snip ... $ ls -alh cve.sqlite3 --rw-r--r-- 1 ec2-user ec2-user 7.0M Mar 24 13:20 cve.sqlite3 +-rw-r--r--. 1 centos centos 51M Aug 6 08:10 cve.sqlite3 +-rw-r--r--. 1 centos centos 32K Aug 6 08:10 cve.sqlite3-shm +-rw-r--r--. 1 centos centos 5.1M Aug 6 08:10 cve.sqlite3-wal ``` -## Step4. Deploy Vuls +## Step4. Deploy goval-dictionary + +[goval-dictionary](https://github.com/kotakanbe/goval-dictionary) + +```bash +$ mkdir -p $GOPATH/src/github.com/kotakanbe +$ cd $GOPATH/src/github.com/kotakanbe +$ git clone https://github.com/kotakanbe/goval-dictionary.git +$ cd goval-dictionary +$ make install +``` +The binary was built under `$GOPATH/bin` +If the installation process stops halfway, try increasing the instance type of EC2. An out of memory error may have occurred. + + Then fetch OVAL data of RedHat since the server to be scanned is CentOS. [README](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-redhat) + +```bash +$ goval-dictionary fetch-redhat 5 6 7 +``` + +## Step5. Deploy Vuls Launch a new terminal and SSH to the ec2 instance. @@ -267,8 +246,9 @@ $ cd vuls $ make install ``` The binary was built under `$GOPATH/bin` +If the installation process stops halfway, try increasing the instance type of EC2. An out of memory error may have occurred. -## Step5. Config +## Step6. Configuration Create a config file(TOML format). ``` @@ -277,15 +257,12 @@ $ cat config.toml [servers] [servers.localhost] -host = "localhost" -port = "local" +host = "localhost" +port = "local" ``` -Root privilege is needed on Some distributions. -Sudo with password is not supported for security reasons. So you have to define NOPASSWORD in /etc/sudoers. -See [Usage: Configtest#Check /etc/sudoers](#check-etcsudoers) -## Step6. Check config.toml and settings on the server before scanning +## Step7. Check config.toml and settings on the server before scanning ``` $ vuls configtest @@ -293,50 +270,54 @@ $ vuls configtest see [Usage: configtest](#usage-configtest) -## Step7. Start Scanning +## Step8. Start Scanning ``` $ vuls scan + ... snip ... -Scan Summary -============ -localhost amazon 2015.09 94 CVEs 103 updatable packages +One Line Summary +================ +localhost centos7.3.1611 31 updatable packages ``` -## Step8. Reporting +## Step9. Reporting View one-line summary ``` -$ vuls report -format-one-line-text -cvedb-path=$PWD/cve.sqlite3 +$ vuls report -format-one-line-text -cvedb-path=$PWD/cve.sqlite3 -ovaldb-path=$PWD/oval.sqlite3 One Line Summary ================ -localhost Total: 94 (High:19 Medium:54 Low:7 ?:14) 103 updatable packages +localhost Total: 109 (High:35 Medium:55 Low:16 ?:3) 31 updatable packages ``` -View short summary. +View short summary ``` $ vuls report -format-short-text -localhost (amazon 2015.09) -=========================== -Total: 94 (High:19 Medium:54 Low:7 ?:14) 103 updatable packages +localhost (centos7.3.1611) +========================== +Total: 109 (High:35 Medium:55 Low:16 ?:3) 31 updatable packages + +CVE-2015-2806 10.0 HIGH (nvd) + Stack-based buffer overflow in asn1_der_decoding in libtasn1 before 4.4 allows + remote attackers to have unspecified impact via unknown vectors. + --- + https://nvd.nist.gov/vuln/detail/CVE-2015-2806 + https://access.redhat.com/security/cve/CVE-2015-2806 (RHEL-CVE) + 10.0/AV:N/AC:L/Au:N/C:C/I:C/A:C (nvd) + 2.6/AV:N/AC:H/Au:N/C:N/I:N/A:P (redhat) + https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?name=CVE-2015-2806 + 3.3/CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L (redhat) + https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?name=CVE-2015-2806 + Confidence: 100 / OvalMatch -CVE-2016-5636 10.0 (High) Integer overflow in the get_data function in zipimport.c in CPython (aka Python) - before 2.7.12, 3.x before 3.4.5, and 3.5.x before 3.5.2 allows remote attackers - to have unspecified impact via a negative data size value, which triggers a - heap-based buffer overflow. - http://www.cvedetails.com/cve/CVE-2016-5636 - https://access.redhat.com/security/cve/CVE-2016-5636 - python27-2.7.10-4.119.amzn1 -> python27-2.7.12-2.120.amzn1 - python27-devel-2.7.10-4.119.amzn1 -> python27-devel-2.7.12-2.120.amzn1 - python27-libs-2.7.10-4.119.amzn1 -> python27-libs-2.7.12-2.120.amzn1 - Confidence: 100 / YumUpdateSecurityMatch ... snip ... ```` @@ -344,35 +325,30 @@ View full report. ``` $ vuls report -format-full-text | less +localhost (centos7.3.1611) +========================== +Total: 109 (High:35 Medium:55 Low:16 ?:3) 31 updatable packages -localhost (amazon 2015.09) -============================ -Total: 94 (High:19 Medium:54 Low:7 ?:14) 103 updatable packages - -CVE-2016-5636 -------------- -Score 10.0 (High) -Vector (AV:N/AC:L/Au:N/C:C/I:C/A:C) -Summary Integer overflow in the get_data function in zipimport.c in CPython (aka Python) - before 2.7.12, 3.x before 3.4.5, and 3.5.x before 3.5.2 allows remote attackers - to have unspecified impact via a negative data size value, which triggers a - heap-based buffer overflow. -CWE https://cwe.mitre.org/data/definitions/190.html -NVD https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2016-5636 -MITRE https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-5636 -CVE Details http://www.cvedetails.com/cve/CVE-2016-5636 -CVSS Claculator https://nvd.nist.gov/cvss/v2-calculator?name=CVE-2016-5636&vector=(AV:N/AC:L/... -RHEL-CVE https://access.redhat.com/security/cve/CVE-2016-5636 -ALAS-2016-724 https://alas.aws.amazon.com/ALAS-2016-724.html -Package python27-2.7.10-4.119.amzn1 -> python27-2.7.12-2.120.amzn1 - python27-devel-2.7.10-4.119.amzn1 -> python27-devel-2.7.12-2.120.amzn1 - python27-libs-2.7.10-4.119.amzn1 -> python27-libs-2.7.12-2.120.amzn1 -Confidence 100 / YumUpdateSecurityMatch +CVE-2015-2806 +---------------- +Max Score 10.0 HIGH (nvd) +nvd 10.0/AV:N/AC:L/Au:N/C:C/I:C/A:C +redhat 2.6/AV:N/AC:H/Au:N/C:N/I:N/A:P +redhat 3.3/CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L +CVSSv2 Calc https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?name=CVE-2015-2806 +CVSSv3 Calc https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?name=CVE-2015-2806 +Summary Stack-based buffer overflow in asn1_der_decoding in libtasn1 before 4.4 allows + remote attackers to have unspecified impact via unknown vectors. +Source https://nvd.nist.gov/vuln/detail/CVE-2015-2806 +RHEL-CVE https://access.redhat.com/security/cve/CVE-2015-2806 +CWE-119 (nvd) https://cwe.mitre.org/data/definitions/119.html +Package/CPE libtasn1-3.8-3.el7 - +Confidence 100 / OvalMatch ... snip ... ``` -## Step9. TUI +## Step10. TUI Vuls has Terminal-Based User Interface to display the scan result. @@ -382,7 +358,7 @@ $ vuls tui ![Vuls-TUI](img/hello-vuls-tui.png) -## Step10. Web UI +## Step11. Web UI [VulsRepo](https://github.com/usiusi360/vulsrepo) is a awesome Web UI for Vuls. Check it out the [Online Demo](http://usiusi360.github.io/vulsrepo/). @@ -394,9 +370,8 @@ Check it out the [Online Demo](http://usiusi360.github.io/vulsrepo/). This tutorial will let you scan the vulnerabilities on the remote host via SSH with Vuls. This can be done in the following steps. -1. Launch Another Amazon Linux -1. Install Dependencies on the Remote Host -1. Enable to SSH from Localhost +1. Launch new Ubuntu Linux +1. Enable to SSH from localhost 1. Configuration 1. Check config.toml and settings on the server before scanning 1. Scan @@ -404,23 +379,18 @@ This can be done in the following steps. We will use the Vuls server (called localhost) created in the previous tutorial. -## Step1. Launch Another Amazon Linux +## Step1. Launch new Ubuntu Linux -Same as [Tutorial: Local Scan Mode#Step1. Launch Amazon Linux](#step1-launch-amazon-linux) -Launch a new terminal and SSH to the Remote Server. +Same like as [Tutorial: Local Scan Mode#Step1. Launch CentOS7](#step1-launch-centos7) +Launch a new terminal and SSH to the Remote host. +To add the remote host's Host Key to $HOME/.ssh/known_hosts, you need to log in to the remote host through SSH before scanning. -## Step2. Install Dependencies on the Remote Server - -Depending on the distribution you need to install dependent modules. -Install these dependencies manually or using Ansible etc. -For details of dependent libraries, see [Dependencies on Target Servers](#dependencies-on-target-servers) - -## Step3. Enable to SSH from Localhost +## Step2. Enable to SSH from localhost Vuls doesn't support SSH password authentication. So you have to use SSH key-based authentication. -Create a keypair on the localhost then append public key to authorized_keys on the remote host. +Create a keypair on the localhost then append the public key to authorized_keys on the remote host. -- Localhost +- localhost ```bash $ ssh-keygen -t rsa ``` @@ -436,47 +406,49 @@ $ vim ~/.ssh/authorized_keys ``` Paste from the clipboard to ~/.ssh/.authorized_keys -SUDO with password is not supported for security reasons. So you have to define NOPASSWORD in /etc/sudoers on target servers. -See [Usage: Configtest#Check /etc/sudoers](#check-etcsudoers) +And also, confirm that the host keys of scan target servers has been registered in the known_hosts of the localhost. +To add the remote host's Host Key to $HOME/.ssh/known_hosts, you need to log in to the remote host through SSH before scanning. -And also, confirm that the host keys of scan target servers has been registered in the known_hosts of the Localhost. +- localhost +``` +$ ssh ubuntu@172.31.4.82 -i ~/.ssh/id_rsa +``` -## Step4. Config +## Step3. Configure (config.toml) -- Localhost +- localhost ``` $ cd $HOME $ cat config.toml [servers] -[servers.172-31-4-82] +[servers.ubuntu] host = "172.31.4.82" port = "22" -user = "ec2-user" -keyPath = "/home/ec2-user/.ssh/id_rsa" +user = "ubuntu" +keyPath = "/home/centos/.ssh/id_rsa" ``` -## Step5. Check config.toml and settings on the server before scanning +## Step4. Check config.toml and settings on the server before scanning ``` -$ vuls configtest +$ vuls configtest ubuntu ``` see [Usage: configtest](#usage-configtest) -## Step6. Start Scanning +## Step5. Start Scanning ``` -$ vuls scan +$ vuls scan ubuntu ... snip ... -Scan Summary -============ -172-31-4-82 amazon 2015.09 94 CVEs 103 updatable packages - +One Line Summary +================ +ubuntu ubuntu16.04 30 updatable packages ``` -## Step7. Reporting +## Step6. Reporting See [Tutorial: Local Scan Mode#Step8. Reporting](#step8-reporting) See [Tutorial: Local Scan Mode#Step9. TUI](#step9-tui) @@ -762,6 +734,7 @@ You can customize your configuration using this template. $ vuls configtest --help configtest: configtest + [-deep] [-config=/path/to/config.toml] [-log-dir=/path/to/log] [-ask-key-password] @@ -779,6 +752,8 @@ configtest: Test containers only. Default: Test both of hosts and containers -debug debug mode + -deep + Config test for deep scan mode -http-proxy string http://proxy-url:port (default: empty) -log-dir string @@ -790,31 +765,31 @@ configtest: ``` -The configtest subcommand checks the following -- Whether vuls is able to connect via SSH to servers/containers defined in the config.toml -- Whether Dependent package is installed on the scan target server -- Check /etc/sudoers +The configtest subcommand checks whether vuls is able to connect via SSH to servers/containers defined in the config.toml -## Dependencies on Target Servers +## Deep Scan Mode -In order to scan, the following dependencies are required, so you need to install them manually or with tools such as Ansible. +Some dependent packages are needed in Deep Scan Mode. +The configtest subcommand with --deep flag checks whether the packages are installed on the scan target server and also check /etc/sudoers + +### Dependencies and /etc/sudoers on Target Servers + +In order to scan with deep scan mode, the following dependencies are required, so you need to install them manually or with tools such as Ansible. | Distribution | Release | Requirements | |:-------------|-------------------:|:-------------| | Ubuntu | 12, 14, 16| - | | Debian | 7, 8| aptitude | | CentOS | 6, 7| yum-plugin-changelog, yum-utils | -| Amazon | All | - | TODO yum-utils?, yum-plugin-changelog -| RHEL | 5 | yum-security | TODO yum-utils? -| RHEL | 6, 7 | - | TODO yum-utils? -| Oracle Linux | 5 | yum-security | TODO yum-utils? -| Oracle Linux | 6, 7 | - |TODO yum-utils? +| Amazon | All | yum-plugin-changelog, yum-utils | +| RHEL | 5 | yum-utils, yum-security, yum-changelog | +| RHEL | 6, 7 | yum-utils, yum-plugin-changelog | +| Oracle Linux | 5 | yum-utils, yum-security, yum-changelog | +| Oracle Linux | 6, 7 | yum-utils, yum-plugin-changelog | | FreeBSD | 10 | - | | Raspbian | Wheezy, Jessie | - | -## Check /etc/sudoers - -The configtest subcommand checks sudo settings on target servers whether Vuls is able to SUDO with nopassword via SSH. And if you run Vuls without -ssh-native-insecure option, requiretty must be defined in /etc/sudoers. +The configtest subcommand also checks sudo settings on target servers whether Vuls is able to SUDO with nopassword via SSH. And if you run Vuls without -ssh-native-insecure option, requiretty must be defined in /etc/sudoers. ``` Defaults:vuls !requiretty ``` @@ -822,37 +797,25 @@ For details, see [-ssh-native-insecure option](#-ssh-native-insecure-option) Example of /etc/sudoers on target servers -- CentOS -``` -vuls ALL=(ALL) NOPASSWD:/usr/bin/yum --changelog --assumeno update * -Defaults:vuls env_keep="http_proxy https_proxy HTTP_PROXY HTTPS_PROXY" -``` - - RHEL 5 / Oracle Linux 5 ``` -vuls ALL=(ALL) NOPASSWD:/usr/bin/yum --color=never repolist, /usr/bin/yum --color=never list-security --security, /usr/bin/yum --color=never check-update, /usr/bin/yum --color=never info-security +vuls ALL=(ALL) NOPASSWD:/usr/bin/yum --color=never repolist, /usr/bin/yum --color=never list-security --security, /usr/bin/yum --color=never info-security Defaults:vuls env_keep="http_proxy https_proxy HTTP_PROXY HTTPS_PROXY" ``` - RHEL 6, 7 / Oracle Linux 6, 7 ``` -vuls ALL=(ALL) NOPASSWD:/usr/bin/yum --color=never repolist, /usr/bin/yum --color=never --security updateinfo list updates, /usr/bin/yum --color=never check-update, /usr/bin/yum --color=never --security updateinfo updates +vuls ALL=(ALL) NOPASSWD:/usr/bin/yum --color=never repolist, /usr/bin/yum --color=never --security updateinfo list updates, /usr/bin/yum --color=never --security updateinfo updates Defaults:vuls env_keep="http_proxy https_proxy HTTP_PROXY HTTPS_PROXY" ``` -- Debian +- Debian/Ubuntu/Raspbian ``` vuls ALL=(ALL) NOPASSWD: /usr/bin/apt-get update Defaults:vuls env_keep="http_proxy https_proxy HTTP_PROXY HTTPS_PROXY" ``` -- Ubuntu/Raspbian -``` -vuls ALL=(ALL) NOPASSWD: /usr/bin/apt-get update -Defaults:vuls env_keep="http_proxy https_proxy HTTP_PROXY HTTPS_PROXY" -``` - -- On Amazon Linux, FreeBSD, it is possible to scan without root privilege for now. +- On CentOS, Amazon Linux, FreeBSD, it is possible to scan without root privilege for now. ---- @@ -862,6 +825,7 @@ Defaults:vuls env_keep="http_proxy https_proxy HTTP_PROXY HTTPS_PROXY" $ vuls scan -help scan: scan + [-deep] [-config=/path/to/config.toml] [-results-dir=/path/to/results] [-log-dir=/path/to/log] @@ -887,6 +851,8 @@ scan: Scan containers only. Default: Scan both of hosts and containers -debug debug mode + -deep + Deep scan mode. Scan accuracy improves and information becomes richer. Since analysis of changelog, issue commands requiring sudo, but it may be slower and high load on the scan tareget server. -http-proxy string http://proxy-url:port (default: empty) -log-dir string @@ -905,6 +871,23 @@ scan: Number of second for scaning vulnerabilities for all servers (default 7200) ``` +## -deep option + +You need to execute `vuls configtest --deep` to check the configuration of the target server before scanning with -deep flag. + +| Distribution | Changelog | +|:-------------|:---------:| +| Ubuntu | yes | +| Debian | yes | +| CentOS | yes | +| Amazon | yes | +| RHEL | yes | +| RHEL | yes | +| Oracle Linux | yes | +| Oracle Linux | yes | +| FreeBSD | no | +| Raspbian | yes | + ## -ssh-native-insecure option Vuls supports different types of SSH. @@ -1054,6 +1037,9 @@ report: [-cvedb-type=sqlite3|mysql|postgres] [-cvedb-path=/path/to/cve.sqlite3] [-cvedb-url=http://127.0.0.1:1323 DB connection string] + [-ovaldb-type=sqlite3|mysql] + [-ovaldb-path=/path/to/oval.sqlite3] + [-ovaldb-url=http://127.0.0.1:1324 or DB connection string] [-cvss-over=7] [-diff] [-ignore-unscored-cves] @@ -1131,6 +1117,12 @@ report: [en|ja] (default "en") -log-dir string /path/to/log (default "/var/log/vuls") + -ovaldb-path string + /path/to/sqlite3 (For get oval detail from oval.sqlite3) (default "/Users/kotakanbe/go/src/github.com/future-architect/vuls/oval.sqlite3") + -ovaldb-type string + DB type for fetching OVAL dictionary (sqlite3 or mysql) (default "sqlite3") + -ovaldb-url string + http://goval-dictionary.com:1324 or mysql connection string -pipe Use stdin via PIPE -refresh-cve @@ -1186,47 +1178,45 @@ Confidence 100 / YumUpdateSecurityMatch ### Summary part ``` -172-31-4-82 (amazon 2015.09) -============================ -Total: 94 (High:19 Medium:54 Low:7 ?:14) 103 updatable packages +cent6 (centos6.6) +================= +Total: 145 (High:23 Medium:101 Low:21 ?:0) 83 updatable packages ``` -- `172-31-4-82` means that it is a scan report of `servers.172-31-4-82` defined in cocnfig.toml. -- `(amazon 2015.09)` means that the version of the OS is Amazon Linux 2015.09. -- `Total: 94 (High:19 Medium:54 Low:7 ?:14)` means that a total of 94 vulnerabilities exist, and the distribution of CVSS Severity is displayed. -- `103 updatable packages` means that there are 103 updateable packages on the target server. +- `cent6` means that it is a scan report of `servers.cent6` defined in cocnfig.toml. +- `(centos6.6)` means that the version of the OS is CentOS6.6. +- `Total: 145 (High:23 Medium:101 Low:21 ?:0)` means that a total of 145 vulnerabilities exist, and the distribution of CVSS Severity is displayed. +- `83 updatable packages` means that there are 83 updateable packages on the target server. ### Detailed Part ``` -CVE-2016-5636 -------------- -Score 10.0 (High) -Vector (AV:N/AC:L/Au:N/C:C/I:C/A:C) -Summary Integer overflow in the get_data function in zipimport.c in CPython (aka Python) - before 2.7.12, 3.x before 3.4.5, and 3.5.x before 3.5.2 allows remote attackers - to have unspecified impact via a negative data size value, which triggers a - heap-based buffer overflow. -CWE https://cwe.mitre.org/data/definitions/190.html -NVD https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2016-5636 -MITRE https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-5636 -CVE Details http://www.cvedetails.com/cve/CVE-2016-5636 -CVSS Claculator https://nvd.nist.gov/cvss/v2-calculator?name=CVE-2016-5636&vector=(AV:N/AC:L/... -RHEL-CVE https://access.redhat.com/security/cve/CVE-2016-5636 -ALAS-2016-724 https://alas.aws.amazon.com/ALAS-2016-724.html -Package python27-2.7.10-4.119.amzn1 -> python27-2.7.12-2.120.amzn1 - python27-devel-2.7.10-4.119.amzn1 -> python27-devel-2.7.12-2.120.amzn1 - python27-libs-2.7.10-4.119.amzn1 -> python27-libs-2.7.12-2.120.amzn1 -Confidence 100 / YumUpdateSecurityMatch +CVE-2016-0702 +---------------- +Max Score 2.6 IMPORTANT (redhat) +nvd 1.9/AV:L/AC:M/Au:N/C:P/I:N/A:N +redhat 2.6/AV:L/AC:H/Au:N/C:P/I:P/A:N +jvn 1.9/AV:L/AC:M/Au:N/C:P/I:N/A:N +CVSSv2 Calc https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?name=CVE-2016-0702 +Summary The MOD_EXP_CTIME_COPY_FROM_PREBUF function in crypto/bn/bn_exp.c in OpenSSL + 1.0.1 before 1.0.1s and 1.0.2 before 1.0.2g does not properly consider + cache-bank access times during modular exponentiation, which makes it easier for + local users to discover RSA keys by running a crafted application on the same + Intel Sandy Bridge CPU core as a victim and leveraging cache-bank conflicts, aka + a "CacheBleed" attack. +Source https://nvd.nist.gov/vuln/detail/CVE-2016-0702 +RHEL-CVE https://access.redhat.com/security/cve/CVE-2016-0702 +CWE-200 (nvd) https://cwe.mitre.org/data/definitions/200.html +Package/CPE openssl-1.0.1e-30.el6 - 1.0.1e-57.el6 +Confidence 100 / OvalMatch ``` -- `Score` means CVSS Score. -- `Vector` means [CVSS Vector](https://nvd.nist.gov/CVSS/Vector-v2.aspx) +- `Max Score` means Max CVSS Score. +- `nvd` shows [CVSS Vector](https://nvd.nist.gov/CVSS/Vector-v2.aspx) of NVD +- `redhat` shows [CVSS Vector](https://nvd.nist.gov/CVSS/Vector-v2.aspx) of RedHat OVAL +- `jvn` shows [CVSS Vector](https://nvd.nist.gov/CVSS/Vector-v2.aspx) of JVN - `Summary` means Summary of the CVE. - `CWE` means [CWE - Common Weakness Enumeration](https://nvd.nist.gov/cwe.cfm) of the CVE. -- `NVD` `MITRE` `CVE Details` `CVSS Caluculator` -- `RHEL-CVE` means the URL of OS distributor support. -- `Oracle-CVE` means the URL of the Oracle Linux errata information. - `Package` shows the package version information including this vulnerability. - `Confidence` means the reliability of detection. - `100` is highly reliable @@ -1235,33 +1225,14 @@ Confidence 100 / YumUpdateSecurityMatch | Detection Method | Confidence | OS |Description| |:-----------------------|-------------------:|:---------------------------------|:--| - | YumUpdateSecurityMatch | 100 | RHEL, Oracle Linux, Amazon Linux |Detection using yum-plugin-security| + | OvalMatch | 100 | CentOS, RHEL, Oracle, Ubuntu, Debian |Detection using OVAL | + | YumUpdateSecurityMatch | 100 | RHEL, Amazon, Oracle |Detection using yum-plugin-security| | ChangelogExactMatch | 95 | CentOS, Ubuntu, Debian, Raspbian |Exact version match between changelog and package version| | ChangelogLenientMatch | 50 | Ubuntu, Debian, Raspbian |Lenient version match between changelog and package version| | PkgAuditMatch | 100 | FreeBSD |Detection using pkg audit| | CpeNameMatch | 100 | All |Search for NVD information with CPE name specified in config.toml| -### Changelog Part - -The scan results of Ubuntu, Debian, Raspbian or CentOS are also output Changelog in TUI or report with -format-full-text. -(RHEL, Amazon or FreeBSD will be available in the near future) - -The output change log includes only the difference between the currently installed version and candidate version. - -``` -tar-1.28-2.1 -> tar-1.28-2.1ubuntu0.1 -------------------------------------- -tar (1.28-2.1ubuntu0.1) xenial-security; urgency=medium - - * SECURITY UPDATE: extract pathname bypass - - debian/patches/CVE-2016-6321.patch: skip members whose names contain - ".." in src/extract.c. - - CVE-2016-6321 - - -- Marc Deslauriers Thu, 17 Nov 2016 11:06:07 -0500 -``` - ## Example: Send scan results to Slack ``` $ vuls report \ @@ -1508,6 +1479,9 @@ tui: [-cvedb-type=sqlite3|mysql|postgres] [-cvedb-path=/path/to/cve.sqlite3] [-cvedb-url=http://127.0.0.1:1323 DB connection string] + [-ovaldb-type=sqlite3|mysql] + [-ovaldb-path=/path/to/oval.sqlite3] + [-ovaldb-url=http://127.0.0.1:1324 or DB connection string] [-refresh-cve] [-results-dir=/path/to/results] [-log-dir=/path/to/log] @@ -1521,6 +1495,12 @@ tui: DB type for fetching CVE dictionary (sqlite3, mysql or postgres) (default "sqlite3") -cvedb-url string http://cve-dictionary.com:8080 DB connection string + -ovaldb-path string + /path/to/sqlite3 (For get oval detail from oval.sqlite3) (default "/Users/kotakanbe/go/src/github.com/future-architect/vuls/oval.sqlite3") + -ovaldb-type string + DB type for fetching OVAL dictionary (sqlite3 or mysql) (default "sqlite3") + -ovaldb-url string + http://goval-dictionary.com:1324 or mysql connection string -debug debug mode -debug-sql @@ -1579,13 +1559,31 @@ $ go-cve-dictionary server -bind=192.168.10.1 -port=1323 Run Vuls with -cvedb-url option. ``` -$ vuls scan -cvedb-url=http://192.168.0.1:1323 +$ vuls report -cvedb-url=http://192.168.0.1:1323 ``` # Usage: Update NVD Data see [go-cve-dictionary#usage-fetch-nvd-data](https://github.com/kotakanbe/go-cve-dictionary#usage-fetch-nvd-data) +# Usage: goval-dictionary on different server + +``` +$ goval-dictionary server -bind=192.168.10.1 -port=1324 +``` + +Run Vuls with -ovaldb-url option. + +``` +$ vuls report -ovaldb-url=http://192.168.0.1:1323 +``` + +# Usage: Update OVAL Data + +- [RedHat, CentOS](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-redhat) +- [Ubuntu](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-ubuntu) +- [Debian](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-debian) +- [Oracle](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-oracle) ---- diff --git a/commands/scan.go b/commands/scan.go index 623b518d..60874c4b 100644 --- a/commands/scan.go +++ b/commands/scan.go @@ -138,7 +138,7 @@ func (p *ScanCmd) SetFlags(f *flag.FlagSet) { &p.deep, "deep", false, - "Deep scan mode. Scan accuracy improves and information becomes richer. Since analysis of changelog, issue commands requiring sudo, but is slower and heavy") + "Deep scan mode. Scan accuracy improves and scanned information becomes richer. Since analysis of changelog, issue commands requiring sudo, but it may be slower and high load on the tareget server") f.BoolVar( &p.pipe, diff --git a/img/vuls-abstract.png b/img/vuls-abstract.png new file mode 100644 index 0000000000000000000000000000000000000000..ab8df8a4c540500c2c9466959c7ff444462618d4 GIT binary patch literal 125909 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 o|Cjl%=gt52@&B#S4A}mCepII43$ Date: Mon, 7 Aug 2017 11:43:53 +0900 Subject: [PATCH 075/113] add serveruuid field (#458) --- models/scanresults.go | 1 + 1 file changed, 1 insertion(+) diff --git a/models/scanresults.go b/models/scanresults.go index 6ca22bcc..5c3076a0 100644 --- a/models/scanresults.go +++ b/models/scanresults.go @@ -31,6 +31,7 @@ type ScanResult struct { ScannedAt time.Time JSONVersion int Lang string + ServerUUID string ServerName string // TOML Section key Family string Release string From cbd1c1277369c0cda2be55981b49102e4f55808a Mon Sep 17 00:00:00 2001 From: sadayuki-matsuno Date: Mon, 7 Aug 2017 11:48:10 +0900 Subject: [PATCH 076/113] add s3 dirctory option (#457) --- Gopkg.lock | 4 ++-- README.ja.md | 5 ++++- README.md | 6 +++++- commands/report.go | 10 +++++++--- config/config.go | 7 ++++--- report/s3.go | 5 +++-- 6 files changed, 25 insertions(+), 12 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 550d3b8c..d38725b4 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -44,10 +44,10 @@ version = "v1.0.0" [[projects]] + branch = "master" name = "github.com/cheggaaa/pb" packages = ["."] - revision = "f6ccf2184de4dd34495277e38dc19b6e7fbe0ea2" - version = "v1.0.15" + revision = "0af82b7d15eb9371fbdf8f468ff10cbba62e0414" [[projects]] name = "github.com/dgrijalva/jwt-go" diff --git a/README.ja.md b/README.ja.md index bafc30c0..ca1c4c44 100644 --- a/README.ja.md +++ b/README.ja.md @@ -1045,6 +1045,7 @@ report: [-aws-profile=default] [-aws-region=us-west-2] [-aws-s3-bucket=bucket_name] + [-aws-s3-results-dir=/bucket/path/to/results] [-azure-account=accout] [-azure-key=key] [-azure-container=container] @@ -1060,6 +1061,8 @@ report: AWS region to use (default "us-east-1") -aws-s3-bucket string S3 bucket name + -aws-s3-results-dir string + /bucket/path/to/results (option) -azure-account string Azure account name to use. AZURE_STORAGE_ACCOUNT environment variable is used if not specified -azure-container string @@ -1123,7 +1126,7 @@ report: -to-localfile Write report to localfile -to-s3 - Write report to S3 (bucket/yyyyMMdd_HHmm/servername.json/xml/txt) + Write report to S3 (bucket/dir/yyyyMMdd_HHmm/servername.json/xml/txt) -to-slack Send report via Slack ``` diff --git a/README.md b/README.md index f1fb88f6..74c43809 100644 --- a/README.md +++ b/README.md @@ -1058,6 +1058,7 @@ report: [-aws-profile=default] [-aws-region=us-west-2] [-aws-s3-bucket=bucket_name] + [-aws-s3-results-dir=/bucket/path/to/results] [-azure-account=accout] [-azure-key=key] [-azure-container=container] @@ -1073,6 +1074,8 @@ report: AWS region to use (default "us-east-1") -aws-s3-bucket string S3 bucket name + -aws-s3-results-dir string + /bucket/path/to/results (option) -azure-account string Azure account name to use. AZURE_STORAGE_ACCOUNT environment variable is used if not specified -azure-container string @@ -1136,7 +1139,7 @@ report: -to-localfile Write report to localfile -to-s3 - Write report to S3 (bucket/yyyyMMdd_HHmm/servername.json/xml/txt) + Write report to S3 (bucket/dir/yyyyMMdd_HHmm/servername.json/xml/txt) -to-slack Send report via Slack ``` @@ -1286,6 +1289,7 @@ $ vuls report \ -format-json \ -aws-region=ap-northeast-1 \ -aws-s3-bucket=vuls \ + -aws-s3-results-dir=/bucket/path/to/results \ -aws-profile=default ``` With this sample command, it will .. diff --git a/commands/report.go b/commands/report.go index 7dfca1dd..2e953ba4 100644 --- a/commands/report.go +++ b/commands/report.go @@ -69,9 +69,10 @@ type ReportCmd struct { gzip bool - awsProfile string - awsS3Bucket string - awsRegion string + awsProfile string + awsS3Bucket string + awsS3ResultsDir string + awsRegion string azureAccount string azureKey string @@ -121,6 +122,7 @@ func (*ReportCmd) Usage() string { [-aws-profile=default] [-aws-region=us-west-2] [-aws-s3-bucket=bucket_name] + [-aws-s3-results-dir=/bucket/path/to/results] [-azure-account=accout] [-azure-key=key] [-azure-container=container] @@ -263,6 +265,7 @@ func (p *ReportCmd) SetFlags(f *flag.FlagSet) { f.StringVar(&p.awsProfile, "aws-profile", "default", "AWS profile to use") f.StringVar(&p.awsRegion, "aws-region", "us-east-1", "AWS region to use") f.StringVar(&p.awsS3Bucket, "aws-s3-bucket", "", "S3 bucket name") + f.StringVar(&p.awsS3ResultsDir, "aws-s3-results-dir", "", "/bucket/path/to/results") f.BoolVar(&p.toAzureBlob, "to-azure-blob", @@ -357,6 +360,7 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} c.Conf.AwsRegion = p.awsRegion c.Conf.AwsProfile = p.awsProfile c.Conf.S3Bucket = p.awsS3Bucket + c.Conf.S3ResultsDir = p.awsS3ResultsDir if err := report.CheckIfBucketExists(); err != nil { util.Log.Errorf("Check if there is a bucket beforehand: %s, err: %s", c.Conf.S3Bucket, err) return subcommands.ExitUsageError diff --git a/config/config.go b/config/config.go index 296401fa..9547fc0b 100644 --- a/config/config.go +++ b/config/config.go @@ -104,9 +104,10 @@ type Config struct { GZIP bool - AwsProfile string - AwsRegion string - S3Bucket string + AwsProfile string + AwsRegion string + S3Bucket string + S3ResultsDir string AzureAccount string AzureKey string diff --git a/report/s3.go b/report/s3.go index 1137324b..9fbaa6a7 100644 --- a/report/s3.go +++ b/report/s3.go @@ -22,6 +22,7 @@ import ( "encoding/json" "encoding/xml" "fmt" + "path" "time" "github.com/aws/aws-sdk-go/aws" @@ -147,8 +148,8 @@ func putObject(svc *s3.S3, k string, b []byte) error { } if _, err := svc.PutObject(&s3.PutObjectInput{ - Bucket: &c.Conf.S3Bucket, - Key: &k, + Bucket: aws.String(c.Conf.S3Bucket), + Key: aws.String(path.Join(c.Conf.S3ResultsDir, k)), Body: bytes.NewReader(b), }); err != nil { return fmt.Errorf("Failed to upload data to %s/%s, %s", From a233e0892937b1077d18be53da9ef4c9ae9c40df Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Tue, 8 Aug 2017 19:44:20 +0900 Subject: [PATCH 077/113] When scanning raspbian, always scan with deep scan mode --- scan/debian.go | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/scan/debian.go b/scan/debian.go index 31d4bb23..20c9fa98 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -137,18 +137,19 @@ func trim(str string) string { } func (o *debian) checkIfSudoNoPasswd() error { - if !config.Conf.Deep { - o.log.Infof("sudo ... No need") + if config.Conf.Deep || o.Distro.Family == config.Raspbian { + cmd := util.PrependProxyEnv("apt-get update") + o.log.Infof("Checking... sudo %s", cmd) + r := o.exec(cmd, sudo) + if !r.isSuccess() { + o.log.Errorf("sudo error on %s", r) + return fmt.Errorf("Failed to sudo: %s", r) + } + o.log.Infof("Sudo... Pass") return nil } - cmd := util.PrependProxyEnv("apt-get update") - o.log.Infof("Checking... sudo %s", cmd) - r := o.exec(cmd, sudo) - if !r.isSuccess() { - o.log.Errorf("sudo error on %s", r) - return fmt.Errorf("Failed to sudo: %s", r) - } - o.log.Infof("Sudo... Pass") + + o.log.Infof("sudo ... No need") return nil } @@ -159,6 +160,7 @@ func (o *debian) checkDependencies() error { } switch o.Distro.Family { case config.Ubuntu, config.Raspbian: + o.log.Infof("Dependencies... No need") return nil case config.Debian: @@ -185,16 +187,16 @@ func (o *debian) scanPackages() error { } o.setPackages(installed) - if !config.Conf.Deep { + if config.Conf.Deep || o.Distro.Family == config.Raspbian { + unsecure, err := o.scanUnsecurePackages(updatable) + if err != nil { + o.log.Errorf("Failed to scan vulnerable packages") + return err + } + o.setVulnInfos(unsecure) return nil } - unsecure, err := o.scanUnsecurePackages(updatable) - if err != nil { - o.log.Errorf("Failed to scan vulnerable packages") - return err - } - o.setVulnInfos(unsecure) return nil } From 29cf4bb517ea1ab2db539c335cd2a159a064ce71 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Tue, 8 Aug 2017 20:16:19 +0900 Subject: [PATCH 078/113] Setup changelog cache only when necessary --- scan/serverapi.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scan/serverapi.go b/scan/serverapi.go index 06b10bb3..d8008553 100644 --- a/scan/serverapi.go +++ b/scan/serverapi.go @@ -421,9 +421,15 @@ func setupChangelogCache() error { needToSetupCache := false for _, s := range servers { switch s.getDistro().Family { - case config.Ubuntu, config.Debian, config.Raspbian: + case config.Raspbian: needToSetupCache = true break + case config.Ubuntu, config.Debian: + //TODO changelopg cache for RedHat, Oracle, Amazon, CentOS is not implemented yet. + if config.Conf.Deep { + needToSetupCache = true + } + break } } if needToSetupCache { From b14406e329f951fd505f165e7aafbebd594178ed Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Tue, 8 Aug 2017 20:36:19 +0900 Subject: [PATCH 079/113] Fix check logic of dependent packages in redhat.go --- scan/redhat.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/scan/redhat.go b/scan/redhat.go index 1fb7b430..38d3e6da 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -143,14 +143,12 @@ func (o *redhat) checkIfSudoNoPasswd() error { if majorVersion < 6 { cmds = []cmd{ {"yum --color=never repolist", zero}, - // {"yum --color=never check-update", []int{0, 100}}, {"yum --color=never list-security --security", zero}, {"yum --color=never info-security", zero}, } } else { cmds = []cmd{ {"yum --color=never repolist", zero}, - // {"yum --color=never check-update", []int{0, 100}}, {"yum --color=never --security updateinfo list updates", zero}, {"yum --color=never --security updateinfo updates", zero}, } @@ -210,13 +208,12 @@ func (o *redhat) checkDependencies() error { var packNames []string switch o.Distro.Family { case config.CentOS, config.Amazon: - packNames = []string{"yum-utils"} + packNames = []string{"yum-utils, yum-plugin-changelog"} case config.RedHat, config.Oracle: if majorVersion < 6 { - packNames = []string{"yum-utils", "yum-security"} + packNames = []string{"yum-utils", "yum-security", "yum-changelog"} } else { - // yum-plugin-security is installed by default on RHEL6, 7 - return nil + packNames = []string{"yum-plugin-changelog"} } default: return fmt.Errorf("Not implemented yet: %s", o.Distro) @@ -648,7 +645,6 @@ type distroAdvisoryCveIDs struct { func (o *redhat) scanCveIDsByCommands(updatable models.Packages) (models.VulnInfos, error) { if o.Distro.Family == config.CentOS { // CentOS has no security channel. - // So use yum check-update && parse changelog return nil, fmt.Errorf( "yum updateinfo is not suppported on CentOS") } @@ -786,7 +782,6 @@ func (o *redhat) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveID switch o.Distro.Family { case config.CentOS: // CentOS has no security channel. - // So use yum check-update && parse changelog return result, fmt.Errorf( "yum updateinfo is not suppported on CentOS") case config.RedHat, config.Amazon, config.Oracle: From 774c78add00392b927a592dadc6c46c872412045 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Wed, 9 Aug 2017 10:32:31 +0900 Subject: [PATCH 080/113] Fix oval-db existence check on reporting --- config/config.go | 15 ++++++++++----- report/report.go | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/config/config.go b/config/config.go index 9547fc0b..ee7ea41b 100644 --- a/config/config.go +++ b/config/config.go @@ -197,6 +197,11 @@ func (c Config) ValidateOnReport() bool { if err := validateDB("cvedb", c.CveDBType, c.CveDBPath, c.CveDBURL); err != nil { errs = append(errs, err) } + if c.CveDBType == "sqlite3" { + if _, err := os.Stat(c.CveDBPath); os.IsNotExist(err) { + errs = append(errs, fmt.Errorf("SQLite3 DB path (%s) is not exist: %s", "cvedb", c.CveDBPath)) + } + } if err := validateDB("ovaldb", c.OvalDBType, c.OvalDBPath, c.OvalDBURL); err != nil { errs = append(errs, err) @@ -236,6 +241,11 @@ func (c Config) ValidateOnTui() bool { if err := validateDB("cvedb", c.CveDBType, c.CveDBPath, c.CveDBURL); err != nil { errs = append(errs, err) } + if c.CveDBType == "sqlite3" { + if _, err := os.Stat(c.CveDBPath); os.IsNotExist(err) { + errs = append(errs, fmt.Errorf("SQLite3 DB path (%s) is not exist: %s", "cvedb", c.CveDBPath)) + } + } for _, err := range errs { log.Error(err) @@ -256,11 +266,6 @@ func validateDB(dictionaryDBName, dbType, dbPath, dbURL string) error { dictionaryDBName, dbPath) } - if _, err := os.Stat(dbPath); os.IsNotExist(err) { - return fmt.Errorf("SQLite3 DB path (%s) is not exist: %s", - dictionaryDBName, - dbPath) - } case "mysql": if dbURL == "" { return fmt.Errorf( diff --git a/report/report.go b/report/report.go index f8610ff1..749c5f9e 100644 --- a/report/report.go +++ b/report/report.go @@ -178,7 +178,7 @@ func fillWithOval(r *models.ScanResult) (err error) { } if !ok { major := strings.Split(r.Release, ".")[0] - util.Log.Warnf("OVAL entries of %s %s are not found. It's recommended to use OVAL to improve scanning accuracy. To fetch OVAL, see https://github.com/kotakanbe/goval-dictionary#usage , Then report with --ovaldb-path or --ovaldb-url flag", ovalFamily, major) + util.Log.Warnf("OVAL entries of %s %s are not found. It's recommended to use OVAL to improve scanning accuracy. For details, see https://github.com/kotakanbe/goval-dictionary#usage , Then report with --ovaldb-path or --ovaldb-url flag", ovalFamily, major) return nil } From 577509bbf92094217ccd00c6d7faa746128435c3 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Wed, 9 Aug 2017 16:13:07 +0900 Subject: [PATCH 081/113] Fix MaxCvssScore logic --- models/vulninfos.go | 44 +++++++++++++++++------------ models/vulninfos_test.go | 61 +++++++++++++++++++++++++++++++--------- 2 files changed, 73 insertions(+), 32 deletions(-) diff --git a/models/vulninfos.go b/models/vulninfos.go index edd0d15a..735e89b2 100644 --- a/models/vulninfos.go +++ b/models/vulninfos.go @@ -218,10 +218,11 @@ func (v VulnInfo) Cvss2Scores() (values []CveContentCvss) { values = append(values, CveContentCvss{ Type: "Vendor", Value: Cvss{ - Type: CVSS2, - Score: severityToV2ScoreRoughly(adv.Severity), - Vector: "-", - Severity: strings.ToUpper(adv.Severity), + Type: CVSS2, + Score: severityToV2ScoreRoughly(adv.Severity), + CalculatedBySeverity: true, + Vector: "-", + Severity: strings.ToUpper(adv.Severity), }, }) } @@ -286,7 +287,11 @@ func (v VulnInfo) MaxCvssScore() CveContentCvss { v3Max := v.MaxCvss3Score() v2Max := v.MaxCvss2Score() max := v3Max - if max.Value.Score < v2Max.Value.Score { + if max.Type == Unknown { + return v2Max + } + + if max.Value.Score < v2Max.Value.Score && !v2Max.Value.CalculatedBySeverity { max = v2Max } return max @@ -325,7 +330,7 @@ func (v VulnInfo) MaxCvss2Score() CveContentCvss { // If CVSS score isn't on NVD, RedHat and JVN, use OVAL and advisory Severity. // Convert severity to cvss srore roughly, then returns max severity. - // Only Ubuntu, RedHat and Oracle OVAL has severity data in OVAL. + // Only Ubuntu, RedHat and Oracle have severity data in OVAL. order = []CveContentType{Ubuntu, RedHat, Oracle} for _, ctype := range order { if cont, found := v.CveContents[ctype]; found && 0 < len(cont.Severity) { @@ -334,10 +339,11 @@ func (v VulnInfo) MaxCvss2Score() CveContentCvss { value = CveContentCvss{ Type: ctype, Value: Cvss{ - Type: CVSS2, - Score: score, - Vector: cont.Cvss2Vector, - Severity: cont.Severity, + Type: CVSS2, + Score: score, + CalculatedBySeverity: true, + Vector: cont.Cvss2Vector, + Severity: cont.Severity, }, } } @@ -353,10 +359,11 @@ func (v VulnInfo) MaxCvss2Score() CveContentCvss { value = CveContentCvss{ Type: "Vendor", Value: Cvss{ - Type: CVSS2, - Score: score, - Vector: "-", - Severity: adv.Severity, + Type: CVSS2, + Score: score, + CalculatedBySeverity: true, + Vector: "-", + Severity: adv.Severity, }, } } @@ -384,10 +391,11 @@ const ( // Cvss has CVSS Score type Cvss struct { - Type CvssType - Score float64 - Vector string - Severity string + Type CvssType + Score float64 + CalculatedBySeverity bool + Vector string + Severity string } // Format CVSS Score and Vector diff --git a/models/vulninfos_test.go b/models/vulninfos_test.go index 7502f4ea..99e054ee 100644 --- a/models/vulninfos_test.go +++ b/models/vulninfos_test.go @@ -516,10 +516,10 @@ func TestCvss2Scores(t *testing.T) { out: nil, }, } - for _, tt := range tests { + for i, tt := range tests { actual := tt.in.Cvss2Scores() if !reflect.DeepEqual(tt.out, actual) { - t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + t.Errorf("[%d] expected: %v\n actual: %v\n", i, tt.out, actual) } } } @@ -575,9 +575,10 @@ func TestMaxCvss2Scores(t *testing.T) { out: CveContentCvss{ Type: Ubuntu, Value: Cvss{ - Type: CVSS2, - Score: 8.9, - Severity: "HIGH", + Type: CVSS2, + Score: 8.9, + CalculatedBySeverity: true, + Severity: "HIGH", }, }, }, @@ -595,10 +596,10 @@ func TestMaxCvss2Scores(t *testing.T) { }, }, } - for _, tt := range tests { + for i, tt := range tests { actual := tt.in.MaxCvss2Score() if !reflect.DeepEqual(tt.out, actual) { - t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) + t.Errorf("[%d] expected: %v\n actual: %v\n", i, tt.out, actual) } } } @@ -742,6 +743,7 @@ func TestMaxCvssScores(t *testing.T) { }, }, }, + //2 { in: VulnInfo{ CveContents: CveContents{ @@ -754,12 +756,14 @@ func TestMaxCvssScores(t *testing.T) { out: CveContentCvss{ Type: Ubuntu, Value: Cvss{ - Type: CVSS2, - Score: 8.9, - Severity: "HIGH", + Type: CVSS2, + Score: 8.9, + CalculatedBySeverity: true, + Severity: "HIGH", }, }, }, + //3 { in: VulnInfo{ CveContents: CveContents{ @@ -782,6 +786,7 @@ func TestMaxCvssScores(t *testing.T) { }, }, }, + //4 { in: VulnInfo{ DistroAdvisories: []DistroAdvisory{ @@ -792,11 +797,39 @@ func TestMaxCvssScores(t *testing.T) { }, out: CveContentCvss{ Type: "Vendor", + Value: Cvss{ + Type: CVSS2, + Score: 8.9, + CalculatedBySeverity: true, + Vector: "-", + Severity: "HIGH", + }, + }, + }, + { + in: VulnInfo{ + CveContents: CveContents{ + Ubuntu: { + Type: Ubuntu, + Severity: "MEDIUM", + }, + NVD: { + Type: NVD, + Cvss2Score: 4.0, + }, + }, + DistroAdvisories: []DistroAdvisory{ + { + Severity: "HIGH", + }, + }, + }, + out: CveContentCvss{ + Type: NVD, Value: Cvss{ Type: CVSS2, - Score: 8.9, - Vector: "-", - Severity: "HIGH", + Score: 4, + Severity: "MEDIUM", }, }, }, @@ -806,7 +839,7 @@ func TestMaxCvssScores(t *testing.T) { out: CveContentCvss{ Type: Unknown, Value: Cvss{ - Type: CVSS3, + Type: CVSS2, Score: 0, }, }, From f738622c287516b1774ffae4d8941f752b0d78a0 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Thu, 10 Aug 2017 15:15:44 +0900 Subject: [PATCH 082/113] Update png in README.md --- README.md | 6 + img/vuls-architecture-localscan.graphml | 257 ++++++++++-------------- img/vuls-architecture-localscan.png | Bin 100539 -> 99592 bytes img/vuls-architecture.graphml | 196 +++++++++--------- img/vuls-architecture.png | Bin 92162 -> 93239 bytes 5 files changed, 211 insertions(+), 248 deletions(-) diff --git a/README.md b/README.md index 74c43809..b9f83e00 100644 --- a/README.md +++ b/README.md @@ -1700,6 +1700,12 @@ Please see [CHANGELOG](https://github.com/future-architect/vuls/blob/master/CHAN ---- +# Stargazers over time + +[![Stargazers over time](https://starcharts.herokuapp.com/future-architect/vuls.svg)](https://starcharts.herokuapp.com/future-architect/vuls) + +---- + # License Please see [LICENSE](https://github.com/future-architect/vuls/blob/master/LICENSE). diff --git a/img/vuls-architecture-localscan.graphml b/img/vuls-architecture-localscan.graphml index 665bd2af..11b4e0d9 100644 --- a/img/vuls-architecture-localscan.graphml +++ b/img/vuls-architecture-localscan.graphml @@ -1,6 +1,6 @@ - + @@ -17,10 +17,10 @@ - + - + @@ -37,20 +37,20 @@ - + - Vulnerbility Database + Vulnerbility Database - + - Folder 1 + Folder 1 @@ -63,10 +63,10 @@ - + - JVN + JVN (Japanese) @@ -81,10 +81,27 @@ - + - NVD + NVD + + + + + + + + + + + + + + + + + OVAL @@ -106,17 +123,17 @@ - Distribution Support + Distribution Support - + - Folder 2 + Folder 2 @@ -132,7 +149,7 @@ - apptitude + apptitude changelog @@ -150,7 +167,7 @@ changelog - yum + yum changelog @@ -165,11 +182,12 @@ changelog - + - RHSA (RedHat) -ALAS (Amazon) + RHSA (RedHat) +ALAS (Amazon) +ELSA(Oracle) @@ -186,7 +204,7 @@ ALAS (Amazon) - FreeBSD Support + FreeBSD Support @@ -205,7 +223,7 @@ ALAS (Amazon) - + @@ -222,7 +240,7 @@ ALAS (Amazon) - System Operator + System Operator @@ -242,7 +260,7 @@ ALAS (Amazon) - + @@ -259,7 +277,7 @@ ALAS (Amazon) - go-cve-dictionary + go-cve-dictionary / goval-dictionary @@ -269,7 +287,7 @@ ALAS (Amazon) - Folder 4 + Folder 4 @@ -285,7 +303,7 @@ ALAS (Amazon) - + @@ -293,7 +311,7 @@ ALAS (Amazon) - SQLite3 + SQLite3 @@ -309,7 +327,7 @@ ALAS (Amazon) - HTTP server + HTTP server @@ -326,7 +344,7 @@ ALAS (Amazon) - Fetcher + Fetcher @@ -345,7 +363,7 @@ ALAS (Amazon) - + @@ -356,7 +374,7 @@ ALAS (Amazon) - Azure + Azure BLOB @@ -374,7 +392,7 @@ BLOB - .xml + .xml @@ -390,7 +408,7 @@ BLOB - .txt + .txt @@ -406,7 +424,7 @@ BLOB - .json + .json @@ -422,7 +440,7 @@ BLOB - .gz + .gz @@ -435,10 +453,10 @@ BLOB - + - + @@ -464,7 +482,7 @@ BLOB - Vuls Reporting Server + Vuls Reporting Server @@ -474,7 +492,7 @@ BLOB - Folder 10 + Folder 10 @@ -493,7 +511,7 @@ BLOB - Vuls + Vuls @@ -503,7 +521,7 @@ BLOB - Folder 9 + Folder 9 @@ -519,7 +537,7 @@ BLOB - Report + Report @@ -536,7 +554,7 @@ BLOB - VulsRepo + VulsRepo (WebUI) @@ -554,7 +572,7 @@ BLOB - TUI + TUI @@ -574,7 +592,7 @@ BLOB - results dir + results dir @@ -584,7 +602,7 @@ BLOB - Folder 7 + Folder 7 @@ -600,7 +618,7 @@ BLOB - JSON + JSON @@ -617,7 +635,7 @@ BLOB - JSON + JSON @@ -634,7 +652,7 @@ BLOB - JSON + JSON @@ -660,7 +678,7 @@ BLOB - Scan Target Server + Scan Target Server @@ -670,7 +688,7 @@ BLOB - Folder 10 + Folder 10 @@ -689,7 +707,7 @@ BLOB - Docker/LXD + Docker/LXD @@ -699,7 +717,7 @@ BLOB - Folder 5 + Folder 5 @@ -715,7 +733,7 @@ BLOB - Container + Container @@ -734,7 +752,7 @@ BLOB - Package Manager + Package Manager @@ -754,7 +772,7 @@ BLOB - Vuls + Vuls @@ -764,7 +782,7 @@ BLOB - Folder 10 + Folder 10 @@ -780,7 +798,7 @@ BLOB - Scan + Scan @@ -800,7 +818,7 @@ BLOB - results dir + results dir @@ -810,7 +828,7 @@ BLOB - Folder 7 + Folder 7 @@ -826,7 +844,7 @@ BLOB - JSON + JSON @@ -843,7 +861,7 @@ BLOB - JSON + JSON @@ -860,7 +878,7 @@ BLOB - JSON + JSON @@ -877,32 +895,13 @@ BLOB - - - - - - - Fetch -Vulnerability data - - - - - - - - - - - - + - HTTP + HTTP @@ -914,38 +913,12 @@ Vulnerability data - - - - - - - HTTP - - - - - - - - - - - - + - WebUI - - - - - - - @@ -956,7 +929,7 @@ Vulnerability data - docker exec + docker exec lxc exec @@ -969,7 +942,7 @@ lxc exec - + @@ -985,7 +958,7 @@ lxc exec - Insert + Insert @@ -997,31 +970,13 @@ lxc exec - - - - - - - Notify - - - - - - - - - - - - Select + Select @@ -1043,7 +998,7 @@ lxc exec - + @@ -1053,7 +1008,7 @@ lxc exec - + @@ -1073,13 +1028,13 @@ lxc exec - + - Put + Put @@ -1091,7 +1046,7 @@ lxc exec - + @@ -1111,13 +1066,13 @@ lxc exec - + - View Results + View Results on Terminal @@ -1136,7 +1091,7 @@ lxc exec - os.exec + os.exec @@ -1148,13 +1103,13 @@ lxc exec - + - HTTP + HTTP @@ -1176,13 +1131,13 @@ lxc exec - + - transfer + transfer @@ -1194,7 +1149,17 @@ lxc exec - + + + + + + + + + + + diff --git a/img/vuls-architecture-localscan.png b/img/vuls-architecture-localscan.png index bd48bdcffc8dfd96683f7445a778bcfc6cd8eafa..e703d12170d4670dbc9716a20f604989d10ffc80 100644 GIT binary patch literal 99592 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
li#N;r0ANiJ>hX5o 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 diff --git a/img/vuls-architecture.graphml b/img/vuls-architecture.graphml index b29d958c..9e263988 100644 --- a/img/vuls-architecture.graphml +++ b/img/vuls-architecture.graphml @@ -1,6 +1,6 @@ - + @@ -20,7 +20,7 @@ - + @@ -37,20 +37,20 @@ - + - Vulnerbility Database + Vulnerbility Database - + - Folder 1 + Folder 1 @@ -63,10 +63,10 @@ - + - JVN + JVN (Japanese) @@ -81,10 +81,27 @@ - + - NVD + NVD + + + + + + + + + + + + + + + + + OVAL @@ -103,20 +120,20 @@ - + - Distribution Support + Distribution Support - + - Folder 2 + Folder 2 @@ -132,7 +149,7 @@ - apptitude + apptitude changelog @@ -150,7 +167,7 @@ changelog - yum + yum changelog @@ -165,11 +182,12 @@ changelog - + - RHSA (RedHat) -ALAS (Amazon) + RHSA (RedHat) +ALAS (Amazon) +ELSA (Oracle) @@ -183,10 +201,10 @@ ALAS (Amazon) - + - FreeBSD Support + FreeBSD Support @@ -205,7 +223,7 @@ ALAS (Amazon) - + @@ -225,7 +243,7 @@ ALAS (Amazon) - Vuls + Vuls @@ -235,7 +253,7 @@ ALAS (Amazon) - Folder 3 + Folder 3 @@ -251,7 +269,7 @@ ALAS (Amazon) - Scan + Scan @@ -268,7 +286,7 @@ ALAS (Amazon) - Report + Report @@ -285,7 +303,7 @@ ALAS (Amazon) - VulsRepo + VulsRepo (WebUI) @@ -303,7 +321,7 @@ ALAS (Amazon) - TUI + TUI @@ -322,7 +340,7 @@ ALAS (Amazon) - System Operator + System Operator @@ -342,7 +360,7 @@ ALAS (Amazon) - + @@ -356,7 +374,7 @@ ALAS (Amazon) - + @@ -382,7 +400,7 @@ ALAS (Amazon) - go-cve-dictionary + go-cve-dictionary / goval-dictionary @@ -392,7 +410,7 @@ ALAS (Amazon) - Folder 4 + Folder 4 @@ -408,7 +426,7 @@ ALAS (Amazon) - + @@ -416,7 +434,7 @@ ALAS (Amazon) - SQLite3 + SQLite3 @@ -432,7 +450,7 @@ ALAS (Amazon) - HTTP server + HTTP server @@ -449,7 +467,7 @@ ALAS (Amazon) - Fetcher + Fetcher @@ -471,7 +489,7 @@ ALAS (Amazon) - Docker/LXD + Docker/LXD @@ -481,7 +499,7 @@ ALAS (Amazon) - Folder 5 + Folder 5 @@ -497,7 +515,7 @@ ALAS (Amazon) - Host + Host @@ -514,7 +532,7 @@ ALAS (Amazon) - Container + Container @@ -536,7 +554,7 @@ ALAS (Amazon) - Linux/FreeBSD + Linux/FreeBSD @@ -546,7 +564,7 @@ ALAS (Amazon) - Folder 6 + Folder 6 @@ -562,7 +580,7 @@ ALAS (Amazon) - Server + Server @@ -579,7 +597,7 @@ ALAS (Amazon) - Server + Server @@ -601,7 +619,7 @@ ALAS (Amazon) - results dir + results dir @@ -611,7 +629,7 @@ ALAS (Amazon) - Folder 7 + Folder 7 @@ -627,7 +645,7 @@ ALAS (Amazon) - JSON + JSON @@ -644,7 +662,7 @@ ALAS (Amazon) - JSON + JSON @@ -661,7 +679,7 @@ ALAS (Amazon) - JSON + JSON @@ -680,7 +698,7 @@ ALAS (Amazon) - + @@ -691,7 +709,7 @@ ALAS (Amazon) - Azure + Azure BLOB @@ -709,7 +727,7 @@ BLOB - .xml + .xml @@ -725,7 +743,7 @@ BLOB - .txt + .txt @@ -741,7 +759,7 @@ BLOB - .json + .json @@ -757,7 +775,7 @@ BLOB - .gz + .gz @@ -773,7 +791,7 @@ BLOB - Fetch + Fetch Vulnerability data @@ -792,7 +810,7 @@ Vulnerability data - HTTP + HTTP @@ -810,7 +828,7 @@ Vulnerability data - HTTP + HTTP @@ -828,14 +846,6 @@ Vulnerability data - WebUI - - - - - - - @@ -846,7 +856,7 @@ Vulnerability data - SSH + SSH @@ -864,7 +874,7 @@ Vulnerability data - SSH + SSH @@ -882,7 +892,7 @@ Vulnerability data - docker exec + docker exec lxc exec @@ -921,7 +931,7 @@ lxc exec - Insert + Insert @@ -933,31 +943,13 @@ lxc exec - - - - - - - Notify - - - - - - - - - - - - Select + Select @@ -969,7 +961,7 @@ lxc exec - + @@ -979,7 +971,7 @@ lxc exec - + @@ -989,7 +981,7 @@ lxc exec - + @@ -999,7 +991,7 @@ lxc exec - + @@ -1009,7 +1001,7 @@ lxc exec - + @@ -1019,7 +1011,7 @@ lxc exec - + @@ -1029,13 +1021,13 @@ lxc exec - + - Put + Put @@ -1047,7 +1039,7 @@ lxc exec - + @@ -1057,7 +1049,7 @@ lxc exec - + @@ -1067,13 +1059,13 @@ lxc exec - + - View Results + View Results on Terminal diff --git a/img/vuls-architecture.png b/img/vuls-architecture.png index 07305b9ce978f7f4d5436cc3813ec2653068d079..6872f1950dd4da31a66d6b461b503f032e0d534b 100644 GIT binary patch literal 93239 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%0A@65nSuap?GF1@Bcj^ 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 Q?>|e5zJFKxR`2uw0UwhER{#J2 literal 92162 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 KKrx?&=l=ueW#O>^ From 95eb980f5847bdfabe6f15ea0e5aed47f592462b Mon Sep 17 00:00:00 2001 From: sadayuki-matsuno Date: Fri, 11 Aug 2017 17:27:10 +0900 Subject: [PATCH 083/113] export FillWithOval (#462) --- report/report.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/report/report.go b/report/report.go index 749c5f9e..ba0922d8 100644 --- a/report/report.go +++ b/report/report.go @@ -81,7 +81,7 @@ func fillCveInfo(r *models.ScanResult) error { util.Log.Debugf("need to refresh") util.Log.Infof("Fill CVE detailed information with OVAL") - if err := fillWithOval(r); err != nil { + if err := FillWithOval(r); err != nil { return fmt.Errorf("Failed to fill OVAL information: %s", err) } @@ -140,7 +140,8 @@ func fillWithCveDB(r *models.ScanResult) error { return nil } -func fillWithOval(r *models.ScanResult) (err error) { +// FillWithOval fetches OVAL database, and then set to fields. +func FillWithOval(r *models.ScanResult) (err error) { var ovalClient oval.Client var ovalFamily string From 47b3b3848bec970fbd7a666cef29c2b2691ba3b9 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Sun, 13 Aug 2017 15:31:14 +0900 Subject: [PATCH 084/113] Refactoring --- oval/debian.go | 64 +++++++++++++++++++++++++++++++------------------- oval/redhat.go | 30 ++++++++++------------- 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/oval/debian.go b/oval/debian.go index 2c18ce7a..ad27dce9 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -7,46 +7,54 @@ import ( "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/util" ver "github.com/knqyf263/go-deb-version" - ovalconf "github.com/kotakanbe/goval-dictionary/config" db "github.com/kotakanbe/goval-dictionary/db" ovallog "github.com/kotakanbe/goval-dictionary/log" ovalmodels "github.com/kotakanbe/goval-dictionary/models" ) // DebianBase is the base struct of Debian and Ubuntu -type DebianBase struct{ Base } +type DebianBase struct { + Base + family string +} // fillFromOvalDB returns scan result after updating CVE info by OVAL func (o DebianBase) fillFromOvalDB(r *models.ScanResult) error { - ovalconf.Conf.DebugSQL = config.Conf.DebugSQL - ovalconf.Conf.DBType = config.Conf.OvalDBType - ovalconf.Conf.DBPath = config.Conf.OvalDBPath - if ovalconf.Conf.DBType == "sqlite3" { - ovalconf.Conf.DBPath = config.Conf.OvalDBPath - } else { - ovalconf.Conf.DBPath = config.Conf.OvalDBURL + defs, err := o.getDefsByPackNameFromOvalDB(r.Release, r.Packages) + if err != nil { + return err } - util.Log.Debugf("Open oval-dictionary db (%s): %s", - ovalconf.Conf.DBType, ovalconf.Conf.DBPath) + for _, def := range defs { + o.update(r, &def) + } + return nil +} + +func (o DebianBase) getDefsByPackNameFromOvalDB(osRelease string, + packs models.Packages) (relatedDefs []ovalmodels.Definition, err error) { ovallog.Initialize(config.Conf.LogDir) + path := config.Conf.OvalDBURL + if config.Conf.OvalDBType == "sqlite3" { + path = config.Conf.OvalDBPath + } + util.Log.Debugf("Open oval-dictionary db (%s): %s", config.Conf.OvalDBType, path) - var err error var ovaldb db.DB if ovaldb, err = db.NewDB( - r.Family, - ovalconf.Conf.DBType, - ovalconf.Conf.DBPath, - ovalconf.Conf.DebugSQL, + o.family, + config.Conf.OvalDBType, + path, + config.Conf.DebugSQL, ); err != nil { - return err + return } defer ovaldb.CloseDB() - for _, pack := range r.Packages { - definitions, err := ovaldb.GetByPackName(r.Release, pack.Name) + for _, pack := range packs { + definitions, err := ovaldb.GetByPackName(osRelease, pack.Name) if err != nil { - return fmt.Errorf("Failed to get Debian OVAL info by package name: %v", err) + return nil, fmt.Errorf("Failed to get %s OVAL info by package name: %v", o.family, err) } for _, def := range definitions { current, _ := ver.NewVersion(pack.Version) @@ -56,12 +64,12 @@ func (o DebianBase) fillFromOvalDB(r *models.ScanResult) error { } affected, _ := ver.NewVersion(p.Version) if current.LessThan(affected) { - o.update(r, &def) + relatedDefs = append(relatedDefs, def) } } } } - return nil + return } func (o DebianBase) update(r *models.ScanResult, definition *ovalmodels.Definition) { @@ -120,7 +128,11 @@ type Debian struct { // NewDebian creates OVAL client for Debian func NewDebian() Debian { - return Debian{} + return Debian{ + DebianBase{ + family: config.Debian, + }, + } } // FillWithOval returns scan result after updating CVE info by OVAL @@ -156,7 +168,11 @@ type Ubuntu struct { // NewUbuntu creates OVAL client for Debian func NewUbuntu() Ubuntu { - return Ubuntu{} + return Ubuntu{ + DebianBase{ + family: config.Ubuntu, + }, + } } // FillWithOval returns scan result after updating CVE info by OVAL diff --git a/oval/redhat.go b/oval/redhat.go index 9684bf8a..b1a76130 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -9,7 +9,6 @@ import ( "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/util" ver "github.com/knqyf263/go-rpm-version" - ovalconf "github.com/kotakanbe/goval-dictionary/config" db "github.com/kotakanbe/goval-dictionary/db" ovallog "github.com/kotakanbe/goval-dictionary/log" ovalmodels "github.com/kotakanbe/goval-dictionary/models" @@ -68,24 +67,19 @@ func (o RedHatBase) fillFromOvalDB(r *models.ScanResult) error { func (o RedHatBase) getDefsByPackNameFromOvalDB(osRelease string, packs models.Packages) (relatedDefs []ovalmodels.Definition, err error) { - ovalconf.Conf.DebugSQL = config.Conf.DebugSQL - ovalconf.Conf.DBType = config.Conf.OvalDBType - if ovalconf.Conf.DBType == "sqlite3" { - ovalconf.Conf.DBPath = config.Conf.OvalDBPath - } else { - ovalconf.Conf.DBPath = config.Conf.OvalDBURL - } - util.Log.Debugf("Open oval-dictionary db (%s): %s", - ovalconf.Conf.DBType, ovalconf.Conf.DBPath) - ovallog.Initialize(config.Conf.LogDir) + path := config.Conf.OvalDBURL + if config.Conf.OvalDBType == "sqlite3" { + path = config.Conf.OvalDBPath + } + util.Log.Debugf("Open oval-dictionary db (%s): %s", config.Conf.OvalDBType, path) var ovaldb db.DB if ovaldb, err = db.NewDB( o.family, - ovalconf.Conf.DBType, - ovalconf.Conf.DBPath, - ovalconf.Conf.DebugSQL, + config.Conf.OvalDBType, + path, + config.Conf.DebugSQL, ); err != nil { return } @@ -98,11 +92,13 @@ func (o RedHatBase) getDefsByPackNameFromOvalDB(osRelease string, for _, def := range definitions { current := ver.NewVersion(fmt.Sprintf("%s-%s", pack.Version, pack.Release)) for _, p := range def.AffectedPacks { - affected := ver.NewVersion(p.Version) - if pack.Name != p.Name || !current.LessThan(affected) { + if pack.Name != p.Name { continue } - relatedDefs = append(relatedDefs, def) + affected := ver.NewVersion(p.Version) + if current.LessThan(affected) { + relatedDefs = append(relatedDefs, def) + } } } } From 5c51d83573a81c174fdccfeee88a0b8fd1400373 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Sun, 13 Aug 2017 17:18:01 +0900 Subject: [PATCH 085/113] Refactoring --- oval/debian.go | 78 ++++++----------- oval/oval.go | 153 +++++--------------------------- oval/redhat.go | 76 ++++++---------- oval/util.go | 233 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 305 insertions(+), 235 deletions(-) create mode 100644 oval/util.go diff --git a/oval/debian.go b/oval/debian.go index ad27dce9..e7e05523 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -1,26 +1,36 @@ +/* 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 oval import ( - "fmt" - "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/util" - ver "github.com/knqyf263/go-deb-version" - db "github.com/kotakanbe/goval-dictionary/db" - ovallog "github.com/kotakanbe/goval-dictionary/log" ovalmodels "github.com/kotakanbe/goval-dictionary/models" ) // DebianBase is the base struct of Debian and Ubuntu type DebianBase struct { Base - family string } // fillFromOvalDB returns scan result after updating CVE info by OVAL func (o DebianBase) fillFromOvalDB(r *models.ScanResult) error { - defs, err := o.getDefsByPackNameFromOvalDB(r.Release, r.Packages) + defs, err := getDefsByPackNameFromOvalDB(o.family, r.Release, r.Packages) if err != nil { return err } @@ -30,51 +40,9 @@ func (o DebianBase) fillFromOvalDB(r *models.ScanResult) error { return nil } -func (o DebianBase) getDefsByPackNameFromOvalDB(osRelease string, - packs models.Packages) (relatedDefs []ovalmodels.Definition, err error) { - - ovallog.Initialize(config.Conf.LogDir) - path := config.Conf.OvalDBURL - if config.Conf.OvalDBType == "sqlite3" { - path = config.Conf.OvalDBPath - } - util.Log.Debugf("Open oval-dictionary db (%s): %s", config.Conf.OvalDBType, path) - - var ovaldb db.DB - if ovaldb, err = db.NewDB( - o.family, - config.Conf.OvalDBType, - path, - config.Conf.DebugSQL, - ); err != nil { - return - } - defer ovaldb.CloseDB() - - for _, pack := range packs { - definitions, err := ovaldb.GetByPackName(osRelease, pack.Name) - if err != nil { - return nil, fmt.Errorf("Failed to get %s OVAL info by package name: %v", o.family, err) - } - for _, def := range definitions { - current, _ := ver.NewVersion(pack.Version) - for _, p := range def.AffectedPacks { - if pack.Name != p.Name { - continue - } - affected, _ := ver.NewVersion(p.Version) - if current.LessThan(affected) { - relatedDefs = append(relatedDefs, def) - } - } - } - } - return -} - func (o DebianBase) update(r *models.ScanResult, definition *ovalmodels.Definition) { ovalContent := *o.convertToModel(definition) - ovalContent.Type = models.NewCveContentType(r.Family) + ovalContent.Type = models.NewCveContentType(o.family) vinfo, ok := r.ScannedCves[definition.Debian.CveID] if !ok { util.Log.Debugf("%s is newly detected by OVAL", definition.Debian.CveID) @@ -86,7 +54,7 @@ func (o DebianBase) update(r *models.ScanResult, definition *ovalmodels.Definiti } } else { cveContents := vinfo.CveContents - ctype := models.NewCveContentType(r.Family) + ctype := models.NewCveContentType(o.family) if _, ok := vinfo.CveContents[ctype]; ok { util.Log.Debugf("%s will be updated by OVAL", definition.Debian.CveID) } else { @@ -130,7 +98,9 @@ type Debian struct { func NewDebian() Debian { return Debian{ DebianBase{ - family: config.Debian, + Base{ + family: config.Debian, + }, }, } } @@ -170,7 +140,9 @@ type Ubuntu struct { func NewUbuntu() Ubuntu { return Ubuntu{ DebianBase{ - family: config.Ubuntu, + Base{ + family: config.Ubuntu, + }, }, } } diff --git a/oval/oval.go b/oval/oval.go index a4f15965..591c8853 100644 --- a/oval/oval.go +++ b/oval/oval.go @@ -1,3 +1,20 @@ +/* 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 oval import ( @@ -7,14 +24,11 @@ import ( "strings" "time" - "github.com/cenkalti/backoff" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/util" - ver "github.com/knqyf263/go-deb-version" "github.com/kotakanbe/goval-dictionary/db" ovallog "github.com/kotakanbe/goval-dictionary/log" - ovalmodels "github.com/kotakanbe/goval-dictionary/models" "github.com/parnurzeal/gorequest" ) @@ -29,7 +43,9 @@ type Client interface { } // Base is a base struct -type Base struct{} +type Base struct { + family string +} // CheckHTTPHealth do health check func (b Base) CheckHTTPHealth() error { @@ -135,132 +151,3 @@ func (b Base) isFetchViaHTTP() bool { } return false } - -type request struct { - pack models.Package -} - -type response struct { - pack *models.Package - defs []ovalmodels.Definition -} - -// getDefsByPackNameViaHTTP fetches OVAL information via HTTP -func getDefsByPackNameViaHTTP(r *models.ScanResult) ( - relatedDefs []ovalmodels.Definition, err error) { - - reqChan := make(chan request, len(r.Packages)) - resChan := make(chan response, len(r.Packages)) - errChan := make(chan error, len(r.Packages)) - defer close(reqChan) - defer close(resChan) - defer close(errChan) - - go func() { - for _, pack := range r.Packages { - reqChan <- request{ - pack: pack, - } - } - }() - - concurrency := 10 - tasks := util.GenWorkers(concurrency) - for range r.Packages { - tasks <- func() { - select { - case req := <-reqChan: - url, err := util.URLPathJoin( - config.Conf.OvalDBURL, - "packs", - r.Family, - r.Release, - req.pack.Name, - ) - if err != nil { - errChan <- err - } else { - util.Log.Debugf("HTTP Request to %s", url) - httpGet(url, &req.pack, resChan, errChan) - } - } - } - } - - timeout := time.After(2 * 60 * time.Second) - var errs []error - for range r.Packages { - select { - case res := <-resChan: - current, _ := ver.NewVersion(fmt.Sprintf("%s-%s", - res.pack.Version, res.pack.Release)) - for _, def := range res.defs { - for _, p := range def.AffectedPacks { - affected, _ := ver.NewVersion(p.Version) - if res.pack.Name != p.Name || !current.LessThan(affected) { - continue - } - relatedDefs = append(relatedDefs, def) - } - } - case err := <-errChan: - errs = append(errs, err) - case <-timeout: - return nil, fmt.Errorf("Timeout Fetching OVAL") - } - } - if len(errs) != 0 { - return nil, fmt.Errorf("Failed to fetch OVAL. err: %v", errs) - } - return -} - -func httpGet(url string, pack *models.Package, resChan chan<- response, errChan chan<- error) { - var body string - var errs []error - var resp *http.Response - count, retryMax := 0, 3 - f := func() (err error) { - // resp, body, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End() - resp, body, errs = gorequest.New().Get(url).End() - if 0 < len(errs) || resp == nil || resp.StatusCode != 200 { - count++ - if count == retryMax { - return nil - } - return fmt.Errorf("HTTP GET error: %v, url: %s, resp: %v", - errs, url, resp) - } - return nil - } - notify := func(err error, t time.Duration) { - util.Log.Warnf("Failed to HTTP GET. retrying in %s seconds. err: %s", t, err) - } - err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify) - if err != nil { - errChan <- fmt.Errorf("HTTP Error %s", err) - return - } - if count == retryMax { - errChan <- fmt.Errorf("HRetry count exceeded") - return - } - - defs := []ovalmodels.Definition{} - if err := json.Unmarshal([]byte(body), &defs); err != nil { - errChan <- fmt.Errorf("Failed to Unmarshall. body: %s, err: %s", - body, err) - return - } - resChan <- response{ - pack: pack, - defs: defs, - } -} - -func getPackages(r *models.ScanResult, d *ovalmodels.Definition) (names []string) { - for _, affectedPack := range d.AffectedPacks { - names = append(names, affectedPack.Name) - } - return -} diff --git a/oval/redhat.go b/oval/redhat.go index b1a76130..2541c3cb 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -1,3 +1,20 @@ +/* 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 oval import ( @@ -8,16 +25,12 @@ import ( "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/util" - ver "github.com/knqyf263/go-rpm-version" - db "github.com/kotakanbe/goval-dictionary/db" - ovallog "github.com/kotakanbe/goval-dictionary/log" ovalmodels "github.com/kotakanbe/goval-dictionary/models" ) // RedHatBase is the base struct for RedHat and CentOS type RedHatBase struct { Base - family string } // FillWithOval returns scan result after updating CVE info by OVAL @@ -54,7 +67,7 @@ func (o RedHatBase) FillWithOval(r *models.ScanResult) error { // fillFromOvalDB returns scan result after updating CVE info by OVAL func (o RedHatBase) fillFromOvalDB(r *models.ScanResult) error { - defs, err := o.getDefsByPackNameFromOvalDB(r.Release, r.Packages) + defs, err := getDefsByPackNameFromOvalDB(o.family, r.Release, r.Packages) if err != nil { return err } @@ -64,47 +77,6 @@ func (o RedHatBase) fillFromOvalDB(r *models.ScanResult) error { return nil } -func (o RedHatBase) getDefsByPackNameFromOvalDB(osRelease string, - packs models.Packages) (relatedDefs []ovalmodels.Definition, err error) { - - ovallog.Initialize(config.Conf.LogDir) - path := config.Conf.OvalDBURL - if config.Conf.OvalDBType == "sqlite3" { - path = config.Conf.OvalDBPath - } - util.Log.Debugf("Open oval-dictionary db (%s): %s", config.Conf.OvalDBType, path) - - var ovaldb db.DB - if ovaldb, err = db.NewDB( - o.family, - config.Conf.OvalDBType, - path, - config.Conf.DebugSQL, - ); err != nil { - return - } - defer ovaldb.CloseDB() - for _, pack := range packs { - definitions, err := ovaldb.GetByPackName(osRelease, pack.Name) - if err != nil { - return nil, fmt.Errorf("Failed to get %s OVAL info by package name: %v", o.family, err) - } - for _, def := range definitions { - current := ver.NewVersion(fmt.Sprintf("%s-%s", pack.Version, pack.Release)) - for _, p := range def.AffectedPacks { - if pack.Name != p.Name { - continue - } - affected := ver.NewVersion(p.Version) - if current.LessThan(affected) { - relatedDefs = append(relatedDefs, def) - } - } - } - } - return -} - func (o RedHatBase) update(r *models.ScanResult, definition *ovalmodels.Definition) { ctype := models.NewCveContentType(o.family) for _, cve := range definition.Advisory.Cves { @@ -210,7 +182,9 @@ type RedHat struct { func NewRedhat() RedHat { return RedHat{ RedHatBase{ - family: config.RedHat, + Base{ + family: config.RedHat, + }, }, } } @@ -224,7 +198,9 @@ type CentOS struct { func NewCentOS() CentOS { return CentOS{ RedHatBase{ - family: config.CentOS, + Base{ + family: config.CentOS, + }, }, } } @@ -238,7 +214,9 @@ type Oracle struct { func NewOracle() Oracle { return Oracle{ RedHatBase{ - family: config.Oracle, + Base{ + family: config.Oracle, + }, }, } } diff --git a/oval/util.go b/oval/util.go new file mode 100644 index 00000000..c84cade1 --- /dev/null +++ b/oval/util.go @@ -0,0 +1,233 @@ +/* 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 oval + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/cenkalti/backoff" + "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/models" + "github.com/future-architect/vuls/util" + debver "github.com/knqyf263/go-deb-version" + rpmver "github.com/knqyf263/go-rpm-version" + "github.com/kotakanbe/goval-dictionary/db" + ovallog "github.com/kotakanbe/goval-dictionary/log" + ovalmodels "github.com/kotakanbe/goval-dictionary/models" + "github.com/parnurzeal/gorequest" +) + +type request struct { + pack models.Package +} + +type response struct { + pack *models.Package + defs []ovalmodels.Definition +} + +// getDefsByPackNameViaHTTP fetches OVAL information via HTTP +func getDefsByPackNameViaHTTP(r *models.ScanResult) ( + relatedDefs []ovalmodels.Definition, err error) { + + reqChan := make(chan request, len(r.Packages)) + resChan := make(chan response, len(r.Packages)) + errChan := make(chan error, len(r.Packages)) + defer close(reqChan) + defer close(resChan) + defer close(errChan) + + go func() { + for _, pack := range r.Packages { + reqChan <- request{ + pack: pack, + } + } + }() + + concurrency := 10 + tasks := util.GenWorkers(concurrency) + for range r.Packages { + tasks <- func() { + select { + case req := <-reqChan: + url, err := util.URLPathJoin( + config.Conf.OvalDBURL, + "packs", + r.Family, + r.Release, + req.pack.Name, + ) + if err != nil { + errChan <- err + } else { + util.Log.Debugf("HTTP Request to %s", url) + httpGet(url, &req.pack, resChan, errChan) + } + } + } + } + + timeout := time.After(2 * 60 * time.Second) + var errs []error + for range r.Packages { + select { + case res := <-resChan: + for _, def := range res.defs { + for _, p := range def.AffectedPacks { + if res.pack.Name != p.Name { + continue + } + if less, err := lessThan(r.Family, *res.pack, p); err != nil { + if !p.NotFixedYet { + util.Log.Debugf("Failed to parse versions: %s", err) + util.Log.Debugf("%#v\n%#v", *res.pack, p) + } + } else if less { + relatedDefs = append(relatedDefs, def) + } + } + } + case err := <-errChan: + errs = append(errs, err) + case <-timeout: + return nil, fmt.Errorf("Timeout Fetching OVAL") + } + } + if len(errs) != 0 { + return nil, fmt.Errorf("Failed to fetch OVAL. err: %v", errs) + } + return +} + +func httpGet(url string, pack *models.Package, resChan chan<- response, errChan chan<- error) { + var body string + var errs []error + var resp *http.Response + count, retryMax := 0, 3 + f := func() (err error) { + // resp, body, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End() + resp, body, errs = gorequest.New().Get(url).End() + if 0 < len(errs) || resp == nil || resp.StatusCode != 200 { + count++ + if count == retryMax { + return nil + } + return fmt.Errorf("HTTP GET error: %v, url: %s, resp: %v", + errs, url, resp) + } + return nil + } + notify := func(err error, t time.Duration) { + util.Log.Warnf("Failed to HTTP GET. retrying in %s seconds. err: %s", t, err) + } + err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify) + if err != nil { + errChan <- fmt.Errorf("HTTP Error %s", err) + return + } + if count == retryMax { + errChan <- fmt.Errorf("HRetry count exceeded") + return + } + + defs := []ovalmodels.Definition{} + if err := json.Unmarshal([]byte(body), &defs); err != nil { + errChan <- fmt.Errorf("Failed to Unmarshall. body: %s, err: %s", + body, err) + return + } + resChan <- response{ + pack: pack, + defs: defs, + } +} + +func getPackages(r *models.ScanResult, d *ovalmodels.Definition) (names []string) { + for _, affectedPack := range d.AffectedPacks { + names = append(names, affectedPack.Name) + } + return +} + +func getDefsByPackNameFromOvalDB(family, osRelease string, + packs models.Packages) (relatedDefs []ovalmodels.Definition, err error) { + + ovallog.Initialize(config.Conf.LogDir) + path := config.Conf.OvalDBURL + if config.Conf.OvalDBType == "sqlite3" { + path = config.Conf.OvalDBPath + } + util.Log.Debugf("Open oval-dictionary db (%s): %s", config.Conf.OvalDBType, path) + + var ovaldb db.DB + if ovaldb, err = db.NewDB( + family, + config.Conf.OvalDBType, + path, + config.Conf.DebugSQL, + ); err != nil { + return + } + defer ovaldb.CloseDB() + for _, pack := range packs { + definitions, err := ovaldb.GetByPackName(osRelease, pack.Name) + if err != nil { + return nil, fmt.Errorf("Failed to get %s OVAL info by package name: %v", family, err) + } + for _, def := range definitions { + for _, p := range def.AffectedPacks { + if pack.Name != p.Name { + continue + } + if less, err := lessThan(family, pack, p); err != nil { + if !p.NotFixedYet { + util.Log.Debugf("Failed to parse versions: %s", err) + util.Log.Debugf("%#v\n%#v", pack, p) + } + } else if less { + relatedDefs = append(relatedDefs, def) + } + } + } + } + return +} + +func lessThan(family string, packA models.Package, packB ovalmodels.Package) (bool, error) { + switch family { + case config.Debian, config.Ubuntu: + vera, err := debver.NewVersion(packA.Version) + if err != nil { + return false, err + } + verb, err := debver.NewVersion(packB.Version) + if err != nil { + return false, err + } + return vera.LessThan(verb), nil + case config.RedHat, config.CentOS, config.Oracle: + vera := rpmver.NewVersion(fmt.Sprintf("%s-%s", packA.Version, packA.Release)) + verb := rpmver.NewVersion(packB.Version) + return vera.LessThan(verb), nil + } + return false, fmt.Errorf("Package version comparison not supported: %s", family) +} From ee20cb59a5e1fbc91554084cf2806c1561f51131 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Sun, 13 Aug 2017 17:56:12 +0900 Subject: [PATCH 086/113] Refactoring --- oval/debian.go | 84 ++++++++++++++++---------------------------------- oval/redhat.go | 30 +++++++----------- 2 files changed, 38 insertions(+), 76 deletions(-) diff --git a/oval/debian.go b/oval/debian.go index e7e05523..cd07d0ac 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -28,15 +28,37 @@ type DebianBase struct { Base } -// fillFromOvalDB returns scan result after updating CVE info by OVAL -func (o DebianBase) fillFromOvalDB(r *models.ScanResult) error { - defs, err := getDefsByPackNameFromOvalDB(o.family, r.Release, r.Packages) - if err != nil { - return err +// FillWithOval returns scan result after updating CVE info by OVAL +func (o DebianBase) FillWithOval(r *models.ScanResult) (err error) { + var defs []ovalmodels.Definition + if o.isFetchViaHTTP() { + if defs, err = getDefsByPackNameViaHTTP(r); err != nil { + return err + } + } else { + if defs, err = getDefsByPackNameFromOvalDB(o.family, r.Release, r.Packages); err != nil { + return err + } } + for _, def := range defs { o.update(r, &def) } + + for _, vuln := range r.ScannedCves { + switch models.NewCveContentType(o.family) { + case models.Debian: + if cont, ok := vuln.CveContents[models.Debian]; ok { + cont.SourceLink = "https://security-tracker.debian.org/tracker/" + cont.CveID + vuln.CveContents[models.Debian] = cont + } + case models.Ubuntu: + 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 } @@ -105,32 +127,6 @@ func NewDebian() Debian { } } -// FillWithOval returns scan result after updating CVE info by OVAL -func (o Debian) FillWithOval(r *models.ScanResult) error { - if o.isFetchViaHTTP() { - defs, err := getDefsByPackNameViaHTTP(r) - if err != nil { - return err - } - for _, def := range defs { - o.update(r, &def) - } - } else { - if err := o.fillFromOvalDB(r); err != nil { - return err - } - } - - // TODO merge to VulnInfo.VendorLinks - 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 @@ -146,29 +142,3 @@ func NewUbuntu() Ubuntu { }, } } - -// FillWithOval returns scan result after updating CVE info by OVAL -func (o Ubuntu) FillWithOval(r *models.ScanResult) error { - if o.isFetchViaHTTP() { - defs, err := getDefsByPackNameViaHTTP(r) - if err != nil { - return err - } - for _, def := range defs { - o.update(r, &def) - } - } else { - if err := o.fillFromOvalDB(r); err != nil { - return err - } - } - - // TODO merge to VulnInfo.VendorLinks - 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 2541c3cb..46e8153c 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -34,49 +34,41 @@ type RedHatBase struct { } // FillWithOval returns scan result after updating CVE info by OVAL -func (o RedHatBase) FillWithOval(r *models.ScanResult) error { +func (o RedHatBase) FillWithOval(r *models.ScanResult) (err error) { + var defs []ovalmodels.Definition if o.isFetchViaHTTP() { - defs, err := getDefsByPackNameViaHTTP(r) - if err != nil { + if defs, err = getDefsByPackNameViaHTTP(r); err != nil { return err } - for _, def := range defs { - o.update(r, &def) - } } else { - if err := o.fillFromOvalDB(r); err != nil { + if defs, err = getDefsByPackNameFromOvalDB( + o.family, r.Release, r.Packages); err != nil { return err } } + for _, def := range defs { + o.update(r, &def) + } + // TODO merge to VulnInfo.VendorLinks for _, vuln := range r.ScannedCves { switch models.NewCveContentType(o.family) { case models.RedHat: if cont, ok := vuln.CveContents[models.RedHat]; ok { cont.SourceLink = "https://access.redhat.com/security/cve/" + cont.CveID + vuln.CveContents[models.RedHat] = cont } case models.Oracle: if cont, ok := vuln.CveContents[models.Oracle]; ok { cont.SourceLink = fmt.Sprintf("https://linux.oracle.com/cve/%s.html", cont.CveID) + vuln.CveContents[models.Oracle] = cont } } } return nil } -// fillFromOvalDB returns scan result after updating CVE info by OVAL -func (o RedHatBase) fillFromOvalDB(r *models.ScanResult) error { - defs, err := getDefsByPackNameFromOvalDB(o.family, r.Release, r.Packages) - if err != nil { - return err - } - for _, def := range defs { - o.update(r, &def) - } - return nil -} - func (o RedHatBase) update(r *models.ScanResult, definition *ovalmodels.Definition) { ctype := models.NewCveContentType(o.family) for _, cve := range definition.Advisory.Cves { From c66898e6089aa5927b73c09a0f6930c6cdc321a6 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Sun, 13 Aug 2017 20:50:26 +0900 Subject: [PATCH 087/113] Set actually affected package's name only to vulnInfo.PackageNames --- oval/debian.go | 43 ++++++++++++-------- oval/debian_test.go | 75 ++++++++++++++++++++++++++++++++++ oval/redhat.go | 34 +++++++++------- oval/redhat_test.go | 62 +++++++++++++++++++++++++++- oval/util.go | 51 ++++++++++++++++------- oval/util_test.go | 98 +++++++++++++++++++++++++++++++++++++++++++++ report/localfile.go | 10 ++++- report/util.go | 1 - 8 files changed, 326 insertions(+), 48 deletions(-) create mode 100644 oval/debian_test.go create mode 100644 oval/util_test.go diff --git a/oval/debian.go b/oval/debian.go index cd07d0ac..5137467b 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -14,9 +14,12 @@ 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 oval import ( + "sort" + "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/util" @@ -30,19 +33,19 @@ type DebianBase struct { // FillWithOval returns scan result after updating CVE info by OVAL func (o DebianBase) FillWithOval(r *models.ScanResult) (err error) { - var defs []ovalmodels.Definition + var relatedDefs ovalResult if o.isFetchViaHTTP() { - if defs, err = getDefsByPackNameViaHTTP(r); err != nil { + if relatedDefs, err = getDefsByPackNameViaHTTP(r); err != nil { return err } } else { - if defs, err = getDefsByPackNameFromOvalDB(o.family, r.Release, r.Packages); err != nil { + if relatedDefs, err = getDefsByPackNameFromOvalDB(o.family, r.Release, r.Packages); err != nil { return err } } - for _, def := range defs { - o.update(r, &def) + for _, defPacks := range relatedDefs.entries { + o.update(r, defPacks) } for _, vuln := range r.ScannedCves { @@ -62,25 +65,26 @@ func (o DebianBase) FillWithOval(r *models.ScanResult) (err error) { return nil } -func (o DebianBase) update(r *models.ScanResult, definition *ovalmodels.Definition) { - ovalContent := *o.convertToModel(definition) +func (o DebianBase) update(r *models.ScanResult, defPacks defPacks) { + ovalContent := *o.convertToModel(&defPacks.def) ovalContent.Type = models.NewCveContentType(o.family) - vinfo, ok := r.ScannedCves[definition.Debian.CveID] + vinfo, ok := r.ScannedCves[defPacks.def.Debian.CveID] if !ok { - util.Log.Debugf("%s is newly detected by OVAL", definition.Debian.CveID) + util.Log.Debugf("%s is newly detected by OVAL", defPacks.def.Debian.CveID) vinfo = models.VulnInfo{ - CveID: definition.Debian.CveID, - Confidence: models.OvalMatch, - PackageNames: getPackages(r, definition), - CveContents: models.NewCveContents(ovalContent), + CveID: defPacks.def.Debian.CveID, + Confidence: models.OvalMatch, + CveContents: models.NewCveContents(ovalContent), } } else { cveContents := vinfo.CveContents ctype := models.NewCveContentType(o.family) if _, ok := vinfo.CveContents[ctype]; ok { - util.Log.Debugf("%s will be updated by OVAL", definition.Debian.CveID) + util.Log.Debugf("%s OVAL will be overwritten", + defPacks.def.Debian.CveID) } else { - util.Log.Debugf("%s is also detected by OVAL", definition.Debian.CveID) + util.Log.Debugf("%s is also detected by OVAL", + defPacks.def.Debian.CveID) cveContents = models.CveContents{} } if vinfo.Confidence.Score < models.OvalMatch.Score { @@ -89,7 +93,14 @@ func (o DebianBase) update(r *models.ScanResult, definition *ovalmodels.Definiti cveContents[ctype] = ovalContent vinfo.CveContents = cveContents } - r.ScannedCves[definition.Debian.CveID] = vinfo + + // uniq(vinfo.PackNames + defPacks.actuallyAffectedPackNames) + for _, name := range vinfo.PackageNames { + defPacks.actuallyAffectedPackNames[name] = true + } + vinfo.PackageNames = defPacks.packNames() + sort.Strings(vinfo.PackageNames) + r.ScannedCves[defPacks.def.Debian.CveID] = vinfo } func (o DebianBase) convertToModel(def *ovalmodels.Definition) *models.CveContent { diff --git a/oval/debian_test.go b/oval/debian_test.go new file mode 100644 index 00000000..d3630acc --- /dev/null +++ b/oval/debian_test.go @@ -0,0 +1,75 @@ +/* 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 oval + +import ( + "reflect" + "testing" + + "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/models" + "github.com/future-architect/vuls/util" + ovalmodels "github.com/kotakanbe/goval-dictionary/models" +) + +func TestPackNamesOfUpdateDebian(t *testing.T) { + var tests = []struct { + in models.ScanResult + defPacks defPacks + out models.ScanResult + }{ + { + in: models.ScanResult{ + ScannedCves: models.VulnInfos{ + "CVE-2000-1000": models.VulnInfo{ + PackageNames: []string{"packA"}, + }, + }, + }, + defPacks: defPacks{ + def: ovalmodels.Definition{ + Debian: ovalmodels.Debian{ + CveID: "CVE-2000-1000", + }, + }, + actuallyAffectedPackNames: map[string]bool{ + "packB": true, + }, + }, + out: models.ScanResult{ + ScannedCves: models.VulnInfos{ + "CVE-2000-1000": models.VulnInfo{ + PackageNames: []string{ + "packA", + "packB", + }, + }, + }, + }, + }, + } + + util.Log = util.NewCustomLogger(config.ServerInfo{}) + for i, tt := range tests { + Debian{}.update(&tt.in, tt.defPacks) + e := tt.out.ScannedCves["CVE-2000-1000"].PackageNames + a := tt.in.ScannedCves["CVE-2000-1000"].PackageNames + if !reflect.DeepEqual(a, e) { + t.Errorf("[%d] expected: %v\n actual: %v\n", i, e, a) + } + } +} diff --git a/oval/redhat.go b/oval/redhat.go index 46e8153c..6927db39 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -19,6 +19,7 @@ package oval import ( "fmt" + "sort" "strconv" "strings" @@ -35,23 +36,22 @@ type RedHatBase struct { // FillWithOval returns scan result after updating CVE info by OVAL func (o RedHatBase) FillWithOval(r *models.ScanResult) (err error) { - var defs []ovalmodels.Definition + var relatedDefs ovalResult if o.isFetchViaHTTP() { - if defs, err = getDefsByPackNameViaHTTP(r); err != nil { + if relatedDefs, err = getDefsByPackNameViaHTTP(r); err != nil { return err } } else { - if defs, err = getDefsByPackNameFromOvalDB( + if relatedDefs, err = getDefsByPackNameFromOvalDB( o.family, r.Release, r.Packages); err != nil { return err } } - for _, def := range defs { - o.update(r, &def) + for _, defPacks := range relatedDefs.entries { + o.update(r, defPacks) } - // TODO merge to VulnInfo.VendorLinks for _, vuln := range r.ScannedCves { switch models.NewCveContentType(o.family) { case models.RedHat: @@ -69,23 +69,22 @@ func (o RedHatBase) FillWithOval(r *models.ScanResult) (err error) { return nil } -func (o RedHatBase) update(r *models.ScanResult, definition *ovalmodels.Definition) { +func (o RedHatBase) update(r *models.ScanResult, defPacks defPacks) { ctype := models.NewCveContentType(o.family) - for _, cve := range definition.Advisory.Cves { - ovalContent := *o.convertToModel(cve.CveID, definition) + for _, cve := range defPacks.def.Advisory.Cves { + ovalContent := *o.convertToModel(cve.CveID, &defPacks.def) vinfo, ok := r.ScannedCves[cve.CveID] if !ok { util.Log.Debugf("%s is newly detected by OVAL", cve.CveID) vinfo = models.VulnInfo{ - CveID: cve.CveID, - Confidence: models.OvalMatch, - PackageNames: getPackages(r, definition), - CveContents: models.NewCveContents(ovalContent), + CveID: cve.CveID, + Confidence: models.OvalMatch, + CveContents: models.NewCveContents(ovalContent), } } else { cveContents := vinfo.CveContents if _, ok := vinfo.CveContents[ctype]; ok { - util.Log.Debugf("%s will be updated by OVAL", cve.CveID) + util.Log.Debugf("%s OVAL will be overwritten", cve.CveID) } else { util.Log.Debugf("%s also detected by OVAL", cve.CveID) cveContents = models.CveContents{} @@ -97,6 +96,13 @@ func (o RedHatBase) update(r *models.ScanResult, definition *ovalmodels.Definiti cveContents[ctype] = ovalContent vinfo.CveContents = cveContents } + + // uniq(vinfo.PackNames + defPacks.actuallyAffectedPackNames) + for _, name := range vinfo.PackageNames { + defPacks.actuallyAffectedPackNames[name] = true + } + vinfo.PackageNames = defPacks.packNames() + sort.Strings(vinfo.PackageNames) r.ScannedCves[cve.CveID] = vinfo } } diff --git a/oval/redhat_test.go b/oval/redhat_test.go index 79b90eab..a69bf838 100644 --- a/oval/redhat_test.go +++ b/oval/redhat_test.go @@ -16,7 +16,15 @@ along with this program. If not, see . */ package oval -import "testing" +import ( + "reflect" + "testing" + + "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/models" + "github.com/future-architect/vuls/util" + ovalmodels "github.com/kotakanbe/goval-dictionary/models" +) func TestParseCvss2(t *testing.T) { type out struct { @@ -83,3 +91,55 @@ func TestParseCvss3(t *testing.T) { } } } + +func TestPackNamesOfUpdate(t *testing.T) { + var tests = []struct { + in models.ScanResult + defPacks defPacks + out models.ScanResult + }{ + { + in: models.ScanResult{ + ScannedCves: models.VulnInfos{ + "CVE-2000-1000": models.VulnInfo{ + PackageNames: []string{"packA"}, + }, + }, + }, + defPacks: defPacks{ + def: ovalmodels.Definition{ + Advisory: ovalmodels.Advisory{ + Cves: []ovalmodels.Cve{ + { + CveID: "CVE-2000-1000", + }, + }, + }, + }, + actuallyAffectedPackNames: map[string]bool{ + "packB": true, + }, + }, + out: models.ScanResult{ + ScannedCves: models.VulnInfos{ + "CVE-2000-1000": models.VulnInfo{ + PackageNames: []string{ + "packA", + "packB", + }, + }, + }, + }, + }, + } + + util.Log = util.NewCustomLogger(config.ServerInfo{}) + for i, tt := range tests { + RedHat{}.update(&tt.in, tt.defPacks) + e := tt.out.ScannedCves["CVE-2000-1000"].PackageNames + a := tt.in.ScannedCves["CVE-2000-1000"].PackageNames + if !reflect.DeepEqual(a, e) { + t.Errorf("[%d] expected: %v\n actual: %v\n", i, e, a) + } + } +} diff --git a/oval/util.go b/oval/util.go index c84cade1..ffdce94e 100644 --- a/oval/util.go +++ b/oval/util.go @@ -35,6 +35,36 @@ import ( "github.com/parnurzeal/gorequest" ) +type ovalResult struct { + entries []defPacks +} + +type defPacks struct { + def ovalmodels.Definition + actuallyAffectedPackNames map[string]bool +} + +func (e defPacks) packNames() (names []string) { + for k := range e.actuallyAffectedPackNames { + names = append(names, k) + } + return +} + +func (e *ovalResult) upsert(def ovalmodels.Definition, packName string) (upserted bool) { + for i, entry := range e.entries { + if entry.def.DefinitionID == def.DefinitionID { + e.entries[i].actuallyAffectedPackNames[packName] = true + return true + } + } + e.entries = append(e.entries, defPacks{ + def: def, + actuallyAffectedPackNames: map[string]bool{packName: true}, + }) + return false +} + type request struct { pack models.Package } @@ -46,7 +76,7 @@ type response struct { // getDefsByPackNameViaHTTP fetches OVAL information via HTTP func getDefsByPackNameViaHTTP(r *models.ScanResult) ( - relatedDefs []ovalmodels.Definition, err error) { + relatedDefs ovalResult, err error) { reqChan := make(chan request, len(r.Packages)) resChan := make(chan response, len(r.Packages)) @@ -102,18 +132,18 @@ func getDefsByPackNameViaHTTP(r *models.ScanResult) ( util.Log.Debugf("%#v\n%#v", *res.pack, p) } } else if less { - relatedDefs = append(relatedDefs, def) + relatedDefs.upsert(def, p.Name) } } } case err := <-errChan: errs = append(errs, err) case <-timeout: - return nil, fmt.Errorf("Timeout Fetching OVAL") + return relatedDefs, fmt.Errorf("Timeout Fetching OVAL") } } if len(errs) != 0 { - return nil, fmt.Errorf("Failed to fetch OVAL. err: %v", errs) + return relatedDefs, fmt.Errorf("Failed to fetch OVAL. err: %v", errs) } return } @@ -161,15 +191,8 @@ func httpGet(url string, pack *models.Package, resChan chan<- response, errChan } } -func getPackages(r *models.ScanResult, d *ovalmodels.Definition) (names []string) { - for _, affectedPack := range d.AffectedPacks { - names = append(names, affectedPack.Name) - } - return -} - func getDefsByPackNameFromOvalDB(family, osRelease string, - packs models.Packages) (relatedDefs []ovalmodels.Definition, err error) { + packs models.Packages) (relatedDefs ovalResult, err error) { ovallog.Initialize(config.Conf.LogDir) path := config.Conf.OvalDBURL @@ -191,7 +214,7 @@ func getDefsByPackNameFromOvalDB(family, osRelease string, for _, pack := range packs { definitions, err := ovaldb.GetByPackName(osRelease, pack.Name) if err != nil { - return nil, fmt.Errorf("Failed to get %s OVAL info by package name: %v", family, err) + return relatedDefs, fmt.Errorf("Failed to get %s OVAL info by package name: %v", family, err) } for _, def := range definitions { for _, p := range def.AffectedPacks { @@ -204,7 +227,7 @@ func getDefsByPackNameFromOvalDB(family, osRelease string, util.Log.Debugf("%#v\n%#v", pack, p) } } else if less { - relatedDefs = append(relatedDefs, def) + relatedDefs.upsert(def, pack.Name) } } } diff --git a/oval/util_test.go b/oval/util_test.go new file mode 100644 index 00000000..48ef395b --- /dev/null +++ b/oval/util_test.go @@ -0,0 +1,98 @@ +package oval + +import ( + "reflect" + "testing" + + ovalmodels "github.com/kotakanbe/goval-dictionary/models" +) + +func TestUpsert(t *testing.T) { + var tests = []struct { + res ovalResult + def ovalmodels.Definition + packName string + upserted bool + out ovalResult + }{ + //insert + { + res: ovalResult{}, + def: ovalmodels.Definition{ + DefinitionID: "1111", + }, + packName: "pack1", + upserted: false, + out: ovalResult{ + []defPacks{ + { + def: ovalmodels.Definition{ + DefinitionID: "1111", + }, + actuallyAffectedPackNames: map[string]bool{ + "pack1": true, + }, + }, + }, + }, + }, + //update + { + res: ovalResult{ + []defPacks{ + { + def: ovalmodels.Definition{ + DefinitionID: "1111", + }, + actuallyAffectedPackNames: map[string]bool{ + "pack1": true, + }, + }, + { + def: ovalmodels.Definition{ + DefinitionID: "2222", + }, + actuallyAffectedPackNames: map[string]bool{ + "pack3": true, + }, + }, + }, + }, + def: ovalmodels.Definition{ + DefinitionID: "1111", + }, + packName: "pack2", + upserted: true, + out: ovalResult{ + []defPacks{ + { + def: ovalmodels.Definition{ + DefinitionID: "1111", + }, + actuallyAffectedPackNames: map[string]bool{ + "pack1": true, + "pack2": true, + }, + }, + { + def: ovalmodels.Definition{ + DefinitionID: "2222", + }, + actuallyAffectedPackNames: map[string]bool{ + "pack3": true, + }, + }, + }, + }, + }, + } + for i, tt := range tests { + upserted := tt.res.upsert(tt.def, tt.packName) + if tt.upserted != upserted { + t.Errorf("[%d]\nexpected: %t\n actual: %t\n", i, tt.upserted, upserted) + } + if !reflect.DeepEqual(tt.out, tt.res) { + t.Errorf("[%d]\nexpected: %v\n actual: %v\n", i, tt.out, tt.res) + } + } +} diff --git a/report/localfile.go b/report/localfile.go index 79f192a1..13e9b98f 100644 --- a/report/localfile.go +++ b/report/localfile.go @@ -58,8 +58,14 @@ func (w LocalFileWriter) Write(rs ...models.ScanResult) (err error) { } var b []byte - if b, err = json.Marshal(r); err != nil { - return fmt.Errorf("Failed to Marshal to JSON: %s", err) + if c.Conf.Debug { + if b, err = json.MarshalIndent(r, "", " "); err != nil { + return fmt.Errorf("Failed to Marshal to JSON: %s", err) + } + } else { + if b, err = json.Marshal(r); err != nil { + return fmt.Errorf("Failed to Marshal to JSON: %s", err) + } } if err := writeFile(p, b, 0600); err != nil { return fmt.Errorf("Failed to write JSON. path: %s, err: %s", p, err) diff --git a/report/util.go b/report/util.go index f9085377..5cbf43c5 100644 --- a/report/util.go +++ b/report/util.go @@ -218,7 +218,6 @@ No CVE-IDs are found in updatable packages. packsVer := []string{} sort.Strings(vuln.PackageNames) 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()) } From dbceca878043bffb976bd8c3ebf096486026a624 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Sun, 13 Aug 2017 21:51:43 +0900 Subject: [PATCH 088/113] Update Gopkg.lock --- Gopkg.lock | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index d38725b4..06d4b320 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -4,14 +4,14 @@ [[projects]] name = "github.com/Azure/azure-sdk-for-go" packages = ["storage"] - revision = "5b6066bbd213e47c49a5fa2be2b29529bbcb6704" - version = "v10.1.1-beta" + revision = "2d49bb8f2cee530cc16f1f1a9f0aae763dee257d" + version = "v10.2.1-beta" [[projects]] name = "github.com/Azure/go-autorest" packages = ["autorest","autorest/adal","autorest/azure","autorest/date"] - revision = "10791a4516e77c53ab10f198f144804cca3d5b43" - version = "v8.1.0" + revision = "f6e08fe5e4d45c9a66e40196d3fed5f37331d224" + version = "v8.1.1" [[projects]] name = "github.com/BurntSushi/toml" @@ -28,8 +28,8 @@ [[projects]] name = "github.com/aws/aws-sdk-go" packages = ["aws","aws/awserr","aws/awsutil","aws/client","aws/client/metadata","aws/corehandlers","aws/credentials","aws/credentials/ec2rolecreds","aws/credentials/endpointcreds","aws/credentials/stscreds","aws/defaults","aws/ec2metadata","aws/endpoints","aws/request","aws/session","aws/signer/v4","internal/shareddefaults","private/protocol","private/protocol/query","private/protocol/query/queryutil","private/protocol/rest","private/protocol/restxml","private/protocol/xml/xmlutil","service/s3","service/sts"] - revision = "6ff7be1a127941b8dc9a408ac9c2dd6eb2167a5d" - version = "v1.10.15" + revision = "19490d4d23f5f6dd8061abf91542af8433d28d63" + version = "v1.10.24" [[projects]] name = "github.com/boltdb/bolt" @@ -58,14 +58,14 @@ [[projects]] name = "github.com/go-ini/ini" packages = ["."] - revision = "d3de07a94d22b4a0972deb4b96d790c2c0ce8333" - version = "v1.28.0" + revision = "20b96f641a5ea98f2f8619ff4f3e061cff4833bd" + version = "v1.28.2" [[projects]] name = "github.com/go-redis/redis" packages = [".","internal","internal/consistenthash","internal/hashtag","internal/pool","internal/proto"] - revision = "a005081ecd2d0d963a1a20efda049223205cf90a" - version = "v6.5.4" + revision = "89515eebd1b5761486abd32d9e84a8f55dda7740" + version = "v6.5.6" [[projects]] name = "github.com/go-sql-driver/mysql" @@ -149,13 +149,13 @@ branch = "master" name = "github.com/kotakanbe/goval-dictionary" packages = ["config","db","db/rdb","log","models"] - revision = "766b881c46d2037c75833ec0021da1c3da1ad2a1" + revision = "597ee7aff9dcf36eb8c254d8b1ba8704ade521a6" [[projects]] branch = "master" name = "github.com/kotakanbe/logrus-prefixed-formatter" packages = ["."] - revision = "5ea278a9a3f980f7cdf1dc787dc4a85ac72502d9" + revision = "75edb2e85a38873f0318be05a458446681d1022f" [[projects]] name = "github.com/labstack/gommon" @@ -167,13 +167,13 @@ branch = "master" name = "github.com/lib/pq" packages = [".","hstore","oid"] - revision = "dd1fe2071026ce53f36a39112e645b4d4f5793a4" + revision = "e42267488fe361b9dc034be7a6bffef5b195bceb" [[projects]] name = "github.com/mattn/go-colorable" packages = ["."] - revision = "941b50ebc6efddf4c41c8e4537a5f68a4e686b24" - version = "v0.0.8" + revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" + version = "v0.0.9" [[projects]] name = "github.com/mattn/go-isatty" @@ -239,7 +239,7 @@ branch = "master" name = "github.com/sirupsen/logrus" packages = ["."] - revision = "86bd21e371d71c8885b29e8dfb161c6034dc4abe" + revision = "181d419aa9e2223811b824e8f0b4af96f9ba9302" [[projects]] branch = "master" @@ -257,35 +257,35 @@ branch = "master" name = "github.com/ymomoi/goval-parser" packages = ["oval"] - revision = "003ac9af5fffac6c97ab1def025d2cb73e88469a" + revision = "0a0be1dd9d0855b50be0be5a10ad3085382b6d59" [[projects]] branch = "master" name = "golang.org/x/crypto" packages = ["curve25519","ed25519","ed25519/internal/edwards25519","ssh","ssh/agent","ssh/terminal"] - revision = "6914964337150723782436d56b3f21610a74ce7b" + revision = "b176d7def5d71bdd214203491f89843ed217f420" [[projects]] branch = "master" name = "golang.org/x/net" packages = ["context","idna","publicsuffix"] - revision = "ab5485076ff3407ad2d02db054635913f017b0ed" + revision = "1c05540f6879653db88113bc4a2b70aec4bd491f" [[projects]] branch = "master" name = "golang.org/x/sys" - packages = ["unix"] - revision = "c4489faa6e5ab84c0ef40d6ee878f7a030281f0f" + packages = ["unix","windows"] + revision = "e42485b6e20ae7d2304ec72e535b103ed350cc02" [[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 = "836efe42bb4aa16aaa17b9c155d8813d336ed720" + revision = "b19bf474d317b857955b12035d2c5acb57ce8b01" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "269ff02f8e4540ba049a340068dac0ff4f0495df9f8eeb21d4a545ea5dedf2dd" + inputs-digest = "3590a66561bd65dbedefd34aa043c662111483be0bc1b68df264de0c53346552" solver-name = "gps-cdcl" solver-version = 1 From 47a444e79514278ff07fa5b152fb15a19daf7b5e Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Sun, 13 Aug 2017 22:17:25 +0900 Subject: [PATCH 089/113] Use CVE>Impact as severity when it is not empty (RedHat OVAL) --- Gopkg.lock | 2 +- oval/redhat.go | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 06d4b320..46be657e 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -149,7 +149,7 @@ branch = "master" name = "github.com/kotakanbe/goval-dictionary" packages = ["config","db","db/rdb","log","models"] - revision = "597ee7aff9dcf36eb8c254d8b1ba8704ade521a6" + revision = "aa1dbe07a21bd51943893086d37e9e57c6020ce0" [[projects]] branch = "master" diff --git a/oval/redhat.go b/oval/redhat.go index 6927db39..c48f42b3 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -124,12 +124,17 @@ func (o RedHatBase) convertToModel(cveID string, def *ovalmodels.Definition) *mo score2, vec2 := o.parseCvss2(cve.Cvss2) score3, vec3 := o.parseCvss3(cve.Cvss3) + severity := def.Advisory.Severity + if cve.Impact != "" { + severity = cve.Impact + } + return &models.CveContent{ Type: models.NewCveContentType(o.family), CveID: cve.CveID, Title: def.Title, Summary: def.Description, - Severity: def.Advisory.Severity, + Severity: severity, Cvss2Score: score2, Cvss2Vector: vec2, Cvss3Score: score3, From 999d8f5866ea67f50e8ee62acca90bc30af2b5e8 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Sun, 13 Aug 2017 23:51:19 +0900 Subject: [PATCH 090/113] Update README --- README.ja.md | 57 +++-- README.md | 69 +++--- img/vuls-scan-flow-fast.graphml | 415 ++++++++++++++++++++++++++++++++ img/vuls-scan-flow-fast.png | Bin 0 -> 76142 bytes img/vuls-scan-flow.graphml | 215 +++++++++++------ img/vuls-scan-flow.png | Bin 74102 -> 87167 bytes 6 files changed, 638 insertions(+), 118 deletions(-) create mode 100644 img/vuls-scan-flow-fast.graphml create mode 100644 img/vuls-scan-flow-fast.png diff --git a/README.ja.md b/README.ja.md index ca1c4c44..c4e82e47 100644 --- a/README.ja.md +++ b/README.ja.md @@ -470,16 +470,38 @@ Vulsをスキャン対象サーバにデプロイする。Vulsはローカルホ - NVDとJVN(日本語)から脆弱性データベースを取得し、SQLite3に格納する。 ## Vuls +### Fast Scan +![Vuls-Scan-Flow](img/vuls-scan-flow-fast.png) +- Root権限不要でスキャン可能なモード(Raspbian以外) +- OVALが提供されているディストリビューションは、スキャン時はパッケージのバージョンを取得するのみ。レポート時にOVAL DBとバージョン比較により脆弱性を検知する +- OVALが提供されいていないディストリビューションはスキャン時にコマンドを発行して脆弱性を検知する + +| Distribution| Scan Speed | Root Privilege | OVAL | +|:------------|:-------------------|:---------------|:-----| +| CentOS | 速い |  不要 | 有 | +| Amazon | 速い |  不要 | 無 | +| RHEL | 速い |  不要 | 有 | +| Oracle | 速い |  不要 | 有 | +| FreeBSD | 速い |  不要 | 無 | +| Ubuntu | 速い |  不要 | 有 | +| Debian | 速い |  不要 | 有 | +| Raspbian | 初回は遅い / 2回目以降速い |  必要 | 無 | + +### Deep Scan ![Vuls-Scan-Flow](img/vuls-scan-flow.png) -- SSHでサーバに存在する脆弱性をスキャンし、CVE IDのリストを作成する - - Dockerコンテナのスキャンする場合、VulsはまずDockerホストにSSHで接続する。その後、Dockerホスト上で `docker exec` 経由でコマンドを実効する。Dockerコンテナ内にSSHデーモンを起動する必要はない -- 検出されたCVEの詳細情報をgo-cve-dictionaryから取得する -- スキャン結果レポートを生成し、SlackやEmailなどで送信する -- スキャン結果をJSONファイルに出力すると詳細情報をターミナル上で参照可能 +- Root権限が必要なコマンドも発行し、より深いスキャンを行うモード +- ChangelogをパースしてCVE-IDを検知するのでFastよりも検知漏れが減る - ----- -# Performance Considerations +| Distribution| Scan Speed | Root Privilege | OVAL | +|:------------|:-------------------|:---------------|:-----| +| CentOS | 遅い |  不要 | 有 | +| Amazon | 遅い |  不要 | 無 | +| RHEL | 遅い |  必要 | 有 | +| Oracle | 遅い |  必要 | 有 | +| Ubuntu | 初回は遅い / 2回目以降速い |  必要 | 有 | +| Debian | 初回は遅い / 2回目以降速い |  必要 | 有 | +| Raspbian | 初回は遅い / 2回目以降速い |  必要 | 無 | +| FreeBSD | 速い |  不要 | 無 | - Ubuntu, Debian, Raspbian `apt-get changelog`でアップデート対象のパッケージのチェンジログを取得し、含まれるCVE IDをパースする。 @@ -487,20 +509,10 @@ Vulsをスキャン対象サーバにデプロイする。Vulsはローカルホ ただ、2回目以降はキャッシュしたchangelogを使うので速くなる。 - CentOS -アップデート対象すべてのchangelogを一度で取得しパースする。スキャンスピードは速い、サーバリソース消費量は小さい。 +`yum changelog`でアップデート対象のパッケージのチェンジログを取得し、含まれるCVE IDをパースする。 - Amazon, RHEL and FreeBSD -高速にスキャンし、スキャン対象サーバのリソース消費量は小さい。 - -| Distribution| Scan Speed | -|:------------|:-------------------| -| Ubuntu | 初回は遅い / 2回目以降速い | -| Debian | 初回は遅い / 2回目以降速い | -| CentOS | 速い | -| Amazon | 速い | -| RHEL | 速い | -| FreeBSD | 速い | -| Raspbian | 初回は遅い / 2回目以降速い | +`yum changelog`でアップデート対象のパッケージのチェンジログを取得する(パースはしない)。 ---- @@ -1739,6 +1751,11 @@ kotakanbe ([@kotakanbe](https://twitter.com/kotakanbe)) created vuls and [these Please see [CHANGELOG](https://github.com/future-architect/vuls/blob/master/CHANGELOG.md). ---- +# Stargazers over time + +[![Stargazers over time](https://starcharts.herokuapp.com/future-architect/vuls.svg)](https://starcharts.herokuapp.com/future-architect/vuls) + +----- # License diff --git a/README.md b/README.md index b9f83e00..d1682f41 100644 --- a/README.md +++ b/README.md @@ -479,13 +479,35 @@ On the aggregation server, you can refer to the scanning result of each scan tar ## [go-cve-dictionary](https://github.com/kotakanbe/go-cve-dictionary) - Fetch vulnerability information from NVD and JVN(Japanese), then insert into SQLite3, MySQL, PostgreSQL or Redis. -## Scanning Flow -![Vuls-Scan-Flow](img/vuls-scan-flow.png) -- Scan vulnerabilities on the servers via SSH and collect a list of the CVE ID - - To scan Docker containers, Vuls connects via SSH to the Docker host and then `docker exec` to the containers. So, no need to run sshd daemon on the containers. +## Vuls +### Fast Scan +![Vuls-Scan-Flow](img/vuls-scan-flow-fast.png) +- Scan without Root Privilege ----- -# Performance Considerations +| Distribution| Scan Speed | Root Privilege | OVAL | +|:------------|:-------------------|:---------------|:-----| +| CentOS | Fast |  No | Yes | +| Amazon | Fast |  No | No | +| RHEL | Fast |  No | Yes | +| Oracle | Fast |  No | Yes | +| FreeBSD | Fast |  No | No | +| Ubuntu | Fast |  No | Yes | +| Debian | Fast |  No | Yes | +| Raspbian |First time: Slow / From the second time: Fast|  Yes | No | + +### Deep Scan +![Vuls-Scan-Flow](img/vuls-scan-flow.png) + +| Distribution| Scan Speed | Root Privilege | OVAL | +|:------------|:-------------------|:---------------|:-----| +| CentOS | Slow |  No | Yes| +| Amazon | Slow |  No | No| +| RHEL | Slow |  Yes| Yes| +| Oracle | Slow |  Yes| Yes| +| Ubuntu |First time: Slow / From the second time: Fast|  Yes| Yes| +| Debian |First time: Slow / From the second time: Fast|  Yes| Yes| +| Raspbian |First time: Slow / From the second time: Fast|  Yes| No | +| FreeBSD | Fast |  No | No| - On Ubuntu, Debian and Raspbian Vuls issues `apt-get changelog` for each upgradable packages and parse the changelog. @@ -493,23 +515,10 @@ Vuls issues `apt-get changelog` for each upgradable packages and parse the chang Vuls stores these changelogs to KVS([boltdb](https://github.com/boltdb/bolt)). From the second time on, the scan speed is fast by using the local cache. -- On CentOS -Vuls issues `yum update --changelog` to get changelogs of upgradable packages at once and parse the changelog. -Scan speed is fast and resource usage is light. - -- On Amazon, RHEL and FreeBSD -High speed scan and resource usage is light because Vuls can get CVE IDs by using package manager(no need to parse a changelog). - -| Distribution | Scan Speed | -|:-------------|:-------------------| -| Ubuntu | First time: Slow / From the second time: Fast | -| Debian | First time: Slow / From the second time: Fast | -| CentOS | Fast | -| Amazon | Fast | -| RHEL | Fast | -| Oracle Linux | Fast | -| FreeBSD | Fast | -| Raspbian | First time: Slow / From the second time: Fast | +- On CentOS +Vuls issues `yum changelog` to get changelogs of upgradable packages at once and parse the changelog. +- On RHEL, Oracle, Amazon and FreeBSD +Detect CVE IDs by using package manager. ---- @@ -1289,7 +1298,6 @@ $ vuls report \ -format-json \ -aws-region=ap-northeast-1 \ -aws-s3-bucket=vuls \ - -aws-s3-results-dir=/bucket/path/to/results \ -aws-profile=default ``` With this sample command, it will .. @@ -1553,6 +1561,8 @@ $ vuls history | peco | vuls tui -pipe [![asciicast](https://asciinema.org/a/emi7y7docxr60bq080z10t7v8.png)](https://asciinema.org/a/emi7y7docxr60bq080z10t7v8) +---- + # Usage: go-cve-dictionary on different server Run go-cve-dictionary as server mode before scanning on 192.168.10.1 @@ -1570,6 +1580,8 @@ $ vuls report -cvedb-url=http://192.168.0.1:1323 see [go-cve-dictionary#usage-fetch-nvd-data](https://github.com/kotakanbe/go-cve-dictionary#usage-fetch-nvd-data) +---- + # Usage: goval-dictionary on different server ``` @@ -1699,12 +1711,11 @@ kotakanbe ([@kotakanbe](https://twitter.com/kotakanbe)) created vuls and [these Please see [CHANGELOG](https://github.com/future-architect/vuls/blob/master/CHANGELOG.md). ---- +# Stargazers over time + +[![Stargazers over time](https://starcharts.herokuapp.com/future-architect/vuls.svg)](https://starcharts.herokuapp.com/future-architect/vuls) -# Stargazers over time - -[![Stargazers over time](https://starcharts.herokuapp.com/future-architect/vuls.svg)](https://starcharts.herokuapp.com/future-architect/vuls) - ----- +----- # License diff --git a/img/vuls-scan-flow-fast.graphml b/img/vuls-scan-flow-fast.graphml new file mode 100644 index 00000000..ded5e2f4 --- /dev/null +++ b/img/vuls-scan-flow-fast.graphml @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + Detect the OS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Get installed packages +Debian/Ubuntu: dpkg-query +Amazon/RHEL/CentOS: rpm +FreeBSD: pkg + + + + + + + + + + + + + + + + Write results to JSON files + + + + + + + + + + + + + + + + Get CVE IDs by using package manager +Amazon: yum plugin security +FreeBSD: pkg audit + + + + + + + + + + + + + + + + Report + + + + + + + + + + + + + + + + + + + Vulnerability Database + + + + + + + + + + Folder 1 + + + + + + + + + + + + + + + + CVE DB (NVD / JVN) + + + + + + + + + + + + + + + + OVAL DB + + + + + + + + + + + + + + + + + + Check upgradable packages +Debian/Ubuntu: apt-get upgrade --dry-run + + + + + + + + + + + + + + + + foreach +upgradable packages + + + + + + + + + + + + + + + + Parse changelog and get CVE IDs +Debian/Ubuntu: aptitude changelog + + + + + + + + + + + + + + + + end loop + + + + + + + + + + + + + + + + + + + + + + + + + + + + Amazon +FreeBSD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CentOS +RHEL +Ubuntu +Debian +Oracle Linux + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Raspbian + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/vuls-scan-flow-fast.png b/img/vuls-scan-flow-fast.png new file mode 100644 index 0000000000000000000000000000000000000000..24710d69ef6bc274a0bd5ffa324fea0d9f84a04e GIT binary patch literal 76142 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 literal 0 HcmV?d00001 diff --git a/img/vuls-scan-flow.graphml b/img/vuls-scan-flow.graphml index 1409090b..39a67280 100644 --- a/img/vuls-scan-flow.graphml +++ b/img/vuls-scan-flow.graphml @@ -1,6 +1,6 @@ - + @@ -20,7 +20,7 @@ - Detect the OS + Detect the OS @@ -36,7 +36,7 @@ - + @@ -53,7 +53,7 @@ - Get installed packages + Get installed packages Debian/Ubuntu: dpkg-query Amazon/RHEL/CentOS: rpm FreeBSD: pkg @@ -72,7 +72,7 @@ FreeBSD: pkg - Check upgradable packages + Check upgradable packages Debian/Ubuntu: apt-get upgrade --dry-run @@ -89,7 +89,7 @@ Debian/Ubuntu: apt-get upgrade --dry-run - foreach + foreach upgradable packages @@ -106,7 +106,7 @@ upgradable packages - Parse changelog and get CVE IDs + Parse changelog and get CVE IDs Debian/Ubuntu: aptitude changelog @@ -123,7 +123,7 @@ Debian/Ubuntu: aptitude changelog - end loop + end loop @@ -139,7 +139,7 @@ Debian/Ubuntu: aptitude changelog - Select the CVE detail information + Write results to JSON files @@ -155,7 +155,7 @@ Debian/Ubuntu: aptitude changelog - Get CVE IDs by using package manager + Get CVE IDs by using package manager Amazon/RHEL: yum plugin security FreeBSD: pkg audit @@ -168,29 +168,12 @@ FreeBSD: pkg audit - - - - - - CVE DB (NVD / JVN) - - - - - - - - - - - Write results to JSON files -Reporting + Report @@ -200,14 +183,14 @@ Reporting - + - + - Get all changelogs of updatable packages at once -CentOS: yum update --changelog + Get all changelogs of updatable packages at once +yum changelog @@ -217,13 +200,13 @@ CentOS: yum update --changelog - + - + - Parse changelogs and get CVE IDs + Parse changelogs and get CVE IDs @@ -233,6 +216,87 @@ CentOS: yum update --changelog + + + + + + + Get all changelogs of updatable packages at once +Amazon / RHEL: yum changelog + + + + + + + + + + + + + + + + + + + + Vulnerability Database + + + + + + + + + + Folder 1 + + + + + + + + + + + + + + + + CVE DB (NVD / JVN) + + + + + + + + + + + + + + + + OVAL DB + + + + + + + + + + + @@ -251,8 +315,9 @@ CentOS: yum update --changelog - Debian -Ubuntu + Debian +Ubuntu +Raspbian @@ -314,7 +379,7 @@ Ubuntu - Amazon + Amazon RHEL FreeBSD @@ -328,17 +393,7 @@ FreeBSD - - - - - - - - - - - + @@ -348,7 +403,7 @@ FreeBSD - + @@ -358,29 +413,17 @@ FreeBSD - - - - - - - - - - - - - + - CentOS + CentOS - + @@ -388,7 +431,7 @@ FreeBSD - + @@ -398,11 +441,12 @@ FreeBSD - + + - - + + @@ -410,6 +454,39 @@ FreeBSD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/vuls-scan-flow.png b/img/vuls-scan-flow.png index f4959b35a3b61ffde18132baf7306930d613dafd..bd0d94c74ff738b7558a09c1a3ab2459d358949e 100644 GIT binary patch literal 87167 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 NT1N4H+FkvZ{|jA(5R(7^ literal 74102 zcmagGc_5Tu*aocpN~IxN7+cww5Dl`&SR(tHy%NU0?^`8fCre~E_FeYuEy@2NoadbToO9pTecjiY5H%Hf$Q9@nJUl#z!hIPHJUsjtczEaA zFP#Tha;8Ru@$jDEDac4^d5kTmUU;abN7JS&doPEEME1W6QtCE=+W&=pe%K(p{BiZV z5uwf729nSBWa=9l;LPM|y6%!At|Ny;v`s{J@WVT=;ve|;?*BGiB{%kIlI-!GboSdL z1}_0W&o{|<&i(oA8S>=4cz6i*KOcB#wZET`3x7ZVbQBLS1NHaMd+@)H!N~vqefrq{ ze=GNx6m;?KDgThX@~5q41Y*5tSJv#`_XQ?H(#K?$8C;2A3?9tatL&{n_BSM#X3)#$5pe z=Dv0uZOH3s?I$}$FFQ$88H+SpU(4%xKJ|-ySkulPhJ)rXXDVdthO)EvBjRK#-#Ju#a8%z;eJ!H||2roY89|Xv#(e zwgC#OdOK;!PDwC3h<*nZsEP@28bPzu!(f@L5Xg)a9PTlQ<|W#LL!kyVP^h*6G>I5Y z1ZWBI1KPQeAv|<7PzQxFp{){uC>JTfM``1VkgP+INF66MLqED|42^y&Wra|WfG&0g zAS>>&yF<1yQs6f7)6U$0%+@iQ5MkM$^MXB(MrRoh~_?Cwu``Y#Uwt>3K3D}|B@Z^__G>i!z=B&sWa zO_P5ILhffat-kU7sLkmQ)ps z+z%CG1+4OUiJ_eBXZ^sF`tx9kq$RGNPNh_8Y9yUJ<)O#CLa@8X^4fQ6x?mCCidwR>t0p>@Tq_)! z9__`7d7#mGrvv*rsAkNyaWch9WbhCqy_j364?Apr2x39J?Y82ddOM;^4ND0TSRS%J zj=nRMjl!^a6Q|A%s(niH^%r&7OBR4t#~_bH9L>v`zci~0S24EObPJkM2(-LuG~hYc z&tK8pSNFJsH-PMlZ`S7TzI1Z6wY}Jnr{;SxbTUi%s4Dgrzj1rzB$y*{FcWmIRVe9A z=F9B3(O=O&LvXF4l=@|46cn0T8g?ViaGNh(ZO!Tv3uMTMtIF+pM2m9_`0LKyXD2OOezm zLJp-$E;4eQWMU4ahG^?(k5_fAb{Y{s$#+gc9Yg2kHG~AtJEG()HUbH`^DasMJ zy}p@TQb-f-eX`*%=Mnn1*JnkSXnc4&bu=7`7|19_{-XTVidL|)S%fa zB`n}lJ5gW-+X{^(zy-6NyR5yCrx)3w<3ii69Q*JYR~NC_a}9U)zuVJiM8RDrK`3m{rj=@o&Cylu)c0Mp%>Si)b#~@R zK?a^IL`h-aC#|A_a+jP56QbuY5}5FZf_;)6Y$+N+^d$hP1`+pMk4$M~>}&nKtlMMv zVe7IjSK7Sb!NU3kob#DF6_D){o@@Pc<;RShN&%mC&i#A-yvGf+@o~1<^tsIoP zWlki|IXc&^PRY#Fxo6MKQ=rgzEivy8R}>U14@;34nxz5%AEqt8-^L|~?ufo>NhnP6 zO0HRnP)1JehR(VBYlx?wh1+W>NIzG#pMB^T^$V)gX2;pju;KX_>0#|9(|(iR@@D*D z$US*;VC0GtsztgaXrlhtVtv)fLq`6)l%>R~92=Ka%e-iMd#gQq(J+x4P`%IeX&){J z)*7AnVXSyqyUxr;$3Rdi#5R-+7TWdFvI`JMz$tz(hI_84?q#8N*tBy{=O-l#js1dYIUFje0%UN4H(m^*$Cgy5U4MFkNa5+P(NFjR-~$lZPXp$?2?PkuG1f^` zN$r}lN3R`}uPR$UBZK1x)Bih#h+0s6(L)8x6SPtS!Z*cbRsVi>rK^Fb){}bRF-PhD+i!!S?TI*1MHH8{Ka~nR#~CDT3f2lD z`vx0Q@IA21sV(3`YN#d9kw5s=*FT2l)SIh-Pc`x1QRn|;TCh-)s}3X}g#rNYPs;Tt zfdaSSo#YIEvn?>If3q^s@}DL0e{cTx#Glmc4d)MUAK_xnD;Oi-Y{|c!q9uaF`wMCSly7mE`5fMOjy>CFa7AT?9AC2ZE5pm<$jo zPq+=Rh;KMRN?xPX?4WxDc}N{`^1Zg&tAWikv{z^i_3!}czXt!W=+ARg>|7fly+6IUXbe9YOtXXzrik{+nF|m2^RUlhaXaNk z!@nk5{##FuHN@lEx#~>7G;5Xj4f85ROa(;%7`c88zZHK zR>T~JI-0I?Z46u2iq`)p_%^hW*q?Lb)K(H!2>hbxH#>3 z9>XI#3fxZ8*v`D&u$`y)$0F@4&5t8Lu9xaDJ!13n&Dn zw6MTn+3NqaZjHb%Y>!G8`}Ec#J@_T9ex$1Jj>uistRxgVt!aOhUbL$Kg>n>LPvmVW zV4TX*Z8?;9r0-6C zoz=n}cW%eO$;{3kM3XR?Y7mA5@aUBrYiTK|D+GEx)^Ot}LPCl88$32|ioccgUrJD) z!C)vX9&$g04IX{(=D8>i&}Ex)k`%2=^mct{t;R5-j3NQ&)Da4)-@AAvR1wqnySSnZ z>Xl>!H}YX@*j?tj$zdk)m67>~dI&8)8GRAilwr{QJxR(rW7jmE!~dKXeMLV*h#New}=&c`_nsm(mkrKwrD ztlBOBMbxjM6*5tM9lfFpi?&myl;o*Bf?RPNow4{7+SPO|bWT1$WMwv^<=Gw4ttq;; z0>D~35z09O=O^+E+Dy9^~pM98Za}PEt_&JlY{Rci+Oaj(j2#c zJ^B)0OY}_>WO`5e9mi5Kz8UUsMuL3Tz=o(C5=c6_l2Kwh@9_C&rlxguO|mT!31q_5 zeGjD@XK_C%y!aqzTs1fVnb&w@>hYoO*YpLYr%t9yi!H!W)k3LbmvyGv)q5QQ|m|Dk2HT$Dw0+22>jZL?lG#@ zCZFZf#(3}EnQUCTu@Y@Dpf;WHVcnD4pr(HF+i|o-E9263R-5uoB7ou9S$S6H805V% zT$HmdR@ft^d4%w!Q*-rba9=S6F3mSw&}K>%wzVj(y1^vj`Z=*$J`I5_#V1-3b@^Vt zCKDK-Q`o+XHLY?R_jCF|m&U@_f6#0J<8F0d>T{n_5nT&jzRqo6vRGu%tHL>w+gw|MSe#?i>;VeY_DnigNF@7VDR-*x}2prutD z_WUL!tZ&A1L~XxPC?C1#zS4uh(ovdhys8LYOzct#p@mv`AAEEOGa(?NXXWB*v7CQi z2)5uy+YKW?2>nfN#e0ptZ*Z`5-x_MoMCBAiy?$c^3Q?jvbZZ$Ua9WqCXlHo0zknb$ zClgn#-MB<1s%9r4SK`a%Wa^j{e(+5Z@0 zLNB_+ev{+#SArbS?qI}RW+rA9hJ16ZXc1`avV|x@<>Y63{oX1;{tu&hAHK9-xJ`0d zxOH)dL87AopP{%i#`8y+K`D(~Lr zVyI$r>_dS;rw<$DvF4NS)|fb1-KNADIZLWkkDqJ7%kI|2GOV-63kgCdY~A&Bdmpvt zUj~;aHyO*C_F=bf&6=(GNoqoTNngn{Lq?u4gQXSnSwB^T4ASJV&w)cS;94C8*)FtF z+%m4qkqwKNQ1^u7o;lYO-gHKLsbr2#{Bpr5zV1hkP!%uq4DT zukZ2adfRncs8^LCyeo6S1YzZxB4X|5@#LH%`WedCvrXFyO1)e{GU%Uq-9FaX~Tm@2&b2srBltcN>F4COv+P>tx}3aygkl-@YgZ|D@Rg zKPq8nwk1lts=|)#FUbQ`YpGxDC}Ee0_vemS1a_O?=rCLv;!XU`eK{sF?L%omo^-77 ztJtyBEoSDNkheR>qv7A!)>T+$hfBt*m@MK;M*Cj1bj?hf@SS>gGrppL;m?t z6sla$ap7}u(J-5N!pkpa{%sN)Qhj>si8n|b4<@XqXRh39>GTQ_cS#*SZ1d#zKl z8+M=g!L3I|Jg{4+8@C0^3TLG))g^8fpxGD$01jXi3*ez#q=^4g{{%abfqB*X(P}4} z_<`pFNs?Rwp)0Y9sR7R*_w5|ZN^{*+FTTLvY|lAd?!_%Ac#*z=Qki^v<#mSj&JU|C zGxY}fVk-R{7;qY;&`#T-y%=^oQ8vd>d5N4k?Irn$bP2DI;iH7Q)z)PK9@uudYAf+G z2Iw9g0^wWS?hN*{-EMT56uju$``Ekec}TfR1h$}SSL{WM=hXYkE0B`M3^-9$>0SD| zw|3*t$tc$Vr8GMsMWc8Yw@;Gk`xLlESk;&>#@~fKP6!e)lZzT63Oss$F|E-OLACP_ z5mtlE8+x$U-C)@C=|7WblP1S`{9W`K^_9cPP#d0{*2fREJLJpFr#!^VMw>NQTNJjloc#) zA$mTCv3D*b;vb&-QHH!?Aq#>QNf06C#wKn}C8}|(##+*^e+$qG+Lj0pX}c$G9qv~H zMH(&;-KJM+lWMU-5a~jij+iJsKN%nH(2E?-P~g@E0PsIU>ZPcU+R?|wC=<>N(0}Xx z>=1hdBZ=DDk9PohDkv&K==eB!N?!G$4}(;RXvMS?6}^7*W|?XdTFOp(yc(S~h-!bn zT^QkYg8w?na=$8dcq`Mzi7h%D4fAHTb#-y|ROth_#x-m^{?3+{7h2yod^9fw2SBo8 zmn{B`>hi$*w<9Y-lY>(GTMQoSxr9ULbCSvQqRzg6!3FgGz%9ya1V(RdO7ipHyohmX z*rChpH@*g08EYDWSlBOSH0>Yz9DQNRM1e3o@x(b;6VBiv2X$HFX5qHxGmj}v1EfDd zQ^d3lcOPX?`KFQq7nFXl{sbXI(RLvy}^QtAKY0YcoFu;*G$sMue>crOFW$t=DB zLfPQJo!dyCT8pypH`~4hC<)o^bMy1{dSq~@tIENvMHfX<8vNG3_*M(r+V0ThK=$}7{Jmw(_(tZ>?>UGr9hutXJB>5Q+BKW+<}lp!z@gC~YIJ}zsvYgTVW5 zJYAErD6v;yZa+$GV~kLUK@(z|HM!baXV6ETvqMR1|9$r-?Ufblr~Ho#U;AtItUK2N zq%OEp!30%w(bP0(+i9I8qg>4|EQpN-IAw%e$yjOuklu8Vm7x)j!FQHvKdISxg~_NV z-hHYm4E0Gr3izU&xI!K8`Fl%Qfg^Ey+_nG-c-{2IjaNj0HFg6%s~AWoF!GyQMh%|? zZoj6rne+{}LKs94VJYkF}LAmNJ0B=J#WQ2Cr%L3fB1BitPp6r4#rN#KJ=T4V^m=JtgBV@hFj_O<`JfXv@+K-xrfXdWK4K$&uWOy>r~?#! z3|)7aCJUxYMkccoUcUVYaGoKr7)ANiA;5=~-al|f?y|C<>!DQrZ>jF2 zl@4rq+93jwtD=~U33Z{83M9SbJA{PO@{NI?U`ZO-67fT={8w3r&>Kx6Er|S;_02ma z*ZH8F92~O_N7u7Zz=FL-4ZR5L`kj#lCG-S=3c_;9X9#_Z2fF7;)BBabiDv?E)cuPB zTU)R9V>J4|7H)$j)c%uGyLZJq%Gzq-Hngy4N?BW3{PwEw5ZZK}mW4UzO$bU0ZgfCe zzq{BAHU6o`#xoiFD$bl0sZ)kxP6`W+>CJp>e#Di9@{TeBS9kVDZRQ$hM;I=4baL&< z%o9F73n@UcFgvBj9z}t>MFh!#C=kXMYpH8L=;5f_{Z^}2X)dL1Y10*0Va4vbk!=}o zBCvI!JkNIYI?XfzS*FW8KC*-X{cbEl4GWO(H3Nw=lphFHwh(j6W>onH-CxDQwW{sG`ck z{^N}ERy`H1sB54$yw}i4OG_i9i`{Mgsx8R#3L)c`QDQ)Q;hXx#D0ev5*2ar33njy| z-2lBPlS@V*?mVg>r^CaU6I`mV`nBI_H3pCi&+?UweDOv1AIQKP+?LAy#wTC>w#fRT zVmWCThd>w(L9Il-K3UCt4KfVM0Q#kxRY-U@jj$WPr)?cFH$mc8umiE;^m4SWnwD{$ zpYB7ApTpwv`c;UIuaHy;Z@zLRP>4zGG1{eT`sHeuf2gf{{GS;`wdaG8ekrud9~3-3 zW1l4BsZENWvv}}q3aW1=+pI|7+YvR#>;_>W z9nO>`1a++DL;q&Cwm`kSjv-N{JZDPATKy(h1-)TP}}R+g{Peqd-3Ia`I6a4Drv>- zuHeWOI4omt&P8_w%D_RbUr}0GLr%wUJnd5zyXuJLbRi13`;Mt{z}^g@Cu-OXv>V;R zx>16QE1-UVJ`a;-WzuP6;4Yv z4B4`tnQ6MIhgBjA(F3+)mOFO@bCzJ`mh9_2KeJdDv94qvwa=6q=ZDmO7PN0xJh$-h z5v0481BJd$NsrkN#hS}V+iBByAV1He^?eYAZURNaycNa`Gqs;yyQvgnqj!lE{IK-U z*ZN=UgDen+c7}2aF8Yp-T&^!Up?-#3z$A*5>N6zjFH=T;D5ipv9*c?9y=v253YghA z%yNF1K$x=>aFLDNBQR&PIjYg%k>Y@Pcj@=dQu86NseG~Cx5XQoVqM?S8LgCHK{WIA z3MUZyE~gtx+|b`H+zvoKEFNoAMR1)_kIh!tbm@oDW!t`(7EAozM4d1n>DZ+;i485d z=XZI(t=|i#tY6>izbkxl)s;D_Xt>f6yIZ2a_M6$TQTD;edl;&uUGkJP{iO>JEMqho zlGw1J@c@;}535Ya+VCYW2G233-WunaPoEp?o-2H(jSEAH)CI1JIz67K!3WNB)^{^I zHi6;0FBBKB-%bV*cRNbEt$PL`U!c)@07B-X(Dq~UX<-3#POj*Yk8h*I=GE9iE&{M~ zdDJP~$(Xc8_?ET%e^NuxX=9i)jKzKhyR3(V-1o%!LObiDeAIlN&&)jPeXFDb*^ct{ z+jn%h_kHtAO_bP7#wio)XNB<1Z#M|1=VjtbNYUg3>@`rWFp;K{Eoj+J$mys^Noy2_ zJ^y)~#^8%o2M!RW{BRT~UEda@R{Sqcq6I)7<}r74F0`<}zI*bM#XG>vRt;SW^_CNW z-CiB78V$pclY8&*E%r6EG`{F}Df%oX%HDw7YZFU`nWnvnQ3RYp?ZBYm?d2IP^dkbh z9LNPgQ^W2e?@bQdC5H%k$n40U(VYTppAogkrS0-8lCg1dF)=YUlHSVqIX8CG{@W1} zM8Q!qT((gr!Ckk+eKwpb%^6kkXRHT-a-jDuB+$uQu+{*w9%ow#4p^T+mU zzg+aAqwagI!`K&+kXHXxkETH_TV zzc>z{o5ZXTUJHPx7`B;&3k(kNMm8P?8bk?uW7fQ7T$cLer8TU6T_bG48d{|Ti3Wp+ zr|mahQ?pqL*4`9VT9?&nflRlU2j#eYSv^|d`TmKybHiuhmqU|0=R*uLbx;&!xXlqWygEh8iSgueh; z0|>cEotfW>MI&qVF)gy7Eudd<-@=%2a4ck}XaHuet=IG7mza4n)h6Gm00hEmwI~DY zLi${N0pE}QdtSQZ6E|u;6%d!csHRaX7RMb=@ZY|TNkIdzEh6oz079X)euoz zUg3JO{l*p2A2f+2Lj{|_U#I=YN+;%P;h^0E>6~(C!g#x z^R!MFCf9{Xwmc+hf$c=@gndx2gAcIEqlH5YVYMAnHdZ%rG`N?ntX_Wlpj*C59*C7dqAbK%2?R4rvVKt$3-R0rjyvu9^7F4Y zg&6iGyrF{r3nF1AY@o9~hbsEfY&&&J&)^=NU7@})onado)eYMqX7X~Y{Lf{Y!6E$O z8(}(jc9V^D9oGoc^S^_{UxshQPV~JNrX!)jrWYN004vI2h0AL|j9NZL>R2VJCzzr|bj&UuX_S9l?}Qq)u7k;_F;Mv^io%j+pM zhI4;W67!xlJ|BkcuS2z{yvd!a_O|g-FoSUKUx$=vMGG;AfB2b6mW2Wezsx634BQ#a zi`q>bo$SYoN6C)=tLe$!pu>b9bKhE@q=z!_rjK{PG@CWK5zJ&rL<^sAlQycA+>|2j z9r_!EPyyJhWTZ-ZrOn`PYQQus$2K6Zn8_YldPw{wA)c8)Ilofi$Sg`1UHJfGSk=<^ zej$Fd*$N@y4ousjuCBl$k@fBKohqY!q9Q>XHln0g+x;2dy^dsgl{@W#fK;{?HuFNE>DnjI-o=U}4|!3>qN9_e_+f2o7}6YQx2X+baXJ_# zO_LfOYnr5o?e$iNAlw4ZhtjT2gTR$gNz854?V*$UJ`o*T?;A?0WIz^)ip9d=CSdaU zF_c{wxUhxWAhzq+=i%DfN*g6PPzcPof+YRa_H%vc5Slj)py=$2k93^FYNquDE)q(%izirQ}@*%SXB*D`qZaDwBb1QXa<-ERxmjLWSl?R zIV`D1vzHTwtPvV+3c!cwWM^+TxS71Oi6~Av5Z7(l99fN)@8ILpf~p>7#%@j;Iy*6S z-p6Pd8u~(^^(YD2v~jhg<)PU0V+fJU~P**SCSA`e#tJTXOHs>;@x%JWS z+(u%p*q1ssg$zr-dy2H%{5L+Q(5lEiuNM{?8ndPY*KE+7=Sg-OC1G%Nxs1)vV&h;@ zu5x;RuM)_#zRgG9seL+2;j(oVkp0Z$dOapLIXFs2hF%%Io;)O||NUhEgcQecVG2vQ>hlBwqdUMCUp+6$sr^b14ZUX^pY=r|&FeRw*G(tzpRFVYPfp zcY?$JA+H>uW3K>f9M+4f{*8XO4Am{!jk^D!{Z@hF&|YBIu-%mNL%jX=UV{qruo)uCv75C`wsqh-v>*KpOwn6eas}67at$D`dUV4sv!hj2X^SHWQ^sWMUmuj^k_WCO|hTUk3 zIiz1(;dZmM&cB-*z|Ee1YfQb<899>8QDW`{X4F?^6#(Nbe}o+sC2pF*_jFggS$ZPv zKaqc&Xxa)GdwC(VAEf*n8BI!%@iM*v0av8qxZ39yss8}5YY9*-_}3yg)S`)1dFt2b zuSaW;ccV~}52Hv4iXbwr=yj;rdKZ3M510x4o?6y6 z8TK`a%klBigpvN&HJjBCjt)K&L+B}@R1StK%3O6A>~L<$mwZRf7LYb9L7{`~XH|-4 zB17meM$z+yC^>v(0AaBM{u#Iv$t@5LyRMDuN^5l5d}~L)^vOi=NJMXiFWe3Ti8}B; zL^Wfyiy0+kvFdZ6ECs~5*6!jDy)l5OmMxyFYEXz}jcUvaL8>H4B=C@T=Ar=5Ta$Ei ze1Go_NKFSGGx=sYFBGfEoxq6EhN<2ozTV(xla7#u{-%Vus$v{A1YcQxS=gm!7%5F- zc*b1ZXoQaa@CGhZziiTJA4M7REdOdD3Yw&EJ7z^I;W;WK7J)zjO3KT5{mDK~++ptkp3)3`0VuK4YB`s&9q@HsW+zxg)C?VZ{G0PCYB z2yt~IW|Pd#%Cjehh&xWM*H;P^1<3j76&PzinEw&d1vJ{~+L}}Ex+1mVso$k%DdKoU zJ<%w%v2TQYMDiR-{7;9D#C9x_t_IAP_Sv%iWd!6eKpZe`>2mw_vg~wgU2-_mZrnFm z2Lqo@KT!)WI6e#0ZOxor0#FZ1n=aq;-E;Z4{mzqU0pUA=edty3eNyG+p^^5_Y)K2z zwZ@gzp3dsPk|13b7JxCAzty`Fjo-$jbB8MqJ2&}h9=rb4b2#^td|wT-li$1_K?D(9 z?V6|*-Jq!01DR}`Z!bbWcx^aq?MtfL|L*5tDpGKd9>*(-Q~YzRK-4+aD*w|3d3ANZ z!_o=Ma4O**P{4MQE3XKX&_*joTJ+^0oqK*@rh@R6T|Yz>I*)uLft z1a=gttC5wuApdTB5t~Slqv=V*gj+a}j*BRVgOeC@A;B<7r#%rsi4ew@3=9eV7*y-> z90qD`IO!GPRkPF_+Yk0C8YzAAF;wgCmukO_2o~^craUm?0*%FN0N|&t!KdM z8PHRP`gB9}>qOJ6*jJ*X6#NSch8)L=FOVnzEjGSsMHZ;)x?%6S4j=q#J)em>*Ed=W zg{JCMxD3Uccgu(cXs*^>5S7^AvX52uwzT1Q$;Xvx!FRO~`6gO|!mr|DCQZGQMDi=W z-x)k&w1^Vx>t3j&BwO3Mq&%?j-E}Q`GaNZKX5zV=o>81BXy2hf#x$i~+XWDLz>e-$ zxo~wgt;wg(R1~;zn@~+7K){vT$Mx!ht$R5*tBUDW4dT&vJ zV4aXtRO>K={s8BO!Kyphh+uRi*EYko;h!{&L49>ZQQB$bc`T}fw@mWanuU$kH}Ch& zrU@X3xzULQf{Gmc^pFqU<3Y4sRg{ll0tI|VSm)|?*w;&{oB*>7@-m9wD$m&S1$ZKC ztuCm4F5CW_yu@oG_zG?5MHX@oBp_p+=be!;+tO#QL5<*Ksw&H1R+9;q%b5eW0%)|gv5gVdCU+AL4da1!9jR>eB zfz+DVI;fKZT-^#&uNi(XL%-T2(ANq5DlSHWIMnvK{+?T|B^X3>>i)eAm^+Rhk!^)4j7&f6^nv)mZ8uW{|VkMB_OX}eYSw`&RJoFfIVFPhziS&&uE5cc(ARg1AD3~CkjEF+&^vMC24pO z&FMwGGha7Y{a4kOReTn_exJnFKT5)XLV{C`;{3mR7PbBg1VL7JM!F3B=>q*^T7+`ano9#-V*|i6(P}_aoSD4!G*2ZJCLoZS zl5%ix&_0!E(&A@lXBQb6*~)r+v~R!ha%rT@!pw|hn>h_iN-EuRu)o&w*@Q~OB|jvj zL-P0lezn@5)=oyd<4#AHL5&SH1%>RCiILIaV1ee(pFf{Gd7_}8@O$L^A9LPg>PNqO z_inhvfXsxsO+-YbrluwwnI`TXPygWleKD`CVA@9S-N=K8jg5_mQ_r!vO-jR(UENS6ph;VBFvaIo3ZNpbSWqaZW}?6WSCmYm-JM zCh2`>0%Cf!DF;UZ7_-x_`E-ANs231h12CcY@F6=pd&*tM7oKnihIEj8BLf5bcvDkT zb8>R@B<;Sw;j>wrsi4B($_j#?ta~mzm_kAHIEb4lLG_rSwd$1f`;) zf_Tm0+NJz-o*qHb3YL3Quo+<+u7uT@R!BSbv;Ly;@ z^0I{Q9$*X^xw(~%2|-&Y;_M7{>-@SpT)Ao6!(PK?A3YV7$akjO(7oL$|J|h#RiN6Y z!pwCiQ;?GbCM-?DSCO8jtD2qj%bAL1;o96 z-51c`-w#bU)Y95opOHU^{i_E5zpMX$@0Vv6yPo3{Ko2H`fk*l)1Xmfav8AA(7#bV| z?RoDkdt%Wj|Kr2jr1{DnMO}YVsD6;7nx*B!WRth(-MeqoS{!FCZq9y*pz+7eQqJL^ z6j&_Q!I%H#_3v^-kbq7Cor?3D2NjR&EKVZZ`ye8(E8``RQ0 z3CZJ~U&V58;RrG?gMW~F9xCor5V4SD|4Td8w8TWqr%#!=xy9XrW)z#Q48N#1b`teEv>S$vZ0{?iM%** zF2*%@Yj1l=Pfw56q*+T%?b7ATsHnoCqI3~gPBAe2gk+^0EYm`n(683;5Vz^%0Kn@ z$MmU*e3N_F;&=G+GTBj4riMM(r}p-$4As@uIcyDXE1U-gYlA>B)0cSx$3;a&b#S!5 zRy^OK`gD@AJu4hZM0NX-{slijzq4@)K^GSm6t1$evVIvF(pCbZP|94T#E%Ge(-cZx z1mEqtk;K{(>*aCho(Z<~pj zWa4|HXXT3*(pp;NuqqW;oWu5_HXmO(aBzzPaYy(WFN1=d)3$bZ<#Yd@+mO=K(2zEY z-2hwI{|Gk^)}@)5nRe4l&-|*YD!)Us&CN}V?h0n8i1XYnk!o-l<4^GVeP3O@4+)`( zi+jzcT3aFMZw*duf*SOL_**PbLc{R?y3KrFMMXuOGCd|;o_4?hivRuLm*CTnA6Srg?02cNlorDI~8OxU#VBA)bcWulm;D~;^*Igvkhcs2&kuYBz8j=)s_-w1w=wad9Q z;G}BXH7oHYW7pDNCB*E`RR%cGTq!}L`7=@F6hEoK@m{;SaGug7e&4@tNPrXF*N6L1 ztD&a$>h)_n5f?1*)C(OzI*eUhB%-~m2E66!vZAEAva+(CUY$j6S`ZQ4i6@Pg`g6+V z9+kl0w{LHFq&UUS*Zc0fv6#(*atM+wo)A$Nzz@~mR>N9Ay4k}xqBNp1o-$@RIc)$7 za)2Pj1t}?U6<;{73ijtnz3WmtUvyV?HnXQEPV(sYOiSpj>G8p)>agm;)`I5!`}e_X z&3n@#uAcwn;;YM0VCVA-2_-~FQ%m|CG`O!}$$;CSq^4Gg0$p9Wjsfe!9~?Z2c<6_x#bIfk2?y$mqPq5Mu`yzY7j)I@J8it zY3ycTsGZ~U^9yZ9DUQh!vQru^$}F`H zSZ<(1Z)Ro&2#6Iauazv45i>}9&sK;{Nugy1XSaac%{POv$W1RVD>VgP7=Fa%PiD-#Xw z3jz%P%k{QsIf%ojPD}w{!SYa1LP7$ixUlP@7Kq+V@{vA!TOmcr+`PQwtpU|4V{pm| z_>z0JR_v8zy};0HN^jJfK=w4&9}CWgD93nhVLy=1UMNG!+|w;H9;mM#iWktXfm z^jBs2kc*lUC5OJ&f$E`9Bn7kUFWd8nNM%{fo^!9rWUCn%G?-k7q8EPxi#y=I7u8XA z4iiK}Nh$9W{V+Ethh?66t|UA>e5~5q;>nYk)YNXDRuCIyWo6qUXxen*@(#8~Oc)s% z0fM;C_-C*G62Jf8!5vcg?c2A1ZEYQtZG1+ zD}PN`TwJ_-`Ld04<>OCC+~)ip!G)!zeUs0V>z$pQ0#@wKU$+B&NY%gw$l)3Wig=4_ z=Sm#Znu!9D3D(%I+oT=5;Mj)OOS7*=6}b^!*3;zjAx#4n?Qlp z?7Ls%xj6?`-c#SWgUuU~@;$1CEq=aWO&;wn@t&^3-}JzHjuh#ZfvEJ#R7gmO(ZOZk zgAoR<9D)62X$el!`~-*iu$%i^3(i|u^=Yn3^KMcKiV|Zu7)t{v8(JAZY&c(^>%+zz z2sAC{nKbzcC(XtDyAb(!NNH*a3rWltC=B}cObb{o+Ugb?c1Q91MnLxfYyu12+S+;# zpGEqsCsG@J3?hNJ_YMfruecJyRsl0l&&bAo>lQeV)4CU8KiPPbfS;dVx*F{LKYPeR z%>O6_!M-m2$5bci# z$^_BU-rhbNY?_lje}P&V=s}N<#GQfBg@9M<1!)nO^KWSf0EEYEiZyRe`5znU>+jL= zVvO$PCF^lp~$BW z5X?0-HTC90PQy~CZ0Hs2%Z>rxfSC%yDKj#Hl5CW#B)K9T?3J9)I{w;kj`%Qg^4AGG z2FA6bx53x*oCNXaM6kz%o##obX93Yfquj2H`3{$@AhtZ{LWX9kt->l%TOXdLAN)5 zL%)#HApw(qq{2^Ja0skTIP(0nRLi;_um%4Hb?bvWV=cO)vlr|)Aay#5i zWR{GS^w*~gctTPL_Inr4-&lV7^r=#sm>#Ht>=oDfJr3EnW8X3keD-8Q4bOoL9ASKV z18>u#YdX_(bEzzLfrJ$qQ9PqzCCpjLZ*`%EM1e66GpBEVUo?60@_@Nv2yoT(YtTNVHTuBlCd%PXWNPy=t zYAn~|d_mc`eH&Zx+m~&6Mka8`)ETe?=Jmb3yu3Vqqx5PJz;MzcA%cGKBs^yAP0gI^rcaqY=tnt z@}c}Uy8mu035K!BIakA3NN7(eAFZUZ#}Uk#<~|_LyoeY@{2#Bdio!ibOj#i8+#Rl~ zpA1J{N|mrY&0hp8c#y*agIq)XHx&Q#x-v|qi!JU3+WLhZ4zX{;CFhXiw}sE9YRPhO zcUnEc{?h93&-RLu!1iPhWOMy804GVpaR`3oV8?bAX@7?ImKlV-z+nwxu|Zm~Q61PG z6F_=-Db36pMt5SW8a2(@WNc@n>=+oVKP7`;zHbl(*eL}11Os$2t0^iD)fnY4p(rK8 zl8Ge$_wOUn(IH~sp}<@qLi-!p2sRePivZ;0;1Nu85OB$VYvSJ}oBzuOm1QB| zlK-ad-zA^Hlm0E6vi@L$^|8vLxzV!cnM_G7!B;^0&js!d)ZzviW8ChCd z`j>j^uN;H8`l*IS=;;=88x9g$5I`_$%Koom2TR4F^X~U2T26yHU44CFett?(k7AGj zgM=x36Z$WOGBN?!&+@{^h`N%}r_Z0`m03@B0E*({>-z=s%jeGk>!CyWXP*BJg`jp# zP0h{ak%h6bYR?^rr9s8A`&=U_Il11V=V|KpSdmP?@!ALX)k)sVqt*Jly1)Oni68$3 z$AJvpn^!+RrJgZ?F8)AZj3Td8Vyx+#Mnoy3pdfI504vtEVU7x1R?xV^Cpw_l|E%BYqC#e$*0vNz|V{ zeacko@9$5K_#bluGYb;}S%$(t;HUR|0>m)$^Yhu++3C@0xjg9t1>(Ni1_m;|yClz4 zN(uqiwYdq>ZID9muB~aAnyRm)#czR)%4l_a&LX@(#GOW}P~5k=we>2UQ3+r;F~ETS zSs9r7apktKKkYFw=yP%LrWtV{Bc7a^^56gd+|f}*XnJ-wM>?`{yV-ee#}mYi4$lhj z=1ZO=f=n>(ycT2#8tPfsUPmik0NVL6lnj6N&yD@hC@~JMt`^GYyS&W6neP0xI#`gzl4((?6%+&&IJZhr})1jE|#E9-#vX- z*8^Z6_XZ8rRbcQ73u(+!1jWR}cz6zb61tgjaT5WY_y2eZ_>OmBe}X_p4V>KG&JI8W z|A2k&3^_2M01|XcO3IS#t*xyY2R43#jrTr2nU$>1v`?zRoUn0n;!{43q15gQIRcg7x{Im-){ufa(Fj?LyCYNw?v^LHfux1^oq(son2qH@1NO4A$1@ zd33eoQ~Cex$9@x||G#}hF%nADu7Wp*XA_JCq5F)k@DNpNTkHymN|-V@u&pt2>esXe zopab@th9OCA1<{4`H16ekb#>R8iM%Z9Wz~|I3=Hp|_hXb4 zo{=f9YEnvyxN5fFfNYOB%nYpUYMD+nFm zKShn=;@(%$l^_67&THc*0h>p;BQ|hRtH5Ou0KD)q9~~WJ4jm%*-u5e*E*eTD+;Dry z5=jAWCTM48C-^7mV*kZkl!%BgU%osKBUKP0fb{i?iy(L&p%o)#Jpeu!S59lzgbJav z1oo{3MK7+i+!_L(wC*GemwRMW3@i!&#lmhV#ZQlqk7b1jBE(}xlb1%G>C6(P7YahBxITtm^uK#`$qN^%aAfC5T~frUm(Kn_|I1M#I8Uw@oqQt+5Ej> zX_~^XIkqcUI9XQKr_LZ6u63-VdM6v3f->j0OPCnOA zQ;s>_jVg#I-_@rN!T!la)>5+v?*Atf>HJS7686I4$3K}!^ptqk^mCz&;nEt($32Q% z&Nf+tA<5dX38u*&MG|61_5WldRnV1UGL`9nXDCt-fB8u`OsT>v&!ObO5R5*d%vMhL zne6vZLM?V668XJ9RdIj&m^<}jJ)zD{KjF$m%`@4LiHg(P$2N(dQBzL>Mq!79`@gdk zMPG!3IkVZL>K8;85g#jgziSotdKXbatzIvU*~Uy~|ImvC{9~ETigv;NEQD$8JS^9O zzL^u9O`K-(@I7-eofluXuoqwX5H(k&7^V!XB~OYm`5zG5x{csESG3HldzVwQG%Vx2>XWkDLXY!&~eTZu~FvW<{1TdomKKDhu z@=D`I5%V68i=(ztljWsxi_+U#^It~qF@pE3lNlP1xkXY+E}>2ZsPxdzjt9^P>c0EX zi>N?KE~Dp1l zK#}-m!&VFD_SgvI(7{)o#DCy6XyR zsq+sq!w;~8^gLRs7ldcrTFQ!6-RorEEtRaU(xch1XNxi{RY-V6O3GC7G6kB^{psczq05xc>$wk2=$J$I z9JP*bYmTU2UwyjNIOw|l{Yesl?X#9$Z9k_nG@KW9*^G+L)r?5$Yu=mf zxQ=0fOmK?L)mU5qARx^Rvay?zI@z`t@)5xA@z)x;`<5_2(7%01d)}oT+$M4=7N|7h z#kEClQFK-yC(}r4qJQUHB zEsJjj@Tug%&zu~=S261IM3Np?tx2!tb>^^2+&wmEeQVJgzO?zAomp0jM$3P+BJ^}r zhMRT4%dOMY;bxstxAEhH6{KFC?kmA--zA-P_7f}vf5^he_Ks$V?ij%6VQWVt{(dyU z;;GZTY0f9atwG{;NdF=)Af0N-Tj&twv`Z1p8lSvfbnjudCwuzE@`+i;>nI{L#vd1t z6wTo7?n{9RY8|hmy`lKLtG!niWqA4`e*K|DlenKL(8s}$d7b0PL$-3J8ydXNlK+Uk zL1eU`S4?Z6uUUJWT$v6E9YBiRm1|nd@E94V0AcG31RdJ!d!aYCG}QaK>6@hid|R~m zgZE`*#aIDBgzux$sE<}LDg4?P%w<0}rVD>cQ*atb>DPIR7{g3F>&MiVf3qZjEh4#8$6D;&JqELYKNU( zSKqTFj#o+%eYkV9SPAis?>#AwWb{}6NNn(QwW_%82`diG9JA9Hi)J`gz70?6%m@lR zWgGYZyZeXxslTbgGgtk>9Eq*%4&|a>e(w1 zK947oeW1r}lFA1yc%(kU^=Z^xjn^^vnU=NfcIWS29F%~q6DbsWelp>Q{ZdL_jm_yX ze(DLjF84QoII^>QzUV)V&L40yX||tT17dSplwpFP^r~zIqW5^{Pb)7n#l!z;vVI7 zW-zg4fQ#bo+R>94*DTmDE8Pw$0qGfo?H>#tTq=X&vFS?Fv0fFvHc6() zGSJ7&=5nXT2}#Fz>~ymfn9e$GbuAXv%hFt{IuWS;DLb9StE$?=QD%ZWW{ zE(lH~nZ5{&dlQi){muh*heTJfemOYDWCoYDe3mBbNt!e9Q9E==pGVI8dDZb0S$3N5 zMPh>wPMN(Wha(IP^3A2oy3uJ$O(IXf@k-H-0b|{a+Ie=F-}g9m*ho>0D#sM(?Bzm` zlKpHM*@u44F2=_ML6axd7(;PWUxQ3hiR2XB^Ti<%UaBU%%fg!6zhW{m$Ce19soDbP z>b6@v@YLBHu2BSiJVPfp$f6RzVZBe!J9>*uY_tk1H{ws+%M7!D7PK?({3KUZQqsg( z`Xx5*7Tz#hN1MPlT$&^ps6 zxGTG08@;WCI$a?U{;mEhMlkRIJPy*`u|+I6lxUZomMsU(71a zK=$fIGC6z1@o;{s%No<$_vGEx-(65kG}Tp6MuZwm$fciInm$)>`uPRd|ICl3SLkfZ z`u2evHqUFsXgNCY!NyDS$8qLG3Xfpzc?-9$jaCRUuqLOcS02oc^A?b1W~Jl1AHf2W z(E{$K+oW#s(W2KUzBDy`WoEJ+Z*aa0GljF8ME$M`^eAdsdBKj7m6j3cVpov&3=5VZ zAR4OkR|ootac=xwp3jwOl^7|+(#vn&&OB^X$T^xJlTBz^f(--3;b8L{D_uNfdVIle zeYc<{BS>F3r6tne=81NTD%~uKB=r zH*Qh+9iH!f`O1s6G~fQfRe(=_YN_Sb%b$OBB-TbJI77Zcx=ajGm(5=Bh|`XDkK zRO6g_-3dTVcZ_W|Ux%Zld?qWR@=uXC&+< zev$TUDUVR=_ub$2-0vM7(LQ?gXDs}$fI*{GVV()Ofgr<8{5G@*=zH1eKllm7&0Gk+ ze_`wAx%{z-(fB6(gZKGr^X0KM?ycBM($mi$=^6O%-wK`GNS$7d47;l)TqBJ5bKUd1 zl)3M*XIVorp?@+O@-9~RJv7M2AYRbW+oPn;f6}%$vtim8a`5G@XkZi?W{h8R>)&~w zyn2siY$$<^^MXbu^zX@e$7&stCD*;qsG}})joa#-_s*XJB{EKfTBl#Za8=qW#4FSG zKtSb3FD|v`yL$8Gr%8o+oi>@=`1p7p-dbJT5jQ85L>;o%%zO1n? z{Nt;_)v#OWUwkIvi@&41Z1UyEbiM9tF9?wZHF@{HNBjZe|N>JQf!!N*BdZM}}6hfoEZPfbkJ8@4^>u19FY2bpbYUd_+8q<%zOe{b8Q z45tcSW!lz734JnScKm`P-y%#Ojv_1f+_TnIV(HF&=ivLdp;1S>ABt7TPot(7vun=` zv=o+B7zd>2FwBjXg=vb6`Ubyw|7>CRFj%~XnOD_*Xe=)OP<|?u$2_Uk%lEdk!24TJ z=G)5%X@eez0QbOCL!Y2QMYqDQAJhq>QRS^*;uu;ezAZ;lUVAhk@xV(;ySwG&Kgqli{yGgQ4jZdmTn@$w)dx|ToxBNKg_`bO+9!GW9J$-P_Xqtb0NiCgkCWUqOCfkpER35(V$hE~ObVjOI zr@0Wm)7n%cZ@taO5&6eDd`9?kOdtMaidpEjTiaj_KD4ogR%qw z+bQe>l}QpouYlQy*A$cP^^<+Zr2m+wPX`>`6N z_WOj{(Z2#C3D>C517l@qLB0`xqfrqP%bVlx3yH(_gCl-ic0tfbu=~@K=0$*EZxDZH zV4)uz9`-w!4UI8>9@zCRo`R8h%8tfdGk0NOm`bbc(DQ|A#Memw+@F*B!5PG`*dwVamvGtXb_d&LEbv{F)N0s?Q_ znmL_7jY^N37o=K??>vd`j8_8BsQE!O;HKl~Ag})>0g+M@mC)$lnpPQ;>h%${L z{Akds7aXj&XXv`k>B%m3Rr;=S^JU@(eSt;=7IKuJiK;ipRqIDsb*8O<3FfqldBtn- z1Scl@K2*EPt83>(Mye?5o=iX3?^=Hm+iR)o^fKdBOVjK31;LfY@dE;wGHW~>&9B`P zv82$5vEF91kLEf#c>bu2kKqDa;L>r-7bw_@A{(xCoE)A5{J0gbljv{2KJ~?L#ExV! zFB8(x{E3>j{`{nvp{(-K^bi#V(8SszB|wnZZ!gpomz=*q2;Z^6RBiV%t6}k!WRBN5 zIkJ`I$6T#b0km47C;Q9_me2GOYdVS0(0;TWNSFkfBX10}r;t^pW1pK~=8n&Qs8%CU z*0G1HO}@M$^iWp)%4(CtuuGyR$gt74rl1F3{3c>*rbCYV?tAy!Zg1*_Tq9FncE+)$ zSMv^I^Z`NMCwP$?n7jz$2e!!v5|{G7J3>y8Y4PY(5}yjAKbc^xloI6`EkTN~R$9F; z38N4qDyRCjj> zkC(DjkCxjm9c&k!dwGQ$aUPiLP(9L`T5$VC5us}7qF>vZh5AUU6fv4|w}7I1?IN{i zLB@U+<-ad2U%<%U=B@icNl9sauJ!$UFLB6E(rf^BL-G92Q~y3(TJFB2P$gu|+5ISU zvQ<!R+7AEy|y<%Hhn&$**;N@>`HXtF!Ryo+bTn6Ky=O)kBPogFXzddu&@P{73KTTuLz#y)a@c}xL8$=CPA&jB88340+mp$p zyh5OxBJ|-@!SOMLk4C=@>{XUh8r|c!RUM^G>&&(i^3U|X4sjbAnPGMd03V*X7pL4N zunWIZ9dMU&wtuDs7H9gpI7iCzJ6!|;-BjWcDmJc}- zlOMMidcS2VQP5i-KLIuV42&hGA1vLhE!|kMVsC24Oyz~nlMDnJ52Ss__$ku$(USNC z1iU>wu5NA^oG>sjK%MJlUf#w2{AqpoA7^^OGjHy`^|^blw~kfLhsc`fZ_A7hvLvK^ zeGM}C9ZD5ciIfz#jg{}K-q^eJX$L!-#EDyk6^D>8`6RXAhdLaDn@l}c7+MpKHq$JU z(3~TxIeP7|8w$UsHe3-MpS(QeWU(ioJLf?nU#M^vyl1yq{9PS0u6aXksJ;WoBb&X* znC)AtuG_sZ5cq(8>KsHE8aUHslNu#PYIbZJ!%DV}A30CDtOf09XU;QPNq+ixxZRE> zQHA$F9mWwTtSK}niWXrIoANU zBMi+*trqVD?B2pxhzmx6y_=Nhq7DAP9~=iM{v{zH%Qn#c##KY#gr7^;?g{Rv(pBC=wL{6$;5c!1#y@;^|&z_U(cc7C6iLIqEout~%MxejbnMON>YmDfEZy@m`o4ru(Tuko- z5~xrl18f!KdFqg~ne6f}zL1QWeHJ8f^W8%-uVJ#1`4!s0aOyYLiG9v+F7cikc7**X zwSO8frKYsHsd>s?EwOU!PMDsdgHV!P)VxoVRTzL`A1p_yBXCQ5`U z{_w(@mgGNM6cXg+#ScsV7tsfcL%0hUeeu;@W+pvPA3_dq5UV&Z;upl*KltEhX3O&S zjPD{+W(x_)$Y#%8XN)~a|MV6iS;IWhK(Zh3mNnkdo0$0mo3Finhe_KiNzHeHXcV7*tfVrU~?>m@4hF^ zMy~dWei05ux?6%JcFtR9%p5fpTiGKpwIIUD*ch>l#_;FzqdeqC0!@R_Jlp2?-1_Mu6eKR%nxrpKeEJAncC)o6bsQAO&jVa(o@nD z;WED~38df)v{^UQnt?)$!(S0bJF`WNa>ig4yb+LQ2VS(KdMg=o496*WePCoLQyc!m z|GU)@;T8=AzQ3#rf3KMp4}C`ouC#%ruJE)>rBg_6O^vpVs-vH+rq^?UI%LDk8M-LB zhU?0|FXyiZY{N4M(J(D>Q=^QsyAI`4j=5Jucd_t>+1ZYM#ZuJZEBgo8lEZY3*E3PC26z>+0b>_Acox!JH-xNtNoNVr9j`GC}o&lwjU1M3lZ~ zW_s5BIw2c0i$m5h+|2z(edlQB0}-~7A2EhVx2IE%k8#l$4-O6hVSs{)3O>E_>z9~> zL_eu0HZE>HC)7;+pg=>7ujjq*&uYEq+Wymmg^t_a-%^;Gd8XZHKQ)zBq{K0@BlE!7Nw{fdVDj6!%0d z4BHleot2Ggcc1e6qyS1ih}TE!9oqeFCkp>4p-8d?97|cbViDaHi;K5jpPsljc%SDJ z_52c?2HQx3|HX!e6q>bVeM_hhz*J;eK5802{jxL!b!3|s9!3*83GhR=utHbdg>ZO$+2UOhV#wqjGvHan&-T>@c+B;r=SD> z8A!?12pEnB+ME||6m727uSuR`KREtz@=$}tc{qPD(0yY)-06pkN^w)?Z~?2|wa}kq zZ{-nZ*(()g&Lt6lRMb-^4E?$iTs5wgFE!HMzYZ_VFbM;wS_3X<1-{f%l`L?hZ>z#f zOG?0?tJL4cUD3Y2J1x48$tTYyuHIcdln(GZKK4{7YK_VI5Wa)QtYxIPys(n34Ud+Y z8Lzp+X$^Pr5@wHpj1qfOq0>ns+k!CDQ7X$y+#L6!1kV~?3UNe6zVO8Hq9uN1dCXQ{>9uk8B%%)KjOX@X zjxhT1iE57Dp)BpC-3qZi@4ANUBqMzziCebCL;_!yo|ah9E^2jD#|4eF|8Evs?AK4> zkEEiWVM~UtBf$6eT0WA0Uz1(Kp6b7e@+EHTVI>aE3)M8ipDK^7Mh+%wC?ZB@g|JWp zCO5@pm8y|%pzz)&x=xN(Zna;sgArg3a%2OThldeAuI8PMgA~ioo-e&zK#4fE!IB{! zTVt9e!h+pQ58>v8S{=w4q5e+Fd%Y`t#Th?VtAv-J#Tcv9eESp+RYzZpS{R@P+?;sbb3>iC=6rQMn zfx<|KV?Q>*s1W|i&6m4tW~7EL7h*o1D2Sn0PL|Myyg2L&I#$)#&&L0DKp0l$4d>o} zXM>{+J;zD{%~YghneuTfDN-l?w+vD|X@(myi^{e;^>)SHFu^E5fx!pSgh?XSJZ8|mW z7yZb{^f@JRtAw)9dwbteTrKWXg=&@~ua>ktu^mm}U3dLZ|AU#y8GF0up>_vJf`W8h z53^*D4Q zJVBVqfi^2TR|Ab#W_lY%gTlMU9e^$AcT;lF@~9F$3Q_on2>Rr7a_wEyC2Da*pDaVh zHxh67!E$s9&yPat7ByXv=nL&}A6LCp`mtUja1xYj%sw=o&^6S0z)aR>Wk{pbh1!|Avy`1O#OdgRZ+?2otA|T> zH)wm5g3}+_<5LPz>^&}#jl(AA=?G)dI6BCBS25BqMXrb2NFIYus}c0!wSBpNNGmM~ z%B=w7{ZV$&ZJ~bfraEVB&VkKWm=Pw?fQL)PI%eeLpskM$uytE&eD*Mvm(0gfLlxxd zj+Kq}Y4Lew6NS-JK)%+LMQcv@Sb~!Z^9`b~!^iu6U8mRFNUG(f2_=#o%jHo3FCCkF z74kRbn`2Nt@-^$rC~S8KL-S_^VC z{ebIblrEf+=rnhHd{ehcGY(*Zg@SEvA#3P4n(0YDb(nt6&ZKYZ#0*&P(o$#YoK_)> z#c!~2)OIKj;rp@7Bp-u+ymyY26?@`4KAtq`cjY&!wiwxUGHcL@`VlZfz-S%<@Q(cn zcNTh%V6s{3Z=1nky=!0ID(@GEuN`&(r)J%~sL40pnJrQ_B45P+VMey>xE5~@!D9^X z#J4DMs`6Ch73D}VD$xGL$%uiTqh5iu%7nc#(!v(ut#ym_CiT@If+jz@(}nLDW32JE zx7b3)_o!~ww3MfZ`xx2Zl8nD~+NbA?REGc1ZlJ$-l_NJ$++Kc&EUiFkuP&Fyi!7Js z659GnYZ9Z7hWI$afA#kQ&ZU6xQ7758(0wd?VleSNl}~rE+I#KuuQ4YFWgiU7@6(CQ zp9A6s(j9kRCIVejacn#m;`AC98bxM;G<0Y(J;O_VE&MZjCQoRy*GcpZd4cwcisFba zENJ&G8Ay4i0mfYj4PUHxhu#%pad~%5Jw!C39$B5Qdq`-qr0 zo#VIjc0wpqo~4dPZ>Pmia};YD%N;C~A*A1*l-Z0+F+sc6V(w;Bb>y>!{P)EGBzMUz zCHe6&yAnJSc4hmoXXcby^Uasfqf*A40(_j(oyx9`oqo^fdDbtQyBd+`x48 zY9y9S8B(gBHsJchYfBR7SNLqr8_o#}nyoVl zKc9sUSIBy@&wtwTJ1?*v8koi&AAeecD16+8Q>G{M9EFTW`Z<;iJ)x(o6g=%|X#es) z_3N>H$uWP<>&rG(gEGmc5+qN=*62^=fH6DMNTvXm8ZK8*^Xm zR-xg&caC66w=#S5rFrKyIu;25kU0V191E1MHshikcXg&bFS6|v?|M%BmZS1xFRzCe^0)? z!0)vjTXR+@id?VT`*4`aPeGVD(${9^aR>K&(r-u89ao+MDh_=wdIGMlpnfnB|5Ut` zd%ab0z5)cIQ#mI860hegC$r1Ode{BcyblZq^X|lbv72~j@p)5jv~_ZFslnJWEmqe> zq=*(ORlA~!P?PnsKjZ-UQYfqaH4!QrnzgKLGX96SwpdakI@!E zVNgCp)n0j<-35e-nx*$DU`WobhFd+Lup=V^J87u=hW)G-zsgHYyA~nWQpI4t98Vwz za-phrfBPO0wl+7xNs=u~w{ZdgdUGhUB}|b~K4vBvuRPnL$z-F4nlX1M`Q%*FIe#5s ze8uQNA+HF%&r{gzke{AfAe;;g)nZ1aVszneCCptnNJRsd$~v(ZN7{?)!d0PCUc1`9 zpYoTgX$VxEbLj@m9Dg|II$bLrim&8+NQ(AxauXQgIO1@o)Z}y<+(wj>Hha|=`EN~{ z(M|MG1@k&8;&DXYMU^-iFS;&$Y&!F6c<;F);Lqbk{c~lc=koJ>XFI(mUuh?1F(+M8 z3=>Jeik0+`OOO#hd$cf3`f=++HyE`=XQA_Hn{z5eN{>65(Y{XSah{pfz-ZgyM-2f8 zw}h2Fg|A(p%nImh>`PU~;^9PgpbveH~KR6QTxp zrkd2cBEZF6yn&i}K(-zYa6UYY)NRN;uYe`cVD;$2*UeHg@gps&kltAf9Uu*9Hu;`9 zL-ZOA`lirMVeIoQuEF2YcM&)Cwnx$h2-L9o!RDxsB}qx@`NF5Dd{{kAog9_oT9bK3 zhJZ7o-mWbz)$^*buy2A95LFJA?;57D$@D?te>zJER^73TwUUU+1?#Y21o8A+?Zs-w zipt$BDtaysM@@xvU3h-bsw#mFlfnR#M%@x6X;Isgt`OGH4diQn^YQVF~U$JF3 z0JgaB`^#{^?zWt%*3*)%Q~} z)ABKn)3lK9&um>OG;_$Kr!;ag7d>#BL#e>0VnnA5^yFc_Yx-XeYFYyhXrzhYRme*8 z1yO74#}OGXn>icU@1Mba&-@v3P%7I~uys+3)ENS=1_A|PsykuHO1-bUV(7scCV1y# z0&EY5ThihcKp;25oK#9akOcCW@hiqg*uvs^AU>&#Bs&XBX%uZ9vt2ja+dcIvfIhf| zmh@bO8K>ItBrzKoaiC*DD~r!lvPtfN_dxG}s3Zxs5h&k_SajMd3KIcnk+3i+TP)~8 zyak;F2EaLw`rH>Et|Tt$maZ@SroNDh#+;1U*p z*Vk!P8hso+H9px3O;1S0`>^>4!^Cf=SHzaS2B|{-?BA5#?V9%{!!F)G>+Su_r&%Ka_AdqM_hm@2FC31r(DM%89`67<$E$XW=8GO? z?#N2^)yd12_CSYWG5(*>P&Ee!qQ_s$T@5Y^B|{XQWg zEciPzt1o@-erJcO0-b=TB(r6uVZP!*xCI}Y^5=SfdS&bI8M>wQ_GcGWI;1O*YGaYA zC$cA~v+Rm@RJ83OVZ1d#EMTsVjT3P7>kstggLnmLUDB`( z{iTfOG%VuN!{bar_cp5efH<3*(T16z70#4HaC1y#ICtINT34Sg$XI@9_RN*HI*R!` z+PA>HdXY6@_Xd>V0`;VRYLx2up*@jk8zr#!bMS2*>XDafEH zic?*fSZd4A-a!*M2T)=-+nV8J|4M^oLMa*ss5)xmgwbCHA7S+_wWn{Er2GOk3^(tm z2gf5-U0A)KOWZ}Eu?3n5#K`~dy9y*^z1W- z9cu2{1KoT)1@B8}zmRU|+Y}4snspv7?}LGPwKAXv5;<>;+tDNBvy4HKtFOHn`)%rT zZOOD&=9pupR4jr<`Z;Q9cp0p{{|eHgoLh;U;qEty8PIwVW8bzjZKaF;GkYewRA5cP z$ZT;&MB6!^Uh;22w?}DlyNS6$jETirnNulF9HSc4`F(%jPVnCdS)k{$@RG*hQhEr-W!VO%-}4GZ1V)BOZHh%h%w<2XAf8$VU$H%>eRxleVC zHaM6y+cfh5KeB?wjszv*I}Hyv+K$%DxVEnT5zSw?vcg^OdZuq}G)vmyRkj#6y!?u6y>ztX|u6@NonEeiMHBbktswk}L@u$AybL%|l5 zTF}0>Pl~E!{$vkOg~)R#ik_(suQB4ram$xfqDCQ|c3qUto_9nH+0kQ&xGZgwlFX?m z#m~P>U!33+WaB^!lcq~t>H7_E6-zG=t9wESqYZm0dx|stTSfR84jSH^uBtJ{!*FE! zBgiS^`_8G_07OC%QCy}NoJux*ejD>9ftebR;28Tn}-|<%lvK0IQpQhz( z-_qzcqxTrX!)u7Hx;{=8|9znJCWkPHUCmP8v+J#9>o4z$;!53WC&e}9aS1{8ijp6k z7n(OEL=^c(u0M%;-m3TAiCJ<_NYCws zN6UPV(_fyyYTm*d)C2yf3?WZr;sZ z_Jn}R3&a{8v~u(lH=_-8Do{NwZ8YL1m^+MB2+)vC0XvFgE^$!*>}}?KH%I4LnrsISRn;QZ@6tBRzPY^F1|05WzP33SsF#U-FV9W@-^{P56tQul0jg zIQ_(l)~wbzD80poE`iJ>O4<-W%~&kIxVm8i^0S##EM2PXhY_^a4p!I@HU|3JC80B_ z%lGEf?YbedB@3r>ly%m84YX+`Go58ZHBG-VnT3@z+4 zYG#aw{0&jzC+gnc!iwO8jk{q4@*7I3gM=*(b6WP%>60%X>?9)jZ=d($%XP$7N~ zy9R}(r+8iBCO%OJ;X1QY_f`&YZD15KdmIvzeCzFvM+*QXfk&b67#e9YsvTm|+>b&4 z=%+AOEJy06m`4(-04CUHu9_#1r^{JI(^_$3LO&~} z?KV$WS!s+QJq?ppuhx3Xu`BPI^=Z^U5Ma2G3Z?gL*SA+mTa#YiB;gODn3vq5B;NH>B zUn$sxT>>U+HS#ojK|=Jmfsw0;ZRqyefK0bCyuw+@2ft^G*V$p0hB2RXO6-iMr>5cb z=dBnZ&v-moeMbDJQe*;r ze=&_8IoLVke_|Bi^hiJJeE(9==eQJMw`tAqG&sWzDNcCBQisfjF4K8jIW1o`$p?g* z?a^^W8PY8@ObIww0{zh1pDDd*I$<8Y#QnAB&5qa!m!5(ZfC4T0ogyzc=>A|*-*;c}vQ zkpDUvA>D8x4|%n4Q)f07x;fJP=M%4%iJHelL7zW}WJ8Q01b7P^Y&3X-59sYJ_N4`}1n1hXrgODr}p+9^ZI}rA3U_2IA zt@PCk!+kCPy5J(Tpl8v@-TD@mVg^ZlV_pQl&qkRR-MO>F_=P8)E) zv~9Q71lt1snAi!JGBVQpX{Yms4nhOf}sqdA;RG+#@nCajIX zU_>tlD}B4}q&}3nP1&skr*cKe1#D)fWh{s9i2kLkf**35DBcD_xnIhI{jkkchBDo5n+EVBOExC`X-sm3@ecT;v5HWq zv=|~lMAfoOAsVE#<04{by&ZD5be;$fig)CC{D$OduY?@j`)%-7;~|mX$4B=RI#(%^o@fp(4qlu*mTx)9cec>QiBxW7p-WtlCDR3=aRtnV6SewgnS;_ z4?67bNHzK~*pAQlho3ubbx&LZ0IWM5U;8neqQd+D z1!aj937ptsdW>_w!&L?bm00dR-nsG1DJQm%V;_hAepn5A>$n?rT7BckCXbhtwCKOT z8+c6yJbgt^P6`bpac1AsABV^f@x8Ui?ffc6y(?f*&m9gQWu!YUXj@U8w-4!_(jT2hI20gXLixJv?9(90uqlZ3l%WV8g5TjZR<0Wd0 zlx?mR?`HcDmHlUc(nV&EQ-l`c*n7-E)1A_H*;`oF6k=t^&Jmfw0VDivQK6}%GvUW1|7YeTu6DCZBl^G$yFf6UZHnvb5{z;w*;8}SQ${; z34$A=4!+rB$iyM^1q3vd^j{t&W#zWpgw-W={k;(n46}s070!9-?RP_PMALP2r-Z&k z*It@-^*k|rot@n8u=&Y1$Q`U1)N3Y;R)R*5 z*xNnIL(IuE#4xAxtHoQDuWz09sP@NIW12Mrzkh}U`MvVekQJcXfY+uK|i`Aq4a zstmdNN*ra~QzE&cDHq70(__>x2VwP0YTXN;9%s+`a2dU4VsE4fC)ua`L+rG!qZT(3 z<+Kr}`q^#}oJO@WIhx#r4m*%`xLCrT>A4x@mwVoDQ|BqGscpZAl0!er8hgTl`;aeFrqv z?;k%38CMbEDrC=uYi7i?W$&5o+Iz2LgpACxT_Jn#Ej#4ek(EtG+1vl|{r-Nx^Z%do zKc{mXZlC8qpY=TN@fsB&M%KphTa$M9k;?Sb0K&7_^iid!E3Iiib_fu0KoF0GMxtJm z^=j+E>xP?8IHkxDa*b>Wqa%-9t+CO7wT=55-DViq`>uzPTQgVkWiTQ+wnIQW^Kd6NB+7txL!i9E)Dtwm zCx-myI;li{5;glSi;xH5iLzqV`|JsrhJl1Hddcm1nSFk_zw7Y@FrnC_nH)Xl{v<#l3FInI>~~)BtxB-dJ>d6{EyV#;tB8F?-f%?1`xIHF89MEl>&u)-$on$%IMI3gDHeTbF3^P zX>8E8_vMWEkJO8k4}d|CwVNE|U_$xyOEzAi2>=&iC>eVF&VYY8u)$8dGRX>F;Pa5U zYx0Zugsqa*XK^Jwu~(~DiDv0US5da}w7sCX&=7l%ss84P8ulSuz5AWITs73kJayZZ zHG`&%pR-S&i?orqo0?d-LFA;yOALn1cPPVo?Ty#X@ zO)WV!mX7uhL%NkLqWVCa_}AJPS#`g z)NQ}-6ehNq@bNWjf^U?A4Ts;)dKCkorNEk!-@CTb>&FXz%4P_e1U+hITJ&>%MSiUZ z54VlB@9r0TlZ_4v2jOZqhQp5Vko0vBEf(&2-l#5&^VL6WonV{3dE^R{3cAhyD&cYCHf7e zvhQlCq=gq>nvnOdVj+@#=;0qko#}Awc$_;P_RT7*CYHoHatV7l@c$vn&2YT3-t)b~ zPXW0~I+A_;Scm^chm&#GkcHMYpjU@eAa@A#2@7qZDL<2~K82o3@cN$J;<0+&cSQZq zm+Y#87D?Gg)8lnGUsLFirRy}AtQJ;R?h4pRD+h)uzRE?y9|o`)G{UH1h3l(&f`aFl zqmBDBlRr-~wnbg>t@4$O?qckX*8F@C=^mpz2$4KxF31-dAa7p|`N-#jGblpVc_2h6nLQl&U8 zRUebhFq`~%%X>6`216)*;=7~!e9exl)mEeIEJI$n`AU&*j*MUUXbk)r%^j$e*JGe~ z`(vJ+guSy!bEMlmA$RTJGyNuq>hwegV^{w)~ zbwrV_{j68>-lB>=oGMx8Gts@gjcxK^GGdaC5(-|YH$cP1N9vlIJrG0=pxA&m?}eza zB`?VcB%Dmhv@k)q9Z(OA<|c>Kv}YwmzV%SXUjiwZpT&Cg#+{eVL8gIX;@HO?39qN6 zG%+ku%F;1L>Sv0t>yb@2O_IuOS9u#f6Frq^x6Gj`_lK4%H>dH+OXnkk&woUke5 z&_#uJ6j7IZt%pAuRc@xZH*955JKt_(?o@T`;Nrq6|7HQ$ zvj;ut(vcW;qJ@_Lrv z3Z@a#ViH+R4jbh0tU0H~4@*0joiIXzyJ?n2V?&A7Lru1)n@<1W-5V-Kwsy_E_yySY zDYEeJzM(8+n6PEp1HVpTsIK?mU~x>Qz#=rlZc(;tuOWz(qm4Q_^pS=~|F@#{r`%rZ zHPNS`^dPp28=X%A#|FI}aD}I#UUApb-{5uRf|0W=`QB5fS|Dr)|k5{c{IWpd6e@%z@UPV;v+5It4~gxle7Z$leO2Ho6?L)<-HPEoNNeXk$Qeid zAZEki5NJxMzFX)pA>g*lWr*>RD*}iY0L=nUIL_VZXNFCwYNMcd6X=iu3S{1xQnXkA z9G0zyn{}7hRT1p!CB)Dzkz`GTRg4pqEIBmli~l!jmcV0sF|b+`S=s9i4W75pW<-j` zyo^)>)pS=t%!2FQII{@mtR~3D#DyY@_uWTXRh_?eAHk0^egU;yW&lrP4?Nw5jgnhA z3V9HqCVPLFnOHPj{iwxVXpGm!z?7r|8ZyF`ZQ6p~Qq2G#>GmfYL-&{c25P<)*kzUI z!*6k4vpy?n0CAJXf8N#ZmL}#5$*m;fU%>ZY7@wySsdNN_5Bcgws3Vo3zW6d0}HoOxUHCC zg_m32(U^?M;H<1Ztan-`B$%IZqX4T$KkV>pq8@fkhWM#5Z}JC8eX_q9QBkI#{^fWd zWcz>kZAB12+xL_SEK#W7!Ac-(ZTXQGEl`dHs%E`04PZV`1jE5n*&PsCLnvAEBeLad z3T6BKtR+(7Kq9ha_443QhO)|_$d^okfM^a#K`+gq)LLVIBSI3>RpCl0`g|ge7z1DO zyY`vErT#5*!2BUp@p~VE5?hWz>*EZ9@aa+0++>uO z-(NTQ&8)B(V(2^%9}JGyq>151Ag&HU{UjZoF{;HuXZioUs9p5*(5KLJivlL!oH*H#^a^-qe*~8C2&V0UUxK_OkH_oreTji zjgGYTYsumbnvqH;|NS;p0#uHk{tpGY{;M5;y9WR0G5@@m=!&z{PBA`AOv=Jnfsg8;d)|*8~3Q6V*|4wG*pt_$+2|viC5-psUL%# z7mNK>lRv$7_e+vLZs6j+ zx3T;AYmGFw&XkYh#g~RCJ-^}9p=OMCN@-`_RmVV@klFChVSXpauJ-{)E zNlZW3GI(E^^jCUVU$a<0WG!^*7qKLyQ=9bznc`o!p?jLV-{tFuaDPkhCco&s`rCla zfeISPbzAbNE2{%*?Sbk_N`uZT`H$?MQ7BGp6zc3*Aj8MfpbvnXN^`iSd)}EHkKJ>< zvb^BDHJe+CTtM$k%5b_1=;ei$My{LZ2I?4Pv(0T*+z#e&zJ&JOSkdDrg5No)$GP_{ z6>*-@wn0(9DbLi@q!*0VDb+E6@B|C6z?L&|*`pv{j8jp5|M8`hEWntN#WaNporm~W zght4I6M9}%Vnx9-6EufDh6;>ud1+DsS|>D4P^LjER*R-vywM&Z50!$0R)E1OVwR-IA^_005B+Q+}4{cf#t(J4$v_ z1g^HBawNaw^DQ}kbLeJ{gE;$hd27MR4%q1D>m6c07}PwE4e?Oed28bQh`Q!u%1PyQ zBMmtiXm57v5_&CwY=y?wP1dmkMQgWL7DCl(fyr-wv2$j0**zg#-r!QAk-Uc5&47eV3x zywlfgB5ol}@mRGHRIG-w(=h=lQDV6p1P$X?_M5DGv$$JSzQk7fmdcN3A&);szLb9# zqe2|Ro-UiZugq0}hWB4cQ1EM4-3Bflk?t>0-4VfOEX?x5?R0cBUR#$6^>zqf^m32k zL1Vz4pbn!>N5EXq9V5!Dq-On1-tD%n|Iw)&Q69DEVpC)3gB>Dsds$4tZS6f?Q8<%@ zv?#Gd1y6p*E8}caT{v!TvFpyPQZ&?io!ym?ptpT2{HCK>_3Z=4p}xC{4H|lT4YK5{fm`<264!aBQKpcXP$V{3S9$8TzuNc z%-6H2^|8G#fv8;e6a$S9V5joNQ2R+vDmak})F&%vsX+4vf&Gyg7)3SQI>XB;PJiPu zdtY9koPHmLO&VU!g;5ewIFkd##rF=$bt=_qvzY4vJ9*mkhLEYY55Xp0BCB0#L|l4gW)D4#9wrtm>6VYrhfJcTW4fx3BeT&}sLNdzCkC``$R6K(F7dE!jQy09 z!P#Z#a?*#d6gXusT<-qole6M04O1n19Hh&J$K$rVT_>hM1C9kASNwRwA2TO;E*o>U zDTF&gd)B(4ju@%lhIPkn>E_(ogJpwnDl>+y}~)I+3OeJHGX%TU5V z(fq!mk<+q_*xX{msd=IY$;ADDWi=ziIc0bAg+Uh+kxW%iNFX`M)${~MTR!>UIhgAt zz2o-4#|?tWD;@ReyXtYd$GqPl`$2 zNY+oZ@NIW+>n7RVu0jSE+&20eKA`(Ri6Q!enf*&4Df%aa5#iL>Pluz?fcE%y8P5rA zY(TIAeFQ*&RE_10J2w*W8Axk7O8_IXz@08Mg6HtHdN%Tpi~!?9cn3$iFdXV*nAza? z(nXYQw`u3Iq|VbK@7wytE?UAy@lX@$OXot?P?8@dF;b zc57oJd?`g&@nKM?nCxPyUvH%B0rQ%*;^12tt%z*FryC-fE9m({Hqgem)@!gPLHez* zCT+M6THAFILj~W-yRLt}`@NsY0H{HE>*ZYXz74$Pt``92BaFEfm*O&O<01MKAiB)NrtxRg=G@&9l$8e>?9gv zLQ~hAazDh?m_QjDnY~R3i>tb2!aUa$)47w1KX&mg5Eu>78&N!-)9l*cgJwI(wo0hin<)e$ecA2 z@#ltmzhhWccU4Ym?@H5s0b&>q_P~V~Y<3;0EdFwk+^_VFSx7=B9lY8A)t3@-#tJ7| z)=Y`)Bk9Qr`43c8u;(xHWU4Py$}$#jc)dY&7*iR;A*?)OU+rGyT|8 zr~y98SuZnYmS;d5JOT~&e5TI#Y#&?6?oAC?5I>hw`mbdKastvfLfqCHCpXl7CNks# zsC}FUGnDELn*#wWb-3C-&SFAre@K{!7yXgJMRd{wtV+0xJio0(s0^Yt-O`<@i| zN$FtMlqp$%sydPtjS5wrlffske|j1p)phxJC@f?g_1c)uqV?m(Gj-L$rk?5IsEsp# zw`)D8hq;!j`4m|C5h0jPEDUAIz4(3MCV=5k?j(se=@DI${aL=hWRTga-uVFex0Uv8 zePzTm`Y&8fIls96;+7&uD1f zm6MYJX*dr&@xye%00WN;Aj{Ih450c#RVU&bi97@V`pfn?2Zxi1zUZ`T)VxuLlMA=- zWN`Zn*nHM1Ky4=H5pOK-sw@w2YA!pQ*kr;@M3T|;{Th+*2$*Zd-RoDzR`2OQMDQeR zC2bH~^UJ$h&Wr!iOWN?aQytvH>s@?26puG|rHtp{&B)vhP+;CXw8o`1(l>)2Qk=XH-(W0=xq2D~X6 zO#d|}@?7p>rLXSsCs*~U%^~TGpome51Xa7=lP(k0O4NtVZ!73*AGb5}jQ7whdo)#m zo(E`v?h4nWD#Baf6}GmO4M;O+qxmD~`Eqo(uO;M)B!zJNK~ilw{hyBjiZW)=9=6-h^gh-LR8RPI*kr{@dF2nv-j>9&m4Ls*tD z?jFy1&p(`(pxBPepwTti(g?;J^wC}j%^XxCxJm6DOLe3-ym{dAAJpMWuj?zodZ!OB zNxu^;L5ClCOnbgx)L8!Vcj2=)rKP{wZyp(QL84wg%LX(f<}w0Qyq0P&tQv1jdEt-x zVriw_+kbEkuLP5#47B;)Y{Y-i{GhZEHV@z#jn z?H9j26s!(vS9?6whld{9*LrYl5p|xnY8Qx+ky)kNUuiT8Vjf~m z50%=xV(^&E2K=nV2j_PsyLN=dW!6L2G~69a4Faxb{O&G3GUim6d-7cbKoYfyY6}u6 zn1f2MM;t3E&Dj_}o6zzNul~$?nHg4pxYIicY9}uu?>_?w04ICIx}N81jn5;JlI0k7 z1Fcg1qxRYGS8yIzcM$DV0Nu=c5CgWrg;oiVB%sk&PIUU7hc;gtZmEVLeS7*qEKChe zbby-2;pu5Q%(Ye3ryz&^R0k45E8J!Q)bmbh!0YIyVDOTI-C_S5!!~%=79D}`Z+t=$ zpWp6Xw3+(TjzrLbsjv5{-Lf4nLcpQ8q<{4RhQpOLy3NSXCkR^8RD)4kzfHNdepU0a>@@Os8zzFL z`5tYsY*P|Hi8o5UI$G^}1hd^^W*YqO%69>{qKdVVRPU`zG4j2k!;(I5mYX5C~Pkm35LE6X+=puzvS%KBqPtW zuJfGCSKyA{1yg9xdhO{frZL54*-f`TG3^s9^4#MOj(lEa(&8*o{P_iBtnzO4JHxG0 z3EL)t0vY?r6>}=#ji}E$ImuDY(JvdgBy$Aj3k=?u=ssfGB-=gnaYotU;VoL*7!m2& z+4q>t`w-R`dXg^?>7m_`tg|hfC|KWJGlekepR&mJqOFfeA z#6)*)+^{0yt`ke1o5GHr#_bVRu+4W+M&uLh17`-ck167JFq)sWJ6)M^vi zR)%Ji1rmpu++E+dA%sZcc8wc?nJ`1TZ-3gQs`=#RAkok|WMG&WXu8(uCJ+qjV;-hy z5bJ4JmXsp1<0P`N&qTd+ks5L_x}4G{|@ws z8ttEfDlov{Z_tb&z>neIfzZIQ5KL`gVgQaU#{wGmMwl44^J#|yLl7_@G;pkT5E?Y% z|2v<5rx12)DS)f}=5YWX+Djr}$^edg<9WKj`o5sR%FvLq7lRJNd$B$6i&EwiJqDUA zIHlW}5OsiJyJ>H>QE$m!rwO<)Tn#^DR9x&v2ZH%o$P#jS`2nExkA;a@QP!TH&jgg`&BV}h zf?{Jw+MuJr=3;bIwVCVI+Y46Ba{%hCKrk&n1b8YmMnvH5_W-{F!O1|jdv9DFot>9Z zC}8okZ2q62skphhQ_k{ZXz(zo&2NnzfXirw4U<~dKNg8#xX#B4@JDyEa`~N>?pB>w z*VK%p-36XEFX2CNA4`vYz(oJR1(?}Do<9fg4{&|-6hi|y9D&daTt+}EsnJGz$Hx;g zqR#eL@eCoimQK(aO(W5iP_}z7-#;KGmUxtvoJ?+r`|l^UM$vgs@i0)T2pb#V7W&KW z)&eLCX@|E4#xhDh55z-zfbXw-d!EEHcYtjVEiG*`f!!9i{m}Z2M!>mfbUw}Bzdq`d zy#cH?zn2X<^H1BK1bmG5yb+$~Qc$StD{Nk9UP?f{zo&W?f zm&#vZ+)qq5eBr*OeLg>_on;gJeuX`T<{rh)QHxl9^!;v4mP|Jf3&Yp+HIa1k%W;ty z`BJ_2H&%ir{3}-MR|)i}8MElVG6ie0aJqdNUn82paDB}#7wPX8^`V5RL*$&WN^68f zm9&MQ1W(vjAO82StXM;;U&Z-_F)+aJ&D@KnFL3nb{WgkaU$5|EM)d7=JEPa#93 zrAQoco`eCh1|FCc;L;N1Aql_2VBr6*Ix34m&lTw)3c;bkq!Xr|;RWmmwshipe*UCb zuv(fz#em1;Bpb0YrJ0Y^C-_!^TzHa@8OrOZJP%Ewc^k?gs|IfqKvcnC26?b*3{?yW zCQ7-1?)h_NNy+ZBY585DAu|L6EBDzaErRO|4~n%J2c0C^hW&4O+g=I%>9fr7?9!8# z(xo}tAMvKEj12ZoM=3oDmy_q~Ve*+tClk4#uCjS^4KGyB@ocQ)JMxl6@vkJS!SiN$sMf8wq&~J1`;AenS z&JPB=5dZ#XgbMKK(WjQUD_@XazB~z*T+5Tn45m(&%nYI<>aMx=O2iXhI5zo%M~oz& zMGM%E7llo{eHLQTg6iiIboE&Fk(87H)|5b`FD(lz{#cG zj}LG7iE5chyfSdKd{-)N6U&ASxR!h@wT zl%~(~>NBYB4DLnz%CS*?&Urkze#&T&L`G~k-=z*Z+X^1W@O&i1r4ZOgpJ*~!O19z* z$zm@3#I^I4yw(#pSw#-ir6(q+3%&j-(OrbRNqh<^NvOKcc|{0QZ^VZGGz(DWx?RPP z$dWm+q94am;QXWV!6d_LF;n6rQhuh5|A-J8Cho4HQBosR5KrU&wyt&g+}as^MRL- zx+D@ce0`l1tDT@MbJsbO0`-RO=kakXrJ}OO1mI-^rcqE9?1jo__J+0sdc`DOk$5CX zfdVng^CbUgbIv_V@p*`RwqwRCjZqftsR6|RReRQLC8B{2)D%h?!Sp5+AQWwb(&kRr z=pym#X6lvw^^mWYI+Wrqtii2W`662I=t}nXN@CjS!b5CBYG>K&wpDX;OT2 zS=@t4XlIR-{RN2^!yQaGoF0j!(OT&q%~Yo;z0Se?=OJ^M?|r{)l&^onDE9A(YC&1j zoshn;!=&{1c``WPp7Q#78NER)6_@%&N>qGSL^E-3$bNAjwiukE0BPsz*y4H~&m{rk zmqqXCO{KU0u3jRr>TZ^OTO7BY9;$%z)%(b0@v3glX9*+9aY#_9*1QwTkFMY!ZVB6asf zSSM{2*^B9!Q57KyhiBXDX(LjWQ1 zb8IO-WDK`+Y|%cCe_n5hbsqm#0);}meeT5Jt2%H!3wfy1nZfZ zTqmEH%9xG-VGlyxS@<|TYp9Byu+k7x;d+`A8f|>OFpYD{A?s35U;=m8nMe` z@aUo5iSlrBGkqqQIxdt&PO??M5F37Gy@6b|yb^zyGnQGs7j`!AhJTY1f3*ZW#0Flc z+chia@!8x(vzHHrFx0ZS3ME9zSdy;ktSIqA{(Wh_^QiAHizm~4D@AUG@N8XSJr?xO zFq6()?An$4=J6!M+;2=d<;fVXD8l;wLiy3hu5GzEg3qe*GBnM8!!=ratvJ&f!>=Xp zPogeOZ~AkZp)~0(q|YGX95RC?{!h|}@9jTRTb z8#Hd8E>ruYm{l&npQ@mh+GJ)fdaw*j5s0Dty3(A&8Tzn~R`4^#f4(8cEQB!wsnkLn zia@yS6z0!-UXPxWzH60_ABh(d8leR5p3Ibh9Y$Z7?$7Nm?cO@lF6NsR95FiC+Vbka?`Uk-VQ4snV6EoYkcgJ+#--r8!8N-0mx~m_MpH zzBl*#*#5SJRPk#nvkQr2-zMvo8HN_WgoiCdj|vXON= zRPlQ^&7=20O{>Lf`K4yZd{FO^6vg50n8!{}$;w4$aR_oA(OnymOS+9;I02Z5=g+d=?3ft_8lK=q#!uvsv^Fx z&3sKLzI;xuIwgD?c*Lwa%Y8H z%&;@;P_6?C?U06BZ3`MHicG1s-K|{V(D!&;U@6o3wR>Pn{?zE-Xg1(6bjBK1?X@fp z;*0ekUt_y4Gnkvbsp&YM!79ODkw}FRWtT<-f7Ke>M0SI#YIob|62Mc@Y|`v_ndyq!~3aZ~q`hJvo6pJ?96#=o-9Tq7a{aN8 zzX(H#S<2sSyd;mxA4L!ck*IO<7Uvy{eY}hb_R0Ec%Xwq7289&sW|N`3jp{h;XY(uY zbfJKfUDCFgpiuml_3gw2y&ckwo)RQ!>C9PhPHQSctiNV zL6Y=BB3W=*JmJ@=1$dBHEUb_ zVoXV1C0qC&kMa~E*UM&&w(daTHU6{LddI0Ju2yD`)S2CXuTeavc$|K`e*L*<(spG- zY2TMb>{>Er&Ue~pB^=Pzy)vGq5q@?pvHwx^ahXve-G3{M~Pm)2;cFg0fc8Hy6!4IB1ltQQUEdAU=KIe}_2Uj+?jT0J~>N|ePqEikeD-1}Q1b}?sMQ`er zKgz6Ia360)#=tK_@{C){wlCgG5#Mj=;aIC5b{PtP-@OezXeaH84D!T|->J3#n->GC zgMep5DN+o_9GJPo>Kdgrui87{S`M?2zh9yZ*)H%$97uaBrZy5`-?uQ9FsP{S&xydqWU;WyU#JEltX<|GzgT+s6!kM0@0les?9 z_F_#bc*#zNyQ(s3ggZyfH&zdxOmDlq(G8V({JP7y|2(*RrXr{Dz<#60P^E?a1? z;|K$Gn37I$rhlRz)K^#+K=fP&UZh2J#Ojd&TQbW31X6aUate^!I_DurACt;0jSH|& zeeAfb6+fwh#2T0N?Fl>g9Jmz+v+N6{Qur?fd|Nd)Gwlrtlni=aKMtKgKcquVg9x zi|u4QG9RErt-U(0Ljvf|#sgyu%rF&rQu>j|>cW>iFqVcagC_SCrOLcYIS~j|fjrOn z#Cx7?Hke3^DFQ^E>+9$%17R{OIW|Lxo>{6%2RsEMzkF1%Z{^Y`u1)_t;Mp!A*Ksk2 ztpq{`vP_NsQlye$C04!{CVgx-KZpa-p<8RDLo6Q|KcZ@^p4+R!AyXq;Udkp} z>nlbb-+NMsB(O(cSy-aT=)ZnJH-h5U+I8kIzS;2IKYGmB)}sbLp=`?M(YIV}q^kS0 zV_zDYr%`hA_hjDh0+S(Oq=qU@|9T*^x5-uGFord@Z_)cdvAix^&sv`;-09($Pp@0S zRz%^xS7{d#*X!`3!SSE;9WPjYMREc6NiouGHSe=4%RWk(u+0ZVJBNnEkSme;`HnNM z%+{?l^BVMD;m{P_chSioVL*_(j_L}&+}{|cz)B5GNzzjJb6ho9PN)|!AA7YXtV3Ji zMe-P$qIDcDvo0QQjzdoGm7575*q+w;bF_8ye&gH`*1kc@@9-yPJWgSv5PFBm(I?y< zft!G2_b9n^d+y8L^Z;M~w{J|P6XTSvPCv^470fKWJNF}qwB-D2yxY*Rqth9CHc39A z3}nx~2U)I=w#5pXS`70&eQzJwA7SWsp3&+WRNR!)ytQ7I-OmvSEQ=)IK-UPoJQj0k zJWF}I{zrGlPpz16_6O(Q4J}s|DWixlu3%G)6!Tlw4u{ zy%4|4vG1;x!h9g-3Pn>cZ8j1wZU_=XVe_D`X0A6d8YM{@fJMc75k!DoXysZAz?hN$B!( zl*Ql^QLB`rxn4o<^N+JAZTHzOu_-{M9dj z^sh~XM~Bg9f8f45r%VI_bC3s^1|R$B=)T3;`le8V;|TyJhp}DhyIrjD#3fBGFM2+B)R`q zZ*tEWVxgRn+!n)4J_}xxX_$03>|6#J2Kw+(I~muRAAjT3HGGP-Tm0w6$5Lx!@`Iqm zX+E86BgCJ{^9Hoy)z8q$aOYj}k9|A`7j~*7^cL z+^fkdEvm6Zig1rx0s9CLO_vpC3oaaDf7Zd1&8C^|n||S5-O*(~mzXzfxJ{uUqxN0E zCQcdBN7{y%_C_EMc+`oo;h3|T2}xRt3o=7~11=^AIYk}gZqn%2qd-8UYN46*dA$g4 z2~*vnY0wLy1_BM>(J;EOx-=>;L9S1gs5*{>W8ZI+IYuH?3M82uG%id3;ztcDv0kvuw3Qlgx*PzDc920+JKM`xy0x?Y3 z-6#50^&c-p7#rQ!vU!F+KH}TOl*oxqnEw1OW#Bz9f|%))nlE>&bnzVtSX@*iEw&P* zi+No(hAJ4d1k#Dmgc?3I8~d;FPH7^7A}&85XHsTp*&SZYdoRvAv{O$Ger&#P$!;rz z5oaj9PPp29vqB~vcE ze}iQPf8sH3zzCO$ef;o>OTg&2n$&)dcWsxgnQSdycpv2uj=~(+B~vK#B0lyTV~{wW z#A*onT*vjyXLjxJdTk;Q53#1+i@_iMTVb{X462{sV=ZQ%G#x)PtfZ;RDm49~7#l7C z>sI^(ORQ)9j9h>AdcCx-YHc8`?i*7fPzaVs=<_P1y;8|&p%HcZcKFbe-3p1?O1d-j zUi7Nb=X%L}8w!p8tEp0vnyQqer92ufdY|2(!O&4MgRWeD^2EyIr^#_|T8R8K>;5tW z`4b_hW?e;3<>7KSkFmL|=dKoto zDG@!9>D4zmxen^aTHW?1nV6#f2S9nRq$bO6VMIrk78AEkvPcPze&m2M``~%_wwD;X8dgIs21nWIr_F$mmN7hs1w zd=S5`Jmd+b@MqbnrN*6d!q1O0TS2a3krJODIyxwKznXLU@O)1C{m0_8BfHkf&qXnD zmALWz(>#Y;cJCzfSS<$)QJ>$M4Xy*2&>3P#;$??20rqMUQUf%ia5-)A+iiq&2w2qN+`)#+I&}JQ zwP0(ek3kGL^>P^tJQ@FV);MVph=ed`$}<8;;{VF*&VSekM$#l;<3k5L{T&YTew!0b zO6j5gAoT{%Wy!*!sfm;m@P{YjijYlWFFrl#Q}Gt!Im`_pPUlej1FoNgjF4AvP8zP@ zGuQ3l{$c?x^Vb;{lCHb6XBwWk{jq9!L;O9tNYT9?Ro-)&DfbFO8=LRhFCgJYucE_- zWEWrwi4(Q>@mXP#DX6F*MfmL6*y{<1)+jMrbERICtaC-;97%t(^hIDQpcz< zWsMQLj(WdU$)A>+?sp)>p~)(fpcJ}+53Tt7ImYadgJ@rKJ2eWn&7P!NixCbIF09vry~3jrpb+ZwBtyYxitg@x4}1;RneM*24x3 z(;6zVZoNj+q!id71U=7NnQF!lqsT_qtfQCtTFTpdN@GYa*v8%`{1K! z%!n<_HfR2D`u#Kg0GLgO>VEK@FQr)YiTB9`ZCB4TUw5}YWnMS;r0j>4Ef4Gp<2{}3 zo)8T|Y`jO!?iJ7zLT3<=MCX7w8Ekk_m(xF8H;^~6KazxtM&u!Dn6uSaFs<`vZ|lYY zUGXM#AB8&i^}YIjwJRql>{Dn^*{q|LaFSK<5wSrNZ>~GjnV6`t#{RsY-Pk~7g9<8Y zdfY6lmbcKdm(ADHeqMWLr~q6cweyh=Hm!r3cvIS)I>aP=SEsC16e%i)9Y$<6&F^;+ zov-~D7QjB1%J=(iQZDOM{Cye9Odbyv6Gi$VAhIpH=5x(raVlf5Ul+s!15qN0(6j2& zh7NVO%cr=wh`(teu?4GL{L&PWj5h*u!$v>LeAImoeOD+YMkh>Tl$X-G2sFG8VL#oN z&Qn9<*j6_PCrg?}3kE#I)xNnM^?GQjQX-4mrfusYtd%al1k&9X_N5fGnz0S~t2S)t z_uO~<(?im|xY^}J%0eFfcS2rZeO+;o&GRBpnrc#N^S^@?FYQ0)QYmH~7|Lq^x&w5e zX72<__Vyuizm4)|5r`E0BgV7;ly{V%=j;#HKl(BGpCp^=GqHaoO$v}3Vqd2uP5GoV z3Zk05Hwgb_@l9WHqCwx}>bEK2t=@G)a$3nE#_-G9oVS?_ zswmJM<7pFRRsfwIsMB-x*8jC#Ng*WufnTapl}w~KrL!U7icNNVX8xNg@u~M0@PsHB zM)q{#wenWxbBx>C<3atbY>jT7)N|fz;}=yT`}vS;T7w+;>aoJKW|P5^-5mk<&6M=i zc08elzauDIM82(czV`jlY|#RqL&lKzSx9!b|Mkd6l#gNhofqKTN>FS1i_8et>gvCG zNE*Y%-ZKm(!a8xMD_K(>;w#w=rGIwD#T$N@`ZFGNgk-C6<5h+UCV##s z%f3Uf^@el?=t1itAu#oo2;XW{gm0%UagP#Qpbpe><50>Ypyw+W5EqO!1v;5Tpp!Y4 zNlf=PG{X76Q))mdm%;{Zz&uh|M(q5POeDG=qlT%3q`WZ3Lp#5u)%xGe9+A_K1=G79BziWm#XSTvaIPbNMonWThDA zEou1DMYWy@oT&N=5~(_h;+@T;+X+H$D8+gIcjMpBL5kOk=c>f6FQgsj8RM>K#d17Q zja~nhb;)wuI3s#3NeN{!gplOY#9Y%AJjPEYr0q}%VfeK`Cah5r|H%nr%!tHg5rAI9D~ zEb6uG`!$xLG6PGb5l}j$hc0pGaA=fNsR0B;kdCF`2nZrAISw5HQqrJw2n;QaFtmU) zL+)!(*L^?Fe&2m;{$Y-pU&nc!@j1WOV+Vy~>j_&!1sQd33Xie+Ar@$K#c|prP^wEm zW?83)P$2#s@Y5WOw*6h($ABc5xsy;z!L?DY8P2`&qWR?=ctQc;+!Xz^!@-N&)MnzR z9Sr+ZJC5dt+UL#O*_^d8=78czZID^sJM3+cVlFPdMGhP z1yk7JO+{#QjX%b(bl}VOqXk58tiE4FHEI5)9Bfb}NL6DfzVg`1~DDM)?K6xjq6y z?#vm&&Ipv>nKP$50&pJqNcf$=id_kn;1^Usa|RCfCN>5e$px0elk zx!=wdfOSs`)8`5ct-BcjHnmEzu)6ACWrZ3d0zVOM%X9g_;9#cQuGUsCdF1Q-&(+n{ zFNvulM7_Pef9`{22qYWFTXIiNPmAx*etdK4PL{@n@|sk>l@*4;04&E?Ee4h%Ky6o8 znrggukDHn*oacYG|GVl9vpQcrJv~b|EXd{z4h~L76HcCh*W9@8x-!y~k4X@*ueT$d zJb~c3JxxwQ0d^GLg9VgdHQS$!r+pc#aL)FB!zjy8a1ne-7>CbseqUX^E4(#P<$yT6|QP}h6vOJDPR}$A7S$k!w_EUQO2r5R7hM%NlS%$p2C&B9L&hcGgsa2FuSdnAL|imYY4O(@SbIk;MY4YH6Q%0mujYFFsNq6nPaFo5Rh;(D>9 zy?y^9B7$Dcz>E;aKN9+`eP$eLXzG35Qg)NKGfFbsyGw_h{H-|Dq) zI8_!#pJTnKo@br6l2gRqU`sxZt1q)7pjk>?DkpI>)>tAI?46m`s+?vJsY z7Ng!*jPl25_h84Y>=s0cDCFR|>8Y4bS!zIc5uPBV093&*;&6+7AR|yaT!~->36Jva zd>9y}#HdG}(#8)yIz@#D4-pNPU<-=|(7Ml$)2xult^;=`V6rC(hUN6iL=jdK>_tbh zxgDv5icxbR{;d0A@B`+NH1){Ne&iLyhkn}SmjLwRk0}7JHQ`m@Ndczyj-NK&8Yo(W z7Xe0I@&OerQ*NriAK55EfUF`Ijro#}({2DD3A`nQ2lz$;Etjo=>8&G`4R?NBR~XE! z9L1)b`!5A@y5Zh|Uk-x`FA*3lgZ%zILrP)YC;~c?_LN^2>?Fm=KQth1=}FVXrt~cmFHn@4UxN z|DG^qKWLj}-)7{`$~5GopctrKI&xhR|5kFGHqExp4jrJin)#_K;lC^lD|H;HQa*W* z^?eUyicKP2{Ir#|7b|({c667eU8i9U>pBCT+xP39%}40bQSlfxtiG4rO@`gCeS<_! z`kvSI%F5nllz-H!X-`75k~(ox@{$J#_APy|-dReWfamRAqCc7>O}wn(!Cvc+(ID_r zrx#cm(4%R+1T5jxkvUU5@2bqoNGb6f)tHelnwRYstk;pqa4}d3&5_E*8MXwYZ9Yl=#FPC3Za`)3J__Vyziz!t?9c%-jYxh#72H9vQs{~ut1yabSq zekZ&ZlQ*NA8mAUh@X5_P!%^(+{ri@k`#d3FX6XN-3TQ42T>rmQ1-7;|WrrpHh25a* zeW0grZJ=sx>6uX=lI(1fjPvR^Q9fIT{;uwF4L*}T#J~26njp2PmO^PuQS>sPg(?8{ zbt%iweFRta<^5n<-hU3n%BLeYhi2HLF3?ELY=y4cc5J(;q=$XWeP^ z*lF`@i~d5_xRT=H8?EJW>!$cl&X=YAG9Vva;jmnUb+JU0nl6_es|xpXbKIYN@}fwU zG;-EXX>myhYDuLTHneWOl>E_sc!b88(xloo%r;BucFJ+N3XU5s+|43Y5tTLHT}Qt_ z;0l5PS0E4v`1W zAGr4+>PIhw^nmFeD2Wb$tfqS)s?%qFZ!6^{u%WB9ct=^qMr)z}0tb?0%)2P;j9j6e zd?$k1N{-VO%#%~)X&;JO8>?SPKv#zR{=ug&cl+FF%iMtmfw_8~ZJKJjc&gh9;D6-v z`3Is%2CipJ_|A9qoS4f50_^&@nGSS-I!7aF_1kc6nDh&Ba`LgGM}er{xB!^K!ePP>-~y`0 zF>pA$x`62?Pjlr*g`qQQOsPtpd0D6kw=th@QXKjJR6A`<15<}lVI5(1AxSE7r-*KF z6a{nCi>9Ei+N_lyM1DH1bn7j-uM01GJbBchDCEf><=vSp8V@p32}UCoc=juU*vMPZ zJXwmRAN25AS-x>^b$RE{H$v@pR^@^mgQ8V82~^0b5g(!5AD3&&!??`osN5gk-;sXv z^f1b%*_zM99B6h#)0Hq4fSOLrN}VNDoptaA%>}*S@k;!~mWxW6M-^)e?ZR}!2a%@L z!8cCwTe`i6M}PAG`Z%SK-qPxa-CE&jnB{)9v%W5zG)WBcdV^VU$~05Y2-%x2BYfi8x?r?DA9<<*f9g?4ffRs*t^_n=AKK`hCn7be zaXMHo7uBE!#kSK?^eqBPQ3^`TTB`^q+WlomFR7zD&cV5H$7%n&*iQp3_1g-u&2qa< zj_(E|0%N6nZyuKDF2j{8NiEbNmM@R*_08sj46xf;RSC#V=I%+qLKStP25qtc84F75 zYcj^w7PDW)KXm(UB(e@*C3n%i^qL!B+Nj+`si>%^IDd>hyxSO5zCg{N2Gsl`GQ;mx zTToP?Ov|1v<)?j3yfR*H-}1wQO&)H~D9s)wLjUyKPsfx>{I43#rKz-sNPG66UP0Jvvc_vHT4l82T$Z81#> zJzrOl$`lSKEnog@V`OcXf>BS9GSW&`N~k^qt+AR{lo^s9x{;E+gE)852D!bI>fkgV z^cT?(91a{++AC?mO;|>di3pVBbF8(>5yfV zu*%QMdI!qIJyjG$b0Nv`H#_zs!dtVu)ES$D*nkQry7p{l&d zp?njIr)Qn8A4JipuzptBr6mdie~H~2`w?*w4hMNnY0?{Jit^e(4+ucM1_hIfFc|D6 z+B*xo=beJdep!lx57;e~9j9fQ{)RQvDSQ%P&n|*1Y?p;u^%eL)=An9dK-1qT@(sGC z*)*Iqu#FYrP9LcTgUMtndIga5U<0a2NypE%8mv-&#>X4T^%qaASm!-t{&ZZ; z44yY9Y}D8lV4=AWACrQkb~z1 zHWvf!+N00W$sqSGWVPcRN$1K-tggSc)VI5pPWQtIQ?^>MvotPxvuXpMyYPiJ-tUKO z8v?F#MJB>}Rs1mnTIbnD{dT?yMUdswt=2FsjkI%e&;Wg=$Lp9#cC?tI1`H-rr3$y( z8_^hL*69dBU1TK(3|#lvP6xvm|K%aa(cx5UKJbh#y`R9)7}zIkLbqRkUHMk16U%Fo zB*h$`it+8i^V&@jxnBJ)f(oZw6XVfcvCz>*n_C?Z>k?TS;|0A^?VE72I;Dj z_oX~YwVewWPVaa924d&u>#0l(-}y(G-HDP9i+Z3&?BhKbLE+CjHiBqIF{GE7@nIlp za4T+;wuB?v+tp=rZOHGz$#Z{6x5bT)eE+XmY+(jHib8X5(6E(}KUMW%VHjtDRmk_h zHUyq2(j6X-$_y~rBBbDWn}r^sn8w4vfAU9*lDtHt0&)>J-5K#2w!6jU+sL&1V{vXH ziOj`p#Q&_O3vX4{aBHb=z|Q(FGxtu|@8iSnUb%8~5yBv->$R8F^XjgVpmTgiXJUcy zWAcM3&lF6OMB!Kz>gbx^Sz1-J%5fX&TE_j+sy-i0bn77iRkrEGET*O~k~_%}R{y%{ z3NT&uBNgHLHH2yhccx<+u69;j0LEJN;o~9kE*9JPwYL(gHL=UyltD2Y<3&fiN3`)Ho0Pov$44sX93stj z8Zu^OA0OMy*WQGe&fmJn1Zecc-SztX2&$(E-WkfYv^Vd;Jv8zhmyT34{*AMgHR0*5 z{88g_c-ySMRuN>eDB$|m`VOt*DsoA9O|PHUt!Qnt5KnpsM#J6!KSZ(yH~YT&G$MT} z&SB^6bGv?)j+7K#RDOVke+I|0`V%(@!rIWQwG{1Upz{fXAzS_vb(F|KkJPeIzRsF* zw1zhbaW)KbTj6&ZObIS~xwg6ZDy<>*5_Hxp)H)v~Tw*8zxN(0%-wz=KU9`ypO#5F3 zxUgg?#=pZGy|{G)QOB9$w4PJn?oUp+hr>#A0U>wV38n$JdufYgibXLv-XABdF>>C;1Z)m(p_We*q0{BYvN62$z~J9qsQfja!Et ze~X-p4Z42?i*@wB1DeDwoLDV_ayyf;{i-p$t}eHHT!_( z2-W0gxtBJ`PyA2{Z~mxP{vsO>7XBU)u*`B)k7md6C!zUUn)>?pEh?p|vf`0h>mMq*9AtFiZj53Xl7@9|=vq6G z$E>|T_QZY8xaw6tpjdLZmuj}>lYY=vp8Q;#vF3f9f{D!^q3(_^08zn%^&;Gu>U7dY zbN2Fs04<^ZQbMF)M*U=OGcta_M8DOM9(ho29`=Ci{k070 zQArOSsL$26V$FmYF083zFrK9T)R(-BZ@2%<>ugloFfErWGhY@9$ND!jf6!T4bO1WKKj+1>n zc2~H+Nm8-5h}k;HF#RYmKGO~-`_*J+pbxt!|z<8aF? z=%aoLqZ~|_bgIjC3wL%_m#B%f{ak%ytUYT#NVWfRV!fcy*PS=e9bfR`J(|;RTDpzZ zJGj{$?~Q-7gt>eR)cQn%V65_lUWe_yXo9{;IV>F|t$!qRO-RjiOV?}l+MTjHuWX^q zLw=ttI)jOz@uV?_LeJp_Uxt7ff;i!qYG-+>~y;m$zSMvFbBw#{v2`!LI^%Ac- z(1Fs6-D}thmmJXCu-NuJf7hgZZi(WKOp?I^<2(BJpW&74UH4zIQ)2TpnM~g}6l9@D zuBSN-eF;Tob;5V8Wgdk(U$u0{%8eAldS zc`QA5Jn~Em#+;p&$Fz2Wn`tfn$a#Zr#Op9PfKVL&Et) z#u{l@Sr*<~Y(Cx3&2xtwEI=b(9&=vn+MdtaG%L3lEGoMRk!<_ecbzL0qjWbpQvrVp zgpa3^#77+vd+#6PsySMo7cNxLemW8*b3Y|-?BYtcam72J=S<_3O z-${`Iq0$H%2Hch&AxhxUj+LVg4WcG@I?$VxJgEwJEzkXJ1-vKpSqk`E2|j!9LTgMj zF+GcG5^jsr^zo{rI2$dQTiI#3k3_AzpSYd47f+JLf4J9z!2p-KZMS*l72h{H5UL(K zO+oTn?EshWgD3G)@cq$fa}Z``1>4#^%Cu211>SYrYqvvlVoBE@xE+Zg%Gs8XB$uC2 z6i%8a@0d2CrJfKw?YtTOWPVbgZZ|C4f2?u_FPOZ~iVdzlF6ZPPIspLN?b_H@{}LwI zkte;i*C>CdbO%8X-wGmUNHbxLc4>N5O=c)}>YWloikw^`On_R+d@-1@yc=bBZxli& zkYfsG9JE`|7*%3ZGw4jNdfPe}N30)$8j+AnCxUM`Qw!4C`m{Vu7b2wS_&g_jW#SA_C{Y_kCUc;UhYdZ6{Ssi6;&chL!9 zBV~>RM+SrWI*bjk+}DM78xD8hFMqrg;=u(d8cEln9;})ueKsn>nhaWFyF8LtCFm>y zV#C*?yaYsM2_Q1XNDucBH>k8*=ksb1IJyj66aqXT&#Khoph=K8UAqxnVHt7d??E)~ zcQlQ>3blXgM^=UE)zXl3TLFIh?T2`!f}ztG55bLlM-95wBwbgG$`+Me0W2H1G*+Re ztnTSFYu1<1J)MgYT^4p3ZZ9Q8S6jezb?o*Sy~6lU`%=*F#en%g9-R=T`=kYZ<~^r2 z(R%Cn{a|jGCSZs^+GRCdW1N1UePIpcTPcBV^Ta~{itk)jxy!c?101#k^k{u(oO{UO zAuQ@ykQ5(O+S(F+J<2vKDU!+wTmB;_*|sJdWHmxh9ehmc)0p{=i}AUudZ{^zBkUrYyEuFqghPvM>eD0 z=8-FEcmguQuF#sbt!f10c47qSG3GL893i`(h)Xa>W!FPG(2GI0I#! z5O@0$u~N5U4s$8_Qr?is+hiHTXVT^gB*&mfxNGs($znS&n6kC1ouxhy0WRTl-w`4} z1~C<3m(A|Fbn-#qmA~nyH+SlL{>C<5dU7k1-m5~mWOZG-M9CE4H+DQj#jLHW5nI5F zj<-cVns^0Bn^hSXHn!Z2J&$D&HHO6-HCgG|AW6)MZ6s?nWjHW_aCoW^HxcPTvh*o@ zlNF+h{`mDGZM+^_xPK}Cwc%PVrp*yVh8IbC#tIL_0C~P6{x~gp{f7cjc#5xi5NB9W z`|7S4D^OO}7O+_Os7Lqp9GPmM|GwgEu^sZko%;acsauhxk2l0GC|H!Ow#Pb1gqO9w z5{xfhNZf9S+l-Bvyhf5(5~2Ag!nw@`E79^_4nmP#W9seG$r}O*JcRV&J1q?W)Tg>A zadYP)k+z-j#sBnR_3T7!ZAjU}x=kKL=xijL5bl-WbZX&l8qVY7*?$ux8X7Ci?;RWKZ}tG9x2U3#(oS&mj0Zrv?9>@&sk3dJ@* z6e5s#Tc)53dA@jZC)~|rLr^z$bYE78b+S^3(1dJ1_Hj>FRn(NDmXkJ^6`kQ*{0&9Mzcj%G9TyC~KFozrs1Tb9A@Y zMuT+h4%^*?JXzo1ih-l=sm<5xo+#ZRB6MjHDp|c)P0mSlRJ@L&sC+h<>&$>s@33lW zC9{95>~IyK+U21(soc(W?TKISS`}$h6=-AFU?IC2 zH(IM*1_l!@;7VPn3EX8*r>G56<+K7bxLN^@Whl_yPh0+}06i6$@;pj^D;$|sn&*7S zLGPaS)t0IKfUzZPz@1e|sTt=SFwOEpI5ul*^rCE(vt)cd9t{_9@STpFeh1B&FObzsd+iNhyoUdzjAPr8uNH zvJ<_KSxQgX@S3iOb4>*pph9>WpxM}EQSV1m6H(04B34In7bd@RZXKuDTh%GB&R(u< zh&&L%vqAD>8;?|U;rMZM;{lgP&&46pZXjVG;(`Um$qVRF z9BT>&4T`)9iNK2PF>#2*mice=VZFc8#^2EWRY*uqf0DmO)uMhC+*&hG!MhK#lqo!9ercAJ+zz2N{DawD=^;Rk zS)hmM{vEE3Ls-dk|BW6^5hBpb9r17U3~F}20T40RmXTisISi)r7gU2P0~C)(s{Ju} z2srmkJ7kdhP9JTv3wGJYWDM!${gP@CATb!CJ`IyO0&RtZ z{|*?RP@h%-5VC;a3Rm`vxPBdCZ|fHk!bl+hPV2Hx(4$=WegLhgEIvSTFC*IRUmmHD zN&l%Rzx{yHPmxt|*pIaB`Lj)=4=oo^G@um+OM?=0@oa(YqJN^z9{}!`XPSrUT|sbN zHSp8Eupb$Z`NNj!{d#gL{J*9Ks7fFOj84YE`d z3XOgBZ&CU$0&ZV47kt)>V%+6_5TNN`5zq3{|DYeP?GvDyoDfR_1_%L zze-cTIGR(LE5h%mB3J+WKmfpU^=_irRSJrxnsYPL(*Qep`CkCc&)r@2T!+34je&sy zmkR(tgZ47BWyT|zBIJgW-WpKyH%ma6iP5&m{%)+n6M;X6UV2g%H}@r;BPLP$uQ z0RZ49Fkxb&epVm_zFpA(r!aCQIyBh(t0**-(hgvQ00KEZUm;qqJ3tM?ihJWQA#zV2 zM{JnGWX7ucUrSu09eKjNb+Y&P<*@--r^-uUp$jQI!av=yOl4wR*MNf###8G?&@UN% zj}pmcGGj^#_EwqU-gp^W&^j-Y$z(R3NKjY;y999IhzP7ifWo7v`K50(5R3sRBaaj> zjQZarVxBBY(Zij9lk!#*Wml5c5wIqSy zh@e<@+1koR0kMk#n=EJB9;LdEC7gZM+?>F7CaLm1k}0NGx)v|dgrxL2OxgCSq)<)v zz@r*;XDUEAwD)=cA+Z?t1b;P^Xd-I{pOqwR}|a8d?ICGL#| zr6to(pO{JF6Ai{I)MEe1QXgUNCZDYk-U_njz-Qu7>3|I>57HU3AHECg$17a&7?Wg) z*2jT6SLwRB6CeSt0Rk6!O^zZ5=3Ot+d5pp{Pwx=y%Dd~pkRr4Cd4HBTyBL5Dc0<1p zrY7%hnM7}Dz#YfE?j%IIDA8#{eWs?<(>~Oz=VgLE58fz(@47iq6$Z!}q0Bo&j(E$t zu_k$Vo`YmW3#e!V;TJ*kE~Xrez8ixMn9AdOM#fH{l?c1=$~goHwCM#PME@BFXKr^K zV?RtRHC_#ubXoX2`CWHUVxtaR`uy}rsU20qddwSQ;p?*aDikGXgH`H2Y?I{?tNq7z z0X75r$?ZWccxpdbih()-;f2eLXc!2GKOnxISQtV-(I48_5 zdM|sI?i*+U+}P`u-0joSkcV4$iqfZutE;Fqx$o5fj4I<1<^=}m_lwy;JCf#v>8$=N z#8xUMTxS^Ngb5PYD~xkg0AKO5iQX)W_p+_=)4gl#O5yyR9aIY>!%_t&E;LgFuAJ*G zQ3RHXpv?!iMzF7QFx_UppJ|;c*Qwln-LM&^Q!2tGO?(7n08IYxT@urBDN(ui&ar^H zjz-Pr%^3ezW)`lm&89B@JAOQbQuLK;UN)I(GlR>1dO<8jL-e0}7lTp-&(CTeR#6xC zTA`qIbL7fwPOA_Jmm~%LA9+G49r!P>l2X48kus*qkU18b>{jC8E78*zk_oZS>APk$ zpqXtvHM|gfn9yv1BbLu=6?Nc-<@SIh%55cL(Vxn^{ChKb$gIDBVM`a!4%yYu`k;qn z1!k|9;gI(GwSv>&xyWXaZv0TS3ME0QulWI?TXO{AYhXngS+Mct#b_bNYtZ zUR|N~WkO_t0dD!z9kWw{;#AA-*l4hA+p;5!n8)r=A6Y1t5NTlzG_bInqKQBI2skz2 z8pi{*$LQeTcDVH}WyNQm>oa#`gT#1sRVnaRK*a*aZc=-cRuxecw#I!+`S;n{vGMc2 z73dj!`rJD2G0^TCz5>A0=y^WVN5X$zh_^w(g-m8QHmTeRFGe;~cb)iRJ=A*oeKKE7 zAvtiRPe-Q?d3v;GJGNX?1&j}TUZ4%b^H5cxH1ky^)!@zF7tDTrX@J}EQ3^sT-SxRO z$?1iNU6WeL`L=@Jq`DUX79$L5pX^dd6USz)j0yejAu)p&k{jiK8EKIFv#xz{SSP!_JhbMLqQo@p} z-{Gqvf7eO4pah2&{QKpw5A~;F=O@2-r#qWbIC zk>7yY+S+FMe{!5;=p?8Y68;u@G7*d^7!(TS&iU&Woavou4yBWaPgZ;Q$DTV~vt%dg z@|nLco?ia90FC(N=9`CckulQATlyT96~$1jm8VRfS2{GgJMH7|-XRa=ZeN<=zLq|a zsC6eLSo+wrL7tU6xFUssxnnufw0ds*Uu0sBq=8KAN2c6|77^~7(t=Af0CQhvPLdA3 z7rrAc6QRtiwI{9pE%hHodqFSU5Sx<%Q;-xHBa-E#5-<0(IMck(b%RDL(LidpUpldD z+$mzI5`w96@>t~)hEtu`k{o{1tMToPCuMGEEiIEHhL%_;oyJ+ZUO7$8+gJa-s!$mF zx7rWIP!#eJY09@IT1|3h4l>=p(>N4@x0vkTVRq#t&T-AHvvqeKRDEZ0W*36Yb2RWz ztyOik$E%aP*q@6WKBz+wS43oUpPUr19CH`27S4DwKVf}x{H#fbD#5t(0mq$7H~&H% zt8`%fAS3kf3Ks|kKQb1=lWC6RWJVoNNW+Qagrc}fQ2JZ%FelVeXW^qP4Oo_oU@;Er z;J@!&QXz&LEl_h=V0o!Wlv^|(`d<+$iQ$Bwy!{)#-1G%Y%LY069D-RXfmrqlL^b_` zId`#h^*B}YD%e04Km5cJe3}*dwW)1U)D>!782p5c6h0a3kKvhqfXtMuP}j#TQX49^ zXrA?t!REIPVGCNR!2j2eIM8wjP&ly-&B&c@tQ0@_&49m`G9i}LOo$Hb#fy-lqy~$2b3Hk&T{LGwV zF7jf%w>Is0l+@z7-fVC+ndcbU>*AYYw`z9hBFSj@VGO7HN_&ebpqpGf`ur>>7zMd{>Q9Kysw+ zKr`lTB&en8l>Iik{@QQN!&-v^(gZ>GVp(@{Vn<`6&m34yulnbuVY11tfPf%~)Dm=F*LNK$5J) zr1@dN{99c$uhi!v_h>vv`(mqajCnIer{rgs`p$^3K{~k|KV+R?cOfe-*0GSz)`HFg zPwwMTObQKGwLm`rg_^b}*acv(Zo-=c&@2q#UjDgT$(Tf@)%SA#_2v-)q9`)tUH!hF zKey7UC+fY?i0aLWG+{Pq71`jeZJG;GYXz|1j^Jf6451eMZ_BHS#6R$M2+0~ zxJmpbg<92g(vP~N-M1#9sJ(1*CG|{vypEM!nYyW5c(uoIaMc;OhE<$wm`dt`jksH_ zpYkn_t0y^Fmzb9?rr#9|+6yK++!2U61LLiEF!xwZT->9|GFN=ok;A`Mo%=@Jr3#PE z5skhO^#B@)A*=B2$V*EvU%7qW7+40k&vASa%L=!C*F^$BU?8>*mYYTo$RB1!cQOr0 zlIG2iwzYA8<1vcV>Q|o+!i_*L~FR0kT$iITYfMDU#mT*V{t2xTvy*!bSd z_wc@=qT+o^sUFk2_m_8xzf$T(yvnZL@9m2^r zH@Ug%K08tFV0rh?nf=lt-sn zD_?JEw-UhDgwW)AZ*4vEMVX!mKZtp$;q5>#u#oQ~>*k%;TOMuZcyo|te=DpNAb*iM zn#M)8{-rBL#Y1aakZr$kdy7XhhWZW@^%Olc0|Cjrt6>m`hQ%x{cGU5>Iuzq{YR~%F zA+u4n>#7M~TzfnB2?Uj4K@pDr zDO!h(kC6p_^!YofxwNur#q9w!IorD_-YxU*aode5IaFZ2c!o$WW+5pN*0P-S)Gsh* zXSHP@z~^cM_jGu91|D?jd~%BxL{Dd^`a~?M`kh_zlg&5MKZ@AaTDk+xE}L?q$dnjl zpK(@wd{bRMJ*OhjZ#z-1F6AC0D=W*V_J?PWSww3a*7Xtb@{u zBRp=ck2)9=u;=Fe;u2iA)aBV0#KC>vL&jS~MK9?|Ro~_^nEZQomLiF7 z;s&G!=gP+ZK$*I@?j17Lfa=$OJ>zFL|{@n*l(@PXFpMyGbSVRwZwpfMH z^3*wbxqe4o=A>wBP>wBoSJh?y{zv&MXIqG!?i+7RZ&{L_1?OP9xMWsygdLZ3pJpkY znK$M{CD+W+Tu-2jHqC!8wlfile1t8jr!?__Un>?5LuV@1<#@+vel|ybetqkl{@Q1; zrBu*avUX8%HXt)9qM&A(N`YFUZ_M-M(HzyHo@fpFY6ye$5pffC*qQPNkTzi|O{{)W z`Q$Z+j-E!}iZAkd?spY(;$Ie+87`@h^Q+l?vW2{_WxioEtN=$r+W7KVA*>;<12r(z zwm_k;&VjiCOk#NXwq5+mwb;bXjWzBB<5HfJJ}5kwRt7>N(ZBiJ&wd#lZzxfX(ym&O+ z|MbvB=e2nfB(!}h0=**lB*M&nu8gE5CjXd?vra+J#BMyMM&Q0MS{nTqiYeglI}s0% zc4ZLgYtv?X)Pi<6r(7lei<(9bM7K3gR%DJ8*w<}1qEQ< zev;Ts3v5lKYSP4gR}`|+9tsv-z6AFSmg?x{chYxLNee=;eaHd~-uD*fYiv3C!X-KlDgzoP;;Y^d z`?ItxOV?zAFx{y-ZEG-`!c$SmHu0OA7kTpRfl`7JHEmKOEe!d0)rX)*VBC)hz|2A! z{h1?QL1-e{b8u)xu=+xIoL;YTk8?&`ItPkU=mGK}JKX1->07GAb`hW?2yBGUs*6~O z>kP>T6O1j_{NS-KeUoP7eaZmL*@v8h4X<9YehiZ7%$wU3+M4D|ZhU|IunmWrU$3Z$ zUhACy=F*0x_aYf_{MOa2IXSiW#s5{cGAE@ly0IH;KS_VD*jMslmZLHg-V~AQ9H`WJ z%bo+}NmR??$Gn@|_!CX7T6_3I4{>B8L|I_0M6)HQc~0&ml4`gysrc61Re%6fL{9SI z*#(eKC+O^JvdQ7nN9L{Flw<}s_|fFu4r9)rJsMlJdhVB^CD7zVr_XlpXU(VLKV*TZ z8rjtZbsAV*$)GI9ix%v3P6nm{6?$EqYmwqrizcZAN?R!aIKbsOFRS0i8k9fV6+MT| zX(r{YjpC^eN%uh?YZGoBK^=~nALvvZV*>-;b+`lW})mUcvzL0Utg-*r95gyhv_`1l%8 z1f5`TJX-PD*vFGrn?YH~c#)m#RcmjN*e5@Wsz_zwLTZMv(ruB);|cMT%8>ZCIy_&o z8k4IX95%1pvJwTKRdzrb>#=657df&+5Oc8omzfAKp`I8{eId>etL|X>(Opm1(?eCx z`QFp3O<3SXN+Y;vMIZKdF}}HqGCdeDQ}OWjb~tLV&W;=?-C=NHV;B(PFZyKS+jbPb z5+RJMJN6z7<%d1*%gRqLbM13094>8qf7K@1>BB|zTMMS1sq%C6*q{CF-QTFMoZerb z&)5^u1zAX*Bfk7(N()$(rKR{vPo0nqIy2|0hLh(D)9Kuh4BCymdF&9@04DyFd@83! zb!t_gwtK9z;?DNx?wDt##$U;eXIIzN>jQyxueirf(a$g2Ne~~dU@*^^g*W9o;2SkJ0 z7Rc5!NE@zh4OFRkgrbD3+S=Qe5keE6GH2{+&d>P3b-vE?!UT#d-Dk=;b*zDGd#ps> zVi}e^iLe;gjuU)tc;mNX{i4t|gQ+I!T5r5b& zatw!WavR=H*BF;-*o-8}@8_=e7#&*oawD4Pmiy`0p>wO$}oKd zdww$IxncF#m1nIS`!TBMFV{Zy$B+x~t00oUEL0Nx2IIJZX5&L@vJnmS(L^0jd4@xP z(!WnEB;6-UYURterAt^uruD+|YBE}0K)3(P_PCU4a@%Gkl99C=hgUiDg3B&Wc_z!n zT{o`VSmptg-1w*YkMS8@-c`{x;0%9-{lOSU5Rv-z3T`p_?*xLdekB4k3rR-zOT)eP z4%58C8pw*d8oy$L8HA$N@G50Kcg)gawnX4Z%*9<*GGg8vOQ8ohx56S$j8V70`KM;6t^O3@U=fiSe<(R`J6zqap8aR2wEE^Lb}-Z z^^l`jIR*ZxHJao+bf+eY4*rEONM;pK@5j1RbG-@0M9F!zWAFMujr-8rCr}dd2Ez)W zHk2o%6ezL26vOHG}ym5#q1xK(>&YraLOqV9&EbBp6d@qagB0ZI*((g^Lqixu8hB-*A)td;^Ut z*G%oi$E3i>bA({ht>7P^!yYjzvbL9eE^DCrrD#p^EtBE(xeWTrO$crBXMs3NIp2$; zP27DO%a`HkmSPZ+#u!i+Pv<&9Afc>HSP?^1Mvy;ddi=MUC9?!c6%LfcIR2obHCtrN*1YN9 zxCyE8-t^Ud>MQs<7nn>Z{qZl+y%sQ{^<^WYL3I)(Yqv&PC=l zNuXVJ<=!9bGH*O;S@jup6-O@Ly}3VDy!J7*Sk2-7W|mfJgQ-<>+T4ol4~sfq;xLRg z-*}on&RI3SRl56;U`nL;0g1Rzsl)5f!PS)+JA)>&A5|wcUvE7B&WS5%7?myRpui8u z46=6})tfI|&3p}N3(3bYDp9^*%&R0`upqr-rDr(RB7PfZeJMoQkD+DcAaW+BK-7{_ zn3Pd~Nih8%+DPkij!D1yNU~q=$%$apVsQ1=SN0$qbB?MHHVfE?aTf}JV87X{6Bwlg zH*%h3Qa7eOsb)a6w?ahY1&TKXO$t5f!!QgJXMz9Z1D9A6NY9_m9lFfFWn;lr;WxGy zluQZ-&Vuw2Fj|R5gYDhe4||@;D`ZUqs_b1R^8#BL1iu_)Iou&Ni?av;7cP(D zNK9FczlDWGhK6W*K9CCYl)e%fsWX){*na^|^OfCF;2olU!EICJ@oBy<;ugrr^>=zl zEZ*8oy*nSG(b-NX(`8nUTL{`T9PympX7p!@BqF7WsLvsP7d~%|eBj<9-8M`Ca$>X2 zlk`ueB%k84#(HDeyCYl8UQHJN#Q;CgtYe8H4O7RF>-v5C7{4app^5P8s-xK#gFQcl z2rQ;o8AH4CZeKr+UM&=KqzI8fSLD9fD}9s?qSCZlQPJn^?IMSHzj%7eW}4{OQBitp_pIKtvHJ08=l3A z4t{}piIGh0IPgx2_GJQ8C?S&v2}-|f7aZ8#gSM4_kb}tAu6W-m|coDp;D=4 zlff5dMcE&U=C>(|XIr1`iKNQzi+_>YD_r7o2};qz0eVVbm<)?FxQO7NKI&ZCp=Wix z7?Al>pd={<^CM^H{Q^Y#JX~D-f-rQYLc}!yb9dhO6$!m!ZAY`cSDZKPK5IAo>A>(7 zMp#7FVV@?r4cDgxpYy&FHi{v1igj>?#?;hn0b5`)1wGb40{n-dVKv(*`< zX&-ZC0TvtZgKO!$maV};ZheOCU6TT-=HfJTCwmhJ;f;S~jG0lYE;8A&490xY-H4nN zKVNO$l|EctL91G~x3(`|*RO-nbQC1tRFU@b)#gl{XFuY#??xAzHObLYNDiNG` zfu`NtJWClB>}Q@8Fc)da()j!EB4F@z2bpX|uKCC`q&Y{duXz>=HCG0nMFps+#AEn= zy}$@+uE9`{&L+ca0jUoW)FYW}Q4!P&!mx@x^B}9glU>D=kkC%^mEk1$l{X2%oZp$+ zPZlciJ=4X$JsOH3OG2DGbs|&*xA?xYZ!dXAScsy0!#7nt9wq$#sSOwk@7@!U*E3U| zU)g=MHdPjnZ2x3e|G|irCxdMYDspc`yhBWHRpHZ8js<>uW@aStlMi%niG~Q5wdqVE zVyWi$Q`o-@Y4X-Si9^`7{7Jw|p@MB>7VTvd5IGM%V~dVKg1#0lP@)+;7ioHWR&Ud2 z+TDR-%%;k->3D=~v$r-Wwc+s_Rxs3`cl>=o|Ie081cu0~pEW{!>!yE##*-8WPwFP6 z{+}wGGBNs^ZQCvynfShX4#~VL$|bvgE@?EGf8-df<~DCLvfmL0EZ2{e0q;n9Ho2f+ zugDp7Rigxr&#){M*k%+v@pw(peB&)%K<63#x?3Y84GCgk`grhjNf0ogB%u|>mFXbs z@2gzwT?h;}&pFc0OH=JMgM&VM*6uSD0qd%oxR7JvJ7#c9v`o12f4%HK4&XEkaBc(K z7*T8i7c@XVl!gqxfF^N3gD@P5;N*y)!8r&h4x_<#2snYWKTtiChD1DY`Xul_`{7OJ VZ=_qXZvf57dAjr From 417df0582d686a92daa6d6c396db4a569dcd85f7 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Mon, 14 Aug 2017 00:07:39 +0900 Subject: [PATCH 091/113] Update README (#463) --- README.ja.md | 57 +++-- README.md | 69 +++--- img/vuls-scan-flow-fast.graphml | 415 ++++++++++++++++++++++++++++++++ img/vuls-scan-flow-fast.png | Bin 0 -> 76142 bytes img/vuls-scan-flow.graphml | 215 +++++++++++------ img/vuls-scan-flow.png | Bin 74102 -> 87167 bytes 6 files changed, 638 insertions(+), 118 deletions(-) create mode 100644 img/vuls-scan-flow-fast.graphml create mode 100644 img/vuls-scan-flow-fast.png diff --git a/README.ja.md b/README.ja.md index ca1c4c44..c4e82e47 100644 --- a/README.ja.md +++ b/README.ja.md @@ -470,16 +470,38 @@ Vulsをスキャン対象サーバにデプロイする。Vulsはローカルホ - NVDとJVN(日本語)から脆弱性データベースを取得し、SQLite3に格納する。 ## Vuls +### Fast Scan +![Vuls-Scan-Flow](img/vuls-scan-flow-fast.png) +- Root権限不要でスキャン可能なモード(Raspbian以外) +- OVALが提供されているディストリビューションは、スキャン時はパッケージのバージョンを取得するのみ。レポート時にOVAL DBとバージョン比較により脆弱性を検知する +- OVALが提供されいていないディストリビューションはスキャン時にコマンドを発行して脆弱性を検知する + +| Distribution| Scan Speed | Root Privilege | OVAL | +|:------------|:-------------------|:---------------|:-----| +| CentOS | 速い |  不要 | 有 | +| Amazon | 速い |  不要 | 無 | +| RHEL | 速い |  不要 | 有 | +| Oracle | 速い |  不要 | 有 | +| FreeBSD | 速い |  不要 | 無 | +| Ubuntu | 速い |  不要 | 有 | +| Debian | 速い |  不要 | 有 | +| Raspbian | 初回は遅い / 2回目以降速い |  必要 | 無 | + +### Deep Scan ![Vuls-Scan-Flow](img/vuls-scan-flow.png) -- SSHでサーバに存在する脆弱性をスキャンし、CVE IDのリストを作成する - - Dockerコンテナのスキャンする場合、VulsはまずDockerホストにSSHで接続する。その後、Dockerホスト上で `docker exec` 経由でコマンドを実効する。Dockerコンテナ内にSSHデーモンを起動する必要はない -- 検出されたCVEの詳細情報をgo-cve-dictionaryから取得する -- スキャン結果レポートを生成し、SlackやEmailなどで送信する -- スキャン結果をJSONファイルに出力すると詳細情報をターミナル上で参照可能 +- Root権限が必要なコマンドも発行し、より深いスキャンを行うモード +- ChangelogをパースしてCVE-IDを検知するのでFastよりも検知漏れが減る - ----- -# Performance Considerations +| Distribution| Scan Speed | Root Privilege | OVAL | +|:------------|:-------------------|:---------------|:-----| +| CentOS | 遅い |  不要 | 有 | +| Amazon | 遅い |  不要 | 無 | +| RHEL | 遅い |  必要 | 有 | +| Oracle | 遅い |  必要 | 有 | +| Ubuntu | 初回は遅い / 2回目以降速い |  必要 | 有 | +| Debian | 初回は遅い / 2回目以降速い |  必要 | 有 | +| Raspbian | 初回は遅い / 2回目以降速い |  必要 | 無 | +| FreeBSD | 速い |  不要 | 無 | - Ubuntu, Debian, Raspbian `apt-get changelog`でアップデート対象のパッケージのチェンジログを取得し、含まれるCVE IDをパースする。 @@ -487,20 +509,10 @@ Vulsをスキャン対象サーバにデプロイする。Vulsはローカルホ ただ、2回目以降はキャッシュしたchangelogを使うので速くなる。 - CentOS -アップデート対象すべてのchangelogを一度で取得しパースする。スキャンスピードは速い、サーバリソース消費量は小さい。 +`yum changelog`でアップデート対象のパッケージのチェンジログを取得し、含まれるCVE IDをパースする。 - Amazon, RHEL and FreeBSD -高速にスキャンし、スキャン対象サーバのリソース消費量は小さい。 - -| Distribution| Scan Speed | -|:------------|:-------------------| -| Ubuntu | 初回は遅い / 2回目以降速い | -| Debian | 初回は遅い / 2回目以降速い | -| CentOS | 速い | -| Amazon | 速い | -| RHEL | 速い | -| FreeBSD | 速い | -| Raspbian | 初回は遅い / 2回目以降速い | +`yum changelog`でアップデート対象のパッケージのチェンジログを取得する(パースはしない)。 ---- @@ -1739,6 +1751,11 @@ kotakanbe ([@kotakanbe](https://twitter.com/kotakanbe)) created vuls and [these Please see [CHANGELOG](https://github.com/future-architect/vuls/blob/master/CHANGELOG.md). ---- +# Stargazers over time + +[![Stargazers over time](https://starcharts.herokuapp.com/future-architect/vuls.svg)](https://starcharts.herokuapp.com/future-architect/vuls) + +----- # License diff --git a/README.md b/README.md index b9f83e00..d1682f41 100644 --- a/README.md +++ b/README.md @@ -479,13 +479,35 @@ On the aggregation server, you can refer to the scanning result of each scan tar ## [go-cve-dictionary](https://github.com/kotakanbe/go-cve-dictionary) - Fetch vulnerability information from NVD and JVN(Japanese), then insert into SQLite3, MySQL, PostgreSQL or Redis. -## Scanning Flow -![Vuls-Scan-Flow](img/vuls-scan-flow.png) -- Scan vulnerabilities on the servers via SSH and collect a list of the CVE ID - - To scan Docker containers, Vuls connects via SSH to the Docker host and then `docker exec` to the containers. So, no need to run sshd daemon on the containers. +## Vuls +### Fast Scan +![Vuls-Scan-Flow](img/vuls-scan-flow-fast.png) +- Scan without Root Privilege ----- -# Performance Considerations +| Distribution| Scan Speed | Root Privilege | OVAL | +|:------------|:-------------------|:---------------|:-----| +| CentOS | Fast |  No | Yes | +| Amazon | Fast |  No | No | +| RHEL | Fast |  No | Yes | +| Oracle | Fast |  No | Yes | +| FreeBSD | Fast |  No | No | +| Ubuntu | Fast |  No | Yes | +| Debian | Fast |  No | Yes | +| Raspbian |First time: Slow / From the second time: Fast|  Yes | No | + +### Deep Scan +![Vuls-Scan-Flow](img/vuls-scan-flow.png) + +| Distribution| Scan Speed | Root Privilege | OVAL | +|:------------|:-------------------|:---------------|:-----| +| CentOS | Slow |  No | Yes| +| Amazon | Slow |  No | No| +| RHEL | Slow |  Yes| Yes| +| Oracle | Slow |  Yes| Yes| +| Ubuntu |First time: Slow / From the second time: Fast|  Yes| Yes| +| Debian |First time: Slow / From the second time: Fast|  Yes| Yes| +| Raspbian |First time: Slow / From the second time: Fast|  Yes| No | +| FreeBSD | Fast |  No | No| - On Ubuntu, Debian and Raspbian Vuls issues `apt-get changelog` for each upgradable packages and parse the changelog. @@ -493,23 +515,10 @@ Vuls issues `apt-get changelog` for each upgradable packages and parse the chang Vuls stores these changelogs to KVS([boltdb](https://github.com/boltdb/bolt)). From the second time on, the scan speed is fast by using the local cache. -- On CentOS -Vuls issues `yum update --changelog` to get changelogs of upgradable packages at once and parse the changelog. -Scan speed is fast and resource usage is light. - -- On Amazon, RHEL and FreeBSD -High speed scan and resource usage is light because Vuls can get CVE IDs by using package manager(no need to parse a changelog). - -| Distribution | Scan Speed | -|:-------------|:-------------------| -| Ubuntu | First time: Slow / From the second time: Fast | -| Debian | First time: Slow / From the second time: Fast | -| CentOS | Fast | -| Amazon | Fast | -| RHEL | Fast | -| Oracle Linux | Fast | -| FreeBSD | Fast | -| Raspbian | First time: Slow / From the second time: Fast | +- On CentOS +Vuls issues `yum changelog` to get changelogs of upgradable packages at once and parse the changelog. +- On RHEL, Oracle, Amazon and FreeBSD +Detect CVE IDs by using package manager. ---- @@ -1289,7 +1298,6 @@ $ vuls report \ -format-json \ -aws-region=ap-northeast-1 \ -aws-s3-bucket=vuls \ - -aws-s3-results-dir=/bucket/path/to/results \ -aws-profile=default ``` With this sample command, it will .. @@ -1553,6 +1561,8 @@ $ vuls history | peco | vuls tui -pipe [![asciicast](https://asciinema.org/a/emi7y7docxr60bq080z10t7v8.png)](https://asciinema.org/a/emi7y7docxr60bq080z10t7v8) +---- + # Usage: go-cve-dictionary on different server Run go-cve-dictionary as server mode before scanning on 192.168.10.1 @@ -1570,6 +1580,8 @@ $ vuls report -cvedb-url=http://192.168.0.1:1323 see [go-cve-dictionary#usage-fetch-nvd-data](https://github.com/kotakanbe/go-cve-dictionary#usage-fetch-nvd-data) +---- + # Usage: goval-dictionary on different server ``` @@ -1699,12 +1711,11 @@ kotakanbe ([@kotakanbe](https://twitter.com/kotakanbe)) created vuls and [these Please see [CHANGELOG](https://github.com/future-architect/vuls/blob/master/CHANGELOG.md). ---- +# Stargazers over time + +[![Stargazers over time](https://starcharts.herokuapp.com/future-architect/vuls.svg)](https://starcharts.herokuapp.com/future-architect/vuls) -# Stargazers over time - -[![Stargazers over time](https://starcharts.herokuapp.com/future-architect/vuls.svg)](https://starcharts.herokuapp.com/future-architect/vuls) - ----- +----- # License diff --git a/img/vuls-scan-flow-fast.graphml b/img/vuls-scan-flow-fast.graphml new file mode 100644 index 00000000..ded5e2f4 --- /dev/null +++ b/img/vuls-scan-flow-fast.graphml @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + Detect the OS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Get installed packages +Debian/Ubuntu: dpkg-query +Amazon/RHEL/CentOS: rpm +FreeBSD: pkg + + + + + + + + + + + + + + + + Write results to JSON files + + + + + + + + + + + + + + + + Get CVE IDs by using package manager +Amazon: yum plugin security +FreeBSD: pkg audit + + + + + + + + + + + + + + + + Report + + + + + + + + + + + + + + + + + + + Vulnerability Database + + + + + + + + + + Folder 1 + + + + + + + + + + + + + + + + CVE DB (NVD / JVN) + + + + + + + + + + + + + + + + OVAL DB + + + + + + + + + + + + + + + + + + Check upgradable packages +Debian/Ubuntu: apt-get upgrade --dry-run + + + + + + + + + + + + + + + + foreach +upgradable packages + + + + + + + + + + + + + + + + Parse changelog and get CVE IDs +Debian/Ubuntu: aptitude changelog + + + + + + + + + + + + + + + + end loop + + + + + + + + + + + + + + + + + + + + + + + + + + + + Amazon +FreeBSD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CentOS +RHEL +Ubuntu +Debian +Oracle Linux + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Raspbian + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/vuls-scan-flow-fast.png b/img/vuls-scan-flow-fast.png new file mode 100644 index 0000000000000000000000000000000000000000..24710d69ef6bc274a0bd5ffa324fea0d9f84a04e GIT binary patch literal 76142 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 literal 0 HcmV?d00001 diff --git a/img/vuls-scan-flow.graphml b/img/vuls-scan-flow.graphml index 1409090b..39a67280 100644 --- a/img/vuls-scan-flow.graphml +++ b/img/vuls-scan-flow.graphml @@ -1,6 +1,6 @@ - + @@ -20,7 +20,7 @@ - Detect the OS + Detect the OS @@ -36,7 +36,7 @@ - + @@ -53,7 +53,7 @@ - Get installed packages + Get installed packages Debian/Ubuntu: dpkg-query Amazon/RHEL/CentOS: rpm FreeBSD: pkg @@ -72,7 +72,7 @@ FreeBSD: pkg - Check upgradable packages + Check upgradable packages Debian/Ubuntu: apt-get upgrade --dry-run @@ -89,7 +89,7 @@ Debian/Ubuntu: apt-get upgrade --dry-run - foreach + foreach upgradable packages @@ -106,7 +106,7 @@ upgradable packages - Parse changelog and get CVE IDs + Parse changelog and get CVE IDs Debian/Ubuntu: aptitude changelog @@ -123,7 +123,7 @@ Debian/Ubuntu: aptitude changelog - end loop + end loop @@ -139,7 +139,7 @@ Debian/Ubuntu: aptitude changelog - Select the CVE detail information + Write results to JSON files @@ -155,7 +155,7 @@ Debian/Ubuntu: aptitude changelog - Get CVE IDs by using package manager + Get CVE IDs by using package manager Amazon/RHEL: yum plugin security FreeBSD: pkg audit @@ -168,29 +168,12 @@ FreeBSD: pkg audit - - - - - - CVE DB (NVD / JVN) - - - - - - - - - - - Write results to JSON files -Reporting + Report @@ -200,14 +183,14 @@ Reporting - + - + - Get all changelogs of updatable packages at once -CentOS: yum update --changelog + Get all changelogs of updatable packages at once +yum changelog @@ -217,13 +200,13 @@ CentOS: yum update --changelog - + - + - Parse changelogs and get CVE IDs + Parse changelogs and get CVE IDs @@ -233,6 +216,87 @@ CentOS: yum update --changelog + + + + + + + Get all changelogs of updatable packages at once +Amazon / RHEL: yum changelog + + + + + + + + + + + + + + + + + + + + Vulnerability Database + + + + + + + + + + Folder 1 + + + + + + + + + + + + + + + + CVE DB (NVD / JVN) + + + + + + + + + + + + + + + + OVAL DB + + + + + + + + + + + @@ -251,8 +315,9 @@ CentOS: yum update --changelog - Debian -Ubuntu + Debian +Ubuntu +Raspbian @@ -314,7 +379,7 @@ Ubuntu - Amazon + Amazon RHEL FreeBSD @@ -328,17 +393,7 @@ FreeBSD - - - - - - - - - - - + @@ -348,7 +403,7 @@ FreeBSD - + @@ -358,29 +413,17 @@ FreeBSD - - - - - - - - - - - - - + - CentOS + CentOS - + @@ -388,7 +431,7 @@ FreeBSD - + @@ -398,11 +441,12 @@ FreeBSD - + + - - + + @@ -410,6 +454,39 @@ FreeBSD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/vuls-scan-flow.png b/img/vuls-scan-flow.png index f4959b35a3b61ffde18132baf7306930d613dafd..bd0d94c74ff738b7558a09c1a3ab2459d358949e 100644 GIT binary patch literal 87167 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 NT1N4H+FkvZ{|jA(5R(7^ literal 74102 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 VZ=_qXZvf57dAjr From 843f1a462fa7f08d1f4d3350f7ed0b4ecd33ebd7 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Mon, 14 Aug 2017 15:53:11 +0900 Subject: [PATCH 092/113] Fix checkDependencies for redhat.go --- scan/redhat.go | 37 +++++++++++++------------------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/scan/redhat.go b/scan/redhat.go index 38d3e6da..aaf07bb9 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -177,11 +177,6 @@ func (o *redhat) checkIfSudoNoPasswd() error { // RHEL 6, 7 ... yum-utils // Amazon ... yum-utils func (o *redhat) checkDependencies() error { - if !config.Conf.Deep { - o.log.Infof("Dependencies... No need") - return nil - } - majorVersion, err := o.Distro.MajorVersion() if err != nil { msg := fmt.Sprintf("Not implemented yet: %s, err: %s", o.Distro, err) @@ -195,28 +190,22 @@ func (o *redhat) checkDependencies() error { o.log.Errorf(msg) return fmt.Errorf(msg) } - - // --assumeno option of yum is needed. - cmd := "yum -h | grep assumeno" - if r := o.exec(cmd, noSudo); !r.isSuccess() { - msg := fmt.Sprintf("Installed yum is old. Please update yum and then retry") - o.log.Errorf(msg) - return fmt.Errorf(msg) - } } - var packNames []string - switch o.Distro.Family { - case config.CentOS, config.Amazon: - packNames = []string{"yum-utils, yum-plugin-changelog"} - case config.RedHat, config.Oracle: - if majorVersion < 6 { - packNames = []string{"yum-utils", "yum-security", "yum-changelog"} - } else { - packNames = []string{"yum-plugin-changelog"} + packNames := []string{"yum-utils"} + if config.Conf.Deep { + switch o.Distro.Family { + case config.CentOS, config.Amazon: + packNames = append(packNames, "yum-plugin-changelog") + case config.RedHat, config.Oracle: + if majorVersion < 6 { + packNames = append(packNames, "yum-security", "yum-changelog") + } else { + packNames = append(packNames, "yum-plugin-changelog") + } + default: + return fmt.Errorf("Not implemented yet: %s", o.Distro) } - default: - return fmt.Errorf("Not implemented yet: %s", o.Distro) } for _, name := range packNames { From fd19fa2082f34faa02b9f0b5710a8cf45ee843ca Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Tue, 15 Aug 2017 10:37:11 +0900 Subject: [PATCH 093/113] nosudo repoquery --- scan/redhat.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scan/redhat.go b/scan/redhat.go index aaf07bb9..716fbdff 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -306,7 +306,7 @@ func (o *redhat) scanUpdatablePackages() (models.Packages, error) { cmd += " --enablerepo=" + repo } - r := o.exec(util.PrependProxyEnv(cmd), o.sudo()) + r := o.exec(util.PrependProxyEnv(cmd), noSudo) if !r.isSuccess() { return nil, fmt.Errorf("Failed to SSH: %s", r) } From 886a21c63384272b654490ef9806bc8b06b2eac4 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Tue, 15 Aug 2017 10:43:59 +0900 Subject: [PATCH 094/113] Bump up version to 0.4.0 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 4e7ff891..1cb67cc9 100644 --- a/main.go +++ b/main.go @@ -29,7 +29,7 @@ import ( ) // Version of Vuls -var version = "0.3.0" +var version = "0.4.0" // Revision of Git var revision string From a36a226ae21e049be661da51927e71383a961537 Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Tue, 15 Aug 2017 17:29:14 +0900 Subject: [PATCH 095/113] Update README.ja.md --- README.ja.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.ja.md b/README.ja.md index c4e82e47..620ba675 100644 --- a/README.ja.md +++ b/README.ja.md @@ -50,16 +50,17 @@ Vulsは上に挙げた手動運用での課題を解決するツールであり - Ubuntu, Debian, CentOS, Amazon Linux, RHEL, Raspbianに対応 - クラウド、オンプレミス、Docker - 高精度なスキャン - - Vulsは複数の脆弱性データベースを使っている - - OVAL +    - Vulsは複数の脆弱性データベース、複数の検知方法を組み合わせることで高精度なスキャンを実現している +        - OVAL - RHSA/ALAS/ELSA/FreeBSD-SA - Changelog - FastスキャンとDeepスキャン - Fastスキャン - root権限必要なし - スキャン対象サーバの負荷ほぼなし - - Deepスキャン - - Changelogの差分を取得し、そこに含まれる脆弱性を検知 + - スキャン対象サーバがインターネットに接続していなくてもスキャンできる +    - Deepスキャン +        - Changelogの差分を取得し、そこに書かれているCVE-IDを検知 - スキャン対象サーバに負荷がかかる場合がある - リモートスキャンとローカルスキャン - リモートスキャン From 477e12d5cf36d6a79b9bce761ecfbc7b58e6c19c Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Wed, 16 Aug 2017 11:54:19 +0900 Subject: [PATCH 096/113] Fix FreeBSD detection --- scan/freebsd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scan/freebsd.go b/scan/freebsd.go index bdcac08c..0bb6016a 100644 --- a/scan/freebsd.go +++ b/scan/freebsd.go @@ -54,7 +54,7 @@ func detectFreebsd(c config.ServerInfo) (itsMe bool, bsd osTypeInterface) { c.Distro = config.Distro{Family: config.FreeBSD} if r := exec(c, "uname", noSudo); r.isSuccess() { - if strings.Contains(r.Stdout, config.FreeBSD) == true { + if strings.Contains(strings.ToLower(r.Stdout), config.FreeBSD) == true { if b := exec(c, "freebsd-version", noSudo); b.isSuccess() { rel := strings.TrimSpace(b.Stdout) bsd.setDistro(config.FreeBSD, rel) From b5d4d273129695c44c2f89c2573b4f08a2b6034f Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Wed, 16 Aug 2017 14:34:59 +0900 Subject: [PATCH 097/113] Fix "Vulnerable package: is not found" error on FreeBSD --- scan/freebsd.go | 6 ++++++ scan/freebsd_test.go | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/scan/freebsd.go b/scan/freebsd.go index 0bb6016a..17f7d415 100644 --- a/scan/freebsd.go +++ b/scan/freebsd.go @@ -205,6 +205,12 @@ func (o *bsd) parsePkgVersion(stdout string) models.Packages { Version: ver, NewVersion: candidate, } + case ">": + o.log.Warn("The installed version of the %s is newer than the current version. *This situation can arise with an out of date index file, or when testing new ports.*", name) + packs[name] = models.Package{ + Name: name, + Version: ver, + } } } return packs diff --git a/scan/freebsd_test.go b/scan/freebsd_test.go index 9ad62ef2..8e1e6f96 100644 --- a/scan/freebsd_test.go +++ b/scan/freebsd_test.go @@ -21,6 +21,7 @@ All repositories are up-to-date. bash-4.2.45 < needs updating (remote has 4.3.42_1) gettext-0.18.3.1 < needs updating (remote has 0.19.7) tcl84-8.4.20_2,1 = up-to-date with remote +ntp-4.2.8p8_1 > succeeds port (port has 4.2.8p6) teTeX-base-3.0_25 ? orphaned: print/teTeX-base`, models.Packages{ @@ -42,6 +43,10 @@ teTeX-base-3.0_25 ? orphaned: print/teTeX-base`, Name: "teTeX-base", Version: "3.0_25", }, + "ntp": { + Name: "ntp", + Version: "4.2.8p8_1", + }, }, }, } From 6129ac7bd47601eac192d7dd161666ac00cd46d1 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Thu, 17 Aug 2017 12:18:06 +0900 Subject: [PATCH 098/113] Change model ScanResult.ScannedCves.AffectedPackages --- models/packages.go | 17 ++++++++--------- models/vulninfos.go | 23 ++++++++++++++++++++--- models/vulninfos_test.go | 24 ++++++++++++++++++++++++ oval/debian.go | 10 ++++------ oval/debian_test.go | 12 ++++++------ oval/redhat.go | 9 ++++----- oval/redhat_test.go | 12 ++++++------ oval/util.go | 8 ++++---- report/slack.go | 10 +++++----- report/tui.go | 10 +++++----- report/util.go | 12 ++++++------ report/util_test.go | 12 ++++++------ scan/debian.go | 11 ++++++++--- scan/freebsd.go | 8 +++++--- scan/redhat.go | 19 ++++++++++--------- 15 files changed, 121 insertions(+), 76 deletions(-) diff --git a/models/packages.go b/models/packages.go index 827ca103..6f67f7fa 100644 --- a/models/packages.go +++ b/models/packages.go @@ -92,15 +92,14 @@ func (ps Packages) FindOne(f func(Package) bool) (string, Package, bool) { // Package has installed packages. type Package struct { - Name string - Version string - Release string - NewVersion string - NewRelease string - Arch string - Repository string - Changelog Changelog - NotFixedYet bool // Ubuntu OVAL Only + Name string + Version string + Release string + NewVersion string + NewRelease string + Arch string + Repository string + Changelog Changelog } // FormatVer returns package version-release diff --git a/models/vulninfos.go b/models/vulninfos.go index 735e89b2..b7451231 100644 --- a/models/vulninfos.go +++ b/models/vulninfos.go @@ -104,11 +104,28 @@ func (v VulnInfos) FormatCveSummary() string { m["High"], m["Medium"], m["Low"], m["Unknown"]) } +// PackageStatuses is a list of PackageStatus +type PackageStatuses []PackageStatus + +// Sort by Name +func (p PackageStatuses) Sort() { + sort.Slice(p, func(i, j int) bool { + return p[i].Name < p[j].Name + }) + return +} + +// PackageStatus has name and other status abount the package +type PackageStatus struct { + Name string + NotFixedYet bool +} + // VulnInfo has a vulnerability information and unsecure packages type VulnInfo struct { CveID string Confidence Confidence - PackageNames []string + AffectedPackages PackageStatuses DistroAdvisories []DistroAdvisory // for Aamazon, RHEL, FreeBSD CpeNames []string CveContents CveContents @@ -547,8 +564,8 @@ func (v *VulnInfo) NilToEmpty() *VulnInfo { if v.DistroAdvisories == nil { v.DistroAdvisories = []DistroAdvisory{} } - if v.PackageNames == nil { - v.PackageNames = []string{} + if v.AffectedPackages == nil { + v.AffectedPackages = PackageStatuses{} } if v.CveContents == nil { v.CveContents = NewCveContents() diff --git a/models/vulninfos_test.go b/models/vulninfos_test.go index 99e054ee..93adaddb 100644 --- a/models/vulninfos_test.go +++ b/models/vulninfos_test.go @@ -910,3 +910,27 @@ func TestFormatMaxCvssScore(t *testing.T) { } } } + +func TestSortPackageStatues(t *testing.T) { + var tests = []struct { + in PackageStatuses + out PackageStatuses + }{ + { + in: PackageStatuses{ + {Name: "b"}, + {Name: "a"}, + }, + out: PackageStatuses{ + {Name: "a"}, + {Name: "b"}, + }, + }, + } + for _, tt := range tests { + tt.in.Sort() + if !reflect.DeepEqual(tt.in, tt.out) { + t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, tt.in) + } + } +} diff --git a/oval/debian.go b/oval/debian.go index 5137467b..f2cd8760 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -18,8 +18,6 @@ along with this program. If not, see . package oval import ( - "sort" - "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/util" @@ -95,11 +93,11 @@ func (o DebianBase) update(r *models.ScanResult, defPacks defPacks) { } // uniq(vinfo.PackNames + defPacks.actuallyAffectedPackNames) - for _, name := range vinfo.PackageNames { - defPacks.actuallyAffectedPackNames[name] = true + for _, pack := range vinfo.AffectedPackages { + defPacks.actuallyAffectedPackNames[pack.Name] = true } - vinfo.PackageNames = defPacks.packNames() - sort.Strings(vinfo.PackageNames) + vinfo.AffectedPackages = defPacks.toPackStatuses() + vinfo.AffectedPackages.Sort() r.ScannedCves[defPacks.def.Debian.CveID] = vinfo } diff --git a/oval/debian_test.go b/oval/debian_test.go index d3630acc..f522d1c6 100644 --- a/oval/debian_test.go +++ b/oval/debian_test.go @@ -36,7 +36,7 @@ func TestPackNamesOfUpdateDebian(t *testing.T) { in: models.ScanResult{ ScannedCves: models.VulnInfos{ "CVE-2000-1000": models.VulnInfo{ - PackageNames: []string{"packA"}, + AffectedPackages: models.PackageStatuses{{Name: "packA"}}, }, }, }, @@ -53,9 +53,9 @@ func TestPackNamesOfUpdateDebian(t *testing.T) { out: models.ScanResult{ ScannedCves: models.VulnInfos{ "CVE-2000-1000": models.VulnInfo{ - PackageNames: []string{ - "packA", - "packB", + AffectedPackages: models.PackageStatuses{ + {Name: "packA"}, + {Name: "packB"}, }, }, }, @@ -66,8 +66,8 @@ func TestPackNamesOfUpdateDebian(t *testing.T) { util.Log = util.NewCustomLogger(config.ServerInfo{}) for i, tt := range tests { Debian{}.update(&tt.in, tt.defPacks) - e := tt.out.ScannedCves["CVE-2000-1000"].PackageNames - a := tt.in.ScannedCves["CVE-2000-1000"].PackageNames + e := tt.out.ScannedCves["CVE-2000-1000"].AffectedPackages + a := tt.in.ScannedCves["CVE-2000-1000"].AffectedPackages if !reflect.DeepEqual(a, e) { t.Errorf("[%d] expected: %v\n actual: %v\n", i, e, a) } diff --git a/oval/redhat.go b/oval/redhat.go index c48f42b3..1b24e91a 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -19,7 +19,6 @@ package oval import ( "fmt" - "sort" "strconv" "strings" @@ -98,11 +97,11 @@ func (o RedHatBase) update(r *models.ScanResult, defPacks defPacks) { } // uniq(vinfo.PackNames + defPacks.actuallyAffectedPackNames) - for _, name := range vinfo.PackageNames { - defPacks.actuallyAffectedPackNames[name] = true + for _, pack := range vinfo.AffectedPackages { + defPacks.actuallyAffectedPackNames[pack.Name] = true } - vinfo.PackageNames = defPacks.packNames() - sort.Strings(vinfo.PackageNames) + vinfo.AffectedPackages = defPacks.toPackStatuses() + vinfo.AffectedPackages.Sort() r.ScannedCves[cve.CveID] = vinfo } } diff --git a/oval/redhat_test.go b/oval/redhat_test.go index a69bf838..7914686c 100644 --- a/oval/redhat_test.go +++ b/oval/redhat_test.go @@ -102,7 +102,7 @@ func TestPackNamesOfUpdate(t *testing.T) { in: models.ScanResult{ ScannedCves: models.VulnInfos{ "CVE-2000-1000": models.VulnInfo{ - PackageNames: []string{"packA"}, + AffectedPackages: models.PackageStatuses{{Name: "packA"}}, }, }, }, @@ -123,9 +123,9 @@ func TestPackNamesOfUpdate(t *testing.T) { out: models.ScanResult{ ScannedCves: models.VulnInfos{ "CVE-2000-1000": models.VulnInfo{ - PackageNames: []string{ - "packA", - "packB", + AffectedPackages: models.PackageStatuses{ + {Name: "packA"}, + {Name: "packB"}, }, }, }, @@ -136,8 +136,8 @@ func TestPackNamesOfUpdate(t *testing.T) { util.Log = util.NewCustomLogger(config.ServerInfo{}) for i, tt := range tests { RedHat{}.update(&tt.in, tt.defPacks) - e := tt.out.ScannedCves["CVE-2000-1000"].PackageNames - a := tt.in.ScannedCves["CVE-2000-1000"].PackageNames + e := tt.out.ScannedCves["CVE-2000-1000"].AffectedPackages + a := tt.in.ScannedCves["CVE-2000-1000"].AffectedPackages if !reflect.DeepEqual(a, e) { t.Errorf("[%d] expected: %v\n actual: %v\n", i, e, a) } diff --git a/oval/util.go b/oval/util.go index ffdce94e..bbbf671d 100644 --- a/oval/util.go +++ b/oval/util.go @@ -44,9 +44,9 @@ type defPacks struct { actuallyAffectedPackNames map[string]bool } -func (e defPacks) packNames() (names []string) { +func (e defPacks) toPackStatuses() (ps models.PackageStatuses) { for k := range e.actuallyAffectedPackNames { - names = append(names, k) + ps = append(ps, models.PackageStatus{Name: k}) } return } @@ -192,7 +192,7 @@ func httpGet(url string, pack *models.Package, resChan chan<- response, errChan } func getDefsByPackNameFromOvalDB(family, osRelease string, - packs models.Packages) (relatedDefs ovalResult, err error) { + installedPacks models.Packages) (relatedDefs ovalResult, err error) { ovallog.Initialize(config.Conf.LogDir) path := config.Conf.OvalDBURL @@ -211,7 +211,7 @@ func getDefsByPackNameFromOvalDB(family, osRelease string, return } defer ovaldb.CloseDB() - for _, pack := range packs { + for _, pack := range installedPacks { definitions, err := ovaldb.GetByPackName(osRelease, pack.Name) if err != nil { return relatedDefs, fmt.Errorf("Failed to get %s OVAL info by package name: %v", family, err) diff --git a/report/slack.go b/report/slack.go index ffac39e5..6926dfb2 100644 --- a/report/slack.go +++ b/report/slack.go @@ -174,12 +174,12 @@ func toSlackAttachments(r models.ScanResult) (attaches []*attachment) { for _, vinfo := range vinfos { curent := []string{} - for _, name := range vinfo.PackageNames { - if p, ok := r.Packages[name]; ok { + for _, affected := range vinfo.AffectedPackages { + if p, ok := r.Packages[affected.Name]; ok { curent = append(curent, fmt.Sprintf("%s-%s", p.Name, p.FormatVer())) } else { - curent = append(curent, name) + curent = append(curent, affected.Name) } } for _, n := range vinfo.CpeNames { @@ -187,8 +187,8 @@ func toSlackAttachments(r models.ScanResult) (attaches []*attachment) { } new := []string{} - for _, name := range vinfo.PackageNames { - if p, ok := r.Packages[name]; ok { + for _, affected := range vinfo.AffectedPackages { + if p, ok := r.Packages[affected.Name]; ok { new = append(new, p.FormatNewVer()) } else { new = append(new, "?") diff --git a/report/tui.go b/report/tui.go index 1364a6b8..5796dddc 100644 --- a/report/tui.go +++ b/report/tui.go @@ -713,8 +713,8 @@ func setChangelogLayout(g *gocui.Gui) error { lines = append(lines, adv.Format()) } - for _, name := range vinfo.PackageNames { - pack := currentScanResult.Packages[name] + for _, affected := range vinfo.AffectedPackages { + pack := currentScanResult.Packages[affected.Name] for _, p := range currentScanResult.Packages { if pack.Name == p.Name { lines = append(lines, p.FormatChangelog(), "\n") @@ -763,10 +763,10 @@ func detailLines() (string, error) { vinfo := vinfos[currentVinfo] packsVer := []string{} - sort.Strings(vinfo.PackageNames) - for _, name := range vinfo.PackageNames { + vinfo.AffectedPackages.Sort() + for _, affected := range vinfo.AffectedPackages { // packages detected by OVAL may not be actually installed - if pack, ok := r.Packages[name]; ok { + if pack, ok := r.Packages[affected.Name]; ok { packsVer = append(packsVer, pack.FormatVersionFromTo()) } } diff --git a/report/util.go b/report/util.go index 5cbf43c5..c90f23bc 100644 --- a/report/util.go +++ b/report/util.go @@ -216,9 +216,9 @@ No CVE-IDs are found in updatable packages. } packsVer := []string{} - sort.Strings(vuln.PackageNames) - for _, name := range vuln.PackageNames { - if pack, ok := r.Packages[name]; ok { + vuln.AffectedPackages.Sort() + for _, affected := range vuln.AffectedPackages { + if pack, ok := r.Packages[affected.Name]; ok { packsVer = append(packsVer, pack.FormatVersionFromTo()) } } @@ -322,9 +322,9 @@ func diff(curResults, preResults models.ScanResults) (diffed models.ScanResults, current.ScannedCves = getDiffCves(previous, current) packages := models.Packages{} for _, s := range current.ScannedCves { - for _, name := range s.PackageNames { - p := current.Packages[name] - packages[name] = p + for _, affected := range s.AffectedPackages { + p := current.Packages[affected.Name] + packages[affected.Name] = p } } current.Packages = packages diff --git a/report/util_test.go b/report/util_test.go index 6cf3c1f9..4deb6934 100644 --- a/report/util_test.go +++ b/report/util_test.go @@ -183,13 +183,13 @@ func TestDiff(t *testing.T) { ScannedCves: models.VulnInfos{ "CVE-2012-6702": { CveID: "CVE-2012-6702", - PackageNames: []string{"libexpat1"}, + AffectedPackages: models.PackageStatuses{{Name: "libexpat1"}}, DistroAdvisories: []models.DistroAdvisory{}, CpeNames: []string{}, }, "CVE-2014-9761": { CveID: "CVE-2014-9761", - PackageNames: []string{"libc-bin"}, + AffectedPackages: models.PackageStatuses{{Name: "libc-bin"}}, DistroAdvisories: []models.DistroAdvisory{}, CpeNames: []string{}, }, @@ -208,13 +208,13 @@ func TestDiff(t *testing.T) { ScannedCves: models.VulnInfos{ "CVE-2012-6702": { CveID: "CVE-2012-6702", - PackageNames: []string{"libexpat1"}, + AffectedPackages: models.PackageStatuses{{Name: "libexpat1"}}, DistroAdvisories: []models.DistroAdvisory{}, CpeNames: []string{}, }, "CVE-2014-9761": { CveID: "CVE-2014-9761", - PackageNames: []string{"libc-bin"}, + AffectedPackages: models.PackageStatuses{{Name: "libc-bin"}}, DistroAdvisories: []models.DistroAdvisory{}, CpeNames: []string{}, }, @@ -245,7 +245,7 @@ func TestDiff(t *testing.T) { ScannedCves: models.VulnInfos{ "CVE-2016-6662": { CveID: "CVE-2016-6662", - PackageNames: []string{"mysql-libs"}, + AffectedPackages: models.PackageStatuses{{Name: "mysql-libs"}}, DistroAdvisories: []models.DistroAdvisory{}, CpeNames: []string{}, }, @@ -283,7 +283,7 @@ func TestDiff(t *testing.T) { ScannedCves: models.VulnInfos{ "CVE-2016-6662": { CveID: "CVE-2016-6662", - PackageNames: []string{"mysql-libs"}, + AffectedPackages: models.PackageStatuses{{Name: "mysql-libs"}}, DistroAdvisories: []models.DistroAdvisory{}, CpeNames: []string{}, }, diff --git a/scan/debian.go b/scan/debian.go index 20c9fa98..1e787b98 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -508,10 +508,15 @@ func (o *debian) scanVulnInfos(updatablePacks models.Packages, meta *cache.Meta) o.log.Debugf("%d Cves are found. cves: %v", len(cveIDs), cveIDs) vinfos := models.VulnInfos{} for cveID, names := range cvePackages { + affected := models.PackageStatuses{} + for _, n := range names { + affected = append(affected, models.PackageStatus{Name: n}) + } + vinfos[cveID.CveID] = models.VulnInfo{ - CveID: cveID.CveID, - Confidence: cveID.Confidence, - PackageNames: names, + CveID: cveID.CveID, + Confidence: cveID.Confidence, + AffectedPackages: affected, } } diff --git a/scan/freebsd.go b/scan/freebsd.go index 17f7d415..597a83e1 100644 --- a/scan/freebsd.go +++ b/scan/freebsd.go @@ -164,13 +164,15 @@ func (o *bsd) scanUnsecurePackages() (models.VulnInfos, error) { }) } - names := []string{} + affected := models.PackageStatuses{} for name := range packs { - names = append(names, name) + affected = append(affected, models.PackageStatus{ + Name: name, + }) } vinfos[cveID] = models.VulnInfo{ CveID: cveID, - PackageNames: names, + AffectedPackages: affected, DistroAdvisories: disAdvs, Confidence: models.PkgAuditMatch, } diff --git a/scan/redhat.go b/scan/redhat.go index 716fbdff..ee68cfbc 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -610,13 +610,13 @@ func (o *redhat) scanCveIDsInChangelog(updatable models.Packages) (models.VulnIn for name, cveIDs := range packCveIDs { for _, cid := range cveIDs { if v, ok := vinfos[cid]; ok { - v.PackageNames = append(v.PackageNames, name) + v.AffectedPackages = append(v.AffectedPackages, models.PackageStatus{Name: name}) vinfos[cid] = v } else { vinfos[cid] = models.VulnInfo{ - CveID: cid, - PackageNames: []string{name}, - Confidence: models.ChangelogExactMatch, + CveID: cid, + AffectedPackages: models.PackageStatuses{{Name: name}}, + Confidence: models.ChangelogExactMatch, } } } @@ -703,18 +703,19 @@ func (o *redhat) scanCveIDsByCommands(updatable models.Packages) (models.VulnInf packs := dict[advIDCveIDs.DistroAdvisory.AdvisoryID] for _, pack := range packs { - vinfo.PackageNames = append(vinfo.PackageNames, pack.Name) + vinfo.AffectedPackages = append(vinfo.AffectedPackages, + models.PackageStatus{Name: pack.Name}) } } else { - names := []string{} packs := dict[advIDCveIDs.DistroAdvisory.AdvisoryID] - for _, pack := range packs { - names = append(names, pack.Name) + affected := models.PackageStatuses{} + for _, p := range packs { + affected = append(affected, models.PackageStatus{Name: p.Name}) } vinfo = models.VulnInfo{ CveID: cveID, DistroAdvisories: []models.DistroAdvisory{advIDCveIDs.DistroAdvisory}, - PackageNames: names, + AffectedPackages: affected, Confidence: models.YumUpdateSecurityMatch, } } From de65073f614692751e0c2003dde62087d84b1dc0 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Thu, 17 Aug 2017 15:32:22 +0900 Subject: [PATCH 099/113] Set NotFixedYet for Ubuntu Scan --- oval/util.go | 9 ++++++++- oval/util_test.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/oval/util.go b/oval/util.go index bbbf671d..ca2b553a 100644 --- a/oval/util.go +++ b/oval/util.go @@ -45,8 +45,15 @@ type defPacks struct { } func (e defPacks) toPackStatuses() (ps models.PackageStatuses) { + packNotFixedYet := map[string]bool{} + for _, p := range e.def.AffectedPacks { + packNotFixedYet[p.Name] = p.NotFixedYet + } for k := range e.actuallyAffectedPackNames { - ps = append(ps, models.PackageStatus{Name: k}) + ps = append(ps, models.PackageStatus{ + Name: k, + NotFixedYet: packNotFixedYet[k], + }) } return } diff --git a/oval/util_test.go b/oval/util_test.go index 48ef395b..6c8879ef 100644 --- a/oval/util_test.go +++ b/oval/util_test.go @@ -2,8 +2,10 @@ package oval import ( "reflect" + "sort" "testing" + "github.com/future-architect/vuls/models" ovalmodels "github.com/kotakanbe/goval-dictionary/models" ) @@ -96,3 +98,50 @@ func TestUpsert(t *testing.T) { } } } + +func TestDefpacksToPackStatuses(t *testing.T) { + var tests = []struct { + in defPacks + out models.PackageStatuses + }{ + { + in: defPacks{ + def: ovalmodels.Definition{ + AffectedPacks: []ovalmodels.Package{ + { + Name: "a", + NotFixedYet: true, + }, + { + Name: "b", + NotFixedYet: false, + }, + }, + }, + actuallyAffectedPackNames: map[string]bool{ + "a": true, + "b": false, + }, + }, + out: models.PackageStatuses{ + { + Name: "a", + NotFixedYet: true, + }, + { + Name: "b", + NotFixedYet: false, + }, + }, + }, + } + for i, tt := range tests { + actual := tt.in.toPackStatuses() + sort.Slice(actual, func(i, j int) bool { + return actual[i].Name < actual[j].Name + }) + if !reflect.DeepEqual(actual, tt.out) { + t.Errorf("[%d]\nexpected: %v\n actual: %v\n", i, tt.out, actual) + } + } +} From 9e90c0f91259be9bc14cb6420f776a6481c3260a Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Thu, 17 Aug 2017 20:03:42 +0900 Subject: [PATCH 100/113] Implement NotFixedYet for CentOS --- oval/debian.go | 2 +- oval/redhat.go | 2 +- oval/util.go | 80 ++++++++++++++++++++++++---- oval/util_test.go | 131 ++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 187 insertions(+), 28 deletions(-) diff --git a/oval/debian.go b/oval/debian.go index f2cd8760..7756bd79 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -96,7 +96,7 @@ func (o DebianBase) update(r *models.ScanResult, defPacks defPacks) { for _, pack := range vinfo.AffectedPackages { defPacks.actuallyAffectedPackNames[pack.Name] = true } - vinfo.AffectedPackages = defPacks.toPackStatuses() + vinfo.AffectedPackages = defPacks.toPackStatuses(r.Family, r.Packages) vinfo.AffectedPackages.Sort() r.ScannedCves[defPacks.def.Debian.CveID] = vinfo } diff --git a/oval/redhat.go b/oval/redhat.go index 1b24e91a..cd5c28f5 100644 --- a/oval/redhat.go +++ b/oval/redhat.go @@ -100,7 +100,7 @@ func (o RedHatBase) update(r *models.ScanResult, defPacks defPacks) { for _, pack := range vinfo.AffectedPackages { defPacks.actuallyAffectedPackNames[pack.Name] = true } - vinfo.AffectedPackages = defPacks.toPackStatuses() + vinfo.AffectedPackages = defPacks.toPackStatuses(r.Family, r.Packages) vinfo.AffectedPackages.Sort() r.ScannedCves[cve.CveID] = vinfo } diff --git a/oval/util.go b/oval/util.go index ca2b553a..0527756b 100644 --- a/oval/util.go +++ b/oval/util.go @@ -44,17 +44,77 @@ type defPacks struct { actuallyAffectedPackNames map[string]bool } -func (e defPacks) toPackStatuses() (ps models.PackageStatuses) { - packNotFixedYet := map[string]bool{} - for _, p := range e.def.AffectedPacks { - packNotFixedYet[p.Name] = p.NotFixedYet - } - for k := range e.actuallyAffectedPackNames { - ps = append(ps, models.PackageStatus{ - Name: k, - NotFixedYet: packNotFixedYet[k], - }) +func (e defPacks) toPackStatuses(family string, packs models.Packages) (ps models.PackageStatuses) { + switch family { + case config.Ubuntu: + packNotFixedYet := map[string]bool{} + for _, p := range e.def.AffectedPacks { + packNotFixedYet[p.Name] = p.NotFixedYet + } + for k := range e.actuallyAffectedPackNames { + ps = append(ps, models.PackageStatus{ + Name: k, + NotFixedYet: packNotFixedYet[k], + }) + } + + case config.CentOS: + // There are many packages that has been fixed in RedHat, but not been fixed in CentOS + for name := range e.actuallyAffectedPackNames { + pack, ok := packs[name] + if !ok { + util.Log.Warnf("Faild to find in Package list: %s", name) + return + } + + ovalPackVer := "" + for _, p := range e.def.AffectedPacks { + if p.Name == name { + ovalPackVer = p.Version + break + } + } + if ovalPackVer == "" { + util.Log.Warnf("Faild to find in Oval Package list: %s", name) + return + } + + packNewVer := fmt.Sprintf("%s-%s", pack.NewVersion, pack.NewRelease) + if packNewVer == "" { + // compare version: installed vs oval + vera := rpmver.NewVersion(fmt.Sprintf("%s-%s", pack.Version, pack.Release)) + verb := rpmver.NewVersion(ovalPackVer) + notFixedYet := false + if vera.LessThan(verb) { + notFixedYet = true + } + ps = append(ps, models.PackageStatus{ + Name: name, + NotFixedYet: notFixedYet, + }) + } else { + // compare version: newVer vs oval + vera := rpmver.NewVersion(packNewVer) + verb := rpmver.NewVersion(ovalPackVer) + notFixedYet := false + if vera.LessThan(verb) { + notFixedYet = true + } + ps = append(ps, models.PackageStatus{ + Name: name, + NotFixedYet: notFixedYet, + }) + } + } + + default: + for k := range e.actuallyAffectedPackNames { + ps = append(ps, models.PackageStatus{ + Name: k, + }) + } } + return } diff --git a/oval/util_test.go b/oval/util_test.go index 6c8879ef..241b16af 100644 --- a/oval/util_test.go +++ b/oval/util_test.go @@ -100,27 +100,37 @@ func TestUpsert(t *testing.T) { } func TestDefpacksToPackStatuses(t *testing.T) { + type in struct { + dp defPacks + family string + packs models.Packages + } var tests = []struct { - in defPacks + in in out models.PackageStatuses }{ + // Ubuntu { - in: defPacks{ - def: ovalmodels.Definition{ - AffectedPacks: []ovalmodels.Package{ - { - Name: "a", - NotFixedYet: true, - }, - { - Name: "b", - NotFixedYet: false, + in: in{ + family: "ubuntu", + packs: models.Packages{}, + dp: defPacks{ + def: ovalmodels.Definition{ + AffectedPacks: []ovalmodels.Package{ + { + Name: "a", + NotFixedYet: true, + }, + { + Name: "b", + NotFixedYet: false, + }, }, }, - }, - actuallyAffectedPackNames: map[string]bool{ - "a": true, - "b": false, + actuallyAffectedPackNames: map[string]bool{ + "a": true, + "b": true, + }, }, }, out: models.PackageStatuses{ @@ -134,9 +144,98 @@ func TestDefpacksToPackStatuses(t *testing.T) { }, }, }, + + // RedHat, Amazon, Debian + { + in: in{ + family: "redhat", + packs: models.Packages{}, + dp: defPacks{ + def: ovalmodels.Definition{ + AffectedPacks: []ovalmodels.Package{ + { + Name: "a", + }, + { + Name: "b", + }, + }, + }, + actuallyAffectedPackNames: map[string]bool{ + "a": true, + "b": true, + }, + }, + }, + out: models.PackageStatuses{ + { + Name: "a", + NotFixedYet: false, + }, + { + Name: "b", + NotFixedYet: false, + }, + }, + }, + + // CentOS + { + in: in{ + family: "centos", + packs: models.Packages{ + "a": {Version: "1.0.0"}, + "b": { + Version: "1.0.0", + NewVersion: "2.0.0", + }, + "c": { + Version: "1.0.0", + NewVersion: "1.5.0", + }, + }, + dp: defPacks{ + def: ovalmodels.Definition{ + AffectedPacks: []ovalmodels.Package{ + { + Name: "a", + Version: "1.0.1", + }, + { + Name: "b", + Version: "1.5.0", + }, + { + Name: "c", + Version: "2.0.0", + }, + }, + }, + actuallyAffectedPackNames: map[string]bool{ + "a": true, + "b": true, + "c": true, + }, + }, + }, + out: models.PackageStatuses{ + { + Name: "a", + NotFixedYet: true, + }, + { + Name: "b", + NotFixedYet: false, + }, + { + Name: "c", + NotFixedYet: true, + }, + }, + }, } for i, tt := range tests { - actual := tt.in.toPackStatuses() + actual := tt.in.dp.toPackStatuses(tt.in.family, tt.in.packs) sort.Slice(actual, func(i, j int) bool { return actual[i].Name < actual[j].Name }) From 71490aebd94b28d182e3e94b14cf3714cc5381af Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Thu, 17 Aug 2017 21:14:48 +0900 Subject: [PATCH 101/113] Fix sudo in deep scan of RHEL --- scan/redhat.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scan/redhat.go b/scan/redhat.go index ee68cfbc..f9243e73 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -173,8 +173,8 @@ func (o *redhat) checkIfSudoNoPasswd() error { // // - Deep scan mode // CentOS 6, 7 ... yum-utils -// RHEL 5 ... yum-security -// RHEL 6, 7 ... yum-utils +// RHEL 5 ... yum-security, yum-changelog +// RHEL 6, 7 ... yum-utils, yum-plugin-changelog // Amazon ... yum-utils func (o *redhat) checkDependencies() error { majorVersion, err := o.Distro.MajorVersion() @@ -306,7 +306,7 @@ func (o *redhat) scanUpdatablePackages() (models.Packages, error) { cmd += " --enablerepo=" + repo } - r := o.exec(util.PrependProxyEnv(cmd), noSudo) + r := o.exec(util.PrependProxyEnv(cmd), o.sudo()) if !r.isSuccess() { return nil, fmt.Errorf("Failed to SSH: %s", r) } @@ -421,7 +421,7 @@ func (o *redhat) getAvailableChangelogs(packNames []string) (map[string]string, cmd := `yum --color=never %s changelog all %s | grep -A 10000 '==================== Available Packages ===================='` cmd = fmt.Sprintf(cmd, yumopts, strings.Join(packNames, " ")) - r := o.exec(util.PrependProxyEnv(cmd), noSudo) + r := o.exec(util.PrependProxyEnv(cmd), o.sudo()) if !r.isSuccess(0, 1) { return nil, fmt.Errorf("Failed to SSH: %s", r) } @@ -997,7 +997,7 @@ func (o *redhat) sudo() bool { case config.Amazon, config.CentOS: return false default: - // RHEL - return true + // RHEL, Oracle + return config.Conf.Deep } } From 648a999514a3b525706fee4706990aa670eeef63 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Fri, 18 Aug 2017 22:39:45 +0900 Subject: [PATCH 102/113] Include config in json result --- config/config.go | 8 ++++---- models/scanresults.go | 8 ++++++++ report/report.go | 7 +++++++ scan/serverapi.go | 1 + 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/config/config.go b/config/config.go index ee7ea41b..033ecc35 100644 --- a/config/config.go +++ b/config/config.go @@ -110,7 +110,7 @@ type Config struct { S3ResultsDir string AzureAccount string - AzureKey string + AzureKey string `json:"-"` AzureContainer string Pipe bool @@ -300,7 +300,7 @@ type SMTPConf struct { SMTPPort string `valid:"port"` User string - Password string + Password string `json:"-"` From string To []string Cc []string @@ -360,7 +360,7 @@ func (c *SMTPConf) Validate() (errs []error) { // SlackConf is slack config type SlackConf struct { - HookURL string `valid:"url"` + HookURL string `valid:"url" json:"-"` Channel string `json:"channel"` IconEmoji string `json:"icon_emoji"` AuthUser string `json:"username"` @@ -410,7 +410,7 @@ type ServerInfo struct { Host string Port string KeyPath string - KeyPassword string + KeyPassword string `json:"-"` CpeNames []string DependencyCheckXMLPath string diff --git a/models/scanresults.go b/models/scanresults.go index 5c3076a0..60024eab 100644 --- a/models/scanresults.go +++ b/models/scanresults.go @@ -21,6 +21,8 @@ import ( "bytes" "fmt" "time" + + "github.com/future-architect/vuls/config" ) // ScanResults is a slide of ScanResult @@ -29,6 +31,7 @@ type ScanResults []ScanResult // ScanResult has the result of scanned CVE information. type ScanResult struct { ScannedAt time.Time + ReportedAt time.Time JSONVersion int Lang string ServerUUID string @@ -44,6 +47,11 @@ type ScanResult struct { Packages Packages Errors []string Optional [][]interface{} + + Config struct { + Scan config.Config + Report config.Config + } } // FilterByCvssOver is filter function. diff --git a/report/report.go b/report/report.go index ba0922d8..cd304885 100644 --- a/report/report.go +++ b/report/report.go @@ -20,6 +20,7 @@ package report import ( "fmt" "strings" + "time" c "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" @@ -35,12 +36,18 @@ const ( // FillCveInfos fills CVE Detailed Information func FillCveInfos(rs []models.ScanResult, dir string) ([]models.ScanResult, error) { var filled []models.ScanResult + reportedAt := time.Now() for _, r := range rs { if c.Conf.RefreshCve || needToRefreshCve(r) { if err := fillCveInfo(&r); err != nil { return nil, err } r.Lang = c.Conf.Lang + r.ReportedAt = reportedAt + r.Config.Report = c.Conf + r.Config.Report.Servers = map[string]c.ServerInfo{ + r.ServerName: c.Conf.Servers[r.ServerName], + } if err := overwriteJSONFile(dir, r); err != nil { return nil, fmt.Errorf("Failed to write JSON: %s", err) } diff --git a/scan/serverapi.go b/scan/serverapi.go index d8008553..71898e74 100644 --- a/scan/serverapi.go +++ b/scan/serverapi.go @@ -449,6 +449,7 @@ func scanVulns(jsonDir string, scannedAt time.Time, timeoutSec int) error { for _, s := range append(servers, errServers...) { r := s.convertToModel() r.ScannedAt = scannedAt + r.Config.Scan = config.Conf results = append(results, r) } From 93f741da352436e6964c5fe04ab149a611b50d4d Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Sat, 19 Aug 2017 00:21:11 +0900 Subject: [PATCH 103/113] Show Not Fixed Yet in report, tui --- models/packages.go | 18 ++++++------------ report/slack.go | 6 +++++- report/tui.go | 2 +- report/util.go | 2 +- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/models/packages.go b/models/packages.go index 6f67f7fa..587c9f52 100644 --- a/models/packages.go +++ b/models/packages.go @@ -60,15 +60,6 @@ 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 @@ -121,9 +112,12 @@ func (p Package) FormatNewVer() string { } // FormatVersionFromTo formats installed and new package version -func (p Package) FormatVersionFromTo() string { - return fmt.Sprintf("%s-%s - %s", - p.Name, p.FormatVer(), p.FormatNewVer()) +func (p Package) FormatVersionFromTo(notFixedYet bool) string { + to := p.FormatNewVer() + if notFixedYet { + to = "Not Fixed Yet" + } + return fmt.Sprintf("%s-%s - %s", p.Name, p.FormatVer(), to) } // FormatChangelog formats the changelog diff --git a/report/slack.go b/report/slack.go index 6926dfb2..141efbfd 100644 --- a/report/slack.go +++ b/report/slack.go @@ -189,7 +189,11 @@ func toSlackAttachments(r models.ScanResult) (attaches []*attachment) { new := []string{} for _, affected := range vinfo.AffectedPackages { if p, ok := r.Packages[affected.Name]; ok { - new = append(new, p.FormatNewVer()) + if affected.NotFixedYet { + new = append(new, "Not Fixed Yet") + } else { + new = append(new, p.FormatNewVer()) + } } else { new = append(new, "?") } diff --git a/report/tui.go b/report/tui.go index 5796dddc..ee3a554c 100644 --- a/report/tui.go +++ b/report/tui.go @@ -767,7 +767,7 @@ func detailLines() (string, error) { for _, affected := range vinfo.AffectedPackages { // packages detected by OVAL may not be actually installed if pack, ok := r.Packages[affected.Name]; ok { - packsVer = append(packsVer, pack.FormatVersionFromTo()) + packsVer = append(packsVer, pack.FormatVersionFromTo(affected.NotFixedYet)) } } sort.Strings(vinfo.CpeNames) diff --git a/report/util.go b/report/util.go index c90f23bc..78330578 100644 --- a/report/util.go +++ b/report/util.go @@ -219,7 +219,7 @@ No CVE-IDs are found in updatable packages. vuln.AffectedPackages.Sort() for _, affected := range vuln.AffectedPackages { if pack, ok := r.Packages[affected.Name]; ok { - packsVer = append(packsVer, pack.FormatVersionFromTo()) + packsVer = append(packsVer, pack.FormatVersionFromTo(affected.NotFixedYet)) } } sort.Strings(vuln.CpeNames) From 4ac5d9e0daf05abe461ca5dc65fb2de26f0f3661 Mon Sep 17 00:00:00 2001 From: sadayuki-matsuno Date: Tue, 22 Aug 2017 12:40:54 +0900 Subject: [PATCH 104/113] add oval docker (#466) * add oval docker * Update README.md --- setup/docker/README.md | 43 ++++++ .../docker/goval-dictionary/latest/Dockerfile | 19 +++ .../docker/goval-dictionary/latest/README.md | 125 ++++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 setup/docker/goval-dictionary/latest/Dockerfile create mode 100644 setup/docker/goval-dictionary/latest/README.md diff --git a/setup/docker/README.md b/setup/docker/README.md index 06cd4e38..0d3c5ac6 100644 --- a/setup/docker/README.md +++ b/setup/docker/README.md @@ -6,6 +6,8 @@ This is the Git repo of the official Docker image for vuls. - go-cve-dictionary - [`latest` (*go-cve-dictionary:latest Dockerfile*)]() +- goval-dictionary + - [`latest` (*goval-dictionary:latest Dockerfile*)]() - vuls - [`latest` (*vuls:latest Dockerfile*)]() - vulsrepo @@ -28,6 +30,14 @@ $ docker run --rm vuls/go-cve-dictionary -v go-cve-dictionary v0.0.xxx xxxx ``` +- goval-dictionary + +```console +$ docker run --rm vuls/goval-dictionary -v + +goval-dictionary v0.0.xxx xxxx +``` + - vuls ```console @@ -44,6 +54,12 @@ vuls v0.0.xxx xxxx $ docker rmi vuls/go-cve-dictionary ``` +- goval-dictionary + +``` +$ docker rmi vuls/goval-dictionary +``` + - vuls ``` @@ -58,6 +74,12 @@ $ docker rmi vuls/vuls $ docker pull vuls/go-cve-dictionary ``` +- goval-dictionary + +``` +$ docker pull vuls/goval-dictionary +``` + - vuls ``` @@ -72,6 +94,12 @@ $ docker run --rm vuls/go-cve-dictionary -v go-cve-dictionary v0.1.xxx xxxx ``` +```console +$ docker run --rm vuls/goval-dictionary -v + +goval-dictionary v0.1.xxx xxxx +``` + - vuls ```console @@ -84,6 +112,7 @@ vuls v0.1.xxx xxxx # How to use this image 1. fetch nvd (vuls/go-cve-dictionary) +1. fetch oval (vuls/goval-dictionary) 1. configuration (vuls/vuls) 1. configtest (vuls/vuls) 1. scan (vuls/vuls) @@ -100,6 +129,19 @@ $ for i in `seq 2002 $(date +"%Y")`; do \ done ``` +- To fetch JVN(Japanese), See [README](https://github.com/kotakanbe/go-cve-dictionary#usage-fetch-jvn-data) + +## Step2. Fetch OVAL (e.g. redhat) + +```console +$ docker run --rm -it \ + -v $PWD:/vuls \ + -v $PWD/goval-dictionary-log:/var/log/vuls \ + vuls/goval-dictionary fetch-redhat 5 6 7 +``` + +- To fetch other OVAL, See [README](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-redhat) + ## Step2. Configuration Create config.toml referring to [this](https://github.com/future-architect/vuls#configuration). @@ -149,6 +191,7 @@ $ docker run --rm -it \ -v /etc/localtime:/etc/localtime:ro \ vuls/vuls report \ -cvedb-path=/vuls/cve.sqlite3 \ + -ovaldb-path=/vuls/oval.sqlite3 \ -format-short-text \ -config=./config.toml # path to config.toml in docker ``` diff --git a/setup/docker/goval-dictionary/latest/Dockerfile b/setup/docker/goval-dictionary/latest/Dockerfile new file mode 100644 index 00000000..6aeeb76b --- /dev/null +++ b/setup/docker/goval-dictionary/latest/Dockerfile @@ -0,0 +1,19 @@ +FROM golang:latest + +MAINTAINER sadayuki-matsuno + +ENV REPOSITORY github.com/kotakanbe/goval-dictionary +ENV LOGDIR /var/log/vuls +ENV WORKDIR /vuls +# goval-dictionary install +RUN git clone https://$REPOSITORY.git $GOPATH/src/$REPOSITORY \ + && cd $GOPATH/src/$REPOSITORY \ + && make install \ + && mkdir -p $LOGDIR + +VOLUME [$WORKDIR, $LOGDIR] +WORKDIR $WORKDIR +ENV PWD $WORKDIR + +ENTRYPOINT ["goval-dictionary"] +CMD ["--help"] diff --git a/setup/docker/goval-dictionary/latest/README.md b/setup/docker/goval-dictionary/latest/README.md new file mode 100644 index 00000000..48b33e68 --- /dev/null +++ b/setup/docker/goval-dictionary/latest/README.md @@ -0,0 +1,125 @@ +# goval-dictionary-Docker + +This is the Git repo of the official Docker image for goval-dictionary. +See the [Hub page](https://hub.docker.com/r/vuls/goval-dictionary/) for the full readme on how to use the Docker image and for information regarding contributing and issues. + +# Supported tags and respective `Dockerfile` links + +- [`latest` (*goval-dictionary:latest Dockerfile*)](https://github.com/future-architect/vuls/blob/master/setup/docker/goval-dictionary/latest/Dockerfile) + +# Caution + +This image is built per commit. +If you want to use the latest docker image, you should remove the existing image, and pull it once again. + +- Remove old docker image + +``` +$ docker rmi vuls/goval-dictionary +``` + +- Pull new docker image + +``` +$ docker pull vuls/goval-dictionary +``` + +# What is goval-dictionary? + +This is tool to build a local copy of the OVAL. The local copy is generated in sqlite format, and the tool has a server mode for easy querying. + +# How to use this image + +## check vuls version + +``` +$ docker run --rm vuls/goval-dictionary -v +``` + +## fetch-redhat + +```console +$ for i in `seq 5 7`; do \ + docker run --rm -it \ + -v $PWD:/vuls \ + -v $PWD/goval-dictionary-log:/var/log/vuls \ + vuls/goval-dictionary fetch-redhat $i; \ + done +``` + +## fetch-debian + +```console +$ for i in `seq 7 10`; do \ + docker run --rm -it \ + -v $PWD:/vuls \ + -v $PWD/goval-dictionary-log:/var/log/vuls \ + vuls/goval-dictionary fetch-debian $i; \ + done +``` + +## fetch-ubuntu + +```console +$ for i in `seq 12 2 16`; do \ + docker run --rm -it \ + -v $PWD:/vuls \ + -v $PWD/goval-dictionary-log:/var/log/vuls \ + vuls/goval-dictionary fetch-ubuntu $i; \ + done +``` + +## fetch-suse + +```console +$ docker run --rm -it \ + -v $PWD:/vuls \ + -v $PWD/goval-dictionary-log:/var/log/vuls \ + vuls/goval-dictionary fetch-suse -opensuse 13.2 +``` + +## fetch-oracle + +```console +$ docker run --rm -it \ + -v $PWD:/vuls \ + -v $PWD/goval-dictionary-log:/var/log/vuls \ + vuls/goval-dictionary fetch-oracle +``` + +## server + +```console +$ docker run -dt \ + --name goval-dictionary \ + -v $PWD:/vuls \ + -v $PWD/goval-dictionary-log:/var/log/vuls \ + --expose 1324 \ + -p 1324:1324 \ + vuls/goval-dictionary server --bind=0.0.0.0 +``` + +Prease refer to [this](https://hub.docker.com/r/vuls/goval-dictionary). + +## vuls + +Please refer to [this](https://hub.docker.com/r/vuls/vuls/). + +# User Feedback + +## Documentation + +Documentation for this image is stored in the [`docker/` directory](https://github.com/future-architect/vuls/tree/master/setup/docker) of the [`future-architect/vuls` GitHub repo](https://github.com/future-architect/vuls). + +## Issues + +If you have any problems with or questions about this image, please contact us through a [GitHub issue](https://github.com/future-architect/vuls/issues). + +## Contributing + +1. fork a repository: github.com/future-architect/vuls to github.com/you/repo +1. get original code: go get github.com/future-architect/vuls +1. work on original code +1. add remote to your repo: git remote add myfork https://github.com/you/repo.git +1. push your changes: git push myfork +1. create a new Pull Request From b5cb08ac4384561e9dae001ba582f3fcfafd472f Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Tue, 22 Aug 2017 17:44:50 +0900 Subject: [PATCH 105/113] Handle kernel's vulns using OVAL --- README.ja.md | 15 +++- README.md | 15 +++- models/scanresults.go | 14 +++- oval/debian.go | 181 ++++++++++++++++++++++++++++++++++-------- oval/oval.go | 5 +- oval/util.go | 43 ++++++---- report/report.go | 4 - scan/base.go | 60 +++++++------- scan/debian.go | 110 ++++++++++++++++--------- scan/debian_test.go | 162 +------------------------------------ scan/freebsd.go | 43 +++++++--- scan/redhat.go | 81 ++++++++++++++----- scan/redhat_test.go | 4 +- scan/serverapi.go | 9 +-- 14 files changed, 417 insertions(+), 329 deletions(-) diff --git a/README.ja.md b/README.ja.md index 620ba675..8ba6d3bd 100644 --- a/README.ja.md +++ b/README.ja.md @@ -767,6 +767,19 @@ configtest: configtestサブコマンドは、config.tomlで定義されたサーバ/コンテナに対してSSH可能かどうかをチェックする。 +## Fast Scan Mode + +| Distribution | Release | Requirements | +|:-------------|-------------------:|:-------------| +| Ubuntu | 12, 14, 16| - | +| Debian | 7, 8| reboot-notifier| +| CentOS | 6, 7| - | +| Amazon | All | - | +| RHEL | 5, 6, 7 | - | +| Oracle Linux | 5, 6, 7 | - | +| FreeBSD | 10, 11 | - | +| Raspbian | Jessie, Stretch | - | + ## Deep Scan Mode Deep Scan Modeではスキャン対象サーバ上にいくつかの依存パッケージが必要。 @@ -781,7 +794,7 @@ Deep Scan Modeでスキャンするためには、下記のパッケージが必 | Distribution | Release | Requirements | |:-------------|-------------------:|:-------------| | Ubuntu | 12, 14, 16| - | -| Debian | 7, 8| aptitude | +| Debian | 7, 8| aptitude, reboot-notifier | | CentOS | 6, 7| yum-plugin-changelog, yum-utils | | Amazon | All | yum-plugin-changelog, yum-utils | | RHEL | 5 | yum-utils, yum-security, yum-changelog | diff --git a/README.md b/README.md index d1682f41..57457401 100644 --- a/README.md +++ b/README.md @@ -776,6 +776,19 @@ configtest: The configtest subcommand checks whether vuls is able to connect via SSH to servers/containers defined in the config.toml + ## Fast Scan Mode + +| Distribution | Release | Requirements | +|:-------------|-------------------:|:-------------| +| Ubuntu | 12, 14, 16| - | +| Debian | 7, 8| reboot-notifier| +| CentOS | 6, 7| - | +| Amazon | All | - | +| RHEL | 5, 6, 7 | - | +| Oracle Linux | 5, 6, 7 | - | +| FreeBSD | 10, 11 | - | +| Raspbian | Jessie, Stretch | - | + ## Deep Scan Mode Some dependent packages are needed in Deep Scan Mode. @@ -788,7 +801,7 @@ In order to scan with deep scan mode, the following dependencies are required, s | Distribution | Release | Requirements | |:-------------|-------------------:|:-------------| | Ubuntu | 12, 14, 16| - | -| Debian | 7, 8| aptitude | +| Debian | 7, 8| aptitude, reboot-notifier | | CentOS | 6, 7| yum-plugin-changelog, yum-utils | | Amazon | All | yum-plugin-changelog, yum-utils | | RHEL | 5 | yum-utils, yum-security, yum-changelog | diff --git a/models/scanresults.go b/models/scanresults.go index 60024eab..f11b6ec0 100644 --- a/models/scanresults.go +++ b/models/scanresults.go @@ -44,9 +44,10 @@ type ScanResult struct { // Scanned Vulns by SSH scan + CPE + OVAL ScannedCves VulnInfos - Packages Packages - Errors []string - Optional [][]interface{} + RunningKernel Kernel + Packages Packages + Errors []string + Optional [][]interface{} Config struct { Scan config.Config @@ -54,6 +55,13 @@ type ScanResult struct { } } +// Kernel has the Release, version and whether need restart +type Kernel struct { + Release string + Version string + RebootRequired bool +} + // FilterByCvssOver is filter function. func (r ScanResult) FilterByCvssOver(over float64) ScanResult { filtered := r.ScannedCves.Find(func(v VulnInfo) bool { diff --git a/oval/debian.go b/oval/debian.go index 7756bd79..873a824d 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -29,40 +29,6 @@ type DebianBase struct { Base } -// FillWithOval returns scan result after updating CVE info by OVAL -func (o DebianBase) FillWithOval(r *models.ScanResult) (err error) { - var relatedDefs ovalResult - if o.isFetchViaHTTP() { - if relatedDefs, err = getDefsByPackNameViaHTTP(r); err != nil { - return err - } - } else { - if relatedDefs, err = getDefsByPackNameFromOvalDB(o.family, r.Release, r.Packages); err != nil { - return err - } - } - - for _, defPacks := range relatedDefs.entries { - o.update(r, defPacks) - } - - for _, vuln := range r.ScannedCves { - switch models.NewCveContentType(o.family) { - case models.Debian: - if cont, ok := vuln.CveContents[models.Debian]; ok { - cont.SourceLink = "https://security-tracker.debian.org/tracker/" + cont.CveID - vuln.CveContents[models.Debian] = cont - } - case models.Ubuntu: - 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 -} - func (o DebianBase) update(r *models.ScanResult, defPacks defPacks) { ovalContent := *o.convertToModel(&defPacks.def) ovalContent.Type = models.NewCveContentType(o.family) @@ -136,6 +102,57 @@ func NewDebian() Debian { } } +// FillWithOval returns scan result after updating CVE info by OVAL +func (o Debian) FillWithOval(r *models.ScanResult) (err error) { + + //Debian's uname gives both of kernel release(uname -r), version(kernel-image version) + linuxImage := "linux-image-" + r.RunningKernel.Release + // Add linux and set the version of running kernel to search OVAL. + if r.Container.ContainerID == "" { + r.Packages["linux"] = models.Package{ + Name: "linux", + Version: r.RunningKernel.Version, + } + } + + var relatedDefs ovalResult + if o.isFetchViaHTTP() { + if relatedDefs, err = getDefsByPackNameViaHTTP(r); err != nil { + return err + } + } else { + if relatedDefs, err = getDefsByPackNameFromOvalDB(o.family, r.Release, r.Packages); err != nil { + return err + } + } + + delete(r.Packages, "linux") + + for _, defPacks := range relatedDefs.entries { + // Remove linux added above to search for oval + // linux is not a real package name (key of affected packages in OVAL) + if _, ok := defPacks.actuallyAffectedPackNames["linux"]; ok { + defPacks.actuallyAffectedPackNames[linuxImage] = true + delete(defPacks.actuallyAffectedPackNames, "linux") + for i, p := range defPacks.def.AffectedPacks { + if p.Name == "linux" { + p.Name = linuxImage + defPacks.def.AffectedPacks[i] = p + } + } + } + o.update(r, defPacks) + } + + 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 @@ -151,3 +168,99 @@ func NewUbuntu() Ubuntu { }, } } + +// FillWithOval returns scan result after updating CVE info by OVAL +func (o Ubuntu) FillWithOval(r *models.ScanResult) (err error) { + ovalKernelImageNames := []string{ + "linux-aws", + "linux-azure", + "linux-flo", + "linux-gcp", + "linux-gke", + "linux-goldfish", + "linux-hwe", + "linux-hwe-edge", + "linux-kvm", + "linux-mako", + "linux-raspi2", + "linux-snapdragon", + } + linuxImage := "linux-image-" + r.RunningKernel.Release + + found := false + if r.Container.ContainerID == "" { + for _, n := range ovalKernelImageNames { + if _, ok := r.Packages[n]; ok { + v, ok := r.Packages[linuxImage] + if ok { + // Set running kernel version + p := r.Packages[n] + p.Version = v.Version + p.NewVersion = v.NewVersion + r.Packages[n] = p + } else { + util.Log.Warnf("Running kernel image %s is not found: %s", + linuxImage, r.RunningKernel.Version) + } + found = true + break + } + } + + if !found { + // linux-generic is described as "linux" in Ubuntu's oval. + // Add "linux" and set the version of running kernel to search OVAL. + v, ok := r.Packages[linuxImage] + if ok { + r.Packages["linux"] = models.Package{ + Name: "linux", + Version: v.Version, + NewVersion: v.NewVersion, + } + } else { + util.Log.Warnf("%s is not found. Running: %s", + linuxImage, r.RunningKernel.Release) + } + } + } + + var relatedDefs ovalResult + if o.isFetchViaHTTP() { + if relatedDefs, err = getDefsByPackNameViaHTTP(r); err != nil { + return err + } + } else { + if relatedDefs, err = getDefsByPackNameFromOvalDB(o.family, r.Release, r.Packages); err != nil { + return err + } + } + + if !found { + delete(r.Packages, "linux") + } + + for _, defPacks := range relatedDefs.entries { + + // Remove "linux" added above to search for oval + // "linux" is not a real package name (key of affected packages in OVAL) + if _, ok := defPacks.actuallyAffectedPackNames["linux"]; !found && ok { + defPacks.actuallyAffectedPackNames[linuxImage] = true + delete(defPacks.actuallyAffectedPackNames, "linux") + for i, p := range defPacks.def.AffectedPacks { + if p.Name == "linux" { + p.Name = linuxImage + defPacks.def.AffectedPacks[i] = p + } + } + } + o.update(r, defPacks) + } + + 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/oval.go b/oval/oval.go index 591c8853..6fb3846b 100644 --- a/oval/oval.go +++ b/oval/oval.go @@ -146,8 +146,5 @@ func (b Base) CheckIfOvalFresh(osFamily, release string) (ok bool, err error) { func (b Base) isFetchViaHTTP() bool { // Default value of OvalDBType is sqlite3 - if config.Conf.OvalDBURL != "" && config.Conf.OvalDBType == "sqlite3" { - return true - } - return false + return config.Conf.OvalDBURL != "" && config.Conf.OvalDBType == "sqlite3" } diff --git a/oval/util.go b/oval/util.go index 0527756b..e6f9542d 100644 --- a/oval/util.go +++ b/oval/util.go @@ -58,7 +58,7 @@ func (e defPacks) toPackStatuses(family string, packs models.Packages) (ps model }) } - case config.CentOS: + case config.CentOS, config.Debian: // There are many packages that has been fixed in RedHat, but not been fixed in CentOS for name := range e.actuallyAffectedPackNames { pack, ok := packs[name] @@ -79,8 +79,7 @@ func (e defPacks) toPackStatuses(family string, packs models.Packages) (ps model return } - packNewVer := fmt.Sprintf("%s-%s", pack.NewVersion, pack.NewRelease) - if packNewVer == "" { + if pack.NewVersion == "" { // compare version: installed vs oval vera := rpmver.NewVersion(fmt.Sprintf("%s-%s", pack.Version, pack.Release)) verb := rpmver.NewVersion(ovalPackVer) @@ -94,6 +93,7 @@ func (e defPacks) toPackStatuses(family string, packs models.Packages) (ps model }) } else { // compare version: newVer vs oval + packNewVer := fmt.Sprintf("%s-%s", pack.NewVersion, pack.NewRelease) vera := rpmver.NewVersion(packNewVer) verb := rpmver.NewVersion(ovalPackVer) notFixedYet := false @@ -193,11 +193,15 @@ func getDefsByPackNameViaHTTP(r *models.ScanResult) ( if res.pack.Name != p.Name { continue } + + if p.NotFixedYet { + relatedDefs.upsert(def, p.Name) + continue + } + if less, err := lessThan(r.Family, *res.pack, p); err != nil { - if !p.NotFixedYet { - util.Log.Debugf("Failed to parse versions: %s", err) - util.Log.Debugf("%#v\n%#v", *res.pack, p) - } + util.Log.Debugf("Failed to parse versions: %s", err) + util.Log.Debugf("%#v\n%#v", *res.pack, p) } else if less { relatedDefs.upsert(def, p.Name) } @@ -278,23 +282,28 @@ func getDefsByPackNameFromOvalDB(family, osRelease string, return } defer ovaldb.CloseDB() - for _, pack := range installedPacks { - definitions, err := ovaldb.GetByPackName(osRelease, pack.Name) + for _, installedPack := range installedPacks { + definitions, err := ovaldb.GetByPackName(osRelease, installedPack.Name) if err != nil { return relatedDefs, fmt.Errorf("Failed to get %s OVAL info by package name: %v", family, err) } for _, def := range definitions { - for _, p := range def.AffectedPacks { - if pack.Name != p.Name { + for _, ovalPack := range def.AffectedPacks { + if installedPack.Name != ovalPack.Name { continue } - if less, err := lessThan(family, pack, p); err != nil { - if !p.NotFixedYet { - util.Log.Debugf("Failed to parse versions: %s", err) - util.Log.Debugf("%#v\n%#v", pack, p) - } + + if ovalPack.NotFixedYet { + relatedDefs.upsert(def, installedPack.Name) + continue + } + + less, err := lessThan(family, installedPack, ovalPack) + if err != nil { + util.Log.Debugf("Failed to parse versions: %s", err) + util.Log.Debugf("%#v\n%#v", installedPack, ovalPack) } else if less { - relatedDefs.upsert(def, pack.Name) + relatedDefs.upsert(def, installedPack.Name) } } } diff --git a/report/report.go b/report/report.go index cd304885..4d07fd6c 100644 --- a/report/report.go +++ b/report/report.go @@ -170,10 +170,6 @@ func FillWithOval(r *models.ScanResult) (err error) { case c.Oracle: ovalClient = oval.NewOracle() ovalFamily = c.Oracle - //TODO - // case c.Suse: - // ovalClient = oval.New() - // ovalFamily = c.Oracle case c.Amazon, c.Raspbian, c.FreeBSD: return nil default: diff --git a/scan/base.go b/scan/base.go index 0f13b63e..ba02a346 100644 --- a/scan/base.go +++ b/scan/base.go @@ -75,6 +75,27 @@ func (l *base) getPlatform() models.Platform { return l.Platform } +func (l *base) runningKernel() (release, version string, err error) { + r := l.exec("uname -r", noSudo) + if !r.isSuccess() { + return "", "", fmt.Errorf("Failed to SSH: %s", r) + } + release = strings.TrimSpace(r.Stdout) + + switch l.Distro.Family { + case config.Debian: + r := l.exec("uname -a", noSudo) + if !r.isSuccess() { + return "", "", fmt.Errorf("Failed to SSH: %s", r) + } + ss := strings.Fields(r.Stdout) + if 6 < len(ss) { + version = ss[6] + } + } + return +} + func (l *base) allContainers() (containers []config.Container, err error) { switch l.ServerInfo.Containers.Type { case "", "docker": @@ -265,16 +286,6 @@ func (l *base) isAwsInstanceID(str string) bool { } func (l *base) convertToModel() models.ScanResult { - //TODO Remove - // for _, p := range l.VulnInfos { - // sort.Slice(p.Packages, func(i, j int) bool { - // return p.Packages[i].Name < p.Packages[j].Name - // }) - // } - // sort.Slice(l.VulnInfos, func(i, j int) bool { - // return l.VulnInfos[i].CveID < l.VulnInfos[j].CveID - // }) - ctype := l.ServerInfo.Containers.Type if l.ServerInfo.Container.ContainerID != "" && ctype == "" { ctype = "docker" @@ -291,24 +302,19 @@ func (l *base) convertToModel() models.ScanResult { errs = append(errs, fmt.Sprintf("%s", e)) } - //TODO Remove - // Avoid null slice being null in JSON - // for cveID := range l.VulnInfos { - // l.VulnInfos[i].NilToEmpty() - // } - return models.ScanResult{ - JSONVersion: models.JSONVersion, - ServerName: l.ServerInfo.ServerName, - ScannedAt: time.Now(), - Family: l.Distro.Family, - Release: l.Distro.Release, - Container: container, - Platform: l.Platform, - ScannedCves: l.VulnInfos, - Packages: l.Packages, - Optional: l.ServerInfo.Optional, - Errors: errs, + JSONVersion: models.JSONVersion, + ServerName: l.ServerInfo.ServerName, + ScannedAt: time.Now(), + Family: l.Distro.Family, + Release: l.Distro.Release, + Container: container, + Platform: l.Platform, + ScannedCves: l.VulnInfos, + RunningKernel: l.Kernel, + Packages: l.Packages, + Optional: l.ServerInfo.Optional, + Errors: errs, } } diff --git a/scan/debian.go b/scan/debian.go index 1e787b98..581f0917 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -154,56 +154,92 @@ func (o *debian) checkIfSudoNoPasswd() error { } func (o *debian) checkDependencies() error { - if !config.Conf.Deep { - o.log.Infof("Dependencies... No need") - return nil - } + packNames := []string{} + switch o.Distro.Family { case config.Ubuntu, config.Raspbian: o.log.Infof("Dependencies... No need") return nil case config.Debian: - // Debian needs aptitude to get changelogs. - // Because unable to get changelogs via apt-get changelog on Debian. - if r := o.exec("test -f /usr/bin/aptitude", noSudo); !r.isSuccess() { - msg := fmt.Sprintf("aptitude is not installed: %s", r) - o.log.Errorf(msg) - return fmt.Errorf(msg) + // https://askubuntu.com/a/742844 + packNames = append(packNames, "reboot-notifier") + + if !config.Conf.Deep { + // Debian needs aptitude to get changelogs. + // Because unable to get changelogs via apt-get changelog on Debian. + packNames = append(packNames, "aptitude") } - o.log.Infof("Dependencies... Pass") - return nil default: return fmt.Errorf("Not implemented yet: %s", o.Distro) } -} -func (o *debian) scanPackages() error { - installed, updatable, err := o.scanInstalledPackages() - if err != nil { - o.log.Errorf("Failed to scan installed packages") - return err - } - o.setPackages(installed) - - if config.Conf.Deep || o.Distro.Family == config.Raspbian { - unsecure, err := o.scanUnsecurePackages(updatable) - if err != nil { - o.log.Errorf("Failed to scan vulnerable packages") - return err + for _, name := range packNames { + //TODO --show-format + cmd := "dpkg-query -W " + name + if r := o.exec(cmd, noSudo); !r.isSuccess() { + msg := fmt.Sprintf("%s is not installed", name) + o.log.Errorf(msg) + return fmt.Errorf(msg) } - o.setVulnInfos(unsecure) - return nil } - + o.log.Infof("Dependencies... Pass") return nil } -func (o *debian) scanInstalledPackages() (models.Packages, models.Packages, error) { - installed := models.Packages{} - updatable := models.Packages{} +func (o *debian) scanPackages() error { + // collect the running kernel information + release, version, err := o.runningKernel() + if err != nil { + o.log.Errorf("Failed to scan the running kernel version: %s", err) + return err + } + rebootRequired, err := o.rebootRequired() + if err != nil { + o.log.Errorf("Failed to detect the kernel reboot required: %s", err) + return err + } + o.Kernel = models.Kernel{ + Version: version, + Release: release, + RebootRequired: rebootRequired, + } + installed, updatable, err := o.scanInstalledPackages() + if err != nil { + o.log.Errorf("Failed to scan installed packages: %s", err) + return err + } + o.Packages = installed + + if config.Conf.Deep || o.Distro.Family == config.Raspbian { + unsecures, err := o.scanUnsecurePackages(updatable) + if err != nil { + o.log.Errorf("Failed to scan vulnerable packages: %s", err) + return err + } + o.VulnInfos = unsecures + return nil + } + return nil +} + +// https://askubuntu.com/a/742844 +func (o *debian) rebootRequired() (bool, error) { + r := o.exec("test -f /var/run/reboot-required", noSudo) + switch r.ExitStatus { + case 0: + return true, nil + case 1: + return false, nil + default: + return false, fmt.Errorf("Failed to check reboot reauired: %s", r) + } +} + +func (o *debian) scanInstalledPackages() (models.Packages, models.Packages, error) { + installed, updatable := models.Packages{}, models.Packages{} r := o.exec("dpkg-query -W", noSudo) if !r.isSuccess() { return nil, nil, fmt.Errorf("Failed to SSH: %s", r) @@ -336,9 +372,9 @@ func (o *debian) ensureChangelogCache(current cache.Meta) (*cache.Meta, error) { return &cached, nil } -func (o *debian) fillCandidateVersion(packages models.Packages) (err error) { +func (o *debian) fillCandidateVersion(updatables models.Packages) (err error) { names := []string{} - for name := range packages { + for name := range updatables { names = append(names, name) } cmd := fmt.Sprintf("LANGUAGE=en_US.UTF-8 apt-cache policy %s", strings.Join(names, " ")) @@ -352,18 +388,18 @@ func (o *debian) fillCandidateVersion(packages models.Packages) (err error) { if err != nil { return fmt.Errorf("Failed to parse %s", err) } - pack, ok := packages[k] + pack, ok := updatables[k] if !ok { return fmt.Errorf("Not found: %s", k) } pack.NewVersion = ver.Candidate - packages[k] = pack + updatables[k] = pack } return } func (o *debian) getUpdatablePackNames() (packNames []string, err error) { - cmd := util.PrependProxyEnv("LANGUAGE=en_US.UTF-8 apt-get upgrade --dry-run") + cmd := util.PrependProxyEnv("LANGUAGE=en_US.UTF-8 apt-get dist-upgrade --dry-run") r := o.exec(cmd, noSudo) if r.isSuccess(0, 1) { return o.parseAptGetUpgrade(r.Stdout) diff --git a/scan/debian_test.go b/scan/debian_test.go index 269214e9..f55f094f 100644 --- a/scan/debian_test.go +++ b/scan/debian_test.go @@ -303,49 +303,7 @@ Reading state information... Done The following packages will be upgraded: apt ca-certificates cpio dpkg e2fslibs e2fsprogs gnupg gpgv libc-bin libc6 libcomerr2 libpcre3 libpng12-0 libss2 libssl1.0.0 libudev0 multiarch-support openssl tzdata udev upstart -21 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. -Inst dpkg [1.16.1.2ubuntu7.5] (1.16.1.2ubuntu7.7 Ubuntu:12.04/precise-updates [amd64]) -Conf dpkg (1.16.1.2ubuntu7.7 Ubuntu:12.04/precise-updates [amd64]) -Inst upstart [1.5-0ubuntu7.2] (1.5-0ubuntu7.3 Ubuntu:12.04/precise-updates [amd64]) -Inst libc-bin [2.15-0ubuntu10.10] (2.15-0ubuntu10.13 Ubuntu:12.04/precise-updates [amd64]) [libc6:amd64 ] -Conf libc-bin (2.15-0ubuntu10.13 Ubuntu:12.04/precise-updates [amd64]) [libc6:amd64 ] -Inst libc6 [2.15-0ubuntu10.10] (2.15-0ubuntu10.13 Ubuntu:12.04/precise-updates [amd64]) -Conf libc6 (2.15-0ubuntu10.13 Ubuntu:12.04/precise-updates [amd64]) -Inst libudev0 [175-0ubuntu9.9] (175-0ubuntu9.10 Ubuntu:12.04/precise-updates [amd64]) -Inst tzdata [2015a-0ubuntu0.12.04] (2015g-0ubuntu0.12.04 Ubuntu:12.04/precise-updates [all]) -Conf tzdata (2015g-0ubuntu0.12.04 Ubuntu:12.04/precise-updates [all]) -Inst e2fslibs [1.42-1ubuntu2] (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64]) [e2fsprogs:amd64 on e2fslibs:amd64] [e2fsprogs:amd64 ] -Conf e2fslibs (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64]) [e2fsprogs:amd64 ] -Inst e2fsprogs [1.42-1ubuntu2] (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64]) -Conf e2fsprogs (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64]) -Inst gpgv [1.4.11-3ubuntu2.7] (1.4.11-3ubuntu2.9 Ubuntu:12.04/precise-updates [amd64]) -Conf gpgv (1.4.11-3ubuntu2.9 Ubuntu:12.04/precise-updates [amd64]) -Inst gnupg [1.4.11-3ubuntu2.7] (1.4.11-3ubuntu2.9 Ubuntu:12.04/precise-updates [amd64]) -Conf gnupg (1.4.11-3ubuntu2.9 Ubuntu:12.04/precise-updates [amd64]) -Inst apt [0.8.16~exp12ubuntu10.22] (0.8.16~exp12ubuntu10.26 Ubuntu:12.04/precise-updates [amd64]) -Conf apt (0.8.16~exp12ubuntu10.26 Ubuntu:12.04/precise-updates [amd64]) -Inst libcomerr2 [1.42-1ubuntu2] (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64]) -Conf libcomerr2 (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64]) -Inst libss2 [1.42-1ubuntu2] (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64]) -Conf libss2 (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64]) -Inst libssl1.0.0 [1.0.1-4ubuntu5.21] (1.0.1-4ubuntu5.34 Ubuntu:12.04/precise-updates [amd64]) -Conf libssl1.0.0 (1.0.1-4ubuntu5.34 Ubuntu:12.04/precise-updates [amd64]) -Inst libpcre3 [8.12-4] (8.12-4ubuntu0.1 Ubuntu:12.04/precise-updates [amd64]) -Inst libpng12-0 [1.2.46-3ubuntu4] (1.2.46-3ubuntu4.2 Ubuntu:12.04/precise-updates [amd64]) -Inst multiarch-support [2.15-0ubuntu10.10] (2.15-0ubuntu10.13 Ubuntu:12.04/precise-updates [amd64]) -Conf multiarch-support (2.15-0ubuntu10.13 Ubuntu:12.04/precise-updates [amd64]) -Inst cpio [2.11-7ubuntu3.1] (2.11-7ubuntu3.2 Ubuntu:12.04/precise-updates [amd64]) -Inst udev [175-0ubuntu9.9] (175-0ubuntu9.10 Ubuntu:12.04/precise-updates [amd64]) -Inst openssl [1.0.1-4ubuntu5.33] (1.0.1-4ubuntu5.34 Ubuntu:12.04/precise-updates [amd64]) -Inst ca-certificates [20141019ubuntu0.12.04.1] (20160104ubuntu0.12.04.1 Ubuntu:12.04/precise-updates [all]) -Conf libudev0 (175-0ubuntu9.10 Ubuntu:12.04/precise-updates [amd64]) -Conf upstart (1.5-0ubuntu7.3 Ubuntu:12.04/precise-updates [amd64]) -Conf libpcre3 (8.12-4ubuntu0.1 Ubuntu:12.04/precise-updates [amd64]) -Conf libpng12-0 (1.2.46-3ubuntu4.2 Ubuntu:12.04/precise-updates [amd64]) -Conf cpio (2.11-7ubuntu3.2 Ubuntu:12.04/precise-updates [amd64]) -Conf udev (175-0ubuntu9.10 Ubuntu:12.04/precise-updates [amd64]) -Conf openssl (1.0.1-4ubuntu5.34 Ubuntu:12.04/precise-updates [amd64]) -Conf ca-certificates (20160104ubuntu0.12.04.1 Ubuntu:12.04/precise-updates [all])`, +21 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.`, []string{ "apt", "ca-certificates", @@ -386,124 +344,6 @@ The following packages will be upgraded: ntpdate passwd python3.4 python3.4-minimal rsyslog sudo sysv-rc sysvinit-utils tzdata udev util-linux 59 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. -Inst base-files [7.2ubuntu5.2] (7.2ubuntu5.4 Ubuntu:14.04/trusty-updates [amd64]) -Conf base-files (7.2ubuntu5.4 Ubuntu:14.04/trusty-updates [amd64]) -Inst coreutils [8.21-1ubuntu5.1] (8.21-1ubuntu5.3 Ubuntu:14.04/trusty-updates [amd64]) -Conf coreutils (8.21-1ubuntu5.3 Ubuntu:14.04/trusty-updates [amd64]) -Inst dpkg [1.17.5ubuntu5.3] (1.17.5ubuntu5.5 Ubuntu:14.04/trusty-updates [amd64]) -Conf dpkg (1.17.5ubuntu5.5 Ubuntu:14.04/trusty-updates [amd64]) -Inst libc-bin [2.19-0ubuntu6.5] (2.19-0ubuntu6.7 Ubuntu:14.04/trusty-updates [amd64]) -Inst libc6 [2.19-0ubuntu6.5] (2.19-0ubuntu6.7 Ubuntu:14.04/trusty-updates [amd64]) -Inst libgcc1 [1:4.9.1-0ubuntu1] (1:4.9.3-0ubuntu4 Ubuntu:14.04/trusty-updates [amd64]) [] -Inst gcc-4.9-base [4.9.1-0ubuntu1] (4.9.3-0ubuntu4 Ubuntu:14.04/trusty-updates [amd64]) -Conf gcc-4.9-base (4.9.3-0ubuntu4 Ubuntu:14.04/trusty-updates [amd64]) -Conf libgcc1 (1:4.9.3-0ubuntu4 Ubuntu:14.04/trusty-updates [amd64]) -Conf libc6 (2.19-0ubuntu6.7 Ubuntu:14.04/trusty-updates [amd64]) -Conf libc-bin (2.19-0ubuntu6.7 Ubuntu:14.04/trusty-updates [amd64]) -Inst e2fslibs [1.42.9-3ubuntu1] (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64]) [e2fsprogs:amd64 on e2fslibs:amd64] [e2fsprogs:amd64 ] -Conf e2fslibs (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64]) [e2fsprogs:amd64 ] -Inst e2fsprogs [1.42.9-3ubuntu1] (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64]) -Conf e2fsprogs (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64]) -Inst login [1:4.1.5.1-1ubuntu9] (1:4.1.5.1-1ubuntu9.2 Ubuntu:14.04/trusty-updates [amd64]) -Conf login (1:4.1.5.1-1ubuntu9.2 Ubuntu:14.04/trusty-updates [amd64]) -Inst mount [2.20.1-5.1ubuntu20.4] (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64]) -Conf mount (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64]) -Inst tzdata [2015a-0ubuntu0.14.04] (2015g-0ubuntu0.14.04 Ubuntu:14.04/trusty-updates [all]) -Conf tzdata (2015g-0ubuntu0.14.04 Ubuntu:14.04/trusty-updates [all]) -Inst sysvinit-utils [2.88dsf-41ubuntu6] (2.88dsf-41ubuntu6.3 Ubuntu:14.04/trusty-updates [amd64]) -Inst sysv-rc [2.88dsf-41ubuntu6] (2.88dsf-41ubuntu6.3 Ubuntu:14.04/trusty-updates [all]) -Conf sysv-rc (2.88dsf-41ubuntu6.3 Ubuntu:14.04/trusty-updates [all]) -Conf sysvinit-utils (2.88dsf-41ubuntu6.3 Ubuntu:14.04/trusty-updates [amd64]) -Inst util-linux [2.20.1-5.1ubuntu20.4] (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64]) -Conf util-linux (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64]) -Inst gcc-4.8-base [4.8.2-19ubuntu1] (4.8.4-2ubuntu1~14.04.1 Ubuntu:14.04/trusty-updates [amd64]) [libstdc++6:amd64 ] -Conf gcc-4.8-base (4.8.4-2ubuntu1~14.04.1 Ubuntu:14.04/trusty-updates [amd64]) [libstdc++6:amd64 ] -Inst libstdc++6 [4.8.2-19ubuntu1] (4.8.4-2ubuntu1~14.04.1 Ubuntu:14.04/trusty-updates [amd64]) -Conf libstdc++6 (4.8.4-2ubuntu1~14.04.1 Ubuntu:14.04/trusty-updates [amd64]) -Inst libapt-pkg4.12 [1.0.1ubuntu2.6] (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64]) -Conf libapt-pkg4.12 (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64]) -Inst gpgv [1.4.16-1ubuntu2.1] (1.4.16-1ubuntu2.3 Ubuntu:14.04/trusty-updates [amd64]) -Conf gpgv (1.4.16-1ubuntu2.3 Ubuntu:14.04/trusty-updates [amd64]) -Inst gnupg [1.4.16-1ubuntu2.1] (1.4.16-1ubuntu2.3 Ubuntu:14.04/trusty-updates [amd64]) -Conf gnupg (1.4.16-1ubuntu2.3 Ubuntu:14.04/trusty-updates [amd64]) -Inst apt [1.0.1ubuntu2.6] (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64]) -Conf apt (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64]) -Inst bsdutils [1:2.20.1-5.1ubuntu20.4] (1:2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64]) -Conf bsdutils (1:2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64]) -Inst passwd [1:4.1.5.1-1ubuntu9] (1:4.1.5.1-1ubuntu9.2 Ubuntu:14.04/trusty-updates [amd64]) -Conf passwd (1:4.1.5.1-1ubuntu9.2 Ubuntu:14.04/trusty-updates [amd64]) -Inst libuuid1 [2.20.1-5.1ubuntu20.4] (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64]) -Conf libuuid1 (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64]) -Inst libblkid1 [2.20.1-5.1ubuntu20.4] (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64]) -Conf libblkid1 (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64]) -Inst libcomerr2 [1.42.9-3ubuntu1] (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64]) -Conf libcomerr2 (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64]) -Inst libmount1 [2.20.1-5.1ubuntu20.4] (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64]) -Conf libmount1 (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64]) -Inst libpcre3 [1:8.31-2ubuntu2] (1:8.31-2ubuntu2.1 Ubuntu:14.04/trusty-updates [amd64]) -Conf libpcre3 (1:8.31-2ubuntu2.1 Ubuntu:14.04/trusty-updates [amd64]) -Inst libss2 [1.42.9-3ubuntu1] (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64]) -Conf libss2 (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64]) -Inst libapt-inst1.5 [1.0.1ubuntu2.6] (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64]) -Inst libexpat1 [2.1.0-4ubuntu1] (2.1.0-4ubuntu1.1 Ubuntu:14.04/trusty-updates [amd64]) -Inst libffi6 [3.1~rc1+r3.0.13-12] (3.1~rc1+r3.0.13-12ubuntu0.1 Ubuntu:14.04/trusty-updates [amd64]) -Inst libgcrypt11 [1.5.3-2ubuntu4.1] (1.5.3-2ubuntu4.3 Ubuntu:14.04/trusty-updates [amd64]) -Inst libtasn1-6 [3.4-3ubuntu0.1] (3.4-3ubuntu0.3 Ubuntu:14.04/trusty-updates [amd64]) -Inst libgnutls-openssl27 [2.12.23-12ubuntu2.1] (2.12.23-12ubuntu2.4 Ubuntu:14.04/trusty-updates [amd64]) [] -Inst libgnutls26 [2.12.23-12ubuntu2.1] (2.12.23-12ubuntu2.4 Ubuntu:14.04/trusty-updates [amd64]) -Inst libsqlite3-0 [3.8.2-1ubuntu2] (3.8.2-1ubuntu2.1 Ubuntu:14.04/trusty-updates [amd64]) -Inst python3.4 [3.4.0-2ubuntu1] (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64]) [] -Inst libpython3.4-stdlib [3.4.0-2ubuntu1] (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64]) [] -Inst python3.4-minimal [3.4.0-2ubuntu1] (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64]) [] -Inst libssl1.0.0 [1.0.1f-1ubuntu2.8] (1.0.1f-1ubuntu2.16 Ubuntu:14.04/trusty-updates [amd64]) [] -Inst libpython3.4-minimal [3.4.0-2ubuntu1] (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64]) -Inst ntpdate [1:4.2.6.p5+dfsg-3ubuntu2.14.04.2] (1:4.2.6.p5+dfsg-3ubuntu2.14.04.8 Ubuntu:14.04/trusty-updates [amd64]) -Inst libdrm2 [2.4.56-1~ubuntu2] (2.4.64-1~ubuntu14.04.1 Ubuntu:14.04/trusty-updates [amd64]) -Inst libpng12-0 [1.2.50-1ubuntu2] (1.2.50-1ubuntu2.14.04.2 Ubuntu:14.04/trusty-updates [amd64]) -Inst initscripts [2.88dsf-41ubuntu6] (2.88dsf-41ubuntu6.3 Ubuntu:14.04/trusty-updates [amd64]) -Inst libcgmanager0 [0.24-0ubuntu7.3] (0.24-0ubuntu7.5 Ubuntu:14.04/trusty-updates [amd64]) -Inst udev [204-5ubuntu20.10] (204-5ubuntu20.18 Ubuntu:14.04/trusty-updates [amd64]) [] -Inst libudev1 [204-5ubuntu20.10] (204-5ubuntu20.18 Ubuntu:14.04/trusty-updates [amd64]) -Inst multiarch-support [2.19-0ubuntu6.5] (2.19-0ubuntu6.7 Ubuntu:14.04/trusty-updates [amd64]) -Conf multiarch-support (2.19-0ubuntu6.7 Ubuntu:14.04/trusty-updates [amd64]) -Inst apt-utils [1.0.1ubuntu2.6] (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64]) -Inst dh-python [1.20140128-1ubuntu8] (1.20140128-1ubuntu8.2 Ubuntu:14.04/trusty-updates [all]) -Inst iproute2 [3.12.0-2] (3.12.0-2ubuntu1 Ubuntu:14.04/trusty-updates [amd64]) -Inst ifupdown [0.7.47.2ubuntu4.1] (0.7.47.2ubuntu4.3 Ubuntu:14.04/trusty-updates [amd64]) -Inst isc-dhcp-client [4.2.4-7ubuntu12] (4.2.4-7ubuntu12.4 Ubuntu:14.04/trusty-updates [amd64]) [] -Inst isc-dhcp-common [4.2.4-7ubuntu12] (4.2.4-7ubuntu12.4 Ubuntu:14.04/trusty-updates [amd64]) -Inst rsyslog [7.4.4-1ubuntu2.5] (7.4.4-1ubuntu2.6 Ubuntu:14.04/trusty-updates [amd64]) -Inst sudo [1.8.9p5-1ubuntu1] (1.8.9p5-1ubuntu1.2 Ubuntu:14.04/trusty-updates [amd64]) -Inst cpio [2.11+dfsg-1ubuntu1.1] (2.11+dfsg-1ubuntu1.2 Ubuntu:14.04/trusty-updates [amd64]) -Conf libapt-inst1.5 (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64]) -Conf libexpat1 (2.1.0-4ubuntu1.1 Ubuntu:14.04/trusty-updates [amd64]) -Conf libffi6 (3.1~rc1+r3.0.13-12ubuntu0.1 Ubuntu:14.04/trusty-updates [amd64]) -Conf libgcrypt11 (1.5.3-2ubuntu4.3 Ubuntu:14.04/trusty-updates [amd64]) -Conf libtasn1-6 (3.4-3ubuntu0.3 Ubuntu:14.04/trusty-updates [amd64]) -Conf libgnutls26 (2.12.23-12ubuntu2.4 Ubuntu:14.04/trusty-updates [amd64]) -Conf libgnutls-openssl27 (2.12.23-12ubuntu2.4 Ubuntu:14.04/trusty-updates [amd64]) -Conf libsqlite3-0 (3.8.2-1ubuntu2.1 Ubuntu:14.04/trusty-updates [amd64]) -Conf libssl1.0.0 (1.0.1f-1ubuntu2.16 Ubuntu:14.04/trusty-updates [amd64]) -Conf libpython3.4-minimal (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64]) -Conf python3.4-minimal (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64]) -Conf libpython3.4-stdlib (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64]) -Conf python3.4 (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64]) -Conf ntpdate (1:4.2.6.p5+dfsg-3ubuntu2.14.04.8 Ubuntu:14.04/trusty-updates [amd64]) -Conf libdrm2 (2.4.64-1~ubuntu14.04.1 Ubuntu:14.04/trusty-updates [amd64]) -Conf libpng12-0 (1.2.50-1ubuntu2.14.04.2 Ubuntu:14.04/trusty-updates [amd64]) -Conf initscripts (2.88dsf-41ubuntu6.3 Ubuntu:14.04/trusty-updates [amd64]) -Conf libcgmanager0 (0.24-0ubuntu7.5 Ubuntu:14.04/trusty-updates [amd64]) -Conf libudev1 (204-5ubuntu20.18 Ubuntu:14.04/trusty-updates [amd64]) -Conf udev (204-5ubuntu20.18 Ubuntu:14.04/trusty-updates [amd64]) -Conf apt-utils (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64]) -Conf dh-python (1.20140128-1ubuntu8.2 Ubuntu:14.04/trusty-updates [all]) -Conf iproute2 (3.12.0-2ubuntu1 Ubuntu:14.04/trusty-updates [amd64]) -Conf ifupdown (0.7.47.2ubuntu4.3 Ubuntu:14.04/trusty-updates [amd64]) -Conf isc-dhcp-common (4.2.4-7ubuntu12.4 Ubuntu:14.04/trusty-updates [amd64]) -Conf isc-dhcp-client (4.2.4-7ubuntu12.4 Ubuntu:14.04/trusty-updates [amd64]) -Conf rsyslog (7.4.4-1ubuntu2.6 Ubuntu:14.04/trusty-updates [amd64]) -Conf sudo (1.8.9p5-1ubuntu1.2 Ubuntu:14.04/trusty-updates [amd64]) -Conf cpio (2.11+dfsg-1ubuntu1.2 Ubuntu:14.04/trusty-updates [amd64]) `, []string{ "apt", diff --git a/scan/freebsd.go b/scan/freebsd.go index 597a83e1..8ac648fa 100644 --- a/scan/freebsd.go +++ b/scan/freebsd.go @@ -78,23 +78,48 @@ func (o *bsd) checkDependencies() error { } func (o *bsd) scanPackages() error { - var err error - var packs models.Packages - if packs, err = o.scanInstalledPackages(); err != nil { - o.log.Errorf("Failed to scan installed packages") + // collect the running kernel information + release, version, err := o.runningKernel() + if err != nil { + o.log.Errorf("Failed to scan the running kernel version: %s", err) return err } - o.setPackages(packs) + o.Kernel = models.Kernel{ + Release: release, + Version: version, + } - var vinfos models.VulnInfos - if vinfos, err = o.scanUnsecurePackages(); err != nil { - o.log.Errorf("Failed to scan vulnerable packages") + rebootRequired, err := o.rebootRequired() + if err != nil { + o.log.Errorf("Failed to detect the kernel reboot required: %s", err) return err } - o.setVulnInfos(vinfos) + o.Kernel.RebootRequired = rebootRequired + + packs, err := o.scanInstalledPackages() + if err != nil { + o.log.Errorf("Failed to scan installed packages: %s", err) + return err + } + o.Packages = packs + + unsecures, err := o.scanUnsecurePackages() + if err != nil { + o.log.Errorf("Failed to scan vulnerable packages: %s", err) + return err + } + o.VulnInfos = unsecures return nil } +func (o *bsd) rebootRequired() (bool, error) { + r := o.exec("freebsd-version -k", noSudo) + if !r.isSuccess() { + return false, fmt.Errorf("Failed to SSH: %s", r) + } + return o.Kernel.Release != strings.TrimSpace(r.Stdout), nil +} + func (o *bsd) scanInstalledPackages() (models.Packages, error) { cmd := util.PrependProxyEnv("pkg version -v") r := o.exec(cmd, noSudo) diff --git a/scan/redhat.go b/scan/redhat.go index f9243e73..0c75f388 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -223,32 +223,58 @@ func (o *redhat) checkDependencies() error { func (o *redhat) scanPackages() error { installed, err := o.scanInstalledPackages() if err != nil { - o.log.Errorf("Failed to scan installed packages") + o.log.Errorf("Failed to scan installed packages: %s", err) return err } + rebootRequired, err := o.rebootRequired() + if err != nil { + o.log.Errorf("Failed to detect the kernel reboot required: %s", err) + return err + } + o.Kernel.RebootRequired = rebootRequired + updatable, err := o.scanUpdatablePackages() if err != nil { - o.log.Errorf("Failed to scan installed packages") + o.log.Errorf("Failed to scan installed packages: %s", err) return err } installed.MergeNewVersion(updatable) - o.setPackages(installed) + o.Packages = installed if !config.Conf.Deep && o.Distro.Family != config.Amazon { return nil } - var vinfos models.VulnInfos - if vinfos, err = o.scanUnsecurePackages(updatable); err != nil { - o.log.Errorf("Failed to scan vulnerable packages") + var unsecures models.VulnInfos + if unsecures, err = o.scanUnsecurePackages(updatable); err != nil { + o.log.Errorf("Failed to scan vulnerable packages: %s", err) return err } - o.setVulnInfos(vinfos) + o.VulnInfos = unsecures return nil } +func (o *redhat) rebootRequired() (bool, error) { + r := o.exec("rpm -q --last kernel | head -n1", noSudo) + if !r.isSuccess() { + return false, fmt.Errorf("Failed to detect the last installed kernel : %v", r) + } + lastInstalledKernelVer := strings.Fields(r.Stdout)[0] + running := fmt.Sprintf("kernel-%s", o.Kernel.Release) + return running != lastInstalledKernelVer, nil +} + func (o *redhat) scanInstalledPackages() (models.Packages, error) { + release, version, err := o.runningKernel() + if err != nil { + return nil, err + } + o.Kernel = models.Kernel{ + Release: release, + Version: version, + } + installed := models.Packages{} var cmd string majorVersion, _ := o.Distro.MajorVersion() @@ -258,24 +284,35 @@ func (o *redhat) scanInstalledPackages() (models.Packages, error) { cmd = "rpm -qa --queryformat '%{NAME} %{EPOCHNUM} %{VERSION} %{RELEASE} %{ARCH}\n'" } r := o.exec(cmd, noSudo) - if r.isSuccess() { - // openssl 0 1.0.1e 30.el6.11 x86_64 - lines := strings.Split(r.Stdout, "\n") - for _, line := range lines { - if trimed := strings.TrimSpace(line); len(trimed) != 0 { - pack, err := o.parseInstalledPackagesLine(line) - if err != nil { - return nil, err - } - installed[pack.Name] = pack - } - } - return installed, nil + if !r.isSuccess() { + return nil, fmt.Errorf("Scan packages failed: %s", r) } - return nil, fmt.Errorf("Scan packages failed. status: %d, stdout: %s, stderr: %s", - r.ExitStatus, r.Stdout, r.Stderr) + // openssl 0 1.0.1e 30.el6.11 x86_64 + lines := strings.Split(r.Stdout, "\n") + for _, line := range lines { + if trimed := strings.TrimSpace(line); len(trimed) != 0 { + pack, err := o.parseInstalledPackagesLine(line) + if err != nil { + return nil, err + } + // Kernel package may be isntalled multiple versions. + // From the viewpoint of vulnerability detection, + // pay attention only to the running kernel + if pack.Name == "kernel" { + ver := fmt.Sprintf("%s-%s.%s", pack.Version, pack.Release, pack.Arch) + if o.Kernel.Release != ver { + o.log.Debugf("Not a running kernel: %s, uname: %s", ver, release) + continue + } else { + o.log.Debugf("Running kernel: %s, uname: %s", ver, release) + } + } + installed[pack.Name] = pack + } + } + return installed, nil } func (o *redhat) parseInstalledPackagesLine(line string) (models.Package, error) { diff --git a/scan/redhat_test.go b/scan/redhat_test.go index 2ee03ce1..9b59f1a4 100644 --- a/scan/redhat_test.go +++ b/scan/redhat_test.go @@ -667,14 +667,14 @@ python-ordereddict 0 1.1 3.el6ev installed bind-utils 30 9.3.6 25.P1.el5_11.8 updates pytalloc 0 2.0.7 2.el6 @CentOS 6.5/6.5` - r.setPackages(models.NewPackages( + r.Packages = models.NewPackages( models.Package{Name: "audit-libs"}, models.Package{Name: "bash"}, models.Package{Name: "python-libs"}, models.Package{Name: "python-ordereddict"}, models.Package{Name: "bind-utils"}, models.Package{Name: "pytalloc"}, - )) + ) var tests = []struct { in string out models.Packages diff --git a/scan/serverapi.go b/scan/serverapi.go index 71898e74..86c06cb5 100644 --- a/scan/serverapi.go +++ b/scan/serverapi.go @@ -63,14 +63,9 @@ type osPackages struct { // unsecure packages VulnInfos models.VulnInfos -} -func (p *osPackages) setPackages(pi models.Packages) { - p.Packages = pi -} - -func (p *osPackages) setVulnInfos(vi models.VulnInfos) { - p.VulnInfos = vi + // kernel information + Kernel models.Kernel } func detectOS(c config.ServerInfo) (osType osTypeInterface) { From feb3f79a13dd9b4ae4bb1e76542e157dff41ccb6 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Tue, 22 Aug 2017 18:14:00 +0900 Subject: [PATCH 106/113] Update Gopkg --- Gopkg.lock | 40 ++++++++++++++++++++-------------------- Gopkg.toml | 4 ++++ 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 46be657e..4829d8f7 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -4,14 +4,14 @@ [[projects]] name = "github.com/Azure/azure-sdk-for-go" packages = ["storage"] - revision = "2d49bb8f2cee530cc16f1f1a9f0aae763dee257d" - version = "v10.2.1-beta" + revision = "57db66900881e9fd21fd041a9d013514700ecab3" + version = "v10.3.0-beta" [[projects]] name = "github.com/Azure/go-autorest" packages = ["autorest","autorest/adal","autorest/azure","autorest/date"] - revision = "f6e08fe5e4d45c9a66e40196d3fed5f37331d224" - version = "v8.1.1" + revision = "77a52603f06947221c672f10275abc9bf2c7d557" + version = "v8.3.0" [[projects]] name = "github.com/BurntSushi/toml" @@ -28,8 +28,8 @@ [[projects]] name = "github.com/aws/aws-sdk-go" packages = ["aws","aws/awserr","aws/awsutil","aws/client","aws/client/metadata","aws/corehandlers","aws/credentials","aws/credentials/ec2rolecreds","aws/credentials/endpointcreds","aws/credentials/stscreds","aws/defaults","aws/ec2metadata","aws/endpoints","aws/request","aws/session","aws/signer/v4","internal/shareddefaults","private/protocol","private/protocol/query","private/protocol/query/queryutil","private/protocol/rest","private/protocol/restxml","private/protocol/xml/xmlutil","service/s3","service/sts"] - revision = "19490d4d23f5f6dd8061abf91542af8433d28d63" - version = "v1.10.24" + revision = "264af29009637e0a9e5d4a276d0969c3ed918ffd" + version = "v1.10.29" [[projects]] name = "github.com/boltdb/bolt" @@ -40,14 +40,14 @@ [[projects]] name = "github.com/cenkalti/backoff" packages = ["."] - revision = "32cd0c5b3aef12c76ed64aaf678f6c79736be7dc" - version = "v1.0.0" + revision = "61153c768f31ee5f130071d08fc82b85208528de" + version = "v1.1.0" [[projects]] - branch = "master" name = "github.com/cheggaaa/pb" packages = ["."] - revision = "0af82b7d15eb9371fbdf8f468ff10cbba62e0414" + revision = "0d6285554e726cc0620cbecc7e6969c945dcc63b" + version = "v1.0.17" [[projects]] name = "github.com/dgrijalva/jwt-go" @@ -64,8 +64,8 @@ [[projects]] name = "github.com/go-redis/redis" packages = [".","internal","internal/consistenthash","internal/hashtag","internal/pool","internal/proto"] - revision = "89515eebd1b5761486abd32d9e84a8f55dda7740" - version = "v6.5.6" + revision = "19c1c2272e00c1aaa903cf574c746cd449f9cd3c" + version = "v6.5.7" [[projects]] name = "github.com/go-sql-driver/mysql" @@ -134,10 +134,10 @@ revision = "74609b86c936dff800c69ec89fcf4bc52d5f13a4" [[projects]] + branch = "master" name = "github.com/kotakanbe/go-cve-dictionary" packages = ["config","db","jvn","log","models","nvd","util"] - revision = "89e381b4e7e5a31097bbd5779cbb555f5bd3fe87" - version = "v0.1.1" + revision = "c20fa7e1d07f7c700baf12c855f7fcf61525f1b6" [[projects]] name = "github.com/kotakanbe/go-pingscanner" @@ -149,7 +149,7 @@ branch = "master" name = "github.com/kotakanbe/goval-dictionary" packages = ["config","db","db/rdb","log","models"] - revision = "aa1dbe07a21bd51943893086d37e9e57c6020ce0" + revision = "3523cc174e68f285d0572d07c68ffa3a9290799c" [[projects]] branch = "master" @@ -239,7 +239,7 @@ branch = "master" name = "github.com/sirupsen/logrus" packages = ["."] - revision = "181d419aa9e2223811b824e8f0b4af96f9ba9302" + revision = "84573d5f03ab3740f524c7842c3a9bf617961d32" [[projects]] branch = "master" @@ -263,7 +263,7 @@ branch = "master" name = "golang.org/x/crypto" packages = ["curve25519","ed25519","ed25519/internal/edwards25519","ssh","ssh/agent","ssh/terminal"] - revision = "b176d7def5d71bdd214203491f89843ed217f420" + revision = "eb71ad9bd329b5ac0fd0148dd99bd62e8be8e035" [[projects]] branch = "master" @@ -275,17 +275,17 @@ branch = "master" name = "golang.org/x/sys" packages = ["unix","windows"] - revision = "e42485b6e20ae7d2304ec72e535b103ed350cc02" + revision = "07c182904dbd53199946ba614a412c61d3c548f5" [[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 = "b19bf474d317b857955b12035d2c5acb57ce8b01" + revision = "e56139fd9c5bc7244c76116c68e500765bb6db6b" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "3590a66561bd65dbedefd34aa043c662111483be0bc1b68df264de0c53346552" + inputs-digest = "36d700add80d36c56484ed310b9a7e622b3e308ab22eb42bdfb02fd8f5c90407" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index da91d653..7f770516 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -84,3 +84,7 @@ [[constraint]] branch = "master" name = "github.com/sirupsen/logrus" + +[[constraint]] + branch = "master" + name = "github.com/kotakanbe/go-cve-dictionary" From 37901976993a1208964dcd61e22f0b619a737f73 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Tue, 22 Aug 2017 20:28:24 +0900 Subject: [PATCH 107/113] Fix ignoreCves option --- models/scanresults.go | 15 ++++++++++++ models/scanresults_test.go | 50 ++++++++++++++++++++++++++++++++++++++ report/report.go | 4 ++- 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/models/scanresults.go b/models/scanresults.go index f11b6ec0..f541e28c 100644 --- a/models/scanresults.go +++ b/models/scanresults.go @@ -82,6 +82,21 @@ func (r ScanResult) FilterByCvssOver(over float64) ScanResult { return copiedScanResult } +// FilterIgnoreCves is filter function. +func (r ScanResult) FilterIgnoreCves(cveIDs []string) ScanResult { + filtered := r.ScannedCves.Find(func(v VulnInfo) bool { + for _, c := range cveIDs { + if v.CveID == c { + return false + } + } + return true + }) + copiedScanResult := r + copiedScanResult.ScannedCves = filtered + return copiedScanResult +} + // ReportFileName returns the filename on localhost without extention func (r ScanResult) ReportFileName() (name string) { if len(r.Container.ContainerID) == 0 { diff --git a/models/scanresults_test.go b/models/scanresults_test.go index 5cd44b8e..9df1a52b 100644 --- a/models/scanresults_test.go +++ b/models/scanresults_test.go @@ -205,3 +205,53 @@ func TestFilterByCvssOver(t *testing.T) { } } } + +func TestFilterIgnoreCveIDs(t *testing.T) { + type in struct { + cves []string + rs ScanResult + } + var tests = []struct { + in in + out ScanResult + }{ + { + in: in{ + cves: []string{"CVE-2017-0002"}, + rs: ScanResult{ + ScannedCves: VulnInfos{ + "CVE-2017-0001": { + CveID: "CVE-2017-0001", + }, + "CVE-2017-0002": { + CveID: "CVE-2017-0002", + }, + "CVE-2017-0003": { + CveID: "CVE-2017-0003", + }, + }, + }, + }, + out: ScanResult{ + ScannedCves: VulnInfos{ + "CVE-2017-0001": { + CveID: "CVE-2017-0001", + }, + "CVE-2017-0003": { + CveID: "CVE-2017-0003", + }, + }, + }, + }, + } + for _, tt := range tests { + actual := tt.in.rs.FilterIgnoreCves(tt.in.cves) + for k := range tt.out.ScannedCves { + if !reflect.DeepEqual(tt.out.ScannedCves[k], actual.ScannedCves[k]) { + o := pp.Sprintf("%v", tt.out.ScannedCves[k]) + a := pp.Sprintf("%v", actual.ScannedCves[k]) + t.Errorf("[%s] expected: %v\n actual: %v\n", k, o, a) + } + } + } +} diff --git a/report/report.go b/report/report.go index 4d07fd6c..32a3a110 100644 --- a/report/report.go +++ b/report/report.go @@ -79,7 +79,9 @@ func FillCveInfos(rs []models.ScanResult, dir string) ([]models.ScanResult, erro filtered := []models.ScanResult{} for _, r := range filled { - filtered = append(filtered, r.FilterByCvssOver(c.Conf.CvssScoreOver)) + r = r.FilterByCvssOver(c.Conf.CvssScoreOver) + r = r.FilterIgnoreCves(c.Conf.Servers[r.ServerName].IgnoreCves) + filtered = append(filtered, r) } return filtered, nil } From 58b0d03e28a25de19d29ba6696e0226cac4d1d24 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Wed, 23 Aug 2017 12:02:58 +0900 Subject: [PATCH 108/113] No escape on details view in TUI --- models/packages.go | 6 +----- report/tui.go | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/models/packages.go b/models/packages.go index 587c9f52..473d52bd 100644 --- a/models/packages.go +++ b/models/packages.go @@ -117,16 +117,12 @@ func (p Package) FormatVersionFromTo(notFixedYet bool) string { if notFixedYet { to = "Not Fixed Yet" } - return fmt.Sprintf("%s-%s - %s", p.Name, p.FormatVer(), to) + return fmt.Sprintf("%s-%s -> %s", p.Name, p.FormatVer(), to) } // FormatChangelog formats the changelog func (p Package) FormatChangelog() string { buf := []string{} - if p.NewVersion == "" { - return "" - } - packVer := fmt.Sprintf("%s-%s -> %s", p.Name, p.FormatVer(), p.FormatNewVer()) var delim bytes.Buffer diff --git a/report/tui.go b/report/tui.go index ee3a554c..59e89273 100644 --- a/report/tui.go +++ b/report/tui.go @@ -20,10 +20,10 @@ package report import ( "bytes" "fmt" - "html/template" "os" "sort" "strings" + "text/template" "time" "github.com/future-architect/vuls/config" From 551fdd50229d0da96180c6bdf52f1f42d890470e Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Wed, 23 Aug 2017 13:38:20 +0900 Subject: [PATCH 109/113] Display "Reboot Required" on report if the kernel has been updated but not restarted --- models/scanresults.go | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/models/scanresults.go b/models/scanresults.go index f541e28c..954ec8ed 100644 --- a/models/scanresults.go +++ b/models/scanresults.go @@ -118,12 +118,11 @@ func (r ScanResult) ReportKeyName() (name string) { func (r ScanResult) ServerInfo() string { if len(r.Container.ContainerID) == 0 { return fmt.Sprintf("%s (%s%s)", - r.ServerName, r.Family, r.Release) + r.FormatServerName(), r.Family, r.Release) } return fmt.Sprintf( - "%s / %s (%s%s) on %s", - r.Container.Name, - r.Container.ContainerID, + "%s (%s%s) on %s", + r.FormatServerName(), r.Family, r.Release, r.ServerName, @@ -133,25 +132,33 @@ func (r ScanResult) ServerInfo() string { // ServerInfoTui returns server infromation for TUI sidebar func (r ScanResult) ServerInfoTui() string { if len(r.Container.ContainerID) == 0 { - return fmt.Sprintf("%s (%s%s)", + line := fmt.Sprintf("%s (%s%s)", r.ServerName, r.Family, r.Release) + if r.RunningKernel.RebootRequired { + return "[Reboot] " + line + } + return line } - return fmt.Sprintf( - "|-- %s (%s%s)", - r.Container.Name, - r.Family, - r.Release, - // r.Container.ContainerID, - ) + + fmtstr := "|-- %s (%s%s)" + if r.RunningKernel.RebootRequired { + fmtstr = "|-- [Reboot] %s (%s%s)" + } + return fmt.Sprintf(fmtstr, r.Container.Name, r.Family, r.Release) } // FormatServerName returns server and container name -func (r ScanResult) FormatServerName() string { +func (r ScanResult) FormatServerName() (name string) { if len(r.Container.ContainerID) == 0 { - return r.ServerName + name = r.ServerName + } else { + name = fmt.Sprintf("%s@%s", + r.Container.Name, r.ServerName) } - return fmt.Sprintf("%s@%s", - r.Container.Name, r.ServerName) + if r.RunningKernel.RebootRequired { + name = "[Reboot Required] " + name + } + return } // FormatTextReportHeadedr returns header of text report From 187598382b1e3c64e553271c69273a8c0173e44c Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Wed, 23 Aug 2017 14:34:03 +0900 Subject: [PATCH 110/113] Update README --- README.ja.md | 83 ++++++++++++++++++++++++------------------ README.md | 74 +++++++++++++++++++++---------------- img/vuls-abstract.png | Bin 125909 -> 125749 bytes 3 files changed, 90 insertions(+), 67 deletions(-) diff --git a/README.ja.md b/README.ja.md index 8ba6d3bd..44db4956 100644 --- a/README.ja.md +++ b/README.ja.md @@ -8,7 +8,8 @@ Vulnerability scanner for Linux/FreeBSD, agentless, written in golang. [README in English](https://github.com/future-architect/vuls/blob/master/README.md) -Slackチームは[こちらから](http://goo.gl/forms/xm5KFo35tu)参加できます。(日本語でオッケーです) +Slackチームは[こちらから](http://goo.gl/forms/xm5KFo35tu)参加できます。(日本語でオッケーです) +Twitter: 日本語: [@vuls_ja](https://twitter.com/vuls_ja), 英語: [@vuls_en](https://twitter.com/vuls_en) ![Vuls-Abstract](img/vuls-abstract.png) @@ -46,21 +47,21 @@ Vulsは上に挙げた手動運用での課題を解決するツールであり # Main Features -- Linuxサーバに存在する脆弱性をスキャン - - Ubuntu, Debian, CentOS, Amazon Linux, RHEL, Raspbianに対応 +- サーバに存在する脆弱性をスキャン + - FreeBSD, Ubuntu, Debian, CentOS, Amazon Linux, RHEL, Raspbianに対応 - クラウド、オンプレミス、Docker - 高精度なスキャン -    - Vulsは複数の脆弱性データベース、複数の検知方法を組み合わせることで高精度なスキャンを実現している -        - OVAL + - Vulsは複数の脆弱性データベース、複数の検知方法を組み合わせることで高精度なスキャンを実現している + - OVAL - RHSA/ALAS/ELSA/FreeBSD-SA - Changelog - FastスキャンとDeepスキャン - Fastスキャン - root権限必要なし - スキャン対象サーバの負荷ほぼなし - - スキャン対象サーバがインターネットに接続していなくてもスキャンできる -    - Deepスキャン -        - Changelogの差分を取得し、そこに書かれているCVE-IDを検知 + - インターネットに接続していない環境でもスキャン可能 (RedHat, CentOS, OracleLinux, Ubuntu, Debian) + - Deepスキャン + - Changelogの差分を取得し、そこに書かれているCVE-IDを検知 - スキャン対象サーバに負荷がかかる場合がある - リモートスキャンとローカルスキャン - リモートスキャン @@ -224,9 +225,16 @@ The binary was built under `$GOPATH/bin` 今回はCentOSがスキャン対象なので、RedHatが公開しているOVAL情報を取り込む. [README](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-redhat) ```bash -$ goval-dictionary fetch-redhat 5 6 7 +$ goval-dictionary fetch-redhat 7 ``` +今回はスキャン対象がCentOS 7なので、RedHat 7のOVALを取得している。 +他の種類のOSをスキャンする場合は以下を参照し、スキャン対象用のOVALを取得しておくこと +- [RedHat, CentOS](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-redhat) +- [Debian](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-debian) +- [Ubuntu](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-ubuntu) +- [Oracle Linux](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-oracle) + ## Step5. Deploy Vuls 新規にターミナルを起動し、先ほど作成したEC2にSSH接続する。 @@ -447,9 +455,9 @@ ubuntu ubuntu16.04 30 updatable packages ## Step6. Reporting -See [Tutorial: Local Scan Mode#Step8. Reporting](#step8-reporting) -See [Tutorial: Local Scan Mode#Step9. TUI](#step9-tui) -See [Tutorial: Local Scan Mode#Step10. Web UI](#step10-web-ui) +See [Tutorial: Local Scan Mode#Step9. Reporting](#step9-reporting) +See [Tutorial: Local Scan Mode#Step10. TUI](#step10-tui) +See [Tutorial: Local Scan Mode#Step11. Web UI](#step11-web-ui) ---- @@ -467,42 +475,45 @@ Vulsをスキャン対象サーバにデプロイする。Vulsはローカルホ ![Vuls-Architecture Local Scan Mode](img/vuls-architecture-localscan.png) [詳細](#example-scan-via-shell-instead-of-ssh) -## [go-cve-dictionary](https://github.com/kotakanbe/go-cve-dictionary) -- NVDとJVN(日本語)から脆弱性データベースを取得し、SQLite3に格納する。 +----- + +## Fast Scan and Deep Scan -## Vuls ### Fast Scan ![Vuls-Scan-Flow](img/vuls-scan-flow-fast.png) - Root権限不要でスキャン可能なモード(Raspbian以外) - OVALが提供されているディストリビューションは、スキャン時はパッケージのバージョンを取得するのみ。レポート時にOVAL DBとバージョン比較により脆弱性を検知する - OVALが提供されいていないディストリビューションはスキャン時にコマンドを発行して脆弱性を検知する -| Distribution| Scan Speed | Root Privilege | OVAL | -|:------------|:-------------------|:---------------|:-----| -| CentOS | 速い |  不要 | 有 | -| Amazon | 速い |  不要 | 無 | -| RHEL | 速い |  不要 | 有 | -| Oracle | 速い |  不要 | 有 | -| FreeBSD | 速い |  不要 | 無 | -| Ubuntu | 速い |  不要 | 有 | -| Debian | 速い |  不要 | 有 | -| Raspbian | 初回は遅い / 2回目以降速い |  必要 | 無 | +| Distribution| Scan Speed | Need Root Privilege | OVAL | Need Internet Access
on scan tareget| +|:------------|:--------------------------------------:|:-------------------:|:----------:|:---------------------------------------:| +| CentOS | Fast |  No | Supported | No | +| RHEL | Fast |  No | Supported | No | +| Oracle | Fast |  No | Supported | No | +| Ubuntu | Fast |  No | Supported | No | +| Debian | Fast |  No | Supported | No | +| FreeBSD | Fast |  No | No | Need | +| Amazon | Fast |  No | No | Need | +| Raspbian |1st time: Slow
From 2nd time: Fast | Need | No | Need | + +---- ### Deep Scan ![Vuls-Scan-Flow](img/vuls-scan-flow.png) - Root権限が必要なコマンドも発行し、より深いスキャンを行うモード - ChangelogをパースしてCVE-IDを検知するのでFastよりも検知漏れが減る -| Distribution| Scan Speed | Root Privilege | OVAL | -|:------------|:-------------------|:---------------|:-----| -| CentOS | 遅い |  不要 | 有 | -| Amazon | 遅い |  不要 | 無 | -| RHEL | 遅い |  必要 | 有 | -| Oracle | 遅い |  必要 | 有 | -| Ubuntu | 初回は遅い / 2回目以降速い |  必要 | 有 | -| Debian | 初回は遅い / 2回目以降速い |  必要 | 有 | -| Raspbian | 初回は遅い / 2回目以降速い |  必要 | 無 | -| FreeBSD | 速い |  不要 | 無 | +| Distribution| Scan Speed | Need Root Privilege | OVAL | Need Internet Access
on scan tareget| +|:------------|:-------------------------------------:|:-------------------------:|:---------:|:---------------------------------------:| +| CentOS | Slow |  No | Supported | Need | +| RHEL | Slow |  Need | Supported | Need | +| Oracle | Slow |  Need | Supported | Need | +| Ubuntu |1st time: Slow
From 2nd time: Fast| Need | Supported | Need | +| Debian |1st time: Slow
From 2nd time: Fast| Need | Supported | Need | +| FreeBSD | Fast |  No | No | Need | +| Amazon | Slow |  No | No | Need | +| Raspbian |1st time: Slow
From 2nd time: Fast| Need | No | Need | + - Ubuntu, Debian, Raspbian `apt-get changelog`でアップデート対象のパッケージのチェンジログを取得し、含まれるCVE IDをパースする。 @@ -541,7 +552,7 @@ web/app server in the same configuration under the load balancer | CentOS | 6, 7| | Amazon Linux| All| | FreeBSD | 10, 11| -| Raspbian | Wheezy, Jessie | +| Raspbian | Jessie, Stretch | ---- diff --git a/README.md b/README.md index 57457401..13d73803 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ ![Vuls-logo](img/vuls_logo.png) -Vulnerability scanner for Linux/FreeBSD, agentless, written in golang. - +Vulnerability scanner for Linux/FreeBSD, agentless, written in golang. We have a slack team. [Join slack team](http://goo.gl/forms/xm5KFo35tu) +Twitter: [@vuls_en](https://twitter.com/vuls_en) [README 日本語](https://github.com/future-architect/vuls/blob/master/README.ja.md) [README in French](https://github.com/future-architect/vuls/blob/master/README.fr.md) @@ -52,7 +52,7 @@ Vuls is a tool created to solve the problems listed above. It has the following # Main Features - Scan for any vulnerabilities in Linux/FreeBSD Server - - Supports Ubuntu, Debian, CentOS, Amazon Linux, RHEL, Oracle Linux, FreeBSD and Raspbian + - Supports FreeBSD, Ubuntu, Debian, CentOS, Amazon Linux, RHEL, Oracle Linux and Raspbian - Cloud, on-premise, Docker - High quality scan - Vuls uses Multiple vulnerability databases @@ -62,6 +62,7 @@ Vuls is a tool created to solve the problems listed above. It has the following - Fast scan and Deep scan - Fast Scan - Scan without root privilege + - Scan with No internet access. (RedHat, CentOS, OracleLinux, Ubuntu, Debian) - Almost no load on the scan target server - Deep Scan - Scan with root privilege @@ -231,9 +232,15 @@ If the installation process stops halfway, try increasing the instance type of E Then fetch OVAL data of RedHat since the server to be scanned is CentOS. [README](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-redhat) ```bash -$ goval-dictionary fetch-redhat 5 6 7 +$ goval-dictionary fetch-redhat 7 ``` +If you want to scan other than CentOS 7, fetch OVAL data according to the OS type and version of scan target server in advance. +- [RedHat, CentOS](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-redhat) +- [Debian](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-debian) +- [Ubuntu](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-ubuntu) +- [Oracle Linux](https://github.com/kotakanbe/goval-dictionary#usage-fetch-oval-data-from-oracle) + ## Step5. Deploy Vuls Launch a new terminal and SSH to the ec2 instance. @@ -450,9 +457,9 @@ ubuntu ubuntu16.04 30 updatable packages ## Step6. Reporting -See [Tutorial: Local Scan Mode#Step8. Reporting](#step8-reporting) -See [Tutorial: Local Scan Mode#Step9. TUI](#step9-tui) -See [Tutorial: Local Scan Mode#Step10. Web UI](#step10-web-ui) +See [Tutorial: Local Scan Mode#Step9. Reporting](#step9-reporting) +See [Tutorial: Local Scan Mode#Step10. TUI](#step10-tui) +See [Tutorial: Local Scan Mode#Step11. Web UI](#step11-web-ui) ---- @@ -476,38 +483,43 @@ On the aggregation server, you can refer to the scanning result of each scan tar ![Vuls-Architecture Local Scan Mode](img/vuls-architecture-localscan.png) [Details](#example-scan-via-shell-instead-of-ssh) -## [go-cve-dictionary](https://github.com/kotakanbe/go-cve-dictionary) -- Fetch vulnerability information from NVD and JVN(Japanese), then insert into SQLite3, MySQL, PostgreSQL or Redis. +---- + +## Fast Scan and Deep Scan -## Vuls ### Fast Scan ![Vuls-Scan-Flow](img/vuls-scan-flow-fast.png) - Scan without Root Privilege +- Scan with No internet access on some OS. -| Distribution| Scan Speed | Root Privilege | OVAL | -|:------------|:-------------------|:---------------|:-----| -| CentOS | Fast |  No | Yes | -| Amazon | Fast |  No | No | -| RHEL | Fast |  No | Yes | -| Oracle | Fast |  No | Yes | -| FreeBSD | Fast |  No | No | -| Ubuntu | Fast |  No | Yes | -| Debian | Fast |  No | Yes | -| Raspbian |First time: Slow / From the second time: Fast|  Yes | No | +| Distribution| Scan Speed | Need Root Privilege | OVAL | Need Internet Access
on scan tareget| +|:------------|:--------------------------------------:|:-------------------:|:----------:|:---------------------------------------:| +| CentOS | Fast |  No | Supported | No | +| RHEL | Fast |  No | Supported | No | +| Oracle | Fast |  No | Supported | No | +| Ubuntu | Fast |  No | Supported | No | +| Debian | Fast |  No | Supported | No | +| Raspbian |1st time: Slow
From 2nd time: Fast | Need | No | Need | +| FreeBSD | Fast |  No | No | Need | +| Amazon | Fast |  No | No | Need | + + +--------- ### Deep Scan ![Vuls-Scan-Flow](img/vuls-scan-flow.png) -| Distribution| Scan Speed | Root Privilege | OVAL | -|:------------|:-------------------|:---------------|:-----| -| CentOS | Slow |  No | Yes| -| Amazon | Slow |  No | No| -| RHEL | Slow |  Yes| Yes| -| Oracle | Slow |  Yes| Yes| -| Ubuntu |First time: Slow / From the second time: Fast|  Yes| Yes| -| Debian |First time: Slow / From the second time: Fast|  Yes| Yes| -| Raspbian |First time: Slow / From the second time: Fast|  Yes| No | -| FreeBSD | Fast |  No | No| +| Distribution| Scan Speed | Need Root Privilege | OVAL | Need Internet Access
on scan tareget| +|:------------|:-------------------------------------:|:-------------------------:|:---------:|:---------------------------------------:| +| CentOS | Slow |  No | Supported | Need | +| RHEL | Slow |  Need | Supported | Need | +| Oracle | Slow |  Need | Supported | Need | +| Ubuntu |1st time: Slow
From 2nd time: Fast| Need | Supported | Need | +| Debian |1st time: Slow
From 2nd time: Fast| Need | Supported | Need | +| Raspbian |1st time: Slow
From 2nd time: Fast| Need | No | Need | +| FreeBSD | Fast |  No | No | Need | +| Amazon | Slow |  No | No | Need | + - On Ubuntu, Debian and Raspbian Vuls issues `apt-get changelog` for each upgradable packages and parse the changelog. @@ -551,7 +563,7 @@ If there is a staging environment with the same configuration as the production | CentOS | 6, 7| | Amazon Linux | All| | FreeBSD | 10, 11| -| Raspbian | Wheezy, Jessie | +| Raspbian | Jessie, Stretch | ---- diff --git a/img/vuls-abstract.png b/img/vuls-abstract.png index ab8df8a4c540500c2c9466959c7ff444462618d4..291d87ad99ac6c6dcb855c6e258b94d2e3eb2428 100644 GIT binary patch delta 79180 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 delta 79312 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 Q1MsCFt0Ge=Wft_`0KZs@VgLXD From 21f13b55eb1901cc214fd4f349a8d5d8761ab715 Mon Sep 17 00:00:00 2001 From: sadayuki-matsuno Date: Wed, 23 Aug 2017 18:09:22 +0900 Subject: [PATCH 111/113] export fill cve info (#467) --- report/report.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/report/report.go b/report/report.go index 32a3a110..df4fca4d 100644 --- a/report/report.go +++ b/report/report.go @@ -39,7 +39,7 @@ func FillCveInfos(rs []models.ScanResult, dir string) ([]models.ScanResult, erro reportedAt := time.Now() for _, r := range rs { if c.Conf.RefreshCve || needToRefreshCve(r) { - if err := fillCveInfo(&r); err != nil { + if err := FillCveInfo(&r); err != nil { return nil, err } r.Lang = c.Conf.Lang @@ -86,7 +86,8 @@ func FillCveInfos(rs []models.ScanResult, dir string) ([]models.ScanResult, erro return filtered, nil } -func fillCveInfo(r *models.ScanResult) error { +// FillCveInfo fill scanResult with cve info. +func FillCveInfo(r *models.ScanResult) error { util.Log.Debugf("need to refresh") util.Log.Infof("Fill CVE detailed information with OVAL") From 6312b97faa0be50381ce851a48f386cdeb67d821 Mon Sep 17 00:00:00 2001 From: Yasunari Momoi Date: Wed, 23 Aug 2017 19:29:31 +0900 Subject: [PATCH 112/113] fix typos in commands. (#464) --- commands/configtest.go | 2 +- commands/discover.go | 18 +++++++++--------- commands/report.go | 8 ++++---- commands/scan.go | 4 ++-- commands/tui.go | 6 +++--- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/commands/configtest.go b/commands/configtest.go index 02a3ca73..6feca8f5 100644 --- a/commands/configtest.go +++ b/commands/configtest.go @@ -174,7 +174,7 @@ func (p *ConfigtestCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interfa return subcommands.ExitFailure } - util.Log.Info("Checking dependendies...") + util.Log.Info("Checking dependencies...") scan.CheckDependencies(p.timeoutSec) util.Log.Info("Checking sudo settings...") diff --git a/commands/discover.go b/commands/discover.go index 12a516db..6f69ed26 100644 --- a/commands/discover.go +++ b/commands/discover.go @@ -87,9 +87,9 @@ func (p *DiscoverCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface return subcommands.ExitSuccess } -// Output the tmeplate of config.toml +// Output the template of config.toml func printConfigToml(ips []string) (err error) { - const tomlTempale = ` + const tomlTemplate = ` [slack] hookURL = "https://hooks.slack.com/services/abc123/defghijklmnopqrstuvwxyz" channel = "#channel-name" @@ -99,13 +99,13 @@ authUser = "username" notifyUsers = ["@username"] [email] -smtpAddr = "smtp.gmail.com" +smtpAddr = "smtp.example.com" smtpPort = "587" user = "username" password = "password" -from = "from@address.com" -to = ["to@address.com"] -cc = ["cc@address.com"] +from = "from@example.com" +to = ["to@example.com"] +cc = ["cc@example.com"] subjectPrefix = "[vuls]" [default] @@ -140,7 +140,7 @@ host = "{{$ip}}" # ["key", "value"], #] #[servers.{{index $names $i}}.containers] -#type = "docker" #or "lxd" defualt: docker +#type = "docker" #or "lxd" default: docker #includes = ["${running}"] #excludes = ["container_name_a", "4aa37a8b63b9"] @@ -149,7 +149,7 @@ host = "{{$ip}}" ` var tpl *template.Template - if tpl, err = template.New("tempalte").Parse(tomlTempale); err != nil { + if tpl, err = template.New("template").Parse(tomlTemplate); err != nil { return } @@ -167,7 +167,7 @@ host = "{{$ip}}" } a.Names = names - fmt.Println("# Create config.toml using below and then ./vuls --config=/path/to/config.toml") + fmt.Println("# Create config.toml using below and then ./vuls -config=/path/to/config.toml") if err = tpl.Execute(os.Stdout, a); err != nil { return } diff --git a/commands/report.go b/commands/report.go index 2e953ba4..1542347d 100644 --- a/commands/report.go +++ b/commands/report.go @@ -123,7 +123,7 @@ func (*ReportCmd) Usage() string { [-aws-region=us-west-2] [-aws-s3-bucket=bucket_name] [-aws-s3-results-dir=/bucket/path/to/results] - [-azure-account=accout] + [-azure-account=account] [-azure-key=key] [-azure-container=container] [-http-proxy=http://192.168.0.1:8080] @@ -381,7 +381,7 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} c.Conf.AzureContainer = p.azureContainer if len(c.Conf.AzureContainer) == 0 { - util.Log.Error("Azure storage container name is requied with --azure-container option") + util.Log.Error("Azure storage container name is required with -azure-container option") return subcommands.ExitUsageError } if err := report.CheckIfAzureContainerExists(); err != nil { @@ -402,7 +402,7 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} } if err := report.CveClient.CheckHealth(); err != nil { util.Log.Errorf("CVE HTTP server is not running. err: %s", err) - util.Log.Errorf("Run go-cve-dictionary as server mode before reporting or run with --cvedb-path option") + util.Log.Errorf("Run go-cve-dictionary as server mode before reporting or run with -cvedb-path option") return subcommands.ExitFailure } if c.Conf.CveDBURL != "" { @@ -417,7 +417,7 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} err := oval.Base{}.CheckHTTPHealth() if err != nil { util.Log.Errorf("OVAL HTTP server is not running. err: %s", err) - util.Log.Errorf("Run goval-dictionary as server mode before reporting or run with --ovaldb-path option") + util.Log.Errorf("Run goval-dictionary as server mode before reporting or run with -ovaldb-path option") return subcommands.ExitFailure } } diff --git a/commands/scan.go b/commands/scan.go index 60874c4b..0ee4dae2 100644 --- a/commands/scan.go +++ b/commands/scan.go @@ -138,7 +138,7 @@ func (p *ScanCmd) SetFlags(f *flag.FlagSet) { &p.deep, "deep", false, - "Deep scan mode. Scan accuracy improves and scanned information becomes richer. Since analysis of changelog, issue commands requiring sudo, but it may be slower and high load on the tareget server") + "Deep scan mode. Scan accuracy improves and scanned information becomes richer. Since analysis of changelog, issue commands requiring sudo, but it may be slower and high load on the target server") f.BoolVar( &p.pipe, @@ -157,7 +157,7 @@ func (p *ScanCmd) SetFlags(f *flag.FlagSet) { &p.scanTimeoutSec, "timeout-scan", 120*60, - "Number of seconds for scaning vulnerabilities for all servers", + "Number of seconds for scanning vulnerabilities for all servers", ) } diff --git a/commands/tui.go b/commands/tui.go index f40d03f7..2e650a7e 100644 --- a/commands/tui.go +++ b/commands/tui.go @@ -56,7 +56,7 @@ type TuiCmd struct { func (*TuiCmd) Name() string { return "tui" } // Synopsis return synopsis -func (*TuiCmd) Synopsis() string { return "Run Tui view to anayze vulnerabilites" } +func (*TuiCmd) Synopsis() string { return "Run Tui view to analyze vulnerabilities" } // Usage return usage func (*TuiCmd) Usage() string { @@ -118,7 +118,7 @@ func (p *TuiCmd) SetFlags(f *flag.FlagSet) { &p.cveDictionaryURL, "cvedb-url", "", - "http://cve-dictionary.com:1323 or mysql connection string") + "http://cve-dictionary.example.com:1323 or mysql connection string") f.StringVar( &p.ovalDBType, @@ -137,7 +137,7 @@ func (p *TuiCmd) SetFlags(f *flag.FlagSet) { &p.ovalDBURL, "ovaldb-url", "", - "http://goval-dictionary.com:1324 or mysql connection string") + "http://goval-dictionary.example.com:1324 or mysql connection string") f.BoolVar( &p.pipe, From cb1c07f998dc311d61f4ed8b0673efe79665b2c4 Mon Sep 17 00:00:00 2001 From: kota kanbe Date: Fri, 25 Aug 2017 09:01:41 +0800 Subject: [PATCH 113/113] Update README --- README.ja.md | 137 +++++++++++++++++++++++++++++++++++++++++++-------- README.md | 137 +++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 233 insertions(+), 41 deletions(-) diff --git a/README.ja.md b/README.ja.md index 44db4956..81d7178e 100644 --- a/README.ja.md +++ b/README.ja.md @@ -21,7 +21,98 @@ Twitter: 日本語: [@vuls_ja](https://twitter.com/vuls_ja), 英語: [@vuls_en] # TOC -TODO +Table of Contents +================= + + * [Vuls: VULnerability Scanner](#vuls-vulnerability-scanner) + * [TOC](#toc) + * [Abstract](#abstract) + * [Main Features](#main-features) + * [What Vuls Doesn't Do](#what-vuls-doesnt-do) + * [Setup Vuls](#setup-vuls) + * [Tutorial](#tutorial) + * [Tutorial: Local Scan Mode](#tutorial-local-scan-mode) + * [Step1. Launch CentOS7](#step1-launch-centos7) + * [Step2. Install requirements](#step2-install-requirements) + * [Step3. Deploy go-cve-dictionary](#step3-deploy-go-cve-dictionary) + * [Step4. Deploy goval-dictionary](#step4-deploy-goval-dictionary) + * [Step5. Deploy Vuls](#step5-deploy-vuls) + * [Step6. Config](#step6-config) + * [Step7. Check config.toml and settings on the server before scanning](#step7-check-configtoml-and-settings-on-the-server-before-scanning) + * [Step8. Start Scanning](#step8-start-scanning) + * [Step9. Reporting](#step9-reporting) + * [Step10. TUI](#step10-tui) + * [Step11. Web UI](#step11-web-ui) + * [Tutorial: Remote Scan Mode](#tutorial-remote-scan-mode) + * [Step1. Launch new Ubuntu Linux (the server to be sacnned)](#step1-launch-new-ubuntu-linux-the-server-to-be-sacnned) + * [Step2. Enable to SSH from localhost](#step2-enable-to-ssh-from-localhost) + * [Step3. config.tomlの設定](#step3-configtomlの設定) + * [Step4. Check config.toml and settings on the server before scanning](#step4-check-configtoml-and-settings-on-the-server-before-scanning) + * [Step5. Start Scanning](#step5-start-scanning) + * [Step6. Reporting](#step6-reporting) + * [Architecture](#architecture) + * [A. Scan via SSH Mode (Remote Scan Mode)](#a-scan-via-ssh-mode-remote-scan-mode) + * [B. Scan without SSH (Local Scan Mode)](#b-scan-without-ssh-local-scan-mode) + * [Fast Scan and Deep Scan](#fast-scan-and-deep-scan) + * [Fast Scan](#fast-scan) + * [Deep Scan](#deep-scan) + * [Use Cases](#use-cases) + * [Scan all servers](#scan-all-servers) + * [Scan a single server](#scan-a-single-server) + * [Support OS](#support-os) + * [Usage: Automatic Server Discovery](#usage-automatic-server-discovery) + * [Example](#example) + * [Configuration](#configuration) + * [Usage: Configtest](#usage-configtest) + * [Fast Scan Mode](#fast-scan-mode) + * [Deep Scan Mode](#deep-scan-mode) + * [Dependencies and /etc/sudoers on Target Servers](#dependencies-and-etcsudoers-on-target-servers) + * [Usage: Scan](#usage-scan) + * [-deep option](#-deep-option) + * [-ssh-native-insecure option](#-ssh-native-insecure-option) + * [-ask-key-password option](#-ask-key-password-option) + * [Example: Scan all servers defined in config file](#example-scan-all-servers-defined-in-config-file) + * [Example: Scan specific servers](#example-scan-specific-servers) + * [Example: Scan via shell instead of SSH.](#example-scan-via-shell-instead-of-ssh) + * [cronで動かす場合](#cronで動かす場合) + * [Example: Scan containers (Docker/LXD)](#example-scan-containers-dockerlxd) + * [Docker](#docker) + * [LXDコンテナをスキャンする場合](#lxdコンテナをスキャンする場合) + * [Usage: Report](#usage-report) + * [How to read a report](#how-to-read-a-report) + * [Example](#example-1) + * [Summary part](#summary-part) + * [Detailed Part](#detailed-part) + * [Example: Send scan results to Slack](#example-send-scan-results-to-slack) + * [Example: Put results in S3 bucket](#example-put-results-in-s3-bucket) + * [Example: Put results in Azure Blob storage](#example-put-results-in-azure-blob-storage) + * [Example: IgnoreCves](#example-ignorecves) + * [Example: Add optional key-value pairs to JSON](#example-add-optional-key-value-pairs-to-json) + * [Example: Use MySQL as a DB storage back-end](#example-use-mysql-as-a-db-storage-back-end) + * [Example: Use PostgreSQL as a DB storage back-end](#example-use-postgresql-as-a-db-storage-back-end) + * [Example: Use Redis as a DB storage back-end](#example-use-redis-as-a-db-storage-back-end) + * [Usage: Scan vulnerability of non-OS package](#usage-scan-vulnerability-of-non-os-package) + * [Usage: Integrate with OWASP Dependency Check to Automatic update when the libraries are updated (Experimental)](#usage-integrate-with-owasp-dependency-check-to-automatic-update-when-the-libraries-are-updated-experimental) + * [Usage: TUI](#usage-tui) + * [Display the latest scan results](#display-the-latest-scan-results) + * [Display the previous scan results](#display-the-previous-scan-results) + * [Display the previous scan results using peco](#display-the-previous-scan-results-using-peco) + * [Usage: go-cve-dictionary on different server](#usage-go-cve-dictionary-on-different-server) + * [Usage: Update NVD Data](#usage-update-nvd-data) + * [Usage: goval-dictionary on different server](#usage-goval-dictionary-on-different-server) + * [Usage: Update OVAL Data](#usage-update-oval-data) + * [レポートの日本語化](#レポートの日本語化) + * [fetchnvd, fetchjvnの実行順序の注意](#fetchnvd-fetchjvnの実行順序の注意) + * [スキャン実行](#スキャン実行) + * [How to Update to the Latest Version](#how-to-update-to-the-latest-version) + * [Misc](#misc) + * [Related Projects](#related-projects) + * [Data Source](#data-source) + * [Authors](#authors) + * [Contribute](#contribute) + * [Change Log](#change-log) + * [Stargazers over time](#stargazers-over-time) + * [License](#license) ---- @@ -68,6 +159,9 @@ Vulsは上に挙げた手動運用での課題を解決するツールであり - スキャン対象サーバにSSH接続可能なマシン1台にセットアップするだけで動作 - ローカルスキャン - もし中央のサーバから各サーバにSSH接続できない環境の場合はローカルスキャンモードでスキャン可能 +- **動的** スキャナ + - サーバにSSH接続してコマンドを発行可能なのでサーバの状態を取得可能 + - カーネルアップデート後再起動していない場合に警告してくれる - OSパッケージ管理対象外のミドルウェアをスキャン - プログラミング言語のライブラリやフレームワーク、ミドルウェアの脆弱性スキャン - CPEに登録されているソフトウェアが対象 @@ -408,10 +502,10 @@ $ touch ~/.ssh/authorized_keys $ chmod 600 ~/.ssh/authorized_keys $ vim ~/.ssh/authorized_keys ``` -Paste from the clipboard to ~/.ssh/.authorized_keys +Paste from the clipboard to `~/.ssh/.authorized_keys` localhostのknown_hostsにremote hostのホストキーが登録されている必要があるので確認すること。 -$HOME/.ssh/known_hostsにリモートホストのHost Keyを追加するために、スキャン前にリモートホストにSSH接続する必要がある。 +`$HOME/.ssh/known_hosts`にリモートホストのHost Keyを追加するために、スキャン前にリモートホストにSSH接続する必要がある。 - localhost @@ -547,7 +641,7 @@ web/app server in the same configuration under the load balancer | Distribution| Release | |:------------|-------------------:| | Ubuntu | 12, 14, 16| -| Debian | 7, 8| +| Debian | 7, 8, 9| | RHEL | 5, 6, 7| | CentOS | 6, 7| | Amazon Linux| All| @@ -783,7 +877,7 @@ configtestサブコマンドは、config.tomlで定義されたサーバ/コン | Distribution | Release | Requirements | |:-------------|-------------------:|:-------------| | Ubuntu | 12, 14, 16| - | -| Debian | 7, 8| reboot-notifier| +| Debian | 7, 8, 9| reboot-notifier| | CentOS | 6, 7| - | | Amazon | All | - | | RHEL | 5, 6, 7 | - | @@ -805,7 +899,7 @@ Deep Scan Modeでスキャンするためには、下記のパッケージが必 | Distribution | Release | Requirements | |:-------------|-------------------:|:-------------| | Ubuntu | 12, 14, 16| - | -| Debian | 7, 8| aptitude, reboot-notifier | +| Debian | 7, 8, 9| aptitude, reboot-notifier | | CentOS | 6, 7| yum-plugin-changelog, yum-utils | | Amazon | All | yum-plugin-changelog, yum-utils | | RHEL | 5 | yum-utils, yum-security, yum-changelog | @@ -901,18 +995,9 @@ scan: You need to execute `vuls configtest --deep` to check the configuration of the target server before scanning with -deep flag. -| Distribution | Changelog | -|:-------------|:---------:| -| Ubuntu | yes | -| Debian | yes | -| CentOS | yes | -| Amazon | yes | -| RHEL | yes | -| RHEL | yes | -| Oracle Linux | yes | -| Oracle Linux | yes | -| FreeBSD | no | -| Raspbian | yes | +For details about deep scan mode, see below. +* [Architecture/Deep Scan](#deep-scan) +* [Configtest/Deep Scan Mode](#deep-scan-mode) ## -ssh-native-insecure option @@ -1664,14 +1749,23 @@ slack, emailは日本語対応済み TUIは日本語表示未対応 ---- -# Update Vuls With Glide +# How to Update to the Latest Version - Update go-cve-dictionary If the DB schema was changed, please specify new SQLite3, MySQL, PostgreSQL or Redis DB file. ``` $ cd $GOPATH/src/github.com/kotakanbe/go-cve-dictionary $ git pull -$ mv vendor /tmp/foo +$ rm -r vendor +$ make install +``` + +- Update goval-dictionary +If the DB schema was changed, please specify new SQLite3, MySQL, PostgreSQL or Redis DB file. +``` +$ cd $GOPATH/src/github.com/kotakanbe/goval-dictionary +$ git pull +$ rm -r vendor $ make install ``` @@ -1679,10 +1773,11 @@ $ make install ``` $ cd $GOPATH/src/github.com/future-architect/vuls $ git pull -$ mv vendor /tmp/bar +$ rm -r vendor $ make install ``` - バイナリファイルは`$GOPATH/bin`以下に作成される +- もしエラーが出る場合は `$GOPATH/pkg` を削除してから実行する --- diff --git a/README.md b/README.md index 13d73803..7c40a6f7 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,99 @@ Twitter: [@vuls_en](https://twitter.com/vuls_en) # TOC -TODO +Table of Contents +================= + + * [Vuls: VULnerability Scanner](#vuls-vulnerability-scanner) + * [TOC](#toc) + * [Abstract](#abstract) + * [Main Features](#main-features) + * [What Vuls Doesn't Do](#what-vuls-doesnt-do) + * [Setup Vuls](#setup-vuls) + * [Tutorial](#tutorial) + * [Tutorial: Local Scan Mode](#tutorial-local-scan-mode) + * [Step1. Launch CentOS7](#step1-launch-centos7) + * [Step2. Install requirements](#step2-install-requirements) + * [Step3. Deploy go-cve-dictionary](#step3-deploy-go-cve-dictionary) + * [Step4. Deploy goval-dictionary](#step4-deploy-goval-dictionary) + * [Step5. Deploy Vuls](#step5-deploy-vuls) + * [Step6. Configuration](#step6-configuration) + * [Step7. Check config.toml and settings on the server before scanning](#step7-check-configtoml-and-settings-on-the-server-before-scanning) + * [Step8. Start Scanning](#step8-start-scanning) + * [Step9. Reporting](#step9-reporting) + * [Step10. TUI](#step10-tui) + * [Step11. Web UI](#step11-web-ui) + * [Tutorial: Remote Scan Mode](#tutorial-remote-scan-mode) + * [Step1. Launch new Ubuntu Linux](#step1-launch-new-ubuntu-linux) + * [Step2. Enable to SSH from localhost](#step2-enable-to-ssh-from-localhost) + * [Step3. Configure (config.toml)](#step3-configure-configtoml) + * [Step4. Check config.toml and settings on the server before scanning](#step4-check-configtoml-and-settings-on-the-server-before-scanning) + * [Step5. Start Scanning](#step5-start-scanning) + * [Step6. Reporting](#step6-reporting) + * [Setup Vuls in a Docker Container](#setup-vuls-in-a-docker-container) + * [Architecture](#architecture) + * [A. Scan via SSH Mode (Remote Scan Mode)](#a-scan-via-ssh-mode-remote-scan-mode) + * [B. Scan without SSH (Local Scan Mode)](#b-scan-without-ssh-local-scan-mode) + * [Fast Scan and Deep Scan](#fast-scan-and-deep-scan) + * [Fast Scan](#fast-scan) + * [Deep Scan](#deep-scan) + * [Use Cases](#use-cases) + * [Scan All Servers](#scan-all-servers) + * [Scan a Single Server](#scan-a-single-server) + * [Scan Staging Environment](#scan-staging-environment) + * [Support OS](#support-os) + * [Usage: Automatic Server Discovery](#usage-automatic-server-discovery) + * [Example](#example) + * [Configuration](#configuration) + * [Usage: Configtest](#usage-configtest) + * [Fast Scan Mode](#fast-scan-mode) + * [Deep Scan Mode](#deep-scan-mode) + * [Dependencies and /etc/sudoers on Target Servers](#dependencies-and-etcsudoers-on-target-servers) + * [Usage: Scan](#usage-scan) + * [-deep option](#-deep-option) + * [-ssh-native-insecure option](#-ssh-native-insecure-option) + * [-ask-key-password option](#-ask-key-password-option) + * [Example: Scan all servers defined in config file](#example-scan-all-servers-defined-in-config-file) + * [Example: Scan specific servers](#example-scan-specific-servers) + * [Example: Scan via shell instead of SSH.](#example-scan-via-shell-instead-of-ssh) + * [cron](#cron) + * [Example: Scan containers (Docker/LXD)](#example-scan-containers-dockerlxd) + * [Docker](#docker) + * [LXD](#lxd) + * [Usage: Report](#usage-report) + * [How to read a report](#how-to-read-a-report) + * [Example](#example-1) + * [Summary part](#summary-part) + * [Detailed Part](#detailed-part) + * [Example: Send scan results to Slack](#example-send-scan-results-to-slack) + * [Example: Put results in S3 bucket](#example-put-results-in-s3-bucket) + * [Example: Put results in Azure Blob storage](#example-put-results-in-azure-blob-storage) + * [Example: IgnoreCves](#example-ignorecves) + * [Example: Add optional key-value pairs to JSON](#example-add-optional-key-value-pairs-to-json) + * [Example: Use MySQL as a DB storage back-end](#example-use-mysql-as-a-db-storage-back-end) + * [Example: Use PostgreSQL as a DB storage back-end](#example-use-postgresql-as-a-db-storage-back-end) + * [Example: Use Redis as a DB storage back-end](#example-use-redis-as-a-db-storage-back-end) + * [Usage: Scan vulnerabilites of non-OS packages](#usage-scan-vulnerabilites-of-non-os-packages) + * [Usage: Integrate with OWASP Dependency Check to Automatic update when the libraries are updated (Experimental)](#usage-integrate-with-owasp-dependency-check-to-automatic-update-when-the-libraries-are-updated-experimental) + * [Usage: TUI](#usage-tui) + * [Display the latest scan results](#display-the-latest-scan-results) + * [Display the previous scan results](#display-the-previous-scan-results) + * [Display the previous scan results using peco](#display-the-previous-scan-results-using-peco) + * [Usage: go-cve-dictionary on different server](#usage-go-cve-dictionary-on-different-server) + * [Usage: Update NVD Data](#usage-update-nvd-data) + * [Usage: goval-dictionary on different server](#usage-goval-dictionary-on-different-server) + * [Usage: Update OVAL Data](#usage-update-oval-data) + * [How to Update to the Latest Version](#how-to-update-to-the-latest-version) + * [Misc](#misc) + * [Related Projects](#related-projects) + * [Data Source](#data-source) + * [Authors](#authors) + * [Contribute](#contribute) + * [Change Log](#change-log) + * [Stargazers over time](#stargazers-over-time) + * [License](#license) + +Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc) ---- @@ -76,6 +168,9 @@ Vuls is a tool created to solve the problems listed above. It has the following - User is required to only setup one machine that is connected to other target servers via SSH - Local Scan - If you don't want the central Vuls server to connect to each server by SSH, you can use Vuls in the Local Scan mode. +- **Dynamic** Analysis + - It is possible to acquire the state of the server by connecting via SSH and executing the command + - Vuls warns when the scan target server was updated the kernel etc. but not restarting it. - Scan middleware that are not included in OS package management - Scan middleware, programming language libraries and framework for vulnerability - Support software registered in CPE @@ -411,10 +506,10 @@ $ touch ~/.ssh/authorized_keys $ chmod 600 ~/.ssh/authorized_keys $ vim ~/.ssh/authorized_keys ``` -Paste from the clipboard to ~/.ssh/.authorized_keys +Paste from the clipboard to `~/.ssh/.authorized_keys` And also, confirm that the host keys of scan target servers has been registered in the known_hosts of the localhost. -To add the remote host's Host Key to $HOME/.ssh/known_hosts, you need to log in to the remote host through SSH before scanning. +To add the remote host's Host Key to `$HOME/.ssh/known_hosts`, you need to log in to the remote host through SSH before scanning. - localhost ``` @@ -557,7 +652,7 @@ If there is a staging environment with the same configuration as the production | Distribution | Release | |:-------------|-------------------:| | Ubuntu | 12, 14, 16| -| Debian | 7, 8| +| Debian | 7, 8, 9| | RHEL | 5, 6, 7| | Oracle Linux | 5, 6, 7| | CentOS | 6, 7| @@ -793,7 +888,7 @@ The configtest subcommand checks whether vuls is able to connect via SSH to serv | Distribution | Release | Requirements | |:-------------|-------------------:|:-------------| | Ubuntu | 12, 14, 16| - | -| Debian | 7, 8| reboot-notifier| +| Debian | 7, 8, 9| reboot-notifier| | CentOS | 6, 7| - | | Amazon | All | - | | RHEL | 5, 6, 7 | - | @@ -813,7 +908,7 @@ In order to scan with deep scan mode, the following dependencies are required, s | Distribution | Release | Requirements | |:-------------|-------------------:|:-------------| | Ubuntu | 12, 14, 16| - | -| Debian | 7, 8| aptitude, reboot-notifier | +| Debian | 7, 8, 9| aptitude, reboot-notifier | | CentOS | 6, 7| yum-plugin-changelog, yum-utils | | Amazon | All | yum-plugin-changelog, yum-utils | | RHEL | 5 | yum-utils, yum-security, yum-changelog | @@ -909,18 +1004,9 @@ scan: You need to execute `vuls configtest --deep` to check the configuration of the target server before scanning with -deep flag. -| Distribution | Changelog | -|:-------------|:---------:| -| Ubuntu | yes | -| Debian | yes | -| CentOS | yes | -| Amazon | yes | -| RHEL | yes | -| RHEL | yes | -| Oracle Linux | yes | -| Oracle Linux | yes | -| FreeBSD | no | -| Raspbian | yes | +For details about deep scan mode, see below. +* [Architecture/Deep Scan](#deep-scan) +* [Configtest/Deep Scan Mode](#deep-scan-mode) ## -ssh-native-insecure option @@ -1628,7 +1714,7 @@ $ vuls report -ovaldb-url=http://192.168.0.1:1323 ---- -# How to Update +# How to Update to the Latest Version - Update go-cve-dictionary If the DB schema was changed, please specify new SQLite3, MySQL, PostgreSQL or Redis DB file. @@ -1639,6 +1725,15 @@ $ rm -r vendor $ make install ``` +- Update goval-dictionary +If the DB schema was changed, please specify new SQLite3, MySQL, PostgreSQL or Redis DB file. +``` +$ cd $GOPATH/src/github.com/kotakanbe/goval-dictionary +$ git pull +$ rm -r vendor +$ make install +``` + - Update vuls ``` $ cd $GOPATH/src/github.com/future-architect/vuls @@ -1646,7 +1741,9 @@ $ git pull $ rm -r vendor $ make install ``` -Binary file was built under $GOPATH/bin + +- Binary file was built under $GOPATH/bin +- If an error occurs, delete `$GOPATH/pkg` before executing it ---