Refactoring

This commit is contained in:
Kota Kanbe
2017-05-21 11:45:21 +09:00
committed by kota kanbe
parent 9128e2748b
commit d9bc4499a4
23 changed files with 2086 additions and 1885 deletions

View File

@@ -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)' \

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -18,307 +18,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
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)

View File

@@ -16,329 +16,3 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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)
}
}
}
}
}

View File

@@ -61,6 +61,8 @@ type Config struct {
OvalDBType string
OvalDBPath string
RefreshCve bool
FormatXML bool
FormatJSON bool
FormatOneEMail bool

527
models/cvecontents.go Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package models

File diff suppressed because it is too large Load Diff

View File

@@ -16,45 +16,3 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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)
}
}

127
models/packages.go Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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
}

59
models/packages_test.go Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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)
}
}

297
models/scanresults.go Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package models

150
models/vulninfos.go Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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}
)

17
models/vulninfos_test.go Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package models

View File

@@ -15,7 +15,7 @@ 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 cveapi
package report
import (
"encoding/json"

204
report/report.go Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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
}

1
report/report_test.go Normal file
View File

@@ -0,0 +1 @@
package report

View File

@@ -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
}

327
report/util_test.go Normal file
View File

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

View File

@@ -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 ""
// }