diff --git a/.gitignore b/.gitignore index 5c2591bc2f..d80b0b3bdf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ vuls *.txt *.json *.sqlite3 +*.db +tags .gitmodules coverage.out issues/ diff --git a/Makefile b/Makefile index f1e0d2ffa1..7b6d761df5 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ clean SRCS = $(shell git ls-files '*.go') -PKGS = ./. ./config ./models ./report ./cveapi ./scan ./util ./commands +PKGS = ./. ./config ./models ./report ./cveapi ./scan ./util ./commands ./cache all: test diff --git a/README.ja.md b/README.ja.md index 550b133013..31481e0a15 100644 --- a/README.ja.md +++ b/README.ja.md @@ -301,22 +301,24 @@ $ vuls tui # Performance Considerations - Ubuntu, Debian -アップデート対象のパッケージが沢山ある場合は、毎回apt-get changelogするので遅いし、スキャン対象サーバのリソースを消費する。 +`apt-get changelog`でアップデート対象のパッケージのチェンジログを取得し、含まれるCVE IDをパースする。 +アップデート対象のパッケージが沢山ある場合、チェンジログの取得に時間がかかるので、初回のスキャンは遅い。 +ただ、2回目以降はキャッシュしたchangelogを使うので速くなる。 - CentOS -アップデート対象すべてのchangelogを一度で取得しパースする。スキャンスピードは高速、サーバリソース消費量は小さい。 +アップデート対象すべてのchangelogを一度で取得しパースする。スキャンスピードは速い、サーバリソース消費量は小さい。 - Amazon, RHEL and FreeBSD 高速にスキャンし、スキャン対象サーバのリソース消費量は小さい。 -| Distribution| Scan Speed | Resource Usage On Target Server | -|:------------|:-------------------|:-------------| -| Ubuntu | Slow | Heavy | -| Debian | Slow | Heavy | -| CentOS | Fast | Light | -| Amazon | Fast | Light | -| RHEL | Fast | Light | -| FreeBSD | Fast | Light | +| Distribution| Scan Speed | +|:------------|:-------------------| +| Ubuntu | 初回は遅い / 2回目以降速い | +| Debian | 初回は遅い / 2回目以降速い | +| CentOS | 速い | +| Amazon | 速い | +| RHEL | 速い | +| FreeBSD | 速い | ---- @@ -340,7 +342,7 @@ web/app server in the same configuration under the load balancer |:------------|-------------------:| | Ubuntu | 12, 14, 16| | Debian | 7, 8| -| RHEL | 4, 5, 6, 7| +| RHEL | 6, 7| | CentOS | 5, 6, 7| | Amazon Linux| All| | FreeBSD | 10| @@ -595,7 +597,6 @@ prepare # Usage: Scan ``` - $ vuls scan -help scan: scan @@ -604,6 +605,7 @@ scan: [-results-dir=/path/to/results] [-cve-dictionary-dbpath=/path/to/cve.sqlite3] [-cve-dictionary-url=http://127.0.0.1:1323] + [-cache-dbpath=/path/to/cache.db] [-cvss-over=7] [-ignore-unscored-cves] [-ssh-external] @@ -626,7 +628,6 @@ scan: [SERVER]... - -ask-key-password Ask ssh privatekey password before scanning -aws-profile string @@ -641,6 +642,8 @@ scan: Azure storage container name -azure-key string Azure account key to use. AZURE_STORAGE_ACCESS_KEY environment variable is used if not specified + -cache-dbpath string + /path/to/cache.db (local cache of changelog for Ubuntu/Debian) (default "$PWD/cache.db") -config string /path/to/toml (default "$PWD/config.toml") -cve-dictionary-dbpath string @@ -649,8 +652,6 @@ scan: http://CVE.Dictionary (default "http://127.0.0.1:1323") -cvss-over float -cvss-over=6.5 means reporting CVSS Score 6.5 and over (default: 0 (means report all)) - -results-dir string - /path/to/results (default "$PWD/results") -debug debug mode -debug-sql @@ -671,6 +672,8 @@ scan: Send report via Slack -report-text Write report to text files ($PWD/results/current) + -results-dir string + /path/to/results (default "$PWD/results") -ssh-external Use external ssh command. Default: Use the Go native implementation ``` diff --git a/README.md b/README.md index b46f63fe1d..97d5ed85c2 100644 --- a/README.md +++ b/README.md @@ -297,25 +297,27 @@ see https://github.com/future-architect/vuls/tree/master/setup/docker ---- # Performance Considerations -- on Ubuntu and Debian +- On Ubuntu and Debian Vuls issues `apt-get changelog` for each upgradable packages and parse the changelog. -`apt-get changelog` is slow and resource usage is heavy when there are many updatable packages on target server. +`apt-get changelog` is slow and resource usage is heavy when there are many updatable packages on target server. +Vuls stores these changelogs to KVS([boltdb](https://github.com/boltdb/bolt)). +From the second time on, the scan speed is fast by using the local cache. -- on CentOS +- On CentOS Vuls issues `yum update --changelog` to get changelogs of upgradable packages at once and parse the changelog. Scan speed is fast and resource usage is light. - On Amazon, RHEL and FreeBSD High speed scan and resource usage is light because Vuls can get CVE IDs by using package manager(no need to parse a changelog). -| Distribution| Scan Speed | Resource Usage On Target Server | +| Distribution| Scan Speed | |:------------|:-------------------|:-------------| -| Ubuntu | Slow | Heavy | -| Debian | Slow | Heavy | -| CentOS | Fast | Light | -| Amazon | Fast | Light | -| RHEL | Fast | Light | -| FreeBSD | Fast | Light | +| Ubuntu | First time: Slow / From the second time: Fast | +| Debian | First time: Slow / From the second time: Fast | +| CentOS | Fast | +| Amazon | Fast | +| RHEL | Fast | +| FreeBSD | Fast | ---- @@ -339,7 +341,7 @@ web/app server in the same configuration under the load balancer |:------------|-------------------:| | Ubuntu | 12, 14, 16| | Debian | 7, 8| -| RHEL | 4, 5, 6, 7| +| RHEL | 6, 7| | CentOS | 5, 6, 7| | Amazon Linux| All| | FreeBSD | 10| @@ -603,6 +605,7 @@ scan: [-results-dir=/path/to/results] [-cve-dictionary-dbpath=/path/to/cve.sqlite3] [-cve-dictionary-url=http://127.0.0.1:1323] + [-cache-dbpath=/path/to/cache.db] [-cvss-over=7] [-ignore-unscored-cves] [-ssh-external] @@ -639,6 +642,8 @@ scan: Azure storage container name -azure-key string Azure account key to use. AZURE_STORAGE_ACCESS_KEY environment variable is used if not specified + -cache-dbpath string + /path/to/cache.db (local cache of changelog for Ubuntu/Debian) (default "$PWD/cache.db") -config string /path/to/toml (default "$PWD/config.toml") -cve-dictionary-dbpath string @@ -647,8 +652,6 @@ scan: http://CVE.Dictionary (default "http://127.0.0.1:1323") -cvss-over float -cvss-over=6.5 means reporting CVSS Score 6.5 and over (default: 0 (means report all)) - -results-dir string - /path/to/results (default "$PWD/results") -debug debug mode -debug-sql @@ -669,6 +672,8 @@ scan: Send report via Slack -report-text Write report to text files ($PWD/results/current) + -results-dir string + /path/to/results (default "$PWD/results") -ssh-external Use external ssh command. Default: Use the Go native implementation ``` diff --git a/cache/bolt.go b/cache/bolt.go new file mode 100644 index 0000000000..b085c71d8c --- /dev/null +++ b/cache/bolt.go @@ -0,0 +1,173 @@ +/* Vuls - Vulnerability Scanner +Copyright (C) 2016 Future Architect, Inc. Japan. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package cache + +import ( + "encoding/json" + "fmt" + + "github.com/Sirupsen/logrus" + "github.com/boltdb/bolt" + "github.com/future-architect/vuls/util" +) + +// Bolt holds a pointer of bolt.DB +// boltdb is used to store a cache of Changelogs of Ubuntu/Debian +type Bolt struct { + Path string + Log *logrus.Entry + db *bolt.DB +} + +// SetupBolt opens a boltdb and creates a meta bucket if not exists. +func SetupBolt(path string, l *logrus.Entry) error { + l.Infof("Open boltDB: %s", path) + db, err := bolt.Open(path, 0600, nil) + if err != nil { + return err + } + + b := Bolt{ + Path: path, + Log: l, + db: db, + } + if err = b.createBucketIfNotExists(metabucket); err != nil { + return err + } + + DB = b + return nil +} + +// Close a db. +func (b Bolt) Close() error { + if b.db == nil { + return nil + } + return b.db.Close() +} + +// CreateBucketIfNotExists creates a buket that is specified by arg. +func (b *Bolt) createBucketIfNotExists(name string) error { + return b.db.Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucketIfNotExists([]byte(name)) + if err != nil { + return fmt.Errorf("Failed to create bucket: %s", err) + } + return nil + }) +} + +// GetMeta gets a Meta Information os the servername to boltdb. +func (b Bolt) GetMeta(serverName string) (meta Meta, found bool, err error) { + err = b.db.View(func(tx *bolt.Tx) error { + bkt := tx.Bucket([]byte(metabucket)) + v := bkt.Get([]byte(serverName)) + if len(v) == 0 { + found = false + return nil + } + if e := json.Unmarshal(v, &meta); e != nil { + return e + } + found = true + return nil + }) + return +} + +// EnsureBuckets puts a Meta information and create a buket that holds changelogs. +func (b Bolt) EnsureBuckets(meta Meta) error { + jsonBytes, err := json.Marshal(meta) + if err != nil { + return fmt.Errorf("Failed to marshal to JSON: %s", err) + } + return b.db.Update(func(tx *bolt.Tx) error { + b.Log.Debugf("Put to meta: %s", meta.Name) + bkt := tx.Bucket([]byte(metabucket)) + if err := bkt.Put([]byte(meta.Name), jsonBytes); err != nil { + return err + } + + // re-create a bucket (bucket name: servername) + bkt = tx.Bucket([]byte(meta.Name)) + if bkt != nil { + b.Log.Debugf("Delete bucket: %s", meta.Name) + if err := tx.DeleteBucket([]byte(meta.Name)); err != nil { + return err + } + b.Log.Debugf("Bucket deleted: %s", meta.Name) + } + b.Log.Debugf("Create bucket: %s", meta.Name) + if _, err := tx.CreateBucket([]byte(meta.Name)); err != nil { + return err + } + b.Log.Debugf("Bucket created: %s", meta.Name) + return nil + }) +} + +// PrettyPrint is for debuging +func (b Bolt) PrettyPrint(meta Meta) error { + return b.db.View(func(tx *bolt.Tx) error { + bkt := tx.Bucket([]byte(metabucket)) + v := bkt.Get([]byte(meta.Name)) + b.Log.Debugf("key:%s, value:%s", meta.Name, v) + + bkt = tx.Bucket([]byte(meta.Name)) + c := bkt.Cursor() + for k, v := c.First(); k != nil; k, v = c.Next() { + b.Log.Debugf("key:%s, len: %d, %s...", + k, len(v), util.Truncate(string(v), 30)) + } + return nil + }) +} + +// GetChangelog get the changelgo of specified packName from the Bucket +func (b Bolt) GetChangelog(servername, packName string) (changelog string, err error) { + err = b.db.View(func(tx *bolt.Tx) error { + bkt := tx.Bucket([]byte(servername)) + if bkt == nil { + return fmt.Errorf("Faild to get Bucket: %s", servername) + } + v := bkt.Get([]byte(packName)) + if v == nil { + changelog = "" + return nil + } + changelog = string(v) + return nil + }) + return +} + +// PutChangelog put the changelgo of specified packName into the Bucket +func (b Bolt) PutChangelog(servername, packName, changelog string) error { + return b.db.Update(func(tx *bolt.Tx) error { + bkt := tx.Bucket([]byte(servername)) + if bkt == nil { + return fmt.Errorf("Faild to get Bucket: %s", servername) + } + if err := bkt.Put([]byte(packName), []byte(changelog)); err != nil { + return err + } + return nil + }) +} diff --git a/cache/bolt_test.go b/cache/bolt_test.go new file mode 100644 index 0000000000..a8dad918ab --- /dev/null +++ b/cache/bolt_test.go @@ -0,0 +1,134 @@ +/* Vuls - Vulnerability Scanner +Copyright (C) 2016 Future Architect, Inc. Japan. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package cache + +import ( + "os" + "reflect" + "testing" + + "github.com/Sirupsen/logrus" + "github.com/boltdb/bolt" + "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/models" +) + +const path = "/tmp/vuls-test-cache-11111111.db" +const servername = "server1" + +var meta = Meta{ + Name: servername, + Distro: config.Distro{ + Family: "ubuntu", + Release: "16.04", + }, + Packs: []models.PackageInfo{ + { + Name: "apt", + Version: "1", + }, + }, +} + +func TestSetupBolt(t *testing.T) { + log := logrus.NewEntry(&logrus.Logger{}) + err := SetupBolt(path, log) + if err != nil { + t.Errorf("Failed to setup bolt: %s", err) + } + defer os.Remove(path) + + if err := DB.Close(); err != nil { + t.Errorf("Failed to close bolt: %s", err) + } + + // check if meta bucket exists + db, err := bolt.Open(path, 0600, nil) + if err != nil { + t.Errorf("Failed to open bolt: %s", err) + } + + db.View(func(tx *bolt.Tx) error { + bkt := tx.Bucket([]byte(metabucket)) + if bkt == nil { + t.Errorf("Meta bucket nof found") + } + return nil + }) + +} + +func TestEnsureBuckets(t *testing.T) { + log := logrus.NewEntry(&logrus.Logger{}) + if err := SetupBolt(path, log); err != nil { + t.Errorf("Failed to setup bolt: %s", err) + } + if err := DB.EnsureBuckets(meta); err != nil { + t.Errorf("Failed to ensure buckets: %s", err) + } + defer os.Remove(path) + + m, found, err := DB.GetMeta(servername) + if err != nil { + t.Errorf("Failed to get meta: %s", err) + } + if !found { + t.Errorf("Not Found in meta") + } + if !reflect.DeepEqual(meta, m) { + t.Errorf("expected %v, actual %v", meta, m) + } + if err := DB.Close(); err != nil { + t.Errorf("Failed to close bolt: %s", err) + } + + db, err := bolt.Open(path, 0600, nil) + if err != nil { + t.Errorf("Failed to open bolt: %s", err) + } + db.View(func(tx *bolt.Tx) error { + bkt := tx.Bucket([]byte(servername)) + if bkt == nil { + t.Errorf("Meta bucket nof found") + } + return nil + }) +} + +func TestPutGetChangelog(t *testing.T) { + clog := "changelog-text" + log := logrus.NewEntry(&logrus.Logger{}) + if err := SetupBolt(path, log); err != nil { + t.Errorf("Failed to setup bolt: %s", err) + } + defer os.Remove(path) + + if err := DB.EnsureBuckets(meta); err != nil { + t.Errorf("Failed to ensure buckets: %s", err) + } + if err := DB.PutChangelog(servername, "apt", clog); err != nil { + t.Errorf("Failed to put changelog: %s", err) + } + if actual, err := DB.GetChangelog(servername, "apt"); err != nil { + t.Errorf("Failed to get changelog: %s", err) + } else { + if actual != clog { + t.Errorf("changelog is not same. e: %s, a: %s", clog, actual) + } + } +} diff --git a/cache/db.go b/cache/db.go new file mode 100644 index 0000000000..607d7bf72f --- /dev/null +++ b/cache/db.go @@ -0,0 +1,56 @@ +/* Vuls - Vulnerability Scanner +Copyright (C) 2016 Future Architect, Inc. Japan. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package cache + +import ( + "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/models" +) + +// DB has a cache instance +var DB Cache + +const metabucket = "changelog-meta" + +// Cache is a interface of cache +type Cache interface { + Close() error + GetMeta(string) (Meta, bool, error) + EnsureBuckets(Meta) error + PrettyPrint(Meta) error + GetChangelog(string, string) (string, error) + PutChangelog(string, string, string) error +} + +// Meta holds a server name, distro information of the scanned server and +// package information that was collected at the last scan. +type Meta struct { + Name string + Distro config.Distro + Packs []models.PackageInfo +} + +// FindPack search a PackageInfo +func (m Meta) FindPack(name string) (pack models.PackageInfo, found bool) { + for _, p := range m.Packs { + if name == p.Name { + return p, true + } + } + return pack, false +} diff --git a/commands/scan.go b/commands/scan.go index 603cc70666..a7082b53eb 100644 --- a/commands/scan.go +++ b/commands/scan.go @@ -47,6 +47,7 @@ type ScanCmd struct { jsonBaseDir string cvedbpath string cveDictionaryURL string + cacheDBPath string cvssScoreOver float64 ignoreUnscoredCves bool @@ -89,6 +90,7 @@ func (*ScanCmd) Usage() string { [-results-dir=/path/to/results] [-cve-dictionary-dbpath=/path/to/cve.sqlite3] [-cve-dictionary-url=http://127.0.0.1:1323] + [-cache-dbpath=/path/to/cache.db] [-cvss-over=7] [-ignore-unscored-cves] [-ssh-external] @@ -140,6 +142,13 @@ func (p *ScanCmd) SetFlags(f *flag.FlagSet) { defaultURL, "http://CVE.Dictionary") + defaultCacheDBPath := filepath.Join(wd, "cache.db") + f.StringVar( + &p.cacheDBPath, + "cache-dbpath", + defaultCacheDBPath, + "/path/to/cache.db (local cache of changelog for Ubuntu/Debian)") + f.Float64Var( &p.cvssScoreOver, "cvss-over", @@ -341,6 +350,7 @@ func (p *ScanCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) c.Conf.JSONBaseDir = p.jsonBaseDir c.Conf.CveDBPath = p.cvedbpath c.Conf.CveDictionaryURL = p.cveDictionaryURL + c.Conf.CacheDBPath = p.cacheDBPath c.Conf.CvssScoreOver = p.cvssScoreOver c.Conf.IgnoreUnscoredCves = p.ignoreUnscoredCves c.Conf.SSHExternal = p.sshExternal diff --git a/config/config.go b/config/config.go index 35cf89dadc..a9de9bb96d 100644 --- a/config/config.go +++ b/config/config.go @@ -49,6 +49,7 @@ type Config struct { HTTPProxy string `valid:"url"` JSONBaseDir string CveDBPath string + CacheDBPath string AwsProfile string AwsRegion string @@ -69,14 +70,21 @@ func (c Config) Validate() bool { if len(c.JSONBaseDir) != 0 { if ok, _ := valid.IsFilePath(c.JSONBaseDir); !ok { errs = append(errs, fmt.Errorf( - "JSON base directory must be a *Absolute* file path. jsonBaseDir: %s", c.JSONBaseDir)) + "JSON base directory must be a *Absolute* file path. -results-dir: %s", c.JSONBaseDir)) } } if len(c.CveDBPath) != 0 { if ok, _ := valid.IsFilePath(c.CveDBPath); !ok { errs = append(errs, fmt.Errorf( - "SQLite3 DB(Cve Dictionary) path must be a *Absolute* file path. dbpath: %s", c.CveDBPath)) + "SQLite3 DB(Cve Dictionary) path must be a *Absolute* file path. -cve-dictionary-dbpath: %s", c.CveDBPath)) + } + } + + if len(c.CacheDBPath) != 0 { + if ok, _ := valid.IsFilePath(c.CacheDBPath); !ok { + errs = append(errs, fmt.Errorf( + "Cache DB path must be a *Absolute* file path. -cache-dbpath: %s", c.CacheDBPath)) } } @@ -230,7 +238,17 @@ type ServerInfo struct { // used internal LogMsgAnsiColor string // DebugLog Color Container Container - Family string + Distro Distro +} + +// Distro has distribution info +type Distro struct { + Family string + Release string +} + +func (l Distro) String() string { + return fmt.Sprintf("%s %s", l.Family, l.Release) } // IsContainer returns whether this ServerInfo is about container diff --git a/cveapi/cve_client.go b/cveapi/cve_client.go index 22a0db2907..eb33838183 100644 --- a/cveapi/cve_client.go +++ b/cveapi/cve_client.go @@ -186,41 +186,6 @@ func (api cvedictClient) httpGet(key, url string, resChan chan<- response, errCh } } -// func (api cvedictClient) httpGet(key, url string, query map[string]string, resChan chan<- response, errChan chan<- error) { - -// var body string -// var errs []error -// var resp *http.Response -// f := func() (err error) { -// req := gorequest.New().SetDebug(true).Proxy(api.httpProxy).Get(url) -// for key := range query { -// req = req.Query(fmt.Sprintf("%s=%s", key, query[key])).Set("Content-Type", "application/x-www-form-urlencoded") -// } -// pp.Println(req) -// resp, body, errs = req.End() -// if 0 < len(errs) || resp.StatusCode != 200 { -// errChan <- fmt.Errorf("HTTP error. errs: %v, url: %s", errs, url) -// } -// return nil -// } -// notify := func(err error, t time.Duration) { -// log.Warnf("Failed to get. retrying in %s seconds. err: %s", t, err) -// } -// err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify) -// if err != nil { -// errChan <- fmt.Errorf("HTTP Error %s", err) -// } -// // resChan <- body -// cveDetail := cve.CveDetail{} -// if err := json.Unmarshal([]byte(body), &cveDetail); err != nil { -// errChan <- fmt.Errorf("Failed to Unmarshall. body: %s, err: %s", body, err) -// } -// resChan <- response{ -// key, -// cveDetail, -// } -// } - type responseGetCveDetailByCpeName struct { CpeName string CveDetails []cve.CveDetail diff --git a/img/vuls-architecture.graphml b/img/vuls-architecture.graphml index c85fe548f2..6b6f723742 100644 --- a/img/vuls-architecture.graphml +++ b/img/vuls-architecture.graphml @@ -222,14 +222,14 @@ ALAS (Amazon) - + - Vuls + Vuls - + @@ -248,7 +248,7 @@ ALAS (Amazon) - + Report @@ -296,6 +296,24 @@ ALAS (Amazon) + + + + + + + Web View +(Vulsrepo) + + + + + + + + + + @@ -319,30 +337,6 @@ ALAS (Amazon) - - - - - - - - - - - - - - SQLite3 - - - - - - - - - - @@ -356,7 +350,7 @@ ALAS (Amazon) - + @@ -379,20 +373,20 @@ ALAS (Amazon) - + - + - go-cve-dictionary + go-cve-dictionary - + @@ -407,12 +401,12 @@ ALAS (Amazon) - - + + - - + + @@ -432,10 +426,10 @@ ALAS (Amazon) - + - + HTTP server @@ -449,7 +443,7 @@ ALAS (Amazon) - + @@ -468,7 +462,7 @@ ALAS (Amazon) - + @@ -496,8 +490,8 @@ ALAS (Amazon) - - + + @@ -514,7 +508,7 @@ ALAS (Amazon) - + @@ -534,7 +528,7 @@ Container - + @@ -562,8 +556,8 @@ Container - - + + @@ -580,7 +574,7 @@ Container - + @@ -599,7 +593,89 @@ Container - + + + + + + + + + + results dir + + + + + + + + + + Folder 7 + + + + + + + + + + + + + + + + JSON + + + + + + + + + + + + + + + + + JSON + + + + + + + + + + + + + + + + + JSON + + + + + + + + + + + + + @@ -636,17 +712,17 @@ Vulnerability data - + - HTTP + HTTP or --cve-dictoianry-dbpath option - + @@ -678,7 +754,7 @@ Vulnerability data - send + send @@ -696,7 +772,7 @@ Vulnerability data - Generate + Generate @@ -714,11 +790,11 @@ Vulnerability data - Detail Information + View Detail Information - + @@ -726,7 +802,7 @@ Vulnerability data - + @@ -744,7 +820,7 @@ Vulnerability data - + @@ -762,7 +838,7 @@ Vulnerability data - + @@ -780,7 +856,7 @@ Vulnerability data - + @@ -790,7 +866,7 @@ Vulnerability data - + @@ -800,13 +876,13 @@ Vulnerability data - + - Insert + Insert @@ -818,18 +894,46 @@ Vulnerability data - + + + + + + + + + + + + + + + + + Notify + + + + + + + + + + + + - Insert -Scan Result + + - + @@ -837,13 +941,13 @@ Scan Result - + - Select + Select @@ -855,7 +959,7 @@ Scan Result - + @@ -865,58 +969,34 @@ Scan Result - + - Notify - - - - - - - - + - --cve-dictoianry-dbpath option - - - - - - - - + - Select - - - - - - - diff --git a/img/vuls-architecture.png b/img/vuls-architecture.png index ae5afebf22..eeeb7d2e01 100644 Binary files a/img/vuls-architecture.png and b/img/vuls-architecture.png differ diff --git a/img/vuls-scan-flow.graphml b/img/vuls-scan-flow.graphml index 8fd15da251..ab1269a46a 100644 --- a/img/vuls-scan-flow.graphml +++ b/img/vuls-scan-flow.graphml @@ -72,7 +72,7 @@ FreeBSD: pkg - Get upgradable packages + Check upgradable packages Debian/Ubuntu: apt-get upgrade --dry-run @@ -222,7 +222,7 @@ Reporting - Get all changelogs by using package manager + Get all changelogs of updatable packages at once CentOS: yum update --changelog @@ -397,7 +397,6 @@ FreeBSD - @@ -416,7 +415,6 @@ FreeBSD - @@ -427,7 +425,6 @@ FreeBSD - diff --git a/img/vuls-scan-flow.png b/img/vuls-scan-flow.png index a29dbee525..4bb215c22f 100644 Binary files a/img/vuls-scan-flow.png and b/img/vuls-scan-flow.png differ diff --git a/scan/base.go b/scan/base.go index 0148caff67..8353081a60 100644 --- a/scan/base.go +++ b/scan/base.go @@ -32,9 +32,8 @@ import ( type base struct { ServerInfo config.ServerInfo + Distro config.Distro - Family string - Release string Platform models.Platform osPackages @@ -54,13 +53,20 @@ func (l base) getServerInfo() config.ServerInfo { return l.ServerInfo } -func (l *base) setDistributionInfo(fam, rel string) { - l.Family = fam - l.Release = rel +func (l *base) setDistro(fam, rel string) { + d := config.Distro{ + Family: fam, + Release: rel, + } + l.Distro = d + + s := l.getServerInfo() + s.Distro = d + l.setServerInfo(s) } -func (l base) getDistributionInfo() string { - return fmt.Sprintf("%s %s", l.Family, l.Release) +func (l base) getDistro() config.Distro { + return l.Distro } func (l *base) setPlatform(p models.Platform) { @@ -250,8 +256,8 @@ func (l *base) convertToModel() (models.ScanResult, error) { return models.ScanResult{ ServerName: l.ServerInfo.ServerName, ScannedAt: time.Now(), - Family: l.Family, - Release: l.Release, + Family: l.Distro.Family, + Release: l.Distro.Release, Container: container, Platform: l.Platform, KnownCves: scoredCves, diff --git a/scan/base_test.go b/scan/base_test.go index 94cbc50377..86d5834292 100644 --- a/scan/base_test.go +++ b/scan/base_test.go @@ -70,8 +70,9 @@ func TestIsAwsInstanceID(t *testing.T) { {"no data", false}, } + r := newRedhat(config.ServerInfo{}) for _, tt := range tests { - actual := isAwsInstanceID(tt.in) + actual := r.isAwsInstanceID(tt.in) if tt.expected != actual { t.Errorf("expected %t, actual %t, str: %s", tt.expected, actual, tt.in) } diff --git a/scan/debian.go b/scan/debian.go index 8bc75aaaf6..2759bb88f1 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -24,6 +24,7 @@ import ( "strings" "time" + "github.com/future-architect/vuls/cache" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/cveapi" "github.com/future-architect/vuls/models" @@ -39,6 +40,7 @@ type debian struct { func newDebian(c config.ServerInfo) *debian { d := &debian{} d.log = util.NewCustomLogger(c) + d.setServerInfo(c) return d } @@ -46,7 +48,6 @@ func newDebian(c config.ServerInfo) *debian { // https://github.com/serverspec/specinfra/blob/master/lib/specinfra/helper/detect_os/debian.rb func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface, err error) { deb = newDebian(c) - deb.setServerInfo(c) if r := sshExec(c, "ls /etc/debian_version", noSudo); !r.isSuccess() { if r.Error != nil { @@ -69,12 +70,12 @@ func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface, err err result := re.FindStringSubmatch(trim(r.Stdout)) if len(result) == 0 { - deb.setDistributionInfo("debian/ubuntu", "unknown") + deb.setDistro("debian/ubuntu", "unknown") Log.Warnf( "Unknown Debian/Ubuntu version. lsb_release -ir: %s", r) } else { distro := strings.ToLower(trim(result[1])) - deb.setDistributionInfo(distro, trim(result[2])) + deb.setDistro(distro, trim(result[2])) } return true, deb, nil } @@ -90,10 +91,10 @@ func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface, err err if len(result) == 0 { Log.Warnf( "Unknown Debian/Ubuntu. cat /etc/lsb-release: %s", r) - deb.setDistributionInfo("debian/ubuntu", "unknown") + deb.setDistro("debian/ubuntu", "unknown") } else { distro := strings.ToLower(trim(result[1])) - deb.setDistributionInfo(distro, trim(result[2])) + deb.setDistro(distro, trim(result[2])) } return true, deb, nil } @@ -101,7 +102,7 @@ func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface, err err // Debian cmd := "cat /etc/debian_version" if r := sshExec(c, cmd, noSudo); r.isSuccess() { - deb.setDistributionInfo("debian", trim(r.Stdout)) + deb.setDistro("debian", trim(r.Stdout)) return true, deb, nil } @@ -133,7 +134,7 @@ func (o *debian) install() error { return fmt.Errorf(msg) } - if o.Family == "debian" { + if o.Distro.Family == "debian" { // install aptitude cmd = util.PrependProxyEnv("apt-get install --force-yes -y aptitude") if r := o.ssh(cmd, sudo); !r.isSuccess() { @@ -208,7 +209,7 @@ func (o *debian) parseScannedPackagesLine(line string) (name, version string, er } func (o *debian) checkRequiredPackagesInstalled() error { - if o.Family == "debian" { + if o.Distro.Family == "debian" { if r := o.ssh("test -f /usr/bin/aptitude", noSudo); !r.isSuccess() { msg := fmt.Sprintf("aptitude is not installed: %s", r) o.log.Errorf(msg) @@ -218,9 +219,8 @@ func (o *debian) checkRequiredPackagesInstalled() error { return nil } -//TODO return whether already expired. func (o *debian) scanUnsecurePackages(packs []models.PackageInfo) ([]CvePacksInfo, error) { - // cmd := prependProxyEnv(conf.HTTPProxy, "apt-get update | cat; echo 1") + o.log.Infof("apt-get update...") cmd := util.PrependProxyEnv("apt-get update") if r := o.ssh(cmd, sudo); !r.isSuccess() { return nil, fmt.Errorf("Failed to SSH: %s", r) @@ -241,12 +241,21 @@ func (o *debian) scanUnsecurePackages(packs []models.PackageInfo) ([]CvePacksInf } } } - unsecurePacks, err = o.fillCandidateVersion(unsecurePacks) if err != nil { return nil, fmt.Errorf("Failed to fill candidate versions. err: %s", err) } + current := cache.Meta{ + Name: o.getServerInfo().ServerName, + Distro: o.getServerInfo().Distro, + Packs: unsecurePacks, + } + o.log.Debugf("Ensure changelog cache: %s", current.Name) + if err := o.ensureChangelogCache(current); err != nil { + return nil, err + } + // Collect CVE information of upgradable packages cvePacksInfos, err := o.scanPackageCveInfos(unsecurePacks) if err != nil { @@ -256,63 +265,61 @@ func (o *debian) scanUnsecurePackages(packs []models.PackageInfo) ([]CvePacksInf return cvePacksInfos, nil } -func (o *debian) fillCandidateVersion(packs []models.PackageInfo) ([]models.PackageInfo, error) { - reqChan := make(chan models.PackageInfo, len(packs)) - resChan := make(chan models.PackageInfo, len(packs)) - errChan := make(chan error, len(packs)) - defer close(resChan) - defer close(errChan) - defer close(reqChan) - - go func() { - for _, pack := range packs { - reqChan <- pack +func (o *debian) ensureChangelogCache(current cache.Meta) error { + // Search from cache + old, found, err := cache.DB.GetMeta(current.Name) + if err != nil { + return fmt.Errorf("Failed to get meta. err: %s", err) + } + if !found { + o.log.Debugf("Not found in meta: %s", current.Name) + err = cache.DB.EnsureBuckets(current) + if err != nil { + return fmt.Errorf("Failed to ensure buckets. err: %s", err) } - }() - - timeout := time.After(5 * 60 * time.Second) - concurrency := 5 - tasks := util.GenWorkers(concurrency) - for range packs { - tasks <- func() { - select { - case pack := <-reqChan: - func(p models.PackageInfo) { - cmd := fmt.Sprintf("LANG=en_US.UTF-8 apt-cache policy %s", p.Name) - r := o.ssh(cmd, sudo) - if !r.isSuccess() { - errChan <- fmt.Errorf("Failed to SSH: %s.", r) - return - } - ver, err := o.parseAptCachePolicy(r.Stdout, p.Name) - if err != nil { - errChan <- fmt.Errorf("Failed to parse %s", err) - } - p.NewVersion = ver.Candidate - resChan <- p - }(pack) + } else { + if current.Distro.Family != old.Distro.Family || + current.Distro.Release != old.Distro.Release { + o.log.Debugf("Need to refesh meta: %s", current.Name) + err = cache.DB.EnsureBuckets(current) + if err != nil { + return fmt.Errorf("Failed to ensure buckets. err: %s", err) } + } else { + o.log.Debugf("Reuse meta: %s", current.Name) } } - errs := []error{} - result := []models.PackageInfo{} - for i := 0; i < len(packs); i++ { - select { - case pack := <-resChan: - result = append(result, pack) - o.log.Infof("(%d/%d) Upgradable: %s-%s -> %s", - i+1, len(packs), pack.Name, pack.Version, pack.NewVersion) - case err := <-errChan: - errs = append(errs, err) - case <-timeout: - return nil, fmt.Errorf("Timeout fillCandidateVersion") - } + if config.Conf.Debug { + cache.DB.PrettyPrint(current) } - if 0 < len(errs) { - return nil, fmt.Errorf("%v", errs) + return nil +} + +func (o *debian) fillCandidateVersion(before models.PackageInfoList) (filled []models.PackageInfo, err error) { + names := []string{} + for _, p := range before { + names = append(names, p.Name) + } + cmd := fmt.Sprintf("LANG=en_US.UTF-8 apt-cache policy %s", strings.Join(names, " ")) + r := o.ssh(cmd, sudo) + if !r.isSuccess() { + return nil, fmt.Errorf("Failed to SSH: %s.", r) } - return result, nil + packChangelog := o.splitAptCachePolicy(r.Stdout) + for k, v := range packChangelog { + ver, err := o.parseAptCachePolicy(v, k) + if err != nil { + return nil, fmt.Errorf("Failed to parse %s", err) + } + p, found := before.FindByName(k) + if !found { + return nil, fmt.Errorf("Not found: %s", k) + } + p.NewVersion = ver.Candidate + filled = append(filled, p) + } + return } func (o *debian) GetUpgradablePackNames() (packNames []string, err error) { @@ -369,9 +376,11 @@ func (o *debian) parseAptGetUpgrade(stdout string) (upgradableNames []string, er } func (o *debian) scanPackageCveInfos(unsecurePacks []models.PackageInfo) (cvePacksList CvePacksList, err error) { - - // { CVE ID: [packageInfo] } - cvePackages := make(map[string][]models.PackageInfo) + meta := cache.Meta{ + Name: o.getServerInfo().ServerName, + Distro: o.getServerInfo().Distro, + Packs: unsecurePacks, + } type strarray []string resChan := make(chan struct { @@ -398,6 +407,18 @@ func (o *debian) scanPackageCveInfos(unsecurePacks []models.PackageInfo) (cvePac select { case pack := <-reqChan: func(p models.PackageInfo) { + changelog := o.getChangelogCache(meta, p) + if 0 < len(changelog) { + cveIDs := o.getCveIDFromChangelog(changelog, p.Name, p.Version) + resChan <- struct { + models.PackageInfo + strarray + }{p, cveIDs} + return + } + + // if the changelog is not in cache or failed to get from local cache, + // get the changelog of the package via internet. if cveIDs, err := o.scanPackageCveIDs(p); err != nil { errChan <- err } else { @@ -411,6 +432,8 @@ func (o *debian) scanPackageCveInfos(unsecurePacks []models.PackageInfo) (cvePac } } + // { CVE ID: [packageInfo] } + cvePackages := make(map[string][]models.PackageInfo) errs := []error{} for i := 0; i < len(unsecurePacks); i++ { select { @@ -429,7 +452,6 @@ func (o *debian) scanPackageCveInfos(unsecurePacks []models.PackageInfo) (cvePac return nil, fmt.Errorf("Timeout scanPackageCveIDs") } } - if 0 < len(errs) { return nil, fmt.Errorf("%v", errs) } @@ -438,7 +460,6 @@ func (o *debian) scanPackageCveInfos(unsecurePacks []models.PackageInfo) (cvePac for k := range cvePackages { cveIDs = append(cveIDs, k) } - o.log.Debugf("%d Cves are found. cves: %v", len(cveIDs), cveIDs) o.log.Info("Fetching CVE details...") @@ -459,9 +480,32 @@ func (o *debian) scanPackageCveInfos(unsecurePacks []models.PackageInfo) (cvePac return } +func (o *debian) getChangelogCache(meta cache.Meta, pack models.PackageInfo) string { + cachedPack, found := meta.FindPack(pack.Name) + if !found { + return "" + } + if cachedPack.NewVersion != pack.NewVersion { + return "" + } + changelog, err := cache.DB.GetChangelog(meta.Name, pack.Name) + if err != nil { + o.log.Warnf("Failed to get chnagelog. bucket: %s, key:%s, err: %s", + meta.Name, pack.Name, err) + return "" + } + if len(changelog) == 0 { + return "" + } + + o.log.Debugf("Cache hit: %s, len: %d, %s...", + meta.Name, len(changelog), util.Truncate(changelog, 30)) + return changelog +} + func (o *debian) scanPackageCveIDs(pack models.PackageInfo) ([]string, error) { cmd := "" - switch o.Family { + switch o.Distro.Family { case "ubuntu": cmd = fmt.Sprintf(`apt-get changelog %s | grep '\(urgency\|CVE\)'`, pack.Name) case "debian": @@ -476,36 +520,38 @@ func (o *debian) scanPackageCveIDs(pack models.PackageInfo) ([]string, error) { return nil, nil } + err := cache.DB.PutChangelog(o.getServerInfo().ServerName, pack.Name, r.Stdout) + if err != nil { + return nil, fmt.Errorf("Failed to put changelog into cache") + } // No error will be returned. Only logging. - return o.getCveIDParsingChangelog(r.Stdout, pack.Name, pack.Version) + return o.getCveIDFromChangelog(r.Stdout, pack.Name, pack.Version), nil } -func (o *debian) getCveIDParsingChangelog(changelog string, - packName string, versionOrLater string) (cveIDs []string, err error) { +func (o *debian) getCveIDFromChangelog(changelog string, + packName string, versionOrLater string) []string { - cveIDs, err = o.parseChangelog(changelog, packName, versionOrLater) - if err == nil { - return + if cveIDs, err := o.parseChangelog(changelog, packName, versionOrLater); err == nil { + return cveIDs } ver := strings.Split(versionOrLater, "ubuntu")[0] - cveIDs, err = o.parseChangelog(changelog, packName, ver) - if err == nil { - return + if cveIDs, err := o.parseChangelog(changelog, packName, ver); err == nil { + return cveIDs } splittedByColon := strings.Split(versionOrLater, ":") if 1 < len(splittedByColon) { ver = splittedByColon[1] } - cveIDs, err = o.parseChangelog(changelog, packName, ver) + cveIDs, err := o.parseChangelog(changelog, packName, ver) if err == nil { - return + return cveIDs } // Only logging the error. o.log.Error(err) - return []string{}, nil + return []string{} } // Collect CVE-IDs included in the changelog. @@ -538,6 +584,29 @@ func (o *debian) parseChangelog(changelog string, return } +func (o *debian) splitAptCachePolicy(stdout string) map[string]string { + // re := regexp.MustCompile(`(?m:^[^ \t]+:$)`) + re := regexp.MustCompile(`(?m:^[^ \t]+:\r\n)`) + ii := re.FindAllStringIndex(stdout, -1) + ri := []int{} + for i := len(ii) - 1; 0 <= i; i-- { + ri = append(ri, ii[i][0]) + } + splitted := []string{} + lasti := len(stdout) + for _, i := range ri { + splitted = append(splitted, stdout[i:lasti]) + lasti = i + } + + packChangelog := map[string]string{} + for _, r := range splitted { + packName := r[:strings.Index(r, ":")] + packChangelog[packName] = r + } + return packChangelog +} + type packCandidateVer struct { Name string Installed string diff --git a/scan/debian_test.go b/scan/debian_test.go index 1656ed6294..b49f528cc0 100644 --- a/scan/debian_test.go +++ b/scan/debian_test.go @@ -18,10 +18,14 @@ along with this program. If not, see . package scan import ( + "os" "reflect" "testing" + "github.com/Sirupsen/logrus" + "github.com/future-architect/vuls/cache" "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/models" "github.com/k0kubun/pp" ) @@ -183,7 +187,7 @@ util-linux (2.26.2-6) unstable; urgency=medium`, d := newDebian(config.ServerInfo{}) for _, tt := range tests { - actual, _ := d.getCveIDParsingChangelog(tt.in[2], tt.in[0], tt.in[1]) + actual := d.getCveIDFromChangelog(tt.in[2], tt.in[0], tt.in[1]) if len(actual) != len(tt.expected) { t.Errorf("Len of return array are'nt same. expected %#v, actual %#v", tt.expected, actual) t.Errorf(pp.Sprintf("%s", tt.in)) @@ -195,13 +199,6 @@ util-linux (2.26.2-6) unstable; urgency=medium`, } } } - - for _, tt := range tests { - _, err := d.getCveIDParsingChangelog(tt.in[2], tt.in[0], "version number do'nt match case") - if err != nil { - t.Errorf("Returning error is unexpected") - } - } } func TestGetUpdatablePackNames(t *testing.T) { @@ -520,6 +517,95 @@ Calculating upgrade... Done } } +func TestGetChangelogCache(t *testing.T) { + const servername = "server1" + pack := models.PackageInfo{ + Name: "apt", + Version: "1.0.0", + NewVersion: "1.0.1", + } + var meta = cache.Meta{ + Name: servername, + Distro: config.Distro{ + Family: "ubuntu", + Release: "16.04", + }, + Packs: []models.PackageInfo{pack}, + } + + const path = "/tmp/vuls-test-cache-11111111.db" + log := logrus.NewEntry(&logrus.Logger{}) + if err := cache.SetupBolt(path, log); err != nil { + t.Errorf("Failed to setup bolt: %s", err) + } + defer os.Remove(path) + + if err := cache.DB.EnsureBuckets(meta); err != nil { + t.Errorf("Failed to ensure buckets: %s", err) + } + + d := newDebian(config.ServerInfo{}) + actual := d.getChangelogCache(meta, pack) + if actual != "" { + t.Errorf("Failed to get empty stirng from cache:") + } + + clog := "changelog-text" + if err := cache.DB.PutChangelog(servername, "apt", clog); err != nil { + t.Errorf("Failed to put changelog: %s", err) + } + + actual = d.getChangelogCache(meta, pack) + if actual != clog { + t.Errorf("Failed to get changelog from cache: %s", actual) + } + + // increment a version of the pack + pack.NewVersion = "1.0.2" + actual = d.getChangelogCache(meta, pack) + if actual != "" { + t.Errorf("The changelog is not invalidated: %s", actual) + } + + // change a name of the pack + pack.Name = "bash" + actual = d.getChangelogCache(meta, pack) + if actual != "" { + t.Errorf("The changelog is not invalidated: %s", actual) + } +} + +func TestSplitAptCachePolicy(t *testing.T) { + var tests = []struct { + stdout string + expected map[string]string + }{ + // This function parse apt-cache policy by using Regexp multi-line mode. + // So, test data includes "\r\n" + { + "apt:\r\n Installed: 1.2.6\r\n Candidate: 1.2.12~ubuntu16.04.1\r\n Version table:\r\n 1.2.12~ubuntu16.04.1 500\r\n 500 http://archive.ubuntu.com/ubuntu xenial-updates/main amd64 Packages\r\n 1.2.10ubuntu1 500\r\n 500 http://archive.ubuntu.com/ubuntu xenial/main amd64 Packages\r\n *** 1.2.6 100\r\n 100 /var/lib/dpkg/status\r\napt-utils:\r\n Installed: 1.2.6\r\n Candidate: 1.2.12~ubuntu16.04.1\r\n Version table:\r\n 1.2.12~ubuntu16.04.1 500\r\n 500 http://archive.ubuntu.com/ubuntu xenial-updates/main amd64 Packages\r\n 1.2.10ubuntu1 500\r\n 500 http://archive.ubuntu.com/ubuntu xenial/main amd64 Packages\r\n *** 1.2.6 100\r\n 100 /var/lib/dpkg/status\r\nbase-files:\r\n Installed: 9.4ubuntu3\r\n Candidate: 9.4ubuntu4.2\r\n Version table:\r\n 9.4ubuntu4.2 500\r\n 500 http://archive.ubuntu.com/ubuntu xenial-updates/main amd64 Packages\r\n 9.4ubuntu4 500\r\n 500 http://archive.ubuntu.com/ubuntu xenial/main amd64 Packages\r\n *** 9.4ubuntu3 100\r\n 100 /var/lib/dpkg/status\r\n", + + map[string]string{ + "apt": "apt:\r\n Installed: 1.2.6\r\n Candidate: 1.2.12~ubuntu16.04.1\r\n Version table:\r\n 1.2.12~ubuntu16.04.1 500\r\n 500 http://archive.ubuntu.com/ubuntu xenial-updates/main amd64 Packages\r\n 1.2.10ubuntu1 500\r\n 500 http://archive.ubuntu.com/ubuntu xenial/main amd64 Packages\r\n *** 1.2.6 100\r\n 100 /var/lib/dpkg/status\r\n", + + "apt-utils": "apt-utils:\r\n Installed: 1.2.6\r\n Candidate: 1.2.12~ubuntu16.04.1\r\n Version table:\r\n 1.2.12~ubuntu16.04.1 500\r\n 500 http://archive.ubuntu.com/ubuntu xenial-updates/main amd64 Packages\r\n 1.2.10ubuntu1 500\r\n 500 http://archive.ubuntu.com/ubuntu xenial/main amd64 Packages\r\n *** 1.2.6 100\r\n 100 /var/lib/dpkg/status\r\n", + + "base-files": "base-files:\r\n Installed: 9.4ubuntu3\r\n Candidate: 9.4ubuntu4.2\r\n Version table:\r\n 9.4ubuntu4.2 500\r\n 500 http://archive.ubuntu.com/ubuntu xenial-updates/main amd64 Packages\r\n 9.4ubuntu4 500\r\n 500 http://archive.ubuntu.com/ubuntu xenial/main amd64 Packages\r\n *** 9.4ubuntu3 100\r\n 100 /var/lib/dpkg/status\r\n", + }, + }, + } + + d := newDebian(config.ServerInfo{}) + for _, tt := range tests { + actual := d.splitAptCachePolicy(tt.stdout) + if !reflect.DeepEqual(tt.expected, actual) { + e := pp.Sprintf("%v", tt.expected) + a := pp.Sprintf("%v", actual) + t.Errorf("expected %s, actual %s", e, a) + } + } +} + func TestParseAptCachePolicy(t *testing.T) { var tests = []struct { diff --git a/scan/freebsd.go b/scan/freebsd.go index 0c6e5225bc..d3d10c3c08 100644 --- a/scan/freebsd.go +++ b/scan/freebsd.go @@ -1,3 +1,20 @@ +/* Vuls - Vulnerability Scanner +Copyright (C) 2016 Future Architect, Inc. Japan. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + package scan import ( @@ -19,18 +36,18 @@ type bsd struct { func newBsd(c config.ServerInfo) *bsd { d := &bsd{} d.log = util.NewCustomLogger(c) + d.setServerInfo(c) return d } //https://github.com/mizzy/specinfra/blob/master/lib/specinfra/helper/detect_os/freebsd.rb func detectFreebsd(c config.ServerInfo) (itsMe bool, bsd osTypeInterface) { bsd = newBsd(c) - c.Family = "FreeBSD" if r := sshExec(c, "uname", noSudo); r.isSuccess() { if strings.Contains(r.Stdout, "FreeBSD") == true { if b := sshExec(c, "uname -r", noSudo); b.isSuccess() { - bsd.setDistributionInfo("FreeBSD", strings.TrimSpace(b.Stdout)) - bsd.setServerInfo(c) + rel := strings.TrimSpace(b.Stdout) + bsd.setDistro("FreeBSD", rel) return true, bsd } } diff --git a/scan/redhat.go b/scan/redhat.go index 13f148ed12..21411cacdb 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -42,16 +42,16 @@ type redhat struct { func newRedhat(c config.ServerInfo) *redhat { r := &redhat{} r.log = util.NewCustomLogger(c) + r.setServerInfo(c) return r } // https://github.com/serverspec/specinfra/blob/master/lib/specinfra/helper/detect_os/redhat.rb func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) { red = newRedhat(c) - red.setServerInfo(c) if r := sshExec(c, "ls /etc/fedora-release", noSudo); r.isSuccess() { - red.setDistributionInfo("fedora", "unknown") + red.setDistro("fedora", "unknown") Log.Warn("Fedora not tested yet: %s", r) return true, red } @@ -72,9 +72,9 @@ func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) { release := result[2] switch strings.ToLower(result[1]) { case "centos", "centos linux": - red.setDistributionInfo("centos", release) + red.setDistro("centos", release) default: - red.setDistributionInfo("rhel", release) + red.setDistro("rhel", release) } return true, red } @@ -90,7 +90,7 @@ func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) { release = fields[4] } } - red.setDistributionInfo(family, release) + red.setDistro(family, release) return true, red } @@ -113,7 +113,7 @@ func (o *redhat) checkIfSudoNoPasswd() error { // CentOS 7 ... yum-plugin-changelog // RHEL, Amazon ... no additinal packages needed func (o *redhat) install() error { - switch o.Family { + switch o.Distro.Family { case "rhel", "amazon": o.log.Infof("Nothing to do") return nil @@ -123,14 +123,13 @@ func (o *redhat) install() error { } func (o *redhat) installYumChangelog() error { - if o.Family == "centos" { + if o.Distro.Family == "centos" { var majorVersion int - if 0 < len(o.Release) { - majorVersion, _ = strconv.Atoi(strings.Split(o.Release, ".")[0]) + if 0 < len(o.Distro.Release) { + majorVersion, _ = strconv.Atoi(strings.Split(o.Distro.Release, ".")[0]) } else { return fmt.Errorf( - "Not implemented yet. family: %s, release: %s", - o.Family, o.Release) + "Not implemented yet: %s", o.Distro) } var packName = "" @@ -157,12 +156,12 @@ func (o *redhat) installYumChangelog() error { } func (o *redhat) checkRequiredPackagesInstalled() error { - if o.Family == "centos" { + if o.Distro.Family == "centos" { var majorVersion int - if 0 < len(o.Release) { - majorVersion, _ = strconv.Atoi(strings.Split(o.Release, ".")[0]) + if 0 < len(o.Distro.Release) { + majorVersion, _ = strconv.Atoi(strings.Split(o.Distro.Release, ".")[0]) } else { - msg := fmt.Sprintf("Not implemented yet. family: %s, release: %s", o.Family, o.Release) + msg := fmt.Sprintf("Not implemented yet: %s", o.Distro) o.log.Errorf(msg) return fmt.Errorf(msg) } @@ -240,7 +239,7 @@ func (o *redhat) parseScannedPackagesLine(line string) (models.PackageInfo, erro } func (o *redhat) scanUnsecurePackages() ([]CvePacksInfo, error) { - if o.Family != "centos" { + if o.Distro.Family != "centos" { // Amazon, RHEL has yum updateinfo as default // yum updateinfo can collenct vendor advisory information. return o.scanUnsecurePackagesUsingYumPluginSecurity() @@ -460,12 +459,10 @@ func (o *redhat) getChangelogCVELines(rpm2changelog map[string]*string, packInfo func (o *redhat) parseAllChangelog(allChangelog string) (map[string]*string, error) { var majorVersion int - if 0 < len(o.Release) && o.Family == "centos" { - majorVersion, _ = strconv.Atoi(strings.Split(o.Release, ".")[0]) + if 0 < len(o.Distro.Release) && o.Distro.Family == "centos" { + majorVersion, _ = strconv.Atoi(strings.Split(o.Distro.Release, ".")[0]) } else { - return nil, fmt.Errorf( - "Not implemented yet. family: %s, release: %s", - o.Family, o.Release) + return nil, fmt.Errorf("Not implemented yet: %s", o.getDistro()) } orglines := strings.Split(allChangelog, "\n") @@ -569,7 +566,7 @@ type distroAdvisoryCveIDs struct { // Scaning unsecure packages using yum-plugin-security. // Amazon, RHEL func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (CvePacksList, error) { - if o.Family == "centos" { + if o.Distro.Family == "centos" { // CentOS has no security channel. // So use yum check-update && parse changelog return CvePacksList{}, fmt.Errorf( @@ -717,7 +714,7 @@ func (o *redhat) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveID switch sectionState { case Header: - switch o.Family { + switch o.Distro.Family { case "centos": // CentOS has no security channel. // So use yum check-update && parse changelog @@ -964,7 +961,7 @@ func (o *redhat) clone() osTypeInterface { } func (o *redhat) sudo() bool { - switch o.Family { + switch o.Distro.Family { case "amazon": return false default: diff --git a/scan/redhat_test.go b/scan/redhat_test.go index 0b968f7081..9bd87f2665 100644 --- a/scan/redhat_test.go +++ b/scan/redhat_test.go @@ -451,7 +451,7 @@ Description : The Berkeley Internet Name Domain (BIND) is an implementation of updated, _ := time.Parse("2006-01-02", "2015-09-04") r := newRedhat(config.ServerInfo{}) - r.Family = "redhat" + r.Distro = config.Distro{Family: "redhat"} var tests = []struct { in string @@ -511,7 +511,7 @@ Description : The Berkeley Internet Name Domain (BIND) is an implementation of func TestParseYumUpdateinfoAmazon(t *testing.T) { r := newRedhat(config.ServerInfo{}) - r.Family = "amazon" + r.Distro = config.Distro{Family: "redhat"} issued, _ := time.Parse("2006-01-02", "2015-12-15") updated, _ := time.Parse("2006-01-02", "2015-12-16") @@ -601,7 +601,7 @@ Description : Package updates are available for Amazon Linux AMI that fix the func TestParseYumCheckUpdateLines(t *testing.T) { r := newRedhat(config.ServerInfo{}) - r.Family = "centos" + r.Distro = config.Distro{Family: "centos"} stdout := `Loaded plugins: changelog, fastestmirror, keys, protect-packages, protectbase, security Loading mirror speeds from cached hostfile * base: mirror.fairway.ne.jp @@ -709,7 +709,7 @@ bind-utils.x86_64 30:9.3.6-25.P1.el5_11.8 updates func TestParseYumCheckUpdateLinesAmazon(t *testing.T) { r := newRedhat(config.ServerInfo{}) - r.Family = "amazon" + r.Distro = config.Distro{Family: "amazon"} stdout := `Loaded plugins: priorities, update-motd, upgrade-helper 34 package(s) needed for security, out of 71 available @@ -1110,8 +1110,10 @@ func TestGetChangelogCVELines(t *testing.T) { } r := newRedhat(config.ServerInfo{}) - r.Family = "centos" - r.Release = "6.7" + r.Distro = config.Distro{ + Family: "centos", + Release: "6.7", + } for _, tt := range testsCentos6 { rpm2changelog, err := r.parseAllChangelog(stdoutCentos6) if err != nil { @@ -1194,7 +1196,10 @@ func TestGetChangelogCVELines(t *testing.T) { }, } - r.Release = "5.6" + r.Distro = config.Distro{ + Family: "centos", + Release: "5.6", + } for _, tt := range testsCentos5 { rpm2changelog, err := r.parseAllChangelog(stdoutCentos5) if err != nil { diff --git a/scan/serverapi.go b/scan/serverapi.go index d338370b1e..bf4a985266 100644 --- a/scan/serverapi.go +++ b/scan/serverapi.go @@ -1,3 +1,20 @@ +/* Vuls - Vulnerability Scanner +Copyright (C) 2016 Future Architect, Inc. Japan. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + package scan import ( @@ -5,6 +22,7 @@ import ( "time" "github.com/Sirupsen/logrus" + "github.com/future-architect/vuls/cache" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" cve "github.com/kotakanbe/go-cve-dictionary/models" @@ -20,8 +38,9 @@ type osTypeInterface interface { setServerInfo(config.ServerInfo) getServerInfo() config.ServerInfo - setDistributionInfo(string, string) - getDistributionInfo() string + setDistro(string, string) + getDistro() config.Distro + // getFamily() string checkIfSudoNoPasswd() error detectPlatform() error @@ -188,7 +207,7 @@ func detectServerOSes() (sshAbleOses []osTypeInterface) { Log.Infof("(%d/%d) Detected: %s: %s", i+1, len(config.Conf.Servers), res.getServerInfo().ServerName, - res.getDistributionInfo()) + res.getDistro()) } case <-timeout: msg := "Timed out while detecting servers" @@ -248,7 +267,7 @@ func detectContainerOSes() (actives []osTypeInterface) { } oses = append(oses, res...) Log.Infof("Detected: %s@%s: %s", - sinfo.Container.Name, sinfo.ServerName, osi.getDistributionInfo()) + sinfo.Container.Name, sinfo.ServerName, osi.getDistro()) } case <-timeout: msg := "Timed out while detecting containers" @@ -417,6 +436,13 @@ func Scan() []error { return errs } + if err := setupCangelogCache(); err != nil { + return []error{err} + } + if cache.DB != nil { + defer cache.DB.Close() + } + Log.Info("Scanning vulnerable OS packages...") if errs := scanPackages(); errs != nil { return errs @@ -429,6 +455,23 @@ func Scan() []error { return nil } +func setupCangelogCache() error { + needToSetupCache := false + for _, s := range servers { + switch s.getDistro().Family { + case "ubuntu", "debian": + needToSetupCache = true + break + } + } + if needToSetupCache { + if err := cache.SetupBolt(config.Conf.CacheDBPath, Log); err != nil { + return err + } + } + return nil +} + func checkRequiredPackagesInstalled() []error { timeoutSec := 30 * 60 return parallelSSHExec(func(o osTypeInterface) error { diff --git a/scan/sshutil.go b/scan/sshutil.go index 55190fc8dd..296adcb6b6 100644 --- a/scan/sshutil.go +++ b/scan/sshutil.go @@ -302,7 +302,7 @@ func decolateCmd(c conf.ServerInfo, cmd string, sudo bool) string { cmd = strings.Replace(cmd, "|", "| sudo ", -1) } - if c.Family != "FreeBSD" { + if c.Distro.Family != "FreeBSD" { // set pipefail option. Bash only // http://unix.stackexchange.com/questions/14270/get-exit-status-of-process-thats-piped-to-another cmd = fmt.Sprintf("set -o pipefail; %s", cmd) diff --git a/util/util.go b/util/util.go index 74781e99d4..6942fb7c2d 100644 --- a/util/util.go +++ b/util/util.go @@ -124,3 +124,14 @@ func PrependProxyEnv(cmd string) string { // } // return time.Unix(i, 0), nil // } + +// Truncate truncates string to the length +func Truncate(str string, length int) string { + if length < 0 { + return str + } + if length <= len(str) { + return str[:length] + } + return str +} diff --git a/util/util_test.go b/util/util_test.go index 50035170f4..2ec538d4ed 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -131,3 +131,43 @@ func TestPrependHTTPProxyEnv(t *testing.T) { } } + +func TestTruncate(t *testing.T) { + var tests = []struct { + in string + length int + out string + }{ + { + in: "abcde", + length: 3, + out: "abc", + }, + { + in: "abcdefg", + length: 5, + out: "abcde", + }, + { + in: "abcdefg", + length: 10, + out: "abcdefg", + }, + { + in: "abcdefg", + length: 0, + out: "", + }, + { + in: "abcdefg", + length: -1, + out: "abcdefg", + }, + } + for _, tt := range tests { + actual := Truncate(tt.in, tt.length) + if actual != tt.out { + t.Errorf("\nexpected: %s\n actual: %s", tt.out, actual) + } + } +}