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