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() { + +}