Notify the difference from the previous scan result (#392)
add diff option
This commit is contained in:
committed by
Kota Kanbe
parent
7131270cad
commit
c7019debb9
@@ -74,6 +74,8 @@ type ReportCmd struct {
|
||||
azureContainer string
|
||||
|
||||
pipe bool
|
||||
|
||||
diff bool
|
||||
}
|
||||
|
||||
// Name return subcommand name
|
||||
@@ -95,6 +97,7 @@ func (*ReportCmd) Usage() string {
|
||||
[-cvedb-path=/path/to/cve.sqlite3]
|
||||
[-cvedb-url=http://127.0.0.1:1323 or mysql connection string]
|
||||
[-cvss-over=7]
|
||||
[-diff]
|
||||
[-ignore-unscored-cves]
|
||||
[-to-email]
|
||||
[-to-slack]
|
||||
@@ -171,6 +174,11 @@ func (p *ReportCmd) SetFlags(f *flag.FlagSet) {
|
||||
0,
|
||||
"-cvss-over=6.5 means reporting CVSS Score 6.5 and over (default: 0 (means report all))")
|
||||
|
||||
f.BoolVar(&p.diff,
|
||||
"diff",
|
||||
false,
|
||||
fmt.Sprintf("Difference between previous result and current result "))
|
||||
|
||||
f.BoolVar(
|
||||
&p.ignoreUnscoredCves,
|
||||
"ignore-unscored-cves",
|
||||
@@ -273,11 +281,6 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}
|
||||
c.Conf.HTTPProxy = p.httpProxy
|
||||
|
||||
c.Conf.Pipe = p.pipe
|
||||
jsonDir, err := jsonDir(f.Args())
|
||||
if err != nil {
|
||||
util.Log.Errorf("Failed to read from JSON: %s", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
|
||||
c.Conf.FormatXML = p.formatXML
|
||||
c.Conf.FormatJSON = p.formatJSON
|
||||
@@ -287,6 +290,19 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}
|
||||
c.Conf.FormatFullText = p.formatFullText
|
||||
|
||||
c.Conf.GZIP = p.gzip
|
||||
c.Conf.Diff = p.diff
|
||||
|
||||
var dir string
|
||||
var err error
|
||||
if p.diff {
|
||||
dir, err = jsonDir([]string{})
|
||||
} else {
|
||||
dir, err = jsonDir(f.Args())
|
||||
}
|
||||
if err != nil {
|
||||
util.Log.Errorf("Failed to read from JSON: %s", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
|
||||
// report
|
||||
reports := []report.ResultWriter{
|
||||
@@ -303,7 +319,7 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}
|
||||
|
||||
if p.toLocalFile {
|
||||
reports = append(reports, report.LocalFileWriter{
|
||||
CurrentDir: jsonDir,
|
||||
CurrentDir: dir,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -363,7 +379,8 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}
|
||||
}
|
||||
}
|
||||
|
||||
history, err := loadOneScanHistory(jsonDir)
|
||||
var history models.ScanHistory
|
||||
history, err = loadOneScanHistory(dir)
|
||||
if err != nil {
|
||||
util.Log.Error(err)
|
||||
return subcommands.ExitFailure
|
||||
@@ -388,8 +405,7 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
filled.Lang = c.Conf.Lang
|
||||
|
||||
if err := overwriteJSONFile(jsonDir, *filled); err != nil {
|
||||
if err := overwriteJSONFile(dir, *filled); err != nil {
|
||||
util.Log.Errorf("Failed to write JSON: %s", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
@@ -400,10 +416,31 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}
|
||||
}
|
||||
}
|
||||
|
||||
if p.diff {
|
||||
currentHistory := models.ScanHistory{ScanResults: results}
|
||||
previousHistory, err := loadPreviousScanHistory(currentHistory)
|
||||
if err != nil {
|
||||
util.Log.Error(err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
|
||||
history, err = diff(currentHistory, previousHistory)
|
||||
if err != nil {
|
||||
util.Log.Error(err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
results = []models.ScanResult{}
|
||||
for _, r := range history.ScanResults {
|
||||
filled, _ := r.FillCveDetail()
|
||||
results = append(results, *filled)
|
||||
}
|
||||
}
|
||||
|
||||
var res models.ScanResults
|
||||
for _, r := range results {
|
||||
res = append(res, r.FilterByCvssOver())
|
||||
}
|
||||
|
||||
for _, w := range reports {
|
||||
if err := w.Write(res...); err != nil {
|
||||
util.Log.Errorf("Failed to report: %s", err)
|
||||
|
||||
123
commands/util.go
123
commands/util.go
@@ -26,6 +26,7 @@ import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
c "github.com/future-architect/vuls/config"
|
||||
"github.com/future-architect/vuls/cveapi"
|
||||
@@ -121,6 +122,19 @@ func jsonDir(args []string) (string, error) {
|
||||
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
|
||||
}
|
||||
|
||||
// loadOneScanHistory read JSON data
|
||||
func loadOneScanHistory(jsonDir string) (scanHistory models.ScanHistory, err error) {
|
||||
var results []models.ScanResult
|
||||
@@ -130,20 +144,16 @@ func loadOneScanHistory(jsonDir string) (scanHistory models.ScanHistory, err err
|
||||
return
|
||||
}
|
||||
for _, f := range files {
|
||||
if filepath.Ext(f.Name()) != ".json" {
|
||||
if filepath.Ext(f.Name()) != ".json" || strings.HasSuffix(f.Name(), "_diff.json") {
|
||||
continue
|
||||
}
|
||||
|
||||
var r models.ScanResult
|
||||
var data []byte
|
||||
path := filepath.Join(jsonDir, f.Name())
|
||||
if data, err = ioutil.ReadFile(path); err != nil {
|
||||
err = fmt.Errorf("Failed to read %s: %s", path, err)
|
||||
return
|
||||
}
|
||||
if json.Unmarshal(data, &r) != nil {
|
||||
err = fmt.Errorf("Failed to parse %s: %s", path, err)
|
||||
if r, err = loadOneServerScanResult(path); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
results = append(results, r)
|
||||
}
|
||||
if len(results) == 0 {
|
||||
@@ -170,14 +180,111 @@ func fillCveInfoFromCveDB(r models.ScanResult) (*models.ScanResult, error) {
|
||||
return r.FillCveDetail()
|
||||
}
|
||||
|
||||
func loadPreviousScanHistory(current models.ScanHistory) (previous models.ScanHistory, err error) {
|
||||
var dirs jsonDirs
|
||||
if dirs, err = lsValidJSONDirs(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, result := range current.ScanResults {
|
||||
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.ScanResults = append(previous.ScanResults, r)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return previous, nil
|
||||
}
|
||||
|
||||
func diff(currentHistory, previousHistory models.ScanHistory) (diffHistory models.ScanHistory, err error) {
|
||||
for _, currentResult := range currentHistory.ScanResults {
|
||||
found := false
|
||||
var previousResult models.ScanResult
|
||||
for _, previousResult = range previousHistory.ScanResults {
|
||||
if currentResult.ServerName == previousResult.ServerName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
currentResult.ScannedCves = getNewCves(previousResult, currentResult)
|
||||
|
||||
currentResult.KnownCves = []models.CveInfo{}
|
||||
currentResult.UnknownCves = []models.CveInfo{}
|
||||
|
||||
currentResult.Packages = models.PackageInfoList{}
|
||||
for _, s := range currentResult.ScannedCves {
|
||||
currentResult.Packages = append(currentResult.Packages, s.Packages...)
|
||||
}
|
||||
currentResult.Packages = currentResult.Packages.UniqByName()
|
||||
}
|
||||
|
||||
diffHistory.ScanResults = append(diffHistory.ScanResults, currentResult)
|
||||
}
|
||||
return diffHistory, err
|
||||
}
|
||||
|
||||
func getNewCves(previousResult, currentResult models.ScanResult) (newVulninfos []models.VulnInfo) {
|
||||
previousCveIDsSet := map[string]bool{}
|
||||
for _, previousVulnInfo := range previousResult.ScannedCves {
|
||||
previousCveIDsSet[previousVulnInfo.CveID] = true
|
||||
}
|
||||
|
||||
for _, v := range currentResult.ScannedCves {
|
||||
if previousCveIDsSet[v.CveID] {
|
||||
if isCveInfoUpdated(currentResult, previousResult, v.CveID) {
|
||||
newVulninfos = append(newVulninfos, v)
|
||||
}
|
||||
} else {
|
||||
newVulninfos = append(newVulninfos, v)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func isCveInfoUpdated(currentResult, previousResult models.ScanResult, CveID string) bool {
|
||||
type lastModified struct {
|
||||
Nvd time.Time
|
||||
Jvn time.Time
|
||||
}
|
||||
|
||||
previousModifies := lastModified{}
|
||||
for _, c := range previousResult.KnownCves {
|
||||
if CveID == c.CveID {
|
||||
previousModifies.Nvd = c.CveDetail.Nvd.LastModifiedDate
|
||||
previousModifies.Jvn = c.CveDetail.Jvn.LastModifiedDate
|
||||
}
|
||||
}
|
||||
|
||||
currentModifies := lastModified{}
|
||||
for _, c := range currentResult.KnownCves {
|
||||
if CveID == c.CveDetail.CveID {
|
||||
currentModifies.Nvd = c.CveDetail.Nvd.LastModifiedDate
|
||||
currentModifies.Jvn = c.CveDetail.Jvn.LastModifiedDate
|
||||
}
|
||||
}
|
||||
return !currentModifies.Nvd.Equal(previousModifies.Nvd) ||
|
||||
!currentModifies.Jvn.Equal(previousModifies.Jvn)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
294
commands/util_test.go
Normal file
294
commands/util_test.go
Normal file
@@ -0,0 +1,294 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reflect"
|
||||
|
||||
"github.com/future-architect/vuls/models"
|
||||
"github.com/k0kubun/pp"
|
||||
cve "github.com/kotakanbe/go-cve-dictionary/models"
|
||||
)
|
||||
|
||||
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
|
||||
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: "",
|
||||
},
|
||||
},
|
||||
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{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
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: "",
|
||||
},
|
||||
},
|
||||
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{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
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{}{},
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
UnknownCves: []models.CveInfo{},
|
||||
IgnoredCves: []models.CveInfo{},
|
||||
},
|
||||
},
|
||||
},
|
||||
models.ScanResult{
|
||||
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{},
|
||||
UnknownCves: []models.CveInfo{},
|
||||
IgnoredCves: []models.CveInfo{},
|
||||
Packages: models.PackageInfoList{
|
||||
models.PackageInfo{
|
||||
Name: "mysql-libs",
|
||||
Version: "5.1.73",
|
||||
Release: "7.el6",
|
||||
NewVersion: "5.1.73",
|
||||
NewRelease: "8.el6_8",
|
||||
Repository: "",
|
||||
Changelog: models.Changelog{
|
||||
Contents: "",
|
||||
Method: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var s models.ScanHistory
|
||||
for _, tt := range tests {
|
||||
s, _ = diff(tt.inCurrent, tt.inPrevious)
|
||||
for _, actual := range s.ScanResults {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user