refactor(config): localize config used like a global variable (#1179)
* refactor(report): LocalFileWriter * refactor -format-json * refacotr: -format-one-email * refactor: -format-csv * refactor: -gzip * refactor: -format-full-text * refactor: -format-one-line-text * refactor: -format-list * refacotr: remove -to-* from config * refactor: IgnoreGitHubDismissed * refactor: GitHub * refactor: IgnoreUnsocred * refactor: diff * refacotr: lang * refacotr: cacheDBPath * refactor: Remove config references * refactor: ScanResults * refacotr: constant pkg * chore: comment * refactor: scanner * refactor: scanner * refactor: serverapi.go * refactor: serverapi * refactor: change pkg structure * refactor: serverapi.go * chore: remove emtpy file * fix(scan): remove -ssh-native-insecure option * fix(scan): remove the deprecated option `keypassword`
This commit is contained in:
		
							
								
								
									
										189
									
								
								detector/cve_client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								detector/cve_client.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,189 @@
 | 
			
		||||
// +build !scanner
 | 
			
		||||
 | 
			
		||||
package detector
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/cenkalti/backoff"
 | 
			
		||||
	"github.com/parnurzeal/gorequest"
 | 
			
		||||
	"golang.org/x/xerrors"
 | 
			
		||||
 | 
			
		||||
	"github.com/future-architect/vuls/config"
 | 
			
		||||
	"github.com/future-architect/vuls/util"
 | 
			
		||||
	cvedb "github.com/kotakanbe/go-cve-dictionary/db"
 | 
			
		||||
	cvemodels "github.com/kotakanbe/go-cve-dictionary/models"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CveClient is api client of CVE dictionary service.
 | 
			
		||||
var CveClient cvedictClient
 | 
			
		||||
 | 
			
		||||
type cvedictClient struct {
 | 
			
		||||
	//  httpProxy string
 | 
			
		||||
	baseURL string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type response struct {
 | 
			
		||||
	Key       string
 | 
			
		||||
	CveDetail cvemodels.CveDetail
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (api cvedictClient) FetchCveDetails(driver cvedb.DB, cveIDs []string) (cveDetails []cvemodels.CveDetail, err error) {
 | 
			
		||||
	if !config.Conf.CveDict.IsFetchViaHTTP() {
 | 
			
		||||
		if driver == nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		for _, cveID := range cveIDs {
 | 
			
		||||
			cveDetail, err := driver.Get(cveID)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, xerrors.Errorf("Failed to fetch CVE. err: %w", err)
 | 
			
		||||
			}
 | 
			
		||||
			if len(cveDetail.CveID) == 0 {
 | 
			
		||||
				cveDetails = append(cveDetails, cvemodels.CveDetail{
 | 
			
		||||
					CveID: cveID,
 | 
			
		||||
				})
 | 
			
		||||
			} else {
 | 
			
		||||
				cveDetails = append(cveDetails, *cveDetail)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	api.baseURL = config.Conf.CveDict.URL
 | 
			
		||||
	reqChan := make(chan string, len(cveIDs))
 | 
			
		||||
	resChan := make(chan response, len(cveIDs))
 | 
			
		||||
	errChan := make(chan error, len(cveIDs))
 | 
			
		||||
	defer close(reqChan)
 | 
			
		||||
	defer close(resChan)
 | 
			
		||||
	defer close(errChan)
 | 
			
		||||
 | 
			
		||||
	go func() {
 | 
			
		||||
		for _, cveID := range cveIDs {
 | 
			
		||||
			reqChan <- cveID
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	concurrency := 10
 | 
			
		||||
	tasks := util.GenWorkers(concurrency)
 | 
			
		||||
	for range cveIDs {
 | 
			
		||||
		tasks <- func() {
 | 
			
		||||
			select {
 | 
			
		||||
			case cveID := <-reqChan:
 | 
			
		||||
				url, err := util.URLPathJoin(api.baseURL, "cves", cveID)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					errChan <- err
 | 
			
		||||
				} else {
 | 
			
		||||
					util.Log.Debugf("HTTP Request to %s", url)
 | 
			
		||||
					api.httpGet(cveID, url, resChan, errChan)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	timeout := time.After(2 * 60 * time.Second)
 | 
			
		||||
	var errs []error
 | 
			
		||||
	for range cveIDs {
 | 
			
		||||
		select {
 | 
			
		||||
		case res := <-resChan:
 | 
			
		||||
			if len(res.CveDetail.CveID) == 0 {
 | 
			
		||||
				cveDetails = append(cveDetails, cvemodels.CveDetail{
 | 
			
		||||
					CveID: res.Key,
 | 
			
		||||
				})
 | 
			
		||||
			} else {
 | 
			
		||||
				cveDetails = append(cveDetails, res.CveDetail)
 | 
			
		||||
			}
 | 
			
		||||
		case err := <-errChan:
 | 
			
		||||
			errs = append(errs, err)
 | 
			
		||||
		case <-timeout:
 | 
			
		||||
			return nil, xerrors.New("Timeout Fetching CVE")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if len(errs) != 0 {
 | 
			
		||||
		return nil,
 | 
			
		||||
			xerrors.Errorf("Failed to fetch CVE. err: %w", errs)
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (api cvedictClient) httpGet(key, url string, resChan chan<- response, errChan chan<- error) {
 | 
			
		||||
	var body string
 | 
			
		||||
	var errs []error
 | 
			
		||||
	var resp *http.Response
 | 
			
		||||
	f := func() (err error) {
 | 
			
		||||
		//  resp, body, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End()
 | 
			
		||||
		resp, body, errs = gorequest.New().Timeout(10 * time.Second).Get(url).End()
 | 
			
		||||
		if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
 | 
			
		||||
			return xerrors.Errorf("HTTP GET Error, url: %s, resp: %v, err: %s",
 | 
			
		||||
				url, resp, errs)
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	notify := func(err error, t time.Duration) {
 | 
			
		||||
		util.Log.Warnf("Failed to HTTP GET. retrying in %s seconds. err: %s",
 | 
			
		||||
			t, err)
 | 
			
		||||
	}
 | 
			
		||||
	err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		errChan <- xerrors.Errorf("HTTP Error: %w", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	cveDetail := cvemodels.CveDetail{}
 | 
			
		||||
	if err := json.Unmarshal([]byte(body), &cveDetail); err != nil {
 | 
			
		||||
		errChan <- xerrors.Errorf("Failed to Unmarshal. body: %s, err: %w", body, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	resChan <- response{
 | 
			
		||||
		key,
 | 
			
		||||
		cveDetail,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (api cvedictClient) FetchCveDetailsByCpeName(driver cvedb.DB, cpeName string) ([]cvemodels.CveDetail, error) {
 | 
			
		||||
	if config.Conf.CveDict.IsFetchViaHTTP() {
 | 
			
		||||
		api.baseURL = config.Conf.CveDict.URL
 | 
			
		||||
		url, err := util.URLPathJoin(api.baseURL, "cpes")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		query := map[string]string{"name": cpeName}
 | 
			
		||||
		util.Log.Debugf("HTTP Request to %s, query: %#v", url, query)
 | 
			
		||||
		return api.httpPost(cpeName, url, query)
 | 
			
		||||
	}
 | 
			
		||||
	return driver.GetByCpeURI(cpeName)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (api cvedictClient) httpPost(key, url string, query map[string]string) ([]cvemodels.CveDetail, error) {
 | 
			
		||||
	var body string
 | 
			
		||||
	var errs []error
 | 
			
		||||
	var resp *http.Response
 | 
			
		||||
	f := func() (err error) {
 | 
			
		||||
		//  req := gorequest.New().SetDebug(config.Conf.Debug).Post(url)
 | 
			
		||||
		req := gorequest.New().Timeout(10 * time.Second).Post(url)
 | 
			
		||||
		for key := range query {
 | 
			
		||||
			req = req.Send(fmt.Sprintf("%s=%s", key, query[key])).Type("json")
 | 
			
		||||
		}
 | 
			
		||||
		resp, body, errs = req.End()
 | 
			
		||||
		if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
 | 
			
		||||
			return xerrors.Errorf("HTTP POST error. url: %s, resp: %v, err: %s", url, resp, errs)
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	notify := func(err error, t time.Duration) {
 | 
			
		||||
		util.Log.Warnf("Failed to HTTP POST. retrying in %s seconds. err: %s", t, err)
 | 
			
		||||
	}
 | 
			
		||||
	err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, xerrors.Errorf("HTTP Error: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cveDetails := []cvemodels.CveDetail{}
 | 
			
		||||
	if err := json.Unmarshal([]byte(body), &cveDetails); err != nil {
 | 
			
		||||
		return nil,
 | 
			
		||||
			xerrors.Errorf("Failed to Unmarshal. body: %s, err: %w", body, err)
 | 
			
		||||
	}
 | 
			
		||||
	return cveDetails, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										233
									
								
								detector/db_client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								detector/db_client.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,233 @@
 | 
			
		||||
// +build !scanner
 | 
			
		||||
 | 
			
		||||
package detector
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"github.com/future-architect/vuls/config"
 | 
			
		||||
	"github.com/future-architect/vuls/util"
 | 
			
		||||
	gostdb "github.com/knqyf263/gost/db"
 | 
			
		||||
	cvedb "github.com/kotakanbe/go-cve-dictionary/db"
 | 
			
		||||
	ovaldb "github.com/kotakanbe/goval-dictionary/db"
 | 
			
		||||
	metasploitdb "github.com/takuzoo3868/go-msfdb/db"
 | 
			
		||||
	exploitdb "github.com/vulsio/go-exploitdb/db"
 | 
			
		||||
	"golang.org/x/xerrors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// DBClient is DB client for reporting
 | 
			
		||||
type DBClient struct {
 | 
			
		||||
	CveDB        cvedb.DB
 | 
			
		||||
	OvalDB       ovaldb.DB
 | 
			
		||||
	GostDB       gostdb.DB
 | 
			
		||||
	ExploitDB    exploitdb.DB
 | 
			
		||||
	MetasploitDB metasploitdb.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DBClientConf has a configuration of Vulnerability DBs
 | 
			
		||||
type DBClientConf struct {
 | 
			
		||||
	CveDictCnf    config.GoCveDictConf
 | 
			
		||||
	OvalDictCnf   config.GovalDictConf
 | 
			
		||||
	GostCnf       config.GostConf
 | 
			
		||||
	ExploitCnf    config.ExploitConf
 | 
			
		||||
	MetasploitCnf config.MetasploitConf
 | 
			
		||||
	DebugSQL      bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewDBClient returns db clients
 | 
			
		||||
func NewDBClient(cnf DBClientConf) (dbclient *DBClient, locked bool, err error) {
 | 
			
		||||
	cveDriver, locked, err := NewCveDB(cnf)
 | 
			
		||||
	if locked {
 | 
			
		||||
		return nil, true, xerrors.Errorf("CveDB is locked: %s",
 | 
			
		||||
			cnf.OvalDictCnf.SQLite3Path)
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		return nil, locked, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ovaldb, locked, err := NewOvalDB(cnf)
 | 
			
		||||
	if locked {
 | 
			
		||||
		return nil, true, xerrors.Errorf("OvalDB is locked: %s",
 | 
			
		||||
			cnf.OvalDictCnf.SQLite3Path)
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		util.Log.Warnf("Unable to use OvalDB: %s, err: %s",
 | 
			
		||||
			cnf.OvalDictCnf.SQLite3Path, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	gostdb, locked, err := NewGostDB(cnf)
 | 
			
		||||
	if locked {
 | 
			
		||||
		return nil, true, xerrors.Errorf("gostDB is locked: %s",
 | 
			
		||||
			cnf.GostCnf.SQLite3Path)
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		util.Log.Warnf("Unable to use gostDB: %s, err: %s",
 | 
			
		||||
			cnf.GostCnf.SQLite3Path, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	exploitdb, locked, err := NewExploitDB(cnf)
 | 
			
		||||
	if locked {
 | 
			
		||||
		return nil, true, xerrors.Errorf("exploitDB is locked: %s",
 | 
			
		||||
			cnf.ExploitCnf.SQLite3Path)
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		util.Log.Warnf("Unable to use exploitDB: %s, err: %s",
 | 
			
		||||
			cnf.ExploitCnf.SQLite3Path, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	metasploitdb, locked, err := NewMetasploitDB(cnf)
 | 
			
		||||
	if locked {
 | 
			
		||||
		return nil, true, xerrors.Errorf("metasploitDB is locked: %s",
 | 
			
		||||
			cnf.MetasploitCnf.SQLite3Path)
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		util.Log.Warnf("Unable to use metasploitDB: %s, err: %s",
 | 
			
		||||
			cnf.MetasploitCnf.SQLite3Path, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &DBClient{
 | 
			
		||||
		CveDB:        cveDriver,
 | 
			
		||||
		OvalDB:       ovaldb,
 | 
			
		||||
		GostDB:       gostdb,
 | 
			
		||||
		ExploitDB:    exploitdb,
 | 
			
		||||
		MetasploitDB: metasploitdb,
 | 
			
		||||
	}, false, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewCveDB returns cve db client
 | 
			
		||||
func NewCveDB(cnf DBClientConf) (driver cvedb.DB, locked bool, err error) {
 | 
			
		||||
	if cnf.CveDictCnf.IsFetchViaHTTP() {
 | 
			
		||||
		return nil, false, nil
 | 
			
		||||
	}
 | 
			
		||||
	util.Log.Debugf("open cve-dictionary db (%s)", cnf.CveDictCnf.Type)
 | 
			
		||||
	path := cnf.CveDictCnf.URL
 | 
			
		||||
	if cnf.CveDictCnf.Type == "sqlite3" {
 | 
			
		||||
		path = cnf.CveDictCnf.SQLite3Path
 | 
			
		||||
		if _, err := os.Stat(path); os.IsNotExist(err) {
 | 
			
		||||
			util.Log.Warnf("--cvedb-path=%s file not found. [CPE-scan](https://vuls.io/docs/en/usage-scan-non-os-packages.html#cpe-scan) needs cve-dictionary. if you specify cpe in config.toml, fetch cve-dictionary before reporting. For details, see `https://github.com/kotakanbe/go-cve-dictionary#deploy-go-cve-dictionary`", path)
 | 
			
		||||
			return nil, false, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	util.Log.Debugf("Open cve-dictionary db (%s): %s", cnf.CveDictCnf.Type, path)
 | 
			
		||||
	driver, locked, err = cvedb.NewDB(cnf.CveDictCnf.Type, path, cnf.DebugSQL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		err = xerrors.Errorf("Failed to init CVE DB. err: %w, path: %s", err, path)
 | 
			
		||||
		return nil, locked, err
 | 
			
		||||
	}
 | 
			
		||||
	return driver, false, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewOvalDB returns oval db client
 | 
			
		||||
func NewOvalDB(cnf DBClientConf) (driver ovaldb.DB, locked bool, err error) {
 | 
			
		||||
	if cnf.OvalDictCnf.IsFetchViaHTTP() {
 | 
			
		||||
		return nil, false, nil
 | 
			
		||||
	}
 | 
			
		||||
	path := cnf.OvalDictCnf.URL
 | 
			
		||||
	if cnf.OvalDictCnf.Type == "sqlite3" {
 | 
			
		||||
		path = cnf.OvalDictCnf.SQLite3Path
 | 
			
		||||
 | 
			
		||||
		if _, err := os.Stat(path); os.IsNotExist(err) {
 | 
			
		||||
			util.Log.Warnf("--ovaldb-path=%s file not found", path)
 | 
			
		||||
			return nil, false, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	util.Log.Debugf("Open oval-dictionary db (%s): %s", cnf.OvalDictCnf.Type, path)
 | 
			
		||||
	driver, locked, err = ovaldb.NewDB("", cnf.OvalDictCnf.Type, path, cnf.DebugSQL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		err = xerrors.Errorf("Failed to new OVAL DB. err: %w", err)
 | 
			
		||||
		if locked {
 | 
			
		||||
			return nil, true, err
 | 
			
		||||
		}
 | 
			
		||||
		return nil, false, err
 | 
			
		||||
	}
 | 
			
		||||
	return driver, false, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewGostDB returns db client for Gost
 | 
			
		||||
func NewGostDB(cnf DBClientConf) (driver gostdb.DB, locked bool, err error) {
 | 
			
		||||
	if cnf.GostCnf.IsFetchViaHTTP() {
 | 
			
		||||
		return nil, false, nil
 | 
			
		||||
	}
 | 
			
		||||
	path := cnf.GostCnf.URL
 | 
			
		||||
	if cnf.GostCnf.Type == "sqlite3" {
 | 
			
		||||
		path = cnf.GostCnf.SQLite3Path
 | 
			
		||||
 | 
			
		||||
		if _, err := os.Stat(path); os.IsNotExist(err) {
 | 
			
		||||
			util.Log.Warnf("--gostdb-path=%s file not found. Vuls can detect `patch-not-released-CVE-ID` using gost if the scan target server is Debian, RHEL or CentOS, For details, see `https://github.com/knqyf263/gost#fetch-redhat`", path)
 | 
			
		||||
			return nil, false, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	util.Log.Debugf("Open gost db (%s): %s", cnf.GostCnf.Type, path)
 | 
			
		||||
	if driver, locked, err = gostdb.NewDB(cnf.GostCnf.Type, path, cnf.DebugSQL); err != nil {
 | 
			
		||||
		if locked {
 | 
			
		||||
			util.Log.Errorf("gostDB is locked. err: %+v", err)
 | 
			
		||||
			return nil, true, err
 | 
			
		||||
		}
 | 
			
		||||
		return nil, false, err
 | 
			
		||||
	}
 | 
			
		||||
	return driver, false, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewExploitDB returns db client for Exploit
 | 
			
		||||
func NewExploitDB(cnf DBClientConf) (driver exploitdb.DB, locked bool, err error) {
 | 
			
		||||
	if cnf.ExploitCnf.IsFetchViaHTTP() {
 | 
			
		||||
		return nil, false, nil
 | 
			
		||||
	}
 | 
			
		||||
	path := cnf.ExploitCnf.URL
 | 
			
		||||
	if cnf.ExploitCnf.Type == "sqlite3" {
 | 
			
		||||
		path = cnf.ExploitCnf.SQLite3Path
 | 
			
		||||
 | 
			
		||||
		if _, err := os.Stat(path); os.IsNotExist(err) {
 | 
			
		||||
			util.Log.Warnf("--exploitdb-path=%s file not found. Fetch go-exploit-db before reporting if you want to display exploit codes of detected CVE-IDs. For details, see `https://github.com/vulsio/go-exploitdb`", path)
 | 
			
		||||
			return nil, false, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	util.Log.Debugf("Open exploit db (%s): %s", cnf.ExploitCnf.Type, path)
 | 
			
		||||
	if driver, locked, err = exploitdb.NewDB(cnf.ExploitCnf.Type, path, cnf.DebugSQL); err != nil {
 | 
			
		||||
		if locked {
 | 
			
		||||
			util.Log.Errorf("exploitDB is locked. err: %+v", err)
 | 
			
		||||
			return nil, true, err
 | 
			
		||||
		}
 | 
			
		||||
		return nil, false, err
 | 
			
		||||
	}
 | 
			
		||||
	return driver, false, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewMetasploitDB returns db client for Metasploit
 | 
			
		||||
func NewMetasploitDB(cnf DBClientConf) (driver metasploitdb.DB, locked bool, err error) {
 | 
			
		||||
	if cnf.MetasploitCnf.IsFetchViaHTTP() {
 | 
			
		||||
		return nil, false, nil
 | 
			
		||||
	}
 | 
			
		||||
	path := cnf.MetasploitCnf.URL
 | 
			
		||||
	if cnf.MetasploitCnf.Type == "sqlite3" {
 | 
			
		||||
		path = cnf.MetasploitCnf.SQLite3Path
 | 
			
		||||
 | 
			
		||||
		if _, err := os.Stat(path); os.IsNotExist(err) {
 | 
			
		||||
			util.Log.Warnf("--msfdb-path=%s file not found. Fetch go-msfdb before reporting if you want to display metasploit modules of detected CVE-IDs. For details, see `https://github.com/takuzoo3868/go-msfdb`", path)
 | 
			
		||||
			return nil, false, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	util.Log.Debugf("Open metasploit db (%s): %s", cnf.MetasploitCnf.Type, path)
 | 
			
		||||
	if driver, locked, err = metasploitdb.NewDB(cnf.MetasploitCnf.Type, path, cnf.DebugSQL, false); err != nil {
 | 
			
		||||
		if locked {
 | 
			
		||||
			util.Log.Errorf("metasploitDB is locked. err: %+v", err)
 | 
			
		||||
			return nil, true, err
 | 
			
		||||
		}
 | 
			
		||||
		return nil, false, err
 | 
			
		||||
	}
 | 
			
		||||
	return driver, false, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CloseDB close dbs
 | 
			
		||||
func (d DBClient) CloseDB() {
 | 
			
		||||
	if d.CveDB != nil {
 | 
			
		||||
		if err := d.CveDB.CloseDB(); err != nil {
 | 
			
		||||
			util.Log.Errorf("Failed to close DB. err: %+v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if d.OvalDB != nil {
 | 
			
		||||
		if err := d.OvalDB.CloseDB(); err != nil {
 | 
			
		||||
			util.Log.Errorf("Failed to close DB. err: %+v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										531
									
								
								detector/detector.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										531
									
								
								detector/detector.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,531 @@
 | 
			
		||||
// +build !scanner
 | 
			
		||||
 | 
			
		||||
package detector
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/future-architect/vuls/config"
 | 
			
		||||
	c "github.com/future-architect/vuls/config"
 | 
			
		||||
	"github.com/future-architect/vuls/constant"
 | 
			
		||||
	"github.com/future-architect/vuls/contrib/owasp-dependency-check/parser"
 | 
			
		||||
	"github.com/future-architect/vuls/cwe"
 | 
			
		||||
	"github.com/future-architect/vuls/exploit"
 | 
			
		||||
	"github.com/future-architect/vuls/gost"
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
	"github.com/future-architect/vuls/msf"
 | 
			
		||||
	"github.com/future-architect/vuls/oval"
 | 
			
		||||
	"github.com/future-architect/vuls/reporter"
 | 
			
		||||
	"github.com/future-architect/vuls/util"
 | 
			
		||||
	gostdb "github.com/knqyf263/gost/db"
 | 
			
		||||
	cvedb "github.com/kotakanbe/go-cve-dictionary/db"
 | 
			
		||||
	cvemodels "github.com/kotakanbe/go-cve-dictionary/models"
 | 
			
		||||
	ovaldb "github.com/kotakanbe/goval-dictionary/db"
 | 
			
		||||
	metasploitdb "github.com/takuzoo3868/go-msfdb/db"
 | 
			
		||||
	exploitdb "github.com/vulsio/go-exploitdb/db"
 | 
			
		||||
	"golang.org/x/xerrors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// type Detector struct {
 | 
			
		||||
// 	Targets map[string]config.ServerInfo
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
// Detect vulns and fill CVE detailed information
 | 
			
		||||
func Detect(dbclient DBClient, rs []models.ScanResult, dir string) ([]models.ScanResult, error) {
 | 
			
		||||
 | 
			
		||||
	// Use the same reportedAt for all rs
 | 
			
		||||
	reportedAt := time.Now()
 | 
			
		||||
	for i, r := range rs {
 | 
			
		||||
		if !c.Conf.RefreshCve && !needToRefreshCve(r) {
 | 
			
		||||
			util.Log.Info("No need to refresh")
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !reuseScannedCves(&r) {
 | 
			
		||||
			r.ScannedCves = models.VulnInfos{}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cpeURIs := []string{}
 | 
			
		||||
		if len(r.Container.ContainerID) == 0 {
 | 
			
		||||
			cpeURIs = c.Conf.Servers[r.ServerName].CpeNames
 | 
			
		||||
			owaspDCXMLPath := c.Conf.Servers[r.ServerName].OwaspDCXMLPath
 | 
			
		||||
			if owaspDCXMLPath != "" {
 | 
			
		||||
				cpes, err := parser.Parse(owaspDCXMLPath)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return nil, xerrors.Errorf("Failed to read OWASP Dependency Check XML on %s, `%s`, err: %w",
 | 
			
		||||
						r.ServerName, owaspDCXMLPath, err)
 | 
			
		||||
				}
 | 
			
		||||
				cpeURIs = append(cpeURIs, cpes...)
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			// runningContainer
 | 
			
		||||
			if s, ok := c.Conf.Servers[r.ServerName]; ok {
 | 
			
		||||
				if con, ok := s.Containers[r.Container.Name]; ok {
 | 
			
		||||
					cpeURIs = con.Cpes
 | 
			
		||||
					owaspDCXMLPath := con.OwaspDCXMLPath
 | 
			
		||||
					if owaspDCXMLPath != "" {
 | 
			
		||||
						cpes, err := parser.Parse(owaspDCXMLPath)
 | 
			
		||||
						if err != nil {
 | 
			
		||||
							return nil, xerrors.Errorf("Failed to read OWASP Dependency Check XML on %s, `%s`, err: %w",
 | 
			
		||||
								r.ServerInfo(), owaspDCXMLPath, err)
 | 
			
		||||
						}
 | 
			
		||||
						cpeURIs = append(cpeURIs, cpes...)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := DetectLibsCves(&r, c.Conf.TrivyCacheDBDir, c.Conf.NoProgress); err != nil {
 | 
			
		||||
			return nil, xerrors.Errorf("Failed to fill with Library dependency: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := DetectPkgCves(dbclient, &r); err != nil {
 | 
			
		||||
			return nil, xerrors.Errorf("Failed to detect Pkg CVE: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := DetectCpeURIsCves(dbclient.CveDB, &r, cpeURIs); err != nil {
 | 
			
		||||
			return nil, xerrors.Errorf("Failed to detect CVE of `%s`: %w", cpeURIs, err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		repos := c.Conf.Servers[r.ServerName].GitHubRepos
 | 
			
		||||
		if err := DetectGitHubCves(&r, repos, c.Conf.IgnoreGitHubDismissed); err != nil {
 | 
			
		||||
			return nil, xerrors.Errorf("Failed to detect GitHub Cves: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := DetectWordPressCves(&r, &config.Conf.WpScan); err != nil {
 | 
			
		||||
			return nil, xerrors.Errorf("Failed to detect WordPress Cves: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := FillCveInfo(dbclient, &r); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		r.ReportedBy, _ = os.Hostname()
 | 
			
		||||
		r.Lang = c.Conf.Lang
 | 
			
		||||
		r.ReportedAt = reportedAt
 | 
			
		||||
		r.ReportedVersion = c.Version
 | 
			
		||||
		r.ReportedRevision = c.Revision
 | 
			
		||||
		r.Config.Report = c.Conf
 | 
			
		||||
		r.Config.Report.Servers = map[string]c.ServerInfo{
 | 
			
		||||
			r.ServerName: c.Conf.Servers[r.ServerName],
 | 
			
		||||
		}
 | 
			
		||||
		rs[i] = r
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Overwrite the json file every time to clear the fields specified in config.IgnoredJSONKeys
 | 
			
		||||
	for _, r := range rs {
 | 
			
		||||
		if s, ok := c.Conf.Servers[r.ServerName]; ok {
 | 
			
		||||
			r = r.ClearFields(s.IgnoredJSONKeys)
 | 
			
		||||
		}
 | 
			
		||||
		//TODO don't call here
 | 
			
		||||
		if err := reporter.OverwriteJSONFile(dir, r); err != nil {
 | 
			
		||||
			return nil, xerrors.Errorf("Failed to write JSON: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if c.Conf.DiffPlus || c.Conf.DiffMinus {
 | 
			
		||||
		prevs, err := loadPrevious(rs)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		rs = diff(rs, prevs, c.Conf.DiffPlus, c.Conf.DiffMinus)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i, r := range rs {
 | 
			
		||||
		r = r.FilterByCvssOver(c.Conf.CvssScoreOver)
 | 
			
		||||
		r = r.FilterUnfixed(c.Conf.IgnoreUnfixed)
 | 
			
		||||
		r = r.FilterInactiveWordPressLibs(c.Conf.WpScan.DetectInactive)
 | 
			
		||||
 | 
			
		||||
		// IgnoreCves
 | 
			
		||||
		ignoreCves := []string{}
 | 
			
		||||
		if r.Container.Name == "" {
 | 
			
		||||
			ignoreCves = c.Conf.Servers[r.ServerName].IgnoreCves
 | 
			
		||||
		} else if con, ok := c.Conf.Servers[r.ServerName].Containers[r.Container.Name]; ok {
 | 
			
		||||
			ignoreCves = con.IgnoreCves
 | 
			
		||||
		}
 | 
			
		||||
		r = r.FilterIgnoreCves(ignoreCves)
 | 
			
		||||
 | 
			
		||||
		// ignorePkgs
 | 
			
		||||
		ignorePkgsRegexps := []string{}
 | 
			
		||||
		if r.Container.Name == "" {
 | 
			
		||||
			ignorePkgsRegexps = config.Conf.Servers[r.ServerName].IgnorePkgsRegexp
 | 
			
		||||
		} else if s, ok := config.Conf.Servers[r.ServerName].Containers[r.Container.Name]; ok {
 | 
			
		||||
			ignorePkgsRegexps = s.IgnorePkgsRegexp
 | 
			
		||||
		}
 | 
			
		||||
		r = r.FilterIgnorePkgs(ignorePkgsRegexps)
 | 
			
		||||
 | 
			
		||||
		// IgnoreUnscored
 | 
			
		||||
		if c.Conf.IgnoreUnscoredCves {
 | 
			
		||||
			r.ScannedCves = r.ScannedCves.FindScoredVulns()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		rs[i] = r
 | 
			
		||||
	}
 | 
			
		||||
	return rs, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DetectPkgCves detects OS pkg cves
 | 
			
		||||
func DetectPkgCves(dbclient DBClient, r *models.ScanResult) error {
 | 
			
		||||
	// Pkg Scan
 | 
			
		||||
	if r.Release != "" {
 | 
			
		||||
		// OVAL
 | 
			
		||||
		if err := detectPkgsCvesWithOval(dbclient.OvalDB, r); err != nil {
 | 
			
		||||
			return xerrors.Errorf("Failed to detect CVE with OVAL: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// gost
 | 
			
		||||
		if err := detectPkgsCvesWithGost(dbclient.GostDB, r); err != nil {
 | 
			
		||||
			return xerrors.Errorf("Failed to detect CVE with gost: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	} else if reuseScannedCves(r) {
 | 
			
		||||
		util.Log.Infof("r.Release is empty. Use CVEs as it as.")
 | 
			
		||||
	} else if r.Family == constant.ServerTypePseudo {
 | 
			
		||||
		util.Log.Infof("pseudo type. Skip OVAL and gost detection")
 | 
			
		||||
	} else {
 | 
			
		||||
		return xerrors.Errorf("Failed to fill CVEs. r.Release is empty")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i, v := range r.ScannedCves {
 | 
			
		||||
		for j, p := range v.AffectedPackages {
 | 
			
		||||
			if p.NotFixedYet && p.FixState == "" {
 | 
			
		||||
				p.FixState = "Not fixed yet"
 | 
			
		||||
				r.ScannedCves[i].AffectedPackages[j] = p
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// To keep backward compatibility
 | 
			
		||||
	// Newer versions use ListenPortStats,
 | 
			
		||||
	// but older versions of Vuls are set to ListenPorts.
 | 
			
		||||
	// Set ListenPorts to ListenPortStats to allow newer Vuls to report old results.
 | 
			
		||||
	for i, pkg := range r.Packages {
 | 
			
		||||
		for j, proc := range pkg.AffectedProcs {
 | 
			
		||||
			for _, ipPort := range proc.ListenPorts {
 | 
			
		||||
				ps, err := models.NewPortStat(ipPort)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					util.Log.Warnf("Failed to parse ip:port: %s, err:%+v", ipPort, err)
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				r.Packages[i].AffectedProcs[j].ListenPortStats = append(
 | 
			
		||||
					r.Packages[i].AffectedProcs[j].ListenPortStats, *ps)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DetectGitHubCves fetches CVEs from GitHub Security Alerts
 | 
			
		||||
func DetectGitHubCves(r *models.ScanResult, githubConfs map[string]c.GitHubConf, ignoreDismissed bool) error {
 | 
			
		||||
	if len(githubConfs) == 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	for ownerRepo, setting := range githubConfs {
 | 
			
		||||
		ss := strings.Split(ownerRepo, "/")
 | 
			
		||||
		if len(ss) != 2 {
 | 
			
		||||
			return xerrors.Errorf("Failed to parse GitHub owner/repo: %s", ownerRepo)
 | 
			
		||||
		}
 | 
			
		||||
		owner, repo := ss[0], ss[1]
 | 
			
		||||
		n, err := DetectGitHubSecurityAlerts(r, owner, repo, setting.Token, ignoreDismissed)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return xerrors.Errorf("Failed to access GitHub Security Alerts: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
		util.Log.Infof("%s: %d CVEs detected with GHSA %s/%s",
 | 
			
		||||
			r.FormatServerName(), n, owner, repo)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DetectWordPressCves detects CVEs of WordPress
 | 
			
		||||
func DetectWordPressCves(r *models.ScanResult, wpCnf *c.WpScanConf) error {
 | 
			
		||||
	if len(r.WordPressPackages) == 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	util.Log.Infof("Detect WordPress CVE. pkgs: %d ", len(r.WordPressPackages))
 | 
			
		||||
	n, err := detectWordPressCves(r, wpCnf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return xerrors.Errorf("Failed to detect WordPress CVE: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	util.Log.Infof("%s: found %d WordPress CVEs", r.FormatServerName(), n)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FillCveInfo fill scanResult with cve info.
 | 
			
		||||
func FillCveInfo(dbclient DBClient, r *models.ScanResult) error {
 | 
			
		||||
	util.Log.Infof("Fill CVE detailed with gost")
 | 
			
		||||
	if err := gost.NewClient(r.Family).FillCVEsWithRedHat(dbclient.GostDB, r); err != nil {
 | 
			
		||||
		return xerrors.Errorf("Failed to fill with gost: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	util.Log.Infof("Fill CVE detailed with CVE-DB")
 | 
			
		||||
	if err := fillCvesWithNvdJvn(dbclient.CveDB, r); err != nil {
 | 
			
		||||
		return xerrors.Errorf("Failed to fill with CVE: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	util.Log.Infof("Fill exploit with Exploit-DB")
 | 
			
		||||
	nExploitCve, err := fillWithExploitDB(dbclient.ExploitDB, r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return xerrors.Errorf("Failed to fill with exploit: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	util.Log.Infof("%s: %d exploits are detected",
 | 
			
		||||
		r.FormatServerName(), nExploitCve)
 | 
			
		||||
 | 
			
		||||
	util.Log.Infof("Fill metasploit module with Metasploit-DB")
 | 
			
		||||
	nMetasploitCve, err := fillWithMetasploit(dbclient.MetasploitDB, r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return xerrors.Errorf("Failed to fill with metasploit: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	util.Log.Infof("%s: %d modules are detected",
 | 
			
		||||
		r.FormatServerName(), nMetasploitCve)
 | 
			
		||||
 | 
			
		||||
	util.Log.Infof("Fill CWE with NVD")
 | 
			
		||||
	fillCweDict(r)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// fillCvesWithNvdJvn fills CVE detail with NVD, JVN
 | 
			
		||||
func fillCvesWithNvdJvn(driver cvedb.DB, r *models.ScanResult) error {
 | 
			
		||||
	cveIDs := []string{}
 | 
			
		||||
	for _, v := range r.ScannedCves {
 | 
			
		||||
		cveIDs = append(cveIDs, v.CveID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ds, err := CveClient.FetchCveDetails(driver, cveIDs)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	for _, d := range ds {
 | 
			
		||||
		nvd, exploits, mitigations := models.ConvertNvdJSONToModel(d.CveID, d.NvdJSON)
 | 
			
		||||
		jvn := models.ConvertJvnToModel(d.CveID, d.Jvn)
 | 
			
		||||
 | 
			
		||||
		alerts := fillCertAlerts(&d)
 | 
			
		||||
		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 != nil && !con.Empty() {
 | 
			
		||||
						vinfo.CveContents[con.Type] = *con
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				vinfo.AlertDict = alerts
 | 
			
		||||
				vinfo.Exploits = append(vinfo.Exploits, exploits...)
 | 
			
		||||
				vinfo.Mitigations = append(vinfo.Mitigations, mitigations...)
 | 
			
		||||
				r.ScannedCves[cveID] = vinfo
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func fillCertAlerts(cvedetail *cvemodels.CveDetail) (dict models.AlertDict) {
 | 
			
		||||
	if cvedetail.NvdJSON != nil {
 | 
			
		||||
		for _, cert := range cvedetail.NvdJSON.Certs {
 | 
			
		||||
			dict.En = append(dict.En, models.Alert{
 | 
			
		||||
				URL:   cert.Link,
 | 
			
		||||
				Title: cert.Title,
 | 
			
		||||
				Team:  "us",
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if cvedetail.Jvn != nil {
 | 
			
		||||
		for _, cert := range cvedetail.Jvn.Certs {
 | 
			
		||||
			dict.Ja = append(dict.Ja, models.Alert{
 | 
			
		||||
				URL:   cert.Link,
 | 
			
		||||
				Title: cert.Title,
 | 
			
		||||
				Team:  "jp",
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return dict
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// detectPkgsCvesWithOval fetches OVAL database
 | 
			
		||||
func detectPkgsCvesWithOval(driver ovaldb.DB, r *models.ScanResult) error {
 | 
			
		||||
	var ovalClient oval.Client
 | 
			
		||||
	var ovalFamily string
 | 
			
		||||
 | 
			
		||||
	switch r.Family {
 | 
			
		||||
	case constant.Debian, constant.Raspbian:
 | 
			
		||||
		ovalClient = oval.NewDebian()
 | 
			
		||||
		ovalFamily = constant.Debian
 | 
			
		||||
	case constant.Ubuntu:
 | 
			
		||||
		ovalClient = oval.NewUbuntu()
 | 
			
		||||
		ovalFamily = constant.Ubuntu
 | 
			
		||||
	case constant.RedHat:
 | 
			
		||||
		ovalClient = oval.NewRedhat()
 | 
			
		||||
		ovalFamily = constant.RedHat
 | 
			
		||||
	case constant.CentOS:
 | 
			
		||||
		ovalClient = oval.NewCentOS()
 | 
			
		||||
		//use RedHat's OVAL
 | 
			
		||||
		ovalFamily = constant.RedHat
 | 
			
		||||
	case constant.Oracle:
 | 
			
		||||
		ovalClient = oval.NewOracle()
 | 
			
		||||
		ovalFamily = constant.Oracle
 | 
			
		||||
	case constant.SUSEEnterpriseServer:
 | 
			
		||||
		// TODO other suse family
 | 
			
		||||
		ovalClient = oval.NewSUSE()
 | 
			
		||||
		ovalFamily = constant.SUSEEnterpriseServer
 | 
			
		||||
	case constant.Alpine:
 | 
			
		||||
		ovalClient = oval.NewAlpine()
 | 
			
		||||
		ovalFamily = constant.Alpine
 | 
			
		||||
	case constant.Amazon:
 | 
			
		||||
		ovalClient = oval.NewAmazon()
 | 
			
		||||
		ovalFamily = constant.Amazon
 | 
			
		||||
	case constant.FreeBSD, constant.Windows:
 | 
			
		||||
		return nil
 | 
			
		||||
	case constant.ServerTypePseudo:
 | 
			
		||||
		return nil
 | 
			
		||||
	default:
 | 
			
		||||
		if r.Family == "" {
 | 
			
		||||
			return xerrors.New("Probably an error occurred during scanning. Check the error message")
 | 
			
		||||
		}
 | 
			
		||||
		return xerrors.Errorf("OVAL for %s is not implemented yet", r.Family)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !c.Conf.OvalDict.IsFetchViaHTTP() {
 | 
			
		||||
		if driver == nil {
 | 
			
		||||
			return xerrors.Errorf("You have to fetch OVAL data for %s before reporting. For details, see `https://github.com/kotakanbe/goval-dictionary#usage`", r.Family)
 | 
			
		||||
		}
 | 
			
		||||
		if err := driver.NewOvalDB(ovalFamily); err != nil {
 | 
			
		||||
			return xerrors.Errorf("Failed to New Oval DB. err: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	util.Log.Debugf("Check whether oval fetched: %s %s", ovalFamily, r.Release)
 | 
			
		||||
	ok, err := ovalClient.CheckIfOvalFetched(driver, ovalFamily, r.Release)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return xerrors.Errorf("OVAL entries of %s %s are not found. Fetch OVAL before reporting. For details, see `https://github.com/kotakanbe/goval-dictionary#usage`", ovalFamily, r.Release)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = ovalClient.CheckIfOvalFresh(driver, ovalFamily, r.Release)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	nCVEs, err := ovalClient.FillWithOval(driver, r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	util.Log.Infof("%s: %d CVEs are detected with OVAL", r.FormatServerName(), nCVEs)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func detectPkgsCvesWithGost(driver gostdb.DB, r *models.ScanResult) error {
 | 
			
		||||
	nCVEs, err := gost.NewClient(r.Family).DetectUnfixed(driver, r, true)
 | 
			
		||||
 | 
			
		||||
	util.Log.Infof("%s: %d unfixed CVEs are detected with gost",
 | 
			
		||||
		r.FormatServerName(), nCVEs)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// fillWithExploitDB fills Exploits with exploit dataabase
 | 
			
		||||
// https://github.com/vulsio/go-exploitdb
 | 
			
		||||
func fillWithExploitDB(driver exploitdb.DB, r *models.ScanResult) (nExploitCve int, err error) {
 | 
			
		||||
	return exploit.FillWithExploit(driver, r, &config.Conf.Exploit)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// fillWithMetasploit fills metasploit modules with metasploit database
 | 
			
		||||
// https://github.com/takuzoo3868/go-msfdb
 | 
			
		||||
func fillWithMetasploit(driver metasploitdb.DB, r *models.ScanResult) (nMetasploitCve int, err error) {
 | 
			
		||||
	return msf.FillWithMetasploit(driver, r)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DetectCpeURIsCves detects CVEs of given CPE-URIs
 | 
			
		||||
func DetectCpeURIsCves(driver cvedb.DB, r *models.ScanResult, cpeURIs []string) error {
 | 
			
		||||
	nCVEs := 0
 | 
			
		||||
	if len(cpeURIs) != 0 && driver == nil && !c.Conf.CveDict.IsFetchViaHTTP() {
 | 
			
		||||
		return xerrors.Errorf("cpeURIs %s specified, but cve-dictionary DB not found. Fetch cve-dictionary before reporting. For details, see `https://github.com/kotakanbe/go-cve-dictionary#deploy-go-cve-dictionary`",
 | 
			
		||||
			cpeURIs)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, name := range cpeURIs {
 | 
			
		||||
		details, err := CveClient.FetchCveDetailsByCpeName(driver, name)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		for _, detail := range details {
 | 
			
		||||
			if val, ok := r.ScannedCves[detail.CveID]; ok {
 | 
			
		||||
				names := val.CpeURIs
 | 
			
		||||
				names = util.AppendIfMissing(names, name)
 | 
			
		||||
				val.CpeURIs = names
 | 
			
		||||
				val.Confidences.AppendIfMissing(models.CpeNameMatch)
 | 
			
		||||
				r.ScannedCves[detail.CveID] = val
 | 
			
		||||
			} else {
 | 
			
		||||
				v := models.VulnInfo{
 | 
			
		||||
					CveID:       detail.CveID,
 | 
			
		||||
					CpeURIs:     []string{name},
 | 
			
		||||
					Confidences: models.Confidences{models.CpeNameMatch},
 | 
			
		||||
				}
 | 
			
		||||
				r.ScannedCves[detail.CveID] = v
 | 
			
		||||
				nCVEs++
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	util.Log.Infof("%s: %d CVEs are detected with CPE", r.FormatServerName(), nCVEs)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func fillCweDict(r *models.ScanResult) {
 | 
			
		||||
	uniqCweIDMap := map[string]bool{}
 | 
			
		||||
	for _, vinfo := range r.ScannedCves {
 | 
			
		||||
		for _, cont := range vinfo.CveContents {
 | 
			
		||||
			for _, id := range cont.CweIDs {
 | 
			
		||||
				if strings.HasPrefix(id, "CWE-") {
 | 
			
		||||
					id = strings.TrimPrefix(id, "CWE-")
 | 
			
		||||
					uniqCweIDMap[id] = true
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dict := map[string]models.CweDictEntry{}
 | 
			
		||||
	for id := range uniqCweIDMap {
 | 
			
		||||
		entry := models.CweDictEntry{}
 | 
			
		||||
		if e, ok := cwe.CweDictEn[id]; ok {
 | 
			
		||||
			if rank, ok := cwe.OwaspTopTen2017[id]; ok {
 | 
			
		||||
				entry.OwaspTopTen2017 = rank
 | 
			
		||||
			}
 | 
			
		||||
			if rank, ok := cwe.CweTopTwentyfive2019[id]; ok {
 | 
			
		||||
				entry.CweTopTwentyfive2019 = rank
 | 
			
		||||
			}
 | 
			
		||||
			if rank, ok := cwe.SansTopTwentyfive[id]; ok {
 | 
			
		||||
				entry.SansTopTwentyfive = rank
 | 
			
		||||
			}
 | 
			
		||||
			entry.En = &e
 | 
			
		||||
		} else {
 | 
			
		||||
			util.Log.Debugf("CWE-ID %s is not found in English CWE Dict", id)
 | 
			
		||||
			entry.En = &cwe.Cwe{CweID: id}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if r.Lang == "ja" {
 | 
			
		||||
			if e, ok := cwe.CweDictJa[id]; ok {
 | 
			
		||||
				if rank, ok := cwe.OwaspTopTen2017[id]; ok {
 | 
			
		||||
					entry.OwaspTopTen2017 = rank
 | 
			
		||||
				}
 | 
			
		||||
				if rank, ok := cwe.CweTopTwentyfive2019[id]; ok {
 | 
			
		||||
					entry.CweTopTwentyfive2019 = rank
 | 
			
		||||
				}
 | 
			
		||||
				if rank, ok := cwe.SansTopTwentyfive[id]; ok {
 | 
			
		||||
					entry.SansTopTwentyfive = rank
 | 
			
		||||
				}
 | 
			
		||||
				entry.Ja = &e
 | 
			
		||||
			} else {
 | 
			
		||||
				util.Log.Debugf("CWE-ID %s is not found in Japanese CWE Dict", id)
 | 
			
		||||
				entry.Ja = &cwe.Cwe{CweID: id}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		dict[id] = entry
 | 
			
		||||
	}
 | 
			
		||||
	r.CweDict = dict
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										199
									
								
								detector/github.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								detector/github.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,199 @@
 | 
			
		||||
package detector
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/future-architect/vuls/errof"
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
	"golang.org/x/oauth2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// DetectGitHubSecurityAlerts access to owner/repo on GitHub and fetch security alerts of the repository via GitHub API v4 GraphQL and then set to the given ScanResult.
 | 
			
		||||
// https://help.github.com/articles/about-security-alerts-for-vulnerable-dependencies/
 | 
			
		||||
//TODO move to report
 | 
			
		||||
func DetectGitHubSecurityAlerts(r *models.ScanResult, owner, repo, token string, ignoreDismissed bool) (nCVEs int, err error) {
 | 
			
		||||
	src := oauth2.StaticTokenSource(
 | 
			
		||||
		&oauth2.Token{AccessToken: token},
 | 
			
		||||
	)
 | 
			
		||||
	//TODO Proxy
 | 
			
		||||
	httpClient := oauth2.NewClient(context.Background(), src)
 | 
			
		||||
 | 
			
		||||
	// TODO Use `https://github.com/shurcooL/githubv4` if the tool supports vulnerabilityAlerts Endpoint
 | 
			
		||||
	// Memo : https://developer.github.com/v4/explorer/
 | 
			
		||||
	const jsonfmt = `{"query":
 | 
			
		||||
	"query { repository(owner:\"%s\", name:\"%s\") { url vulnerabilityAlerts(first: %d, %s) { pageInfo { endCursor hasNextPage startCursor } edges { node { id dismissReason dismissedAt securityVulnerability{ package { name ecosystem } severity vulnerableVersionRange firstPatchedVersion { identifier } } securityAdvisory { description ghsaId permalink publishedAt summary updatedAt withdrawnAt origin severity references { url } identifiers { type value } } } } } } } "}`
 | 
			
		||||
	after := ""
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		jsonStr := fmt.Sprintf(jsonfmt, owner, repo, 100, after)
 | 
			
		||||
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 | 
			
		||||
		req, err := http.NewRequestWithContext(ctx, http.MethodPost,
 | 
			
		||||
			"https://api.github.com/graphql",
 | 
			
		||||
			bytes.NewBuffer([]byte(jsonStr)),
 | 
			
		||||
		)
 | 
			
		||||
		defer cancel()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// https://developer.github.com/v4/previews/#repository-vulnerability-alerts
 | 
			
		||||
		// To toggle this preview and access data, need to provide a custom media type in the Accept header:
 | 
			
		||||
		// MEMO: I tried to get the affected version via GitHub API. Bit it seems difficult to determin the affected version if there are multiple dependency files such as package.json.
 | 
			
		||||
		// TODO remove this header if it is no longer preview status in the future.
 | 
			
		||||
		req.Header.Set("Accept", "application/vnd.github.package-deletes-preview+json")
 | 
			
		||||
		req.Header.Set("Content-Type", "application/json")
 | 
			
		||||
 | 
			
		||||
		resp, err := httpClient.Do(req)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
		defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
		body, err := ioutil.ReadAll(resp.Body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		alerts := SecurityAlerts{}
 | 
			
		||||
		if err := json.Unmarshal(body, &alerts); err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// util.Log.Debugf("%s", pp.Sprint(alerts))
 | 
			
		||||
		// util.Log.Debugf("%s", string(body))
 | 
			
		||||
		if alerts.Data.Repository.URL == "" {
 | 
			
		||||
			return 0, errof.New(errof.ErrFailedToAccessGithubAPI,
 | 
			
		||||
				fmt.Sprintf("Failed to access to GitHub API. Response: %s", string(body)))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, v := range alerts.Data.Repository.VulnerabilityAlerts.Edges {
 | 
			
		||||
			if ignoreDismissed && v.Node.DismissReason != "" {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			pkgName := fmt.Sprintf("%s %s",
 | 
			
		||||
				alerts.Data.Repository.URL, v.Node.SecurityVulnerability.Package.Name)
 | 
			
		||||
 | 
			
		||||
			m := models.GitHubSecurityAlert{
 | 
			
		||||
				PackageName:   pkgName,
 | 
			
		||||
				FixedIn:       v.Node.SecurityVulnerability.FirstPatchedVersion.Identifier,
 | 
			
		||||
				AffectedRange: v.Node.SecurityVulnerability.VulnerableVersionRange,
 | 
			
		||||
				Dismissed:     len(v.Node.DismissReason) != 0,
 | 
			
		||||
				DismissedAt:   v.Node.DismissedAt,
 | 
			
		||||
				DismissReason: v.Node.DismissReason,
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			cveIDs, other := []string{}, []string{}
 | 
			
		||||
			for _, identifier := range v.Node.SecurityAdvisory.Identifiers {
 | 
			
		||||
				if identifier.Type == "CVE" {
 | 
			
		||||
					cveIDs = append(cveIDs, identifier.Value)
 | 
			
		||||
				} else {
 | 
			
		||||
					other = append(other, identifier.Value)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// If CVE-ID has not been assigned, use the GHSA ID etc as a ID.
 | 
			
		||||
			if len(cveIDs) == 0 {
 | 
			
		||||
				cveIDs = other
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			refs := []models.Reference{}
 | 
			
		||||
			for _, r := range v.Node.SecurityAdvisory.References {
 | 
			
		||||
				refs = append(refs, models.Reference{Link: r.URL})
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for _, cveID := range cveIDs {
 | 
			
		||||
				cveContent := models.CveContent{
 | 
			
		||||
					Type:          models.GitHub,
 | 
			
		||||
					CveID:         cveID,
 | 
			
		||||
					Title:         v.Node.SecurityAdvisory.Summary,
 | 
			
		||||
					Summary:       v.Node.SecurityAdvisory.Description,
 | 
			
		||||
					Cvss2Severity: v.Node.SecurityVulnerability.Severity,
 | 
			
		||||
					Cvss3Severity: v.Node.SecurityVulnerability.Severity,
 | 
			
		||||
					SourceLink:    v.Node.SecurityAdvisory.Permalink,
 | 
			
		||||
					References:    refs,
 | 
			
		||||
					Published:     v.Node.SecurityAdvisory.PublishedAt,
 | 
			
		||||
					LastModified:  v.Node.SecurityAdvisory.UpdatedAt,
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if val, ok := r.ScannedCves[cveID]; ok {
 | 
			
		||||
					val.GitHubSecurityAlerts = val.GitHubSecurityAlerts.Add(m)
 | 
			
		||||
					val.CveContents[models.GitHub] = cveContent
 | 
			
		||||
					r.ScannedCves[cveID] = val
 | 
			
		||||
				} else {
 | 
			
		||||
					v := models.VulnInfo{
 | 
			
		||||
						CveID:                cveID,
 | 
			
		||||
						Confidences:          models.Confidences{models.GitHubMatch},
 | 
			
		||||
						GitHubSecurityAlerts: models.GitHubSecurityAlerts{m},
 | 
			
		||||
						CveContents:          models.NewCveContents(cveContent),
 | 
			
		||||
					}
 | 
			
		||||
					r.ScannedCves[cveID] = v
 | 
			
		||||
				}
 | 
			
		||||
				nCVEs++
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if !alerts.Data.Repository.VulnerabilityAlerts.PageInfo.HasNextPage {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		after = fmt.Sprintf(`after: \"%s\"`, alerts.Data.Repository.VulnerabilityAlerts.PageInfo.EndCursor)
 | 
			
		||||
	}
 | 
			
		||||
	return nCVEs, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//SecurityAlerts has detected CVE-IDs, PackageNames, Refs
 | 
			
		||||
type SecurityAlerts struct {
 | 
			
		||||
	Data struct {
 | 
			
		||||
		Repository struct {
 | 
			
		||||
			URL                 string `json:"url"`
 | 
			
		||||
			VulnerabilityAlerts struct {
 | 
			
		||||
				PageInfo struct {
 | 
			
		||||
					EndCursor   string `json:"endCursor"`
 | 
			
		||||
					HasNextPage bool   `json:"hasNextPage"`
 | 
			
		||||
					StartCursor string `json:"startCursor"`
 | 
			
		||||
				} `json:"pageInfo"`
 | 
			
		||||
				Edges []struct {
 | 
			
		||||
					Node struct {
 | 
			
		||||
						ID                    string    `json:"id"`
 | 
			
		||||
						DismissReason         string    `json:"dismissReason"`
 | 
			
		||||
						DismissedAt           time.Time `json:"dismissedAt"`
 | 
			
		||||
						SecurityVulnerability struct {
 | 
			
		||||
							Package struct {
 | 
			
		||||
								Name      string `json:"name"`
 | 
			
		||||
								Ecosystem string `json:"ecosystem"`
 | 
			
		||||
							} `json:"package"`
 | 
			
		||||
							Severity               string `json:"severity"`
 | 
			
		||||
							VulnerableVersionRange string `json:"vulnerableVersionRange"`
 | 
			
		||||
							FirstPatchedVersion    struct {
 | 
			
		||||
								Identifier string `json:"identifier"`
 | 
			
		||||
							} `json:"firstPatchedVersion"`
 | 
			
		||||
						} `json:"securityVulnerability"`
 | 
			
		||||
						SecurityAdvisory struct {
 | 
			
		||||
							Description string    `json:"description"`
 | 
			
		||||
							GhsaID      string    `json:"ghsaId"`
 | 
			
		||||
							Permalink   string    `json:"permalink"`
 | 
			
		||||
							PublishedAt time.Time `json:"publishedAt"`
 | 
			
		||||
							Summary     string    `json:"summary"`
 | 
			
		||||
							UpdatedAt   time.Time `json:"updatedAt"`
 | 
			
		||||
							WithdrawnAt time.Time `json:"withdrawnAt"`
 | 
			
		||||
							Origin      string    `json:"origin"`
 | 
			
		||||
							Severity    string    `json:"severity"`
 | 
			
		||||
							References  []struct {
 | 
			
		||||
								URL string `json:"url"`
 | 
			
		||||
							} `json:"references"`
 | 
			
		||||
							Identifiers []struct {
 | 
			
		||||
								Type  string `json:"type"`
 | 
			
		||||
								Value string `json:"value"`
 | 
			
		||||
							} `json:"identifiers"`
 | 
			
		||||
						} `json:"securityAdvisory"`
 | 
			
		||||
					} `json:"node"`
 | 
			
		||||
				} `json:"edges"`
 | 
			
		||||
			} `json:"vulnerabilityAlerts"`
 | 
			
		||||
		} `json:"repository"`
 | 
			
		||||
	} `json:"data"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										111
									
								
								detector/library.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								detector/library.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,111 @@
 | 
			
		||||
package detector
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
 | 
			
		||||
	db2 "github.com/aquasecurity/trivy-db/pkg/db"
 | 
			
		||||
	"github.com/aquasecurity/trivy/pkg/db"
 | 
			
		||||
	"github.com/aquasecurity/trivy/pkg/github"
 | 
			
		||||
	"github.com/aquasecurity/trivy/pkg/indicator"
 | 
			
		||||
	"github.com/aquasecurity/trivy/pkg/log"
 | 
			
		||||
	"github.com/spf13/afero"
 | 
			
		||||
	"golang.org/x/xerrors"
 | 
			
		||||
	"k8s.io/utils/clock"
 | 
			
		||||
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
	"github.com/future-architect/vuls/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// DetectLibsCves fills LibraryScanner information
 | 
			
		||||
func DetectLibsCves(r *models.ScanResult, cacheDir string, noProgress bool) (err error) {
 | 
			
		||||
	totalCnt := 0
 | 
			
		||||
	if len(r.LibraryScanners) == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// initialize trivy's logger and db
 | 
			
		||||
	err = log.InitLogger(false, false)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	util.Log.Info("Updating library db...")
 | 
			
		||||
	if err := downloadDB("", cacheDir, noProgress, false, false); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := db2.Init(cacheDir); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer db2.Close()
 | 
			
		||||
 | 
			
		||||
	for _, lib := range r.LibraryScanners {
 | 
			
		||||
		vinfos, err := lib.Scan()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		for _, vinfo := range vinfos {
 | 
			
		||||
			vinfo.Confidences.AppendIfMissing(models.TrivyMatch)
 | 
			
		||||
			if v, ok := r.ScannedCves[vinfo.CveID]; !ok {
 | 
			
		||||
				r.ScannedCves[vinfo.CveID] = vinfo
 | 
			
		||||
			} else {
 | 
			
		||||
				v.LibraryFixedIns = append(v.LibraryFixedIns, vinfo.LibraryFixedIns...)
 | 
			
		||||
				r.ScannedCves[vinfo.CveID] = v
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		totalCnt += len(vinfos)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	util.Log.Infof("%s: %d CVEs are detected with Library",
 | 
			
		||||
		r.FormatServerName(), totalCnt)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func downloadDB(appVersion, cacheDir string, quiet, light, skipUpdate bool) error {
 | 
			
		||||
	client := initializeDBClient(cacheDir, quiet)
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	needsUpdate, err := client.NeedsUpdate(appVersion, light, skipUpdate)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return xerrors.Errorf("database error: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if needsUpdate {
 | 
			
		||||
		util.Log.Info("Need to update DB")
 | 
			
		||||
		util.Log.Info("Downloading DB...")
 | 
			
		||||
		if err := client.Download(ctx, cacheDir, light); err != nil {
 | 
			
		||||
			return xerrors.Errorf("failed to download vulnerability DB: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
		if err = client.UpdateMetadata(cacheDir); err != nil {
 | 
			
		||||
			return xerrors.Errorf("unable to update database metadata: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// for debug
 | 
			
		||||
	if err := showDBInfo(cacheDir); err != nil {
 | 
			
		||||
		return xerrors.Errorf("failed to show database info: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func initializeDBClient(cacheDir string, quiet bool) db.Client {
 | 
			
		||||
	config := db2.Config{}
 | 
			
		||||
	client := github.NewClient()
 | 
			
		||||
	progressBar := indicator.NewProgressBar(quiet)
 | 
			
		||||
	realClock := clock.RealClock{}
 | 
			
		||||
	fs := afero.NewOsFs()
 | 
			
		||||
	metadata := db.NewMetadata(fs, cacheDir)
 | 
			
		||||
	dbClient := db.NewClient(config, client, progressBar, realClock, metadata)
 | 
			
		||||
	return dbClient
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func showDBInfo(cacheDir string) error {
 | 
			
		||||
	m := db.NewMetadata(afero.NewOsFs(), cacheDir)
 | 
			
		||||
	metadata, err := m.Get()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return xerrors.Errorf("something wrong with DB: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	util.Log.Debugf("DB Schema: %d, Type: %d, UpdatedAt: %s, NextUpdate: %s",
 | 
			
		||||
		metadata.Version, metadata.Type, metadata.UpdatedAt, metadata.NextUpdate)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										270
									
								
								detector/util.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										270
									
								
								detector/util.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,270 @@
 | 
			
		||||
package detector
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/future-architect/vuls/config"
 | 
			
		||||
	"github.com/future-architect/vuls/constant"
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
	"github.com/future-architect/vuls/util"
 | 
			
		||||
	"golang.org/x/xerrors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func reuseScannedCves(r *models.ScanResult) bool {
 | 
			
		||||
	switch r.Family {
 | 
			
		||||
	case constant.FreeBSD, constant.Raspbian:
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	if isTrivyResult(r) {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isTrivyResult(r *models.ScanResult) bool {
 | 
			
		||||
	_, ok := r.Optional["trivy-target"]
 | 
			
		||||
	return ok
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func needToRefreshCve(r models.ScanResult) bool {
 | 
			
		||||
	for _, cve := range r.ScannedCves {
 | 
			
		||||
		if 0 < len(cve.CveContents) {
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func loadPrevious(currs models.ScanResults) (prevs models.ScanResults, err error) {
 | 
			
		||||
	dirs, err := ListValidJSONDirs()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, result := range currs {
 | 
			
		||||
		filename := result.ServerName + ".json"
 | 
			
		||||
		if result.Container.Name != "" {
 | 
			
		||||
			filename = fmt.Sprintf("%s@%s.json", result.Container.Name, result.ServerName)
 | 
			
		||||
		}
 | 
			
		||||
		for _, dir := range dirs[1:] {
 | 
			
		||||
			path := filepath.Join(dir, filename)
 | 
			
		||||
			r, err := loadOneServerScanResult(path)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				util.Log.Debugf("%+v", err)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			if r.Family == result.Family && r.Release == result.Release {
 | 
			
		||||
				prevs = append(prevs, *r)
 | 
			
		||||
				util.Log.Infof("Previous json found: %s", path)
 | 
			
		||||
				break
 | 
			
		||||
			} else {
 | 
			
		||||
				util.Log.Infof("Previous json is different family.Release: %s, pre: %s.%s cur: %s.%s",
 | 
			
		||||
					path, r.Family, r.Release, result.Family, result.Release)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return prevs, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func diff(curResults, preResults models.ScanResults, isPlus, isMinus bool) (diffed models.ScanResults) {
 | 
			
		||||
	for _, current := range curResults {
 | 
			
		||||
		found := false
 | 
			
		||||
		var previous models.ScanResult
 | 
			
		||||
		for _, r := range preResults {
 | 
			
		||||
			if current.ServerName == r.ServerName && current.Container.Name == r.Container.Name {
 | 
			
		||||
				found = true
 | 
			
		||||
				previous = r
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !found {
 | 
			
		||||
			diffed = append(diffed, current)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cves := models.VulnInfos{}
 | 
			
		||||
		if isPlus {
 | 
			
		||||
			cves = getPlusDiffCves(previous, current)
 | 
			
		||||
		}
 | 
			
		||||
		if isMinus {
 | 
			
		||||
			minus := getMinusDiffCves(previous, current)
 | 
			
		||||
			if len(cves) == 0 {
 | 
			
		||||
				cves = minus
 | 
			
		||||
			} else {
 | 
			
		||||
				for k, v := range minus {
 | 
			
		||||
					cves[k] = v
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		packages := models.Packages{}
 | 
			
		||||
		for _, s := range cves {
 | 
			
		||||
			for _, affected := range s.AffectedPackages {
 | 
			
		||||
				var p models.Package
 | 
			
		||||
				if s.DiffStatus == models.DiffPlus {
 | 
			
		||||
					p = current.Packages[affected.Name]
 | 
			
		||||
				} else {
 | 
			
		||||
					p = previous.Packages[affected.Name]
 | 
			
		||||
				}
 | 
			
		||||
				packages[affected.Name] = p
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		current.ScannedCves = cves
 | 
			
		||||
		current.Packages = packages
 | 
			
		||||
		diffed = append(diffed, current)
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getPlusDiffCves(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) {
 | 
			
		||||
				v.DiffStatus = models.DiffPlus
 | 
			
		||||
				updated[v.CveID] = v
 | 
			
		||||
				util.Log.Debugf("updated: %s", v.CveID)
 | 
			
		||||
 | 
			
		||||
				// TODO commented out because  a bug of diff logic when multiple oval defs found for a certain CVE-ID and same updated_at
 | 
			
		||||
				// if these OVAL defs have different affected packages, this logic detects as updated.
 | 
			
		||||
				// This logic will be uncomented after integration with gost https://github.com/knqyf263/gost
 | 
			
		||||
				// } else if isCveFixed(v, previous) {
 | 
			
		||||
				// updated[v.CveID] = v
 | 
			
		||||
				// util.Log.Debugf("fixed: %s", v.CveID)
 | 
			
		||||
 | 
			
		||||
			} else {
 | 
			
		||||
				util.Log.Debugf("same: %s", v.CveID)
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			util.Log.Debugf("new: %s", v.CveID)
 | 
			
		||||
			v.DiffStatus = models.DiffPlus
 | 
			
		||||
			new[v.CveID] = v
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(updated) == 0 && len(new) == 0 {
 | 
			
		||||
		util.Log.Infof("%s: There are %d vulnerabilities, but no difference between current result and previous one.", current.FormatServerName(), len(current.ScannedCves))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for cveID, vuln := range new {
 | 
			
		||||
		updated[cveID] = vuln
 | 
			
		||||
	}
 | 
			
		||||
	return updated
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getMinusDiffCves(previous, current models.ScanResult) models.VulnInfos {
 | 
			
		||||
	currentCveIDsSet := map[string]bool{}
 | 
			
		||||
	for _, currentVulnInfo := range current.ScannedCves {
 | 
			
		||||
		currentCveIDsSet[currentVulnInfo.CveID] = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	clear := models.VulnInfos{}
 | 
			
		||||
	for _, v := range previous.ScannedCves {
 | 
			
		||||
		if !currentCveIDsSet[v.CveID] {
 | 
			
		||||
			v.DiffStatus = models.DiffMinus
 | 
			
		||||
			clear[v.CveID] = v
 | 
			
		||||
			util.Log.Debugf("clear: %s", v.CveID)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if len(clear) == 0 {
 | 
			
		||||
		util.Log.Infof("%s: There are %d vulnerabilities, but no difference between current result and previous one.", current.FormatServerName(), len(current.ScannedCves))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return clear
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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{}
 | 
			
		||||
	preVinfo, ok := previous.ScannedCves[cveID]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	for _, cType := range cTypes {
 | 
			
		||||
		if content, ok := preVinfo.CveContents[cType]; ok {
 | 
			
		||||
			prevLastModified[cType] = content.LastModified
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	curLastModified := map[models.CveContentType]time.Time{}
 | 
			
		||||
	curVinfo, ok := current.ScannedCves[cveID]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	for _, cType := range cTypes {
 | 
			
		||||
		if content, ok := curVinfo.CveContents[cType]; ok {
 | 
			
		||||
			curLastModified[cType] = content.LastModified
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, t := range cTypes {
 | 
			
		||||
		if !curLastModified[t].Equal(prevLastModified[t]) {
 | 
			
		||||
			util.Log.Debugf("%s LastModified not equal: \n%s\n%s",
 | 
			
		||||
				cveID, curLastModified[t], prevLastModified[t])
 | 
			
		||||
			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 = xerrors.Errorf("Failed to read %s: %w",
 | 
			
		||||
			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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// loadOneServerScanResult read JSON data of one server
 | 
			
		||||
func loadOneServerScanResult(jsonFile string) (*models.ScanResult, error) {
 | 
			
		||||
	var (
 | 
			
		||||
		data []byte
 | 
			
		||||
		err  error
 | 
			
		||||
	)
 | 
			
		||||
	if data, err = ioutil.ReadFile(jsonFile); err != nil {
 | 
			
		||||
		return nil, xerrors.Errorf("Failed to read %s: %w", jsonFile, err)
 | 
			
		||||
	}
 | 
			
		||||
	result := &models.ScanResult{}
 | 
			
		||||
	if err := json.Unmarshal(data, result); err != nil {
 | 
			
		||||
		return nil, xerrors.Errorf("Failed to parse %s: %w", jsonFile, err)
 | 
			
		||||
	}
 | 
			
		||||
	return result, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										267
									
								
								detector/wordpress.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										267
									
								
								detector/wordpress.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,267 @@
 | 
			
		||||
package detector
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/future-architect/vuls/config"
 | 
			
		||||
	c "github.com/future-architect/vuls/config"
 | 
			
		||||
	"github.com/future-architect/vuls/errof"
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
	"github.com/future-architect/vuls/util"
 | 
			
		||||
	version "github.com/hashicorp/go-version"
 | 
			
		||||
	"golang.org/x/xerrors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
//WpCveInfos is for wpscan json
 | 
			
		||||
type WpCveInfos struct {
 | 
			
		||||
	ReleaseDate  string `json:"release_date"`
 | 
			
		||||
	ChangelogURL string `json:"changelog_url"`
 | 
			
		||||
	// Status        string `json:"status"`
 | 
			
		||||
	LatestVersion string `json:"latest_version"`
 | 
			
		||||
	LastUpdated   string `json:"last_updated"`
 | 
			
		||||
	// Popular         bool        `json:"popular"`
 | 
			
		||||
	Vulnerabilities []WpCveInfo `json:"vulnerabilities"`
 | 
			
		||||
	Error           string      `json:"error"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//WpCveInfo is for wpscan json
 | 
			
		||||
type WpCveInfo struct {
 | 
			
		||||
	ID         string     `json:"id"`
 | 
			
		||||
	Title      string     `json:"title"`
 | 
			
		||||
	CreatedAt  time.Time  `json:"created_at"`
 | 
			
		||||
	UpdatedAt  time.Time  `json:"updated_at"`
 | 
			
		||||
	VulnType   string     `json:"vuln_type"`
 | 
			
		||||
	References References `json:"references"`
 | 
			
		||||
	FixedIn    string     `json:"fixed_in"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//References is for wpscan json
 | 
			
		||||
type References struct {
 | 
			
		||||
	URL     []string `json:"url"`
 | 
			
		||||
	Cve     []string `json:"cve"`
 | 
			
		||||
	Secunia []string `json:"secunia"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DetectWordPressCves access to wpscan and fetch scurity alerts and then set to the given ScanResult.
 | 
			
		||||
// https://wpscan.com/
 | 
			
		||||
func detectWordPressCves(r *models.ScanResult, cnf *c.WpScanConf) (int, error) {
 | 
			
		||||
	if len(r.WordPressPackages) == 0 {
 | 
			
		||||
		return 0, nil
 | 
			
		||||
	}
 | 
			
		||||
	// Core
 | 
			
		||||
	ver := strings.Replace(r.WordPressPackages.CoreVersion(), ".", "", -1)
 | 
			
		||||
	if ver == "" {
 | 
			
		||||
		return 0, errof.New(errof.ErrFailedToAccessWpScan,
 | 
			
		||||
			fmt.Sprintf("Failed to get WordPress core version."))
 | 
			
		||||
	}
 | 
			
		||||
	url := fmt.Sprintf("https://wpscan.com/api/v3/wordpresses/%s", ver)
 | 
			
		||||
	wpVinfos, err := wpscan(url, ver, cnf.Token)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Themes
 | 
			
		||||
	themes := r.WordPressPackages.Themes()
 | 
			
		||||
	if !cnf.DetectInactive {
 | 
			
		||||
		themes = removeInactives(themes)
 | 
			
		||||
	}
 | 
			
		||||
	for _, p := range themes {
 | 
			
		||||
		url := fmt.Sprintf("https://wpscan.com/api/v3/themes/%s", p.Name)
 | 
			
		||||
		candidates, err := wpscan(url, p.Name, cnf.Token)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
		vulns := detect(p, candidates)
 | 
			
		||||
		wpVinfos = append(wpVinfos, vulns...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Plugins
 | 
			
		||||
	plugins := r.WordPressPackages.Plugins()
 | 
			
		||||
	if !cnf.DetectInactive {
 | 
			
		||||
		plugins = removeInactives(plugins)
 | 
			
		||||
	}
 | 
			
		||||
	for _, p := range plugins {
 | 
			
		||||
		url := fmt.Sprintf("https://wpscan.com/api/v3/plugins/%s", p.Name)
 | 
			
		||||
		candidates, err := wpscan(url, p.Name, cnf.Token)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
		vulns := detect(p, candidates)
 | 
			
		||||
		wpVinfos = append(wpVinfos, vulns...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, wpVinfo := range wpVinfos {
 | 
			
		||||
		if vinfo, ok := r.ScannedCves[wpVinfo.CveID]; ok {
 | 
			
		||||
			vinfo.CveContents[models.WpScan] = wpVinfo.CveContents[models.WpScan]
 | 
			
		||||
			vinfo.VulnType = wpVinfo.VulnType
 | 
			
		||||
			vinfo.Confidences = append(vinfo.Confidences, wpVinfo.Confidences...)
 | 
			
		||||
			vinfo.WpPackageFixStats = append(vinfo.WpPackageFixStats, wpVinfo.WpPackageFixStats...)
 | 
			
		||||
			r.ScannedCves[wpVinfo.CveID] = vinfo
 | 
			
		||||
		} else {
 | 
			
		||||
			r.ScannedCves[wpVinfo.CveID] = wpVinfo
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return len(wpVinfos), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func wpscan(url, name, token string) (vinfos []models.VulnInfo, err error) {
 | 
			
		||||
	body, err := httpRequest(url, token)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if body == "" {
 | 
			
		||||
		util.Log.Debugf("wpscan.com response body is empty. URL: %s", url)
 | 
			
		||||
	}
 | 
			
		||||
	return convertToVinfos(name, body)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func detect(installed models.WpPackage, candidates []models.VulnInfo) (vulns []models.VulnInfo) {
 | 
			
		||||
	for _, v := range candidates {
 | 
			
		||||
		for _, fixstat := range v.WpPackageFixStats {
 | 
			
		||||
			ok, err := match(installed.Version, fixstat.FixedIn)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				util.Log.Errorf("Failed to compare versions %s installed: %s, fixedIn: %s, v: %+v",
 | 
			
		||||
					installed.Name, installed.Version, fixstat.FixedIn, v)
 | 
			
		||||
				// continue scanning
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			if ok {
 | 
			
		||||
				vulns = append(vulns, v)
 | 
			
		||||
				util.Log.Debugf("Affected: %s installed: %s, fixedIn: %s",
 | 
			
		||||
					installed.Name, installed.Version, fixstat.FixedIn)
 | 
			
		||||
			} else {
 | 
			
		||||
				util.Log.Debugf("Not affected: %s : %s, fixedIn: %s",
 | 
			
		||||
					installed.Name, installed.Version, fixstat.FixedIn)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func match(installedVer, fixedIn string) (bool, error) {
 | 
			
		||||
	v1, err := version.NewVersion(installedVer)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
	v2, err := version.NewVersion(fixedIn)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
	return v1.LessThan(v2), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func convertToVinfos(pkgName, body string) (vinfos []models.VulnInfo, err error) {
 | 
			
		||||
	if body == "" {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// "pkgName" : CVE Detailed data
 | 
			
		||||
	pkgnameCves := map[string]WpCveInfos{}
 | 
			
		||||
	if err = json.Unmarshal([]byte(body), &pkgnameCves); err != nil {
 | 
			
		||||
		return nil, xerrors.Errorf("Failed to unmarshal %s. err: %w", body, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, v := range pkgnameCves {
 | 
			
		||||
		vs := extractToVulnInfos(pkgName, v.Vulnerabilities)
 | 
			
		||||
		vinfos = append(vinfos, vs...)
 | 
			
		||||
	}
 | 
			
		||||
	return vinfos, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func extractToVulnInfos(pkgName string, cves []WpCveInfo) (vinfos []models.VulnInfo) {
 | 
			
		||||
	for _, vulnerability := range cves {
 | 
			
		||||
		var cveIDs []string
 | 
			
		||||
 | 
			
		||||
		if len(vulnerability.References.Cve) == 0 {
 | 
			
		||||
			cveIDs = append(cveIDs, fmt.Sprintf("WPVDBID-%s", vulnerability.ID))
 | 
			
		||||
		}
 | 
			
		||||
		for _, cveNumber := range vulnerability.References.Cve {
 | 
			
		||||
			cveIDs = append(cveIDs, "CVE-"+cveNumber)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var refs []models.Reference
 | 
			
		||||
		for _, url := range vulnerability.References.URL {
 | 
			
		||||
			refs = append(refs, models.Reference{
 | 
			
		||||
				Link: url,
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, cveID := range cveIDs {
 | 
			
		||||
			vinfos = append(vinfos, models.VulnInfo{
 | 
			
		||||
				CveID: cveID,
 | 
			
		||||
				CveContents: models.NewCveContents(
 | 
			
		||||
					models.CveContent{
 | 
			
		||||
						Type:         models.WpScan,
 | 
			
		||||
						CveID:        cveID,
 | 
			
		||||
						Title:        vulnerability.Title,
 | 
			
		||||
						References:   refs,
 | 
			
		||||
						Published:    vulnerability.CreatedAt,
 | 
			
		||||
						LastModified: vulnerability.UpdatedAt,
 | 
			
		||||
					},
 | 
			
		||||
				),
 | 
			
		||||
				VulnType: vulnerability.VulnType,
 | 
			
		||||
				Confidences: []models.Confidence{
 | 
			
		||||
					models.WpScanMatch,
 | 
			
		||||
				},
 | 
			
		||||
				WpPackageFixStats: []models.WpPackageFixStatus{{
 | 
			
		||||
					Name:    pkgName,
 | 
			
		||||
					FixedIn: vulnerability.FixedIn,
 | 
			
		||||
				}},
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func httpRequest(url, token string) (string, error) {
 | 
			
		||||
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 | 
			
		||||
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
 | 
			
		||||
	defer cancel()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", errof.New(errof.ErrFailedToAccessWpScan,
 | 
			
		||||
			fmt.Sprintf("Failed to access to wpscan.com. err: %s", err))
 | 
			
		||||
	}
 | 
			
		||||
	req.Header.Set("Authorization", fmt.Sprintf("Token token=%s", token))
 | 
			
		||||
	client, err := util.GetHTTPClient(config.Conf.HTTPProxy)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	resp, err := client.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", errof.New(errof.ErrFailedToAccessWpScan,
 | 
			
		||||
			fmt.Sprintf("Failed to access to wpscan.com. err: %s", err))
 | 
			
		||||
	}
 | 
			
		||||
	body, err := ioutil.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", errof.New(errof.ErrFailedToAccessWpScan,
 | 
			
		||||
			fmt.Sprintf("Failed to access to wpscan.com. err: %s", err))
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
	if resp.StatusCode == 200 {
 | 
			
		||||
		return string(body), nil
 | 
			
		||||
	} else if resp.StatusCode == 404 {
 | 
			
		||||
		// This package is not in wpscan
 | 
			
		||||
		return "", nil
 | 
			
		||||
	} else if resp.StatusCode == 429 {
 | 
			
		||||
		return "", errof.New(errof.ErrWpScanAPILimitExceeded,
 | 
			
		||||
			fmt.Sprintf("wpscan.com API limit exceeded: %+v", resp.Status))
 | 
			
		||||
	} else {
 | 
			
		||||
		util.Log.Warnf("wpscan.com unknown status code: %+v", resp.Status)
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func removeInactives(pkgs models.WordPressPackages) (removed models.WordPressPackages) {
 | 
			
		||||
	for _, p := range pkgs {
 | 
			
		||||
		if p.Status == "inactive" {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		removed = append(removed, p)
 | 
			
		||||
	}
 | 
			
		||||
	return removed
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										81
									
								
								detector/wordpress_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								detector/wordpress_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
			
		||||
package detector
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestRemoveInactive(t *testing.T) {
 | 
			
		||||
	var tests = []struct {
 | 
			
		||||
		in       models.WordPressPackages
 | 
			
		||||
		expected models.WordPressPackages
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			in: models.WordPressPackages{
 | 
			
		||||
				{
 | 
			
		||||
					Name:    "akismet",
 | 
			
		||||
					Status:  "inactive",
 | 
			
		||||
					Update:  "",
 | 
			
		||||
					Version: "",
 | 
			
		||||
					Type:    "",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			expected: nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			in: models.WordPressPackages{
 | 
			
		||||
				{
 | 
			
		||||
					Name:    "akismet",
 | 
			
		||||
					Status:  "inactive",
 | 
			
		||||
					Update:  "",
 | 
			
		||||
					Version: "",
 | 
			
		||||
					Type:    "",
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					Name:    "BackWPup",
 | 
			
		||||
					Status:  "inactive",
 | 
			
		||||
					Update:  "",
 | 
			
		||||
					Version: "",
 | 
			
		||||
					Type:    "",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			expected: nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			in: models.WordPressPackages{
 | 
			
		||||
				{
 | 
			
		||||
					Name:    "akismet",
 | 
			
		||||
					Status:  "active",
 | 
			
		||||
					Update:  "",
 | 
			
		||||
					Version: "",
 | 
			
		||||
					Type:    "",
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					Name:    "BackWPup",
 | 
			
		||||
					Status:  "inactive",
 | 
			
		||||
					Update:  "",
 | 
			
		||||
					Version: "",
 | 
			
		||||
					Type:    "",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			expected: models.WordPressPackages{
 | 
			
		||||
				{
 | 
			
		||||
					Name:    "akismet",
 | 
			
		||||
					Status:  "active",
 | 
			
		||||
					Update:  "",
 | 
			
		||||
					Version: "",
 | 
			
		||||
					Type:    "",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i, tt := range tests {
 | 
			
		||||
		actual := removeInactives(tt.in)
 | 
			
		||||
		if !reflect.DeepEqual(actual, tt.expected) {
 | 
			
		||||
			t.Errorf("[%d] WordPressPackages error ", i)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user