Add report subcommand, change scan options. Bump up ver #239
This commit is contained in:
		
							
								
								
									
										249
									
								
								models/models.go
									
									
									
									
									
								
							
							
						
						
									
										249
									
								
								models/models.go
									
									
									
									
									
								
							@@ -23,15 +23,13 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/future-architect/vuls/config"
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
	"github.com/future-architect/vuls/cveapi"
 | 
			
		||||
	cve "github.com/kotakanbe/go-cve-dictionary/models"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ScanHistory is the history of Scanning.
 | 
			
		||||
type ScanHistory struct {
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	ScanResults ScanResults
 | 
			
		||||
	ScannedAt   time.Time
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ScanResults is slice of ScanResult.
 | 
			
		||||
@@ -55,43 +53,111 @@ func (s ScanResults) Less(i, j int) bool {
 | 
			
		||||
	return s[i].ServerName < s[j].ServerName
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FilterByCvssOver is filter function.
 | 
			
		||||
func (s ScanResults) FilterByCvssOver() (filtered ScanResults) {
 | 
			
		||||
	for _, result := range s {
 | 
			
		||||
		cveInfos := []CveInfo{}
 | 
			
		||||
		for _, cveInfo := range result.KnownCves {
 | 
			
		||||
			if config.Conf.CvssScoreOver < cveInfo.CveDetail.CvssScore(config.Conf.Lang) {
 | 
			
		||||
				cveInfos = append(cveInfos, cveInfo)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		result.KnownCves = cveInfos
 | 
			
		||||
		filtered = append(filtered, result)
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ScanResult has the result of scanned CVE information.
 | 
			
		||||
type ScanResult struct {
 | 
			
		||||
	gorm.Model    `json:"-" xml:"-"`
 | 
			
		||||
	ScanHistoryID uint `json:"-" xml:"-"`
 | 
			
		||||
	ScannedAt     time.Time
 | 
			
		||||
	ScannedAt time.Time
 | 
			
		||||
 | 
			
		||||
	Lang       string
 | 
			
		||||
	ServerName string // TOML Section key
 | 
			
		||||
	//  Hostname    string
 | 
			
		||||
	Family  string
 | 
			
		||||
	Release string
 | 
			
		||||
	Family     string
 | 
			
		||||
	Release    string
 | 
			
		||||
	Container  Container
 | 
			
		||||
	Platform   Platform
 | 
			
		||||
 | 
			
		||||
	Container Container
 | 
			
		||||
	// Scanned Vulns via SSH + CPE Vulns
 | 
			
		||||
	ScannedCves []VulnInfo
 | 
			
		||||
 | 
			
		||||
	Platform Platform
 | 
			
		||||
 | 
			
		||||
	//  Fqdn        string
 | 
			
		||||
	//  NWLinks     []NWLink
 | 
			
		||||
	KnownCves   []CveInfo
 | 
			
		||||
	UnknownCves []CveInfo
 | 
			
		||||
	IgnoredCves []CveInfo
 | 
			
		||||
 | 
			
		||||
	Optional [][]interface{} `gorm:"-"`
 | 
			
		||||
	Packages PackageInfoList
 | 
			
		||||
 | 
			
		||||
	Optional [][]interface{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FillCveDetail fetches CVE detailed information from
 | 
			
		||||
// CVE Database, and then set to fields.
 | 
			
		||||
func (r ScanResult) FillCveDetail() (ScanResult, error) {
 | 
			
		||||
	set := map[string]VulnInfo{}
 | 
			
		||||
	var cveIDs []string
 | 
			
		||||
	for _, v := range r.ScannedCves {
 | 
			
		||||
		set[v.CveID] = v
 | 
			
		||||
		cveIDs = append(cveIDs, v.CveID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ds, err := cveapi.CveClient.FetchCveDetails(cveIDs)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return r, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	icves := config.Conf.Servers[r.ServerName].IgnoreCves
 | 
			
		||||
 | 
			
		||||
	var known, unknown, ignored CveInfos
 | 
			
		||||
	for _, d := range ds {
 | 
			
		||||
		cinfo := CveInfo{
 | 
			
		||||
			CveDetail: d,
 | 
			
		||||
			VulnInfo:  set[d.CveID],
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// ignored
 | 
			
		||||
		found := false
 | 
			
		||||
		for _, icve := range icves {
 | 
			
		||||
			if icve == d.CveID {
 | 
			
		||||
				ignored = append(ignored, cinfo)
 | 
			
		||||
				found = true
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if found {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// unknown
 | 
			
		||||
		if d.CvssScore(config.Conf.Lang) <= 0 {
 | 
			
		||||
			unknown = append(unknown, cinfo)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// known
 | 
			
		||||
		known = append(known, cinfo)
 | 
			
		||||
	}
 | 
			
		||||
	sort.Sort(known)
 | 
			
		||||
	sort.Sort(unknown)
 | 
			
		||||
	sort.Sort(ignored)
 | 
			
		||||
	r.KnownCves = known
 | 
			
		||||
	r.UnknownCves = unknown
 | 
			
		||||
	r.IgnoredCves = ignored
 | 
			
		||||
	return r, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FilterByCvssOver is filter function.
 | 
			
		||||
func (r ScanResult) FilterByCvssOver() ScanResult {
 | 
			
		||||
	cveInfos := []CveInfo{}
 | 
			
		||||
	for _, cveInfo := range r.KnownCves {
 | 
			
		||||
		if config.Conf.CvssScoreOver < cveInfo.CveDetail.CvssScore(config.Conf.Lang) {
 | 
			
		||||
			cveInfos = append(cveInfos, cveInfo)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	r.KnownCves = cveInfos
 | 
			
		||||
	return r
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
@@ -141,15 +207,15 @@ func (r ScanResult) ServerInfoTui() string {
 | 
			
		||||
 | 
			
		||||
// CveSummary summarize the number of CVEs group by CVSSv2 Severity
 | 
			
		||||
func (r ScanResult) CveSummary() string {
 | 
			
		||||
	var high, middle, low, unknown int
 | 
			
		||||
	var high, medium, low, unknown int
 | 
			
		||||
	cves := append(r.KnownCves, r.UnknownCves...)
 | 
			
		||||
	for _, cveInfo := range cves {
 | 
			
		||||
		score := cveInfo.CveDetail.CvssScore(config.Conf.Lang)
 | 
			
		||||
		switch {
 | 
			
		||||
		case 7.0 < score:
 | 
			
		||||
		case 7.0 <= score:
 | 
			
		||||
			high++
 | 
			
		||||
		case 4.0 < score:
 | 
			
		||||
			middle++
 | 
			
		||||
		case 4.0 <= score:
 | 
			
		||||
			medium++
 | 
			
		||||
		case 0 < score:
 | 
			
		||||
			low++
 | 
			
		||||
		default:
 | 
			
		||||
@@ -158,11 +224,11 @@ func (r ScanResult) CveSummary() string {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if config.Conf.IgnoreUnscoredCves {
 | 
			
		||||
		return fmt.Sprintf("Total: %d (High:%d Middle:%d Low:%d)",
 | 
			
		||||
			high+middle+low, high, middle, low)
 | 
			
		||||
		return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d)",
 | 
			
		||||
			high+medium+low, high, medium, low)
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("Total: %d (High:%d Middle:%d Low:%d ?:%d)",
 | 
			
		||||
		high+middle+low+unknown, high, middle, low, unknown)
 | 
			
		||||
	return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d ?:%d)",
 | 
			
		||||
		high+medium+low+unknown, high, medium, low, unknown)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AllCves returns Known and Unknown CVEs
 | 
			
		||||
@@ -172,15 +238,59 @@ func (r ScanResult) AllCves() []CveInfo {
 | 
			
		||||
 | 
			
		||||
// NWLink has network link information.
 | 
			
		||||
type NWLink struct {
 | 
			
		||||
	gorm.Model   `json:"-" xml:"-"`
 | 
			
		||||
	ScanResultID uint `json:"-" xml:"-"`
 | 
			
		||||
 | 
			
		||||
	IPAddress string
 | 
			
		||||
	Netmask   string
 | 
			
		||||
	DevName   string
 | 
			
		||||
	LinkState string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// VulnInfos is VulnInfo list, getter/setter, sortable methods.
 | 
			
		||||
type VulnInfos []VulnInfo
 | 
			
		||||
 | 
			
		||||
// VulnInfo holds a vulnerability information and unsecure packages
 | 
			
		||||
type VulnInfo struct {
 | 
			
		||||
	CveID            string
 | 
			
		||||
	Packages         PackageInfoList
 | 
			
		||||
	DistroAdvisories []DistroAdvisory // for Aamazon, RHEL, FreeBSD
 | 
			
		||||
	CpeNames         []string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FindByCveID find by CVEID
 | 
			
		||||
func (s VulnInfos) FindByCveID(cveID string) (VulnInfo, bool) {
 | 
			
		||||
	for _, p := range s {
 | 
			
		||||
		if cveID == p.CveID {
 | 
			
		||||
			return p, true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return VulnInfo{CveID: cveID}, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// immutable
 | 
			
		||||
func (s VulnInfos) set(cveID string, v VulnInfo) VulnInfos {
 | 
			
		||||
	for i, p := range s {
 | 
			
		||||
		if cveID == p.CveID {
 | 
			
		||||
			s[i] = v
 | 
			
		||||
			return s
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return append(s, v)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Len implement Sort Interface
 | 
			
		||||
func (s VulnInfos) Len() int {
 | 
			
		||||
	return len(s)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Swap implement Sort Interface
 | 
			
		||||
func (s VulnInfos) Swap(i, j int) {
 | 
			
		||||
	s[i], s[j] = s[j], s[i]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Less implement Sort Interface
 | 
			
		||||
func (s VulnInfos) Less(i, j int) bool {
 | 
			
		||||
	return s[i].CveID < s[j].CveID
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CveInfos is for sorting
 | 
			
		||||
type CveInfos []CveInfo
 | 
			
		||||
 | 
			
		||||
@@ -202,21 +312,8 @@ func (c CveInfos) Less(i, j int) bool {
 | 
			
		||||
 | 
			
		||||
// CveInfo has Cve Information.
 | 
			
		||||
type CveInfo struct {
 | 
			
		||||
	gorm.Model   `json:"-" xml:"-"`
 | 
			
		||||
	ScanResultID uint `json:"-" xml:"-"`
 | 
			
		||||
 | 
			
		||||
	CveDetail        cve.CveDetail
 | 
			
		||||
	Packages         []PackageInfo
 | 
			
		||||
	DistroAdvisories []DistroAdvisory
 | 
			
		||||
	CpeNames         []CpeName
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CpeName has CPE name
 | 
			
		||||
type CpeName struct {
 | 
			
		||||
	gorm.Model `json:"-" xml:"-"`
 | 
			
		||||
	CveInfoID  uint `json:"-" xml:"-"`
 | 
			
		||||
 | 
			
		||||
	Name string
 | 
			
		||||
	CveDetail cve.CveDetail
 | 
			
		||||
	VulnInfo
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PackageInfoList is slice of PackageInfo
 | 
			
		||||
@@ -260,6 +357,34 @@ func (ps PackageInfoList) FindByName(name string) (result PackageInfo, found boo
 | 
			
		||||
	return PackageInfo{}, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MergeNewVersion merges candidate version information to the receiver struct
 | 
			
		||||
func (ps PackageInfoList) MergeNewVersion(as PackageInfoList) {
 | 
			
		||||
	for _, a := range as {
 | 
			
		||||
		for i, p := range ps {
 | 
			
		||||
			if p.Name == a.Name {
 | 
			
		||||
				ps[i].NewVersion = a.NewVersion
 | 
			
		||||
				ps[i].NewRelease = a.NewRelease
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ps PackageInfoList) countUpdatablePacks() int {
 | 
			
		||||
	count := 0
 | 
			
		||||
	for _, p := range ps {
 | 
			
		||||
		if len(p.NewVersion) != 0 {
 | 
			
		||||
			count++
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return count
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ToUpdatablePacksSummary returns a summary of updatable packages
 | 
			
		||||
func (ps PackageInfoList) ToUpdatablePacksSummary() string {
 | 
			
		||||
	return fmt.Sprintf("%d updatable packages",
 | 
			
		||||
		ps.countUpdatablePacks())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Find search PackageInfo by name-version-release
 | 
			
		||||
//  func (ps PackageInfoList) find(nameVersionRelease string) (PackageInfo, bool) {
 | 
			
		||||
//      for _, p := range ps {
 | 
			
		||||
@@ -287,9 +412,6 @@ func (a PackageInfosByName) Less(i, j int) bool { return a[i].Name < a[j].Name }
 | 
			
		||||
 | 
			
		||||
// PackageInfo has installed packages.
 | 
			
		||||
type PackageInfo struct {
 | 
			
		||||
	gorm.Model `json:"-" xml:"-"`
 | 
			
		||||
	CveInfoID  uint `json:"-" xml:"-"`
 | 
			
		||||
 | 
			
		||||
	Name       string
 | 
			
		||||
	Version    string
 | 
			
		||||
	Release    string
 | 
			
		||||
@@ -324,9 +446,6 @@ func (p PackageInfo) ToStringNewVersion() string {
 | 
			
		||||
 | 
			
		||||
// DistroAdvisory has Amazon Linux, RHEL, FreeBSD Security Advisory information.
 | 
			
		||||
type DistroAdvisory struct {
 | 
			
		||||
	gorm.Model `json:"-" xml:"-"`
 | 
			
		||||
	CveInfoID  uint `json:"-" xml:"-"`
 | 
			
		||||
 | 
			
		||||
	AdvisoryID string
 | 
			
		||||
	Severity   string
 | 
			
		||||
	Issued     time.Time
 | 
			
		||||
@@ -335,18 +454,12 @@ type DistroAdvisory struct {
 | 
			
		||||
 | 
			
		||||
// Container has Container information
 | 
			
		||||
type Container struct {
 | 
			
		||||
	gorm.Model   `json:"-" xml:"-"`
 | 
			
		||||
	ScanResultID uint `json:"-" xml:"-"`
 | 
			
		||||
 | 
			
		||||
	ContainerID string
 | 
			
		||||
	Name        string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Platform has platform information
 | 
			
		||||
type Platform struct {
 | 
			
		||||
	gorm.Model   `json:"-" xml:"-"`
 | 
			
		||||
	ScanResultID uint `json:"-" xml:"-"`
 | 
			
		||||
 | 
			
		||||
	Name       string // aws or azure or gcp or other...
 | 
			
		||||
	InstanceID string
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,9 +17,14 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package models
 | 
			
		||||
 | 
			
		||||
import "testing"
 | 
			
		||||
import (
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
func TestPackageInfosUniqByName(t *testing.T) {
 | 
			
		||||
	"github.com/k0kubun/pp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestPackageInfoListUniqByName(t *testing.T) {
 | 
			
		||||
	var test = struct {
 | 
			
		||||
		in  PackageInfoList
 | 
			
		||||
		out PackageInfoList
 | 
			
		||||
@@ -52,3 +57,81 @@ func TestPackageInfosUniqByName(t *testing.T) {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMergeNewVersion(t *testing.T) {
 | 
			
		||||
	var test = struct {
 | 
			
		||||
		a        PackageInfoList
 | 
			
		||||
		b        PackageInfoList
 | 
			
		||||
		expected PackageInfoList
 | 
			
		||||
	}{
 | 
			
		||||
		PackageInfoList{
 | 
			
		||||
			{
 | 
			
		||||
				Name: "hoge",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		PackageInfoList{
 | 
			
		||||
			{
 | 
			
		||||
				Name:       "hoge",
 | 
			
		||||
				NewVersion: "1.0.0",
 | 
			
		||||
				NewRelease: "release1",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		PackageInfoList{
 | 
			
		||||
			{
 | 
			
		||||
				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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
func TestVulnInfosSetGet(t *testing.T) {
 | 
			
		||||
	var test = struct {
 | 
			
		||||
		in  []string
 | 
			
		||||
		out []string
 | 
			
		||||
	}{
 | 
			
		||||
		[]string{
 | 
			
		||||
			"CVE1",
 | 
			
		||||
			"CVE2",
 | 
			
		||||
			"CVE3",
 | 
			
		||||
			"CVE1",
 | 
			
		||||
			"CVE1",
 | 
			
		||||
			"CVE2",
 | 
			
		||||
			"CVE3",
 | 
			
		||||
		},
 | 
			
		||||
		[]string{
 | 
			
		||||
			"CVE1",
 | 
			
		||||
			"CVE2",
 | 
			
		||||
			"CVE3",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//  var ps packageCveInfos
 | 
			
		||||
	var ps VulnInfos
 | 
			
		||||
	for _, cid := range test.in {
 | 
			
		||||
		ps = ps.set(cid, VulnInfo{CveID: cid})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(test.out) != len(ps) {
 | 
			
		||||
		t.Errorf("length: expected %d, actual %d", len(test.out), len(ps))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i, expectedCid := range test.out {
 | 
			
		||||
		if expectedCid != ps[i].CveID {
 | 
			
		||||
			t.Errorf("expected %s, actual %s", expectedCid, ps[i].CveID)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	for _, cid := range test.in {
 | 
			
		||||
		p, _ := ps.FindByCveID(cid)
 | 
			
		||||
		if p.CveID != cid {
 | 
			
		||||
			t.Errorf("expected %s, actual %s", cid, p.CveID)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user