diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..de3f6539
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+.vscode
+coverage.out
+issues/
+*.txt
+vendor/
+log/
+.gitmodules
+vuls
+*.sqlite3
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..7a337d18
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+
+TODO
+
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..b4199289
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,52 @@
+.PHONY: \
+ all \
+ vendor \
+ lint \
+ vet \
+ fmt \
+ fmtcheck \
+ pretest \
+ test \
+ integration \
+ cov \
+ clean
+
+SRCS = $(shell git ls-files '*.go')
+PKGS = ./. ./db ./config ./models ./report ./cveapi ./scan ./util ./commands
+
+all: test
+
+vendor:
+ @ go get -v github.com/mjibson/party
+ party -d external -c -u
+
+lint:
+ @ go get -v github.com/golang/lint/golint
+ $(foreach file,$(SRCS),golint $(file) || exit;)
+
+vet:
+ @-go get -v golang.org/x/tools/cmd/vet
+ $(foreach pkg,$(PKGS),go vet $(pkg);)
+
+fmt:
+ gofmt -w $(SRCS)
+
+fmtcheck:
+ $(foreach file,$(SRCS),gofmt -d $(file);)
+
+pretest: lint vet fmtcheck
+
+test: pretest
+ $(foreach pkg,$(PKGS),go test -v $(pkg) || exit;)
+
+unused :
+ $(foreach pkg,$(PKGS),unused $(pkg);)
+
+cov:
+ @ go get -v github.com/axw/gocov/gocov
+ @ go get golang.org/x/tools/cmd/cover
+ gocov test | gocov report
+
+clean:
+ $(foreach pkg,$(PKGS),go clean $(pkg) || exit;)
+
diff --git a/commands/discover.go b/commands/discover.go
new file mode 100644
index 00000000..22964371
--- /dev/null
+++ b/commands/discover.go
@@ -0,0 +1,158 @@
+/* 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 commands
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+ "text/template"
+
+ "github.com/google/subcommands"
+ "golang.org/x/net/context"
+
+ "github.com/Sirupsen/logrus"
+ ps "github.com/kotakanbe/go-pingscanner"
+)
+
+// DiscoverCmd is Subcommand of host discovery mode
+type DiscoverCmd struct {
+}
+
+// Name return subcommand name
+func (*DiscoverCmd) Name() string { return "discover" }
+
+// Synopsis return synopsis
+func (*DiscoverCmd) Synopsis() string { return "Host discovery in the CIDR." }
+
+// Usage return usage
+func (*DiscoverCmd) Usage() string {
+ return `discover:
+ discover 192.168.0.0/24
+
+`
+}
+
+// SetFlags set flag
+func (p *DiscoverCmd) SetFlags(f *flag.FlagSet) {
+}
+
+// Execute execute
+func (p *DiscoverCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
+ // validate
+ if len(f.Args()) == 0 {
+ return subcommands.ExitUsageError
+ }
+
+ for _, cidr := range f.Args() {
+ scanner := ps.PingScanner{
+ CIDR: cidr,
+ PingOptions: []string{
+ "-c1",
+ "-t1",
+ },
+ NumOfConcurrency: 100,
+ }
+ hosts, err := scanner.Scan()
+
+ if err != nil {
+ logrus.Errorf("Host Discovery failed. err: %s", err)
+ return subcommands.ExitFailure
+ }
+
+ if len(hosts) < 1 {
+ logrus.Errorf("Active hosts not found in %s.", cidr)
+ return subcommands.ExitSuccess
+ } else if err := printConfigToml(hosts); err != nil {
+ logrus.Errorf("Failed to parse template. err: %s", err)
+ return subcommands.ExitFailure
+ }
+ }
+ return subcommands.ExitSuccess
+}
+
+// Output the tmeplate of config.toml
+func printConfigToml(ips []string) (err error) {
+ const tomlTempale = `
+[slack]
+hookURL = "https://hooks.slack.com/services/abc123/defghijklmnopqrstuvwxyz"
+channel = "#channel-name"
+#channel = "#{servername}"
+iconEmoji = ":ghost:"
+authUser = "username"
+notifyUsers = ["@username"]
+
+[mail]
+smtpAddr = "smtp.gmail.com"
+smtpPort = 465
+user = "username"
+password = "password"
+from = "from@address.com"
+to = ["to@address.com"]
+cc = ["cc@address.com"]
+subjectPrefix = "[vuls]"
+
+[default]
+#port = "22"
+#user = "username"
+#password = "password"
+#keyPath = "/home/username/.ssh/id_rsa"
+#keyPassword = "password"
+
+[servers]
+{{- $names:= .Names}}
+{{range $i, $ip := .IPs}}
+[servers.{{index $names $i}}]
+host = "{{$ip}}"
+#port = "22"
+#user = "root"
+#password = "password"
+#keyPath = "/home/username/.ssh/id_rsa"
+#keyPassword = "password"
+#cpeNames = [
+# "cpe:/a:rubyonrails:ruby_on_rails:4.2.1",
+#]
+{{end}}
+
+`
+ var tpl *template.Template
+ if tpl, err = template.New("tempalte").Parse(tomlTempale); err != nil {
+ return
+ }
+
+ type activeHosts struct {
+ IPs []string
+ Names []string
+ }
+
+ a := activeHosts{IPs: ips}
+ names := []string{}
+ for _, ip := range ips {
+ // TOML section header must not contain "."
+ name := strings.Replace(ip, ".", "-", -1)
+ names = append(names, name)
+ }
+ a.Names = names
+
+ fmt.Println("# Create config.toml using below and then ./vuls --config=/path/to/config.toml")
+ if err = tpl.Execute(os.Stdout, a); err != nil {
+ return
+ }
+ return
+}
diff --git a/commands/prepare.go b/commands/prepare.go
new file mode 100644
index 00000000..50d83f32
--- /dev/null
+++ b/commands/prepare.go
@@ -0,0 +1,131 @@
+/* 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 commands
+
+import (
+ "flag"
+ "os"
+
+ "github.com/Sirupsen/logrus"
+ c "github.com/future-architect/vuls/config"
+ "github.com/future-architect/vuls/scan"
+ "github.com/future-architect/vuls/util"
+ "github.com/google/subcommands"
+ "golang.org/x/net/context"
+)
+
+// PrepareCmd is Subcommand of host discovery mode
+type PrepareCmd struct {
+ debug bool
+ configPath string
+
+ useUnattendedUpgrades bool
+}
+
+// Name return subcommand name
+func (*PrepareCmd) Name() string { return "prepare" }
+
+// Synopsis return synopsis
+func (*PrepareCmd) Synopsis() string {
+ // return "Install packages Ubuntu: unattended-upgrade, CentOS: yum-plugin-security)"
+ return `Install required packages to scan.
+ CentOS: yum-plugin-security, yum-plugin-changelog
+ Amazon: None
+ RHEL: TODO
+ Ubuntu: None
+
+ `
+}
+
+// Usage return usage
+func (*PrepareCmd) Usage() string {
+ return `prepare:
+ prepare [-config=/path/to/config.toml] [-debug]
+
+`
+}
+
+// SetFlags set flag
+func (p *PrepareCmd) SetFlags(f *flag.FlagSet) {
+
+ f.BoolVar(&p.debug, "debug", false, "debug mode")
+
+ defaultConfPath := os.Getenv("PWD") + "/config.toml"
+ f.StringVar(&p.configPath, "config", defaultConfPath, "/path/to/toml")
+
+ f.BoolVar(
+ &p.useUnattendedUpgrades,
+ "use-unattended-upgrades",
+ false,
+ "[Depricated] For Ubuntu, install unattended-upgrades",
+ )
+}
+
+// Execute execute
+func (p *PrepareCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
+ logrus.Infof("Begin Preparing (config: %s)", p.configPath)
+
+ err := c.Load(p.configPath)
+ if err != nil {
+ logrus.Errorf("Error loading %s, %s", p.configPath, err)
+ return subcommands.ExitUsageError
+ }
+
+ target := make(map[string]c.ServerInfo)
+ for _, arg := range f.Args() {
+ found := false
+ for servername, info := range c.Conf.Servers {
+ if servername == arg {
+ target[servername] = info
+ found = true
+ break
+ }
+ }
+ if !found {
+ logrus.Errorf("%s is not in config", arg)
+ return subcommands.ExitUsageError
+ }
+ }
+ if 0 < len(f.Args()) {
+ c.Conf.Servers = target
+ }
+
+ c.Conf.Debug = p.debug
+ c.Conf.UseUnattendedUpgrades = p.useUnattendedUpgrades
+
+ // Set up custom logger
+ logger := util.NewCustomLogger(c.ServerInfo{})
+
+ logger.Info("Detecting OS... ")
+ err = scan.InitServers(logger)
+ if err != nil {
+ logger.Errorf("Failed to init servers. err: %s", err)
+ return subcommands.ExitFailure
+ }
+
+ logger.Info("Installing...")
+ if errs := scan.Prepare(); 0 < len(errs) {
+ for _, e := range errs {
+ logger.Errorf("Failed: %s.", e)
+ }
+ return subcommands.ExitFailure
+ }
+
+ logger.Info("Success")
+ return subcommands.ExitSuccess
+}
diff --git a/commands/scan.go b/commands/scan.go
new file mode 100644
index 00000000..7e97ff75
--- /dev/null
+++ b/commands/scan.go
@@ -0,0 +1,241 @@
+/* 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 commands
+
+import (
+ "flag"
+ "os"
+
+ "github.com/Sirupsen/logrus"
+ c "github.com/future-architect/vuls/config"
+ "github.com/future-architect/vuls/cveapi"
+ "github.com/future-architect/vuls/db"
+ "github.com/future-architect/vuls/report"
+ "github.com/future-architect/vuls/scan"
+ "github.com/future-architect/vuls/util"
+ "github.com/google/subcommands"
+ "golang.org/x/net/context"
+)
+
+// ScanCmd is Subcommand of host discovery mode
+type ScanCmd struct {
+ lang string
+ debug bool
+ debugSQL bool
+
+ configPath string
+
+ dbpath string
+ cveDictionaryURL string
+ cvssScoreOver float64
+ httpProxy string
+
+ useYumPluginSecurity bool
+ useUnattendedUpgrades bool
+
+ // reporting
+ reportSlack bool
+ reportMail bool
+}
+
+// Name return subcommand name
+func (*ScanCmd) Name() string { return "scan" }
+
+// Synopsis return synopsis
+func (*ScanCmd) Synopsis() string { return "Scan vulnerabilities." }
+
+// Usage return usage
+func (*ScanCmd) Usage() string {
+ return `scan:
+ scan
+ [-lang=en|ja]
+ [-config=/path/to/config.toml]
+ [-dbpath=/path/to/vuls.sqlite3]
+ [-cve-dictionary-url=http://127.0.0.1:1323]
+ [-cvss-over=7]
+ [-report-slack]
+ [-report-mail]
+ [-http-proxy=http://192.168.0.1:8080]
+ [-debug]
+ [-debug-sql]
+`
+}
+
+// SetFlags set flag
+func (p *ScanCmd) SetFlags(f *flag.FlagSet) {
+ f.StringVar(&p.lang, "lang", "en", "[en|ja]")
+ f.BoolVar(&p.debug, "debug", false, "debug mode")
+ f.BoolVar(&p.debugSQL, "debug-sql", false, "SQL debug mode")
+
+ defaultConfPath := os.Getenv("PWD") + "/config.toml"
+ f.StringVar(&p.configPath, "config", defaultConfPath, "/path/to/toml")
+
+ defaultDBPath := os.Getenv("PWD") + "/vuls.sqlite3"
+ f.StringVar(&p.dbpath, "dbpath", defaultDBPath, "/path/to/sqlite3")
+
+ defaultURL := "http://127.0.0.1:1323"
+ f.StringVar(
+ &p.cveDictionaryURL,
+ "cve-dictionary-url",
+ defaultURL,
+ "http://CVE.Dictionary")
+
+ f.Float64Var(
+ &p.cvssScoreOver,
+ "cvss-over",
+ 0,
+ "-cvss-over=6.5 means reporting CVSS Score 6.5 and over (default: 0 (means report all))")
+
+ f.StringVar(
+ &p.httpProxy,
+ "http-proxy",
+ "",
+ "http://proxy-url:port (default: empty)",
+ )
+
+ f.BoolVar(&p.reportSlack, "report-slack", false, "Slack report")
+ f.BoolVar(&p.reportMail, "report-mail", false, "Email report")
+
+ f.BoolVar(
+ &p.useYumPluginSecurity,
+ "use-yum-plugin-security",
+ false,
+ "[Depricated] For CentOS 5. Scan by yum-plugin-security or not (use yum check-update by default)",
+ )
+
+ f.BoolVar(
+ &p.useUnattendedUpgrades,
+ "use-unattended-upgrades",
+ false,
+ "[Depricated] For Ubuntu. Scan by unattended-upgrades or not (use apt-get upgrade --dry-run by default)",
+ )
+
+}
+
+// Execute execute
+func (p *ScanCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
+
+ logrus.Infof("Begin scannig (config: %s)", p.configPath)
+ err := c.Load(p.configPath)
+ if err != nil {
+ logrus.Errorf("Error loading %s, %s", p.configPath, err)
+ return subcommands.ExitUsageError
+ }
+
+ target := make(map[string]c.ServerInfo)
+ for _, arg := range f.Args() {
+ found := false
+ for servername, info := range c.Conf.Servers {
+ if servername == arg {
+ target[servername] = info
+ found = true
+ break
+ }
+ }
+ if !found {
+ logrus.Errorf("%s is not in config", arg)
+ return subcommands.ExitUsageError
+ }
+ }
+ if 0 < len(f.Args()) {
+ c.Conf.Servers = target
+ }
+
+ c.Conf.Lang = p.lang
+ c.Conf.Debug = p.debug
+ c.Conf.DebugSQL = p.debugSQL
+
+ // logger
+ Log := util.NewCustomLogger(c.ServerInfo{})
+
+ // report
+ reports := []report.ResultWriter{
+ report.TextWriter{},
+ report.LogrusWriter{},
+ }
+ if p.reportSlack {
+ reports = append(reports, report.SlackWriter{})
+ }
+ if p.reportMail {
+ reports = append(reports, report.MailWriter{})
+ }
+
+ c.Conf.DBPath = p.dbpath
+ c.Conf.CveDictionaryURL = p.cveDictionaryURL
+ c.Conf.HTTPProxy = p.httpProxy
+ c.Conf.UseYumPluginSecurity = p.useYumPluginSecurity
+ c.Conf.UseUnattendedUpgrades = p.useUnattendedUpgrades
+
+ Log.Info("Validating Config...")
+ if !c.Conf.Validate() {
+ return subcommands.ExitUsageError
+ }
+
+ if ok, err := cveapi.CveClient.CheckHealth(); !ok {
+ Log.Errorf("CVE HTTP server is not running. %#v", cveapi.CveClient)
+ Log.Fatal(err)
+ return subcommands.ExitFailure
+ }
+
+ Log.Info("Detecting OS... ")
+ err = scan.InitServers(Log)
+ if err != nil {
+ Log.Errorf("Failed to init servers. err: %s", err)
+ return subcommands.ExitFailure
+ }
+
+ Log.Info("Scanning vulnerabilities... ")
+ if errs := scan.Scan(); 0 < len(errs) {
+ for _, e := range errs {
+ Log.Errorf("Failed to scan. err: %s.", e)
+ }
+ return subcommands.ExitFailure
+ }
+
+ scanResults, err := scan.GetScanResults()
+ if err != nil {
+ Log.Fatal(err)
+ return subcommands.ExitFailure
+ }
+
+ Log.Info("Reporting...")
+ filtered := scanResults.FilterByCvssOver()
+ for _, w := range reports {
+ if err := w.Write(filtered); err != nil {
+ Log.Fatalf("Failed to output report, err: %s", err)
+ return subcommands.ExitFailure
+ }
+ }
+
+ Log.Info("Insert to DB...")
+ if err := db.OpenDB(); err != nil {
+ Log.Errorf("Failed to open DB. datafile: %s, err: %s", c.Conf.DBPath, err)
+ return subcommands.ExitFailure
+ }
+ if err := db.MigrateDB(); err != nil {
+ Log.Errorf("Failed to migrate. err: %s", err)
+ return subcommands.ExitFailure
+ }
+
+ if err := db.Insert(scanResults); err != nil {
+ Log.Fatalf("Failed to insert. dbpath: %s, err: %s", c.Conf.DBPath, err)
+ return subcommands.ExitFailure
+ }
+
+ return subcommands.ExitSuccess
+}
diff --git a/commands/tui.go b/commands/tui.go
new file mode 100644
index 00000000..de3a6212
--- /dev/null
+++ b/commands/tui.go
@@ -0,0 +1,68 @@
+/* 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 commands
+
+import (
+ "flag"
+ "fmt"
+ "os"
+
+ c "github.com/future-architect/vuls/config"
+ "github.com/future-architect/vuls/report"
+ "github.com/google/subcommands"
+ "golang.org/x/net/context"
+)
+
+// TuiCmd is Subcommand of host discovery mode
+type TuiCmd struct {
+ lang string
+ debugSQL bool
+ dbpath string
+}
+
+// Name return subcommand name
+func (*TuiCmd) Name() string { return "tui" }
+
+// Synopsis return synopsis
+func (*TuiCmd) Synopsis() string { return "Run Tui view to anayze vulnerabilites." }
+
+// Usage return usage
+func (*TuiCmd) Usage() string {
+ return `tui:
+ tui [-dbpath=/path/to/vuls.sqlite3]
+
+`
+}
+
+// SetFlags set flag
+func (p *TuiCmd) SetFlags(f *flag.FlagSet) {
+ // f.StringVar(&p.lang, "lang", "en", "[en|ja]")
+ f.BoolVar(&p.debugSQL, "debug-sql", false, "debug SQL")
+
+ defaultDBPath := os.Getenv("PWD") + "/vuls.sqlite3"
+ f.StringVar(&p.dbpath, "dbpath", defaultDBPath,
+ fmt.Sprintf("/path/to/sqlite3 (default: %s)", defaultDBPath))
+}
+
+// Execute execute
+func (p *TuiCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
+ c.Conf.Lang = "en"
+ c.Conf.DebugSQL = p.debugSQL
+ c.Conf.DBPath = p.dbpath
+ return report.RunTui()
+}
diff --git a/config/color.go b/config/color.go
new file mode 100644
index 00000000..4c501535
--- /dev/null
+++ b/config/color.go
@@ -0,0 +1,32 @@
+/* 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 config
+
+var (
+ // Colors has ansi color list
+ Colors = []string{
+ "\033[32m", // green
+ "\033[33m", // yellow
+ "\033[36m", // cyan
+ "\033[35m", // magenta
+ "\033[31m", // red
+ "\033[34m", // blue
+ }
+ // ResetColor is reset color
+ ResetColor = "\033[0m"
+)
diff --git a/config/config.go b/config/config.go
new file mode 100644
index 00000000..97171a09
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,221 @@
+/* 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 config
+
+import (
+ "fmt"
+ "strings"
+
+ log "github.com/Sirupsen/logrus"
+ valid "github.com/asaskevich/govalidator"
+)
+
+// Conf has Configuration
+var Conf Config
+
+//Config is struct of Configuration
+type Config struct {
+ Debug bool
+ DebugSQL bool
+ Lang string
+
+ Mail smtpConf
+ Slack SlackConf
+ Default ServerInfo
+ Servers map[string]ServerInfo
+
+ CveDictionaryURL string `valid:"url"`
+
+ CvssScoreOver float64
+ HTTPProxy string `valid:"url"`
+ DBPath string
+ // CpeNames []string
+ // SummaryMode bool
+ UseYumPluginSecurity bool
+ UseUnattendedUpgrades bool
+}
+
+// Validate configuration
+func (c Config) Validate() bool {
+ errs := []error{}
+
+ if len(c.DBPath) != 0 {
+ if ok, _ := valid.IsFilePath(c.DBPath); !ok {
+ errs = append(errs, fmt.Errorf(
+ "SQLite3 DB path must be a *Absolute* file path. dbpath: %s", c.DBPath))
+ }
+ }
+
+ _, err := valid.ValidateStruct(c)
+ if err != nil {
+ errs = append(errs, err)
+ }
+
+ if mailerrs := c.Mail.Validate(); 0 < len(mailerrs) {
+ errs = append(errs, mailerrs...)
+ }
+
+ if slackerrs := c.Slack.Validate(); 0 < len(slackerrs) {
+ errs = append(errs, slackerrs...)
+ }
+
+ for _, err := range errs {
+ log.Error(err)
+ }
+
+ return len(errs) == 0
+}
+
+// smtpConf is smtp config
+type smtpConf struct {
+ SMTPAddr string
+ SMTPPort string `valid:"port"`
+
+ User string
+ Password string
+ From string
+ To []string
+ Cc []string
+ SubjectPrefix string
+
+ UseThisTime bool
+}
+
+func checkEmails(emails []string) (errs []error) {
+ for _, addr := range emails {
+ if len(addr) == 0 {
+ return
+ }
+ if ok := valid.IsEmail(addr); !ok {
+ errs = append(errs, fmt.Errorf("Invalid email address. email: %s", addr))
+ }
+ }
+ return
+}
+
+// Validate SMTP configuration
+func (c *smtpConf) Validate() (errs []error) {
+
+ if !c.UseThisTime {
+ return
+ }
+
+ // Check Emails fromat
+ emails := []string{}
+ emails = append(emails, c.From)
+ emails = append(emails, c.To...)
+ emails = append(emails, c.Cc...)
+
+ if emailErrs := checkEmails(emails); 0 < len(emailErrs) {
+ errs = append(errs, emailErrs...)
+ }
+
+ if len(c.SMTPAddr) == 0 {
+ errs = append(errs, fmt.Errorf("smtpAddr must not be empty"))
+ }
+ if len(c.SMTPPort) == 0 {
+ errs = append(errs, fmt.Errorf("smtpPort must not be empty"))
+ }
+ if len(c.To) == 0 {
+ errs = append(errs, fmt.Errorf("To required at least one address"))
+ }
+ if len(c.From) == 0 {
+ errs = append(errs, fmt.Errorf("From required at least one address"))
+ }
+
+ _, err := valid.ValidateStruct(c)
+ if err != nil {
+ errs = append(errs, err)
+ }
+ return
+}
+
+// SlackConf is slack config
+type SlackConf struct {
+ HookURL string `valid:"url"`
+ Channel string `json:"channel"`
+ IconEmoji string `json:"icon_emoji"`
+ AuthUser string `json:"username"`
+
+ NotifyUsers []string
+ Text string `json:"text"`
+
+ UseThisTime bool
+}
+
+// Validate validates configuration
+func (c *SlackConf) Validate() (errs []error) {
+
+ if !c.UseThisTime {
+ return
+ }
+
+ if len(c.HookURL) == 0 {
+ errs = append(errs, fmt.Errorf("hookURL must not be empty"))
+ }
+
+ if len(c.Channel) == 0 {
+ errs = append(errs, fmt.Errorf("channel must not be empty"))
+ } else {
+ if !(strings.HasPrefix(c.Channel, "#") ||
+ c.Channel == "${servername}") {
+ errs = append(errs, fmt.Errorf(
+ "channel's prefix must be '#', channel: %s", c.Channel))
+ }
+ }
+
+ if len(c.AuthUser) == 0 {
+ errs = append(errs, fmt.Errorf("authUser must not be empty"))
+ }
+
+ _, err := valid.ValidateStruct(c)
+ if err != nil {
+ errs = append(errs, err)
+ }
+
+ // TODO check if slack configration is valid
+
+ return
+}
+
+// ServerInfo has SSH Info, additional CPE packages to scan.
+type ServerInfo struct {
+ ServerName string
+ User string
+ Password string
+ Host string
+ Port string
+ KeyPath string
+ KeyPassword string
+ SudoOpt SudoOption
+
+ CpeNames []string
+
+ // DebugLog Color
+ LogMsgAnsiColor string
+}
+
+// SudoOption is flag of sudo option.
+type SudoOption struct {
+
+ // echo pass | sudo -S ls
+ ExecBySudo bool
+
+ // echo pass | sudo sh -C 'ls'
+ ExecBySudoSh bool
+}
diff --git a/config/jsonloader.go b/config/jsonloader.go
new file mode 100644
index 00000000..a5a1e406
--- /dev/null
+++ b/config/jsonloader.go
@@ -0,0 +1,29 @@
+/* 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 config
+
+import "fmt"
+
+// JSONLoader loads configuration
+type JSONLoader struct {
+}
+
+// Load load the configuraiton JSON file specified by path arg.
+func (c JSONLoader) Load(path string) (err error) {
+ return fmt.Errorf("Not implement yet")
+}
diff --git a/config/loader.go b/config/loader.go
new file mode 100644
index 00000000..bb1151cb
--- /dev/null
+++ b/config/loader.go
@@ -0,0 +1,33 @@
+/* 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 config
+
+// Load loads configuration
+func Load(path string) error {
+
+ //TODO if path's suffix .toml
+ var loader Loader
+ loader = TOMLLoader{}
+
+ return loader.Load(path)
+}
+
+// Loader is interface of concrete loader
+type Loader interface {
+ Load(string) error
+}
diff --git a/config/tomlloader.go b/config/tomlloader.go
new file mode 100644
index 00000000..3056d624
--- /dev/null
+++ b/config/tomlloader.go
@@ -0,0 +1,98 @@
+/* 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 config
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/BurntSushi/toml"
+ log "github.com/Sirupsen/logrus"
+ "github.com/k0kubun/pp"
+)
+
+// TOMLLoader loads config
+type TOMLLoader struct {
+}
+
+// Load load the configuraiton TOML file specified by path arg.
+func (c TOMLLoader) Load(pathToToml string) (err error) {
+ var conf Config
+ if _, err := toml.DecodeFile(pathToToml, &conf); err != nil {
+ log.Error("Load config failed.", err)
+ return err
+ }
+
+ Conf.Mail = conf.Mail
+ Conf.Slack = conf.Slack
+
+ d := conf.Default
+ Conf.Default = d
+ servers := make(map[string]ServerInfo)
+
+ i := 0
+ for name, v := range conf.Servers {
+ s := ServerInfo{ServerName: name}
+ s.User = v.User
+ if s.User == "" {
+ s.User = d.User
+ }
+
+ s.Password = v.Password
+ if s.Password == "" {
+ s.Password = d.Password
+ }
+
+ s.Host = v.Host
+
+ s.Port = v.Port
+ if s.Port == "" {
+ s.Port = d.Port
+ }
+
+ s.KeyPath = v.KeyPath
+ if s.KeyPath == "" {
+ s.KeyPath = d.KeyPath
+ }
+ if s.KeyPath != "" {
+ if _, err := os.Stat(s.KeyPath); err != nil {
+ return fmt.Errorf(
+ "config.toml is invalid. keypath: %s not exists", s.KeyPath)
+ }
+ }
+
+ s.KeyPassword = v.KeyPassword
+ if s.KeyPassword == "" {
+ s.KeyPassword = d.KeyPassword
+ }
+
+ s.CpeNames = v.CpeNames
+ if len(s.CpeNames) == 0 {
+ s.CpeNames = d.CpeNames
+ }
+
+ s.LogMsgAnsiColor = Colors[i%len(conf.Servers)]
+ i++
+
+ servers[name] = s
+ }
+ log.Debug("Config loaded.")
+ log.Debugf("%s", pp.Sprintf("%v", servers))
+ Conf.Servers = servers
+ return
+}
diff --git a/cveapi/cve_client.go b/cveapi/cve_client.go
new file mode 100644
index 00000000..728359d4
--- /dev/null
+++ b/cveapi/cve_client.go
@@ -0,0 +1,240 @@
+/* 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 cveapi
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "sort"
+ "time"
+
+ "github.com/cenkalti/backoff"
+ "github.com/parnurzeal/gorequest"
+
+ log "github.com/Sirupsen/logrus"
+ "github.com/future-architect/vuls/config"
+ "github.com/future-architect/vuls/util"
+ cve "github.com/kotakanbe/go-cve-dictionary/models"
+)
+
+// CveClient is api client of CVE disctionary service.
+var CveClient cvedictClient
+
+type cvedictClient struct {
+ // httpProxy string
+ baseURL string
+}
+
+func (api *cvedictClient) initialize() {
+ api.baseURL = config.Conf.CveDictionaryURL
+}
+
+func (api cvedictClient) CheckHealth() (ok bool, err error) {
+ api.initialize()
+ url := fmt.Sprintf("%s/health", api.baseURL)
+ var errs []error
+ var resp *http.Response
+ resp, _, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End()
+ // resp, _, errs = gorequest.New().Proxy(api.httpProxy).Get(url).End()
+ if len(errs) > 0 || resp.StatusCode != 200 {
+ return false, fmt.Errorf("Failed to request to CVE server. url: %s, errs: %v",
+ url,
+ errs,
+ )
+ }
+ return true, nil
+}
+
+type response struct {
+ Key string
+ CveDetail cve.CveDetail
+}
+
+func (api cvedictClient) FetchCveDetails(cveIDs []string) (cveDetails cve.CveDetails, err error) {
+ api.baseURL = config.Conf.CveDictionaryURL
+ 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 {
+ 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, cve.CveDetail{
+ CveID: res.Key,
+ })
+ } else {
+ cveDetails = append(cveDetails, res.CveDetail)
+ }
+ case err := <-errChan:
+ errs = append(errs, err)
+ case <-timeout:
+ return []cve.CveDetail{}, fmt.Errorf("Timeout Fetching CVE")
+ }
+ }
+ if len(errs) != 0 {
+ return []cve.CveDetail{},
+ fmt.Errorf("Failed to fetch CVE. err: %v", errs)
+ }
+
+ // order by CVE ID desc
+ sort.Sort(cveDetails)
+ 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()
+ if len(errs) > 0 || 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)
+ }
+ 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,
+ }
+}
+
+// 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 len(errs) > 0 || 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
+}
+
+func (api cvedictClient) FetchCveDetailsByCpeName(cpeName string) ([]cve.CveDetail, error) {
+ api.baseURL = config.Conf.CveDictionaryURL
+
+ url, err := util.URLPathJoin(api.baseURL, "cpes")
+ if err != nil {
+ return []cve.CveDetail{}, err
+ }
+
+ query := map[string]string{"name": cpeName}
+ log.Debugf("HTTP Request to %s, query: %#v", url, query)
+ return api.httpPost(cpeName, url, query)
+}
+
+func (api cvedictClient) httpPost(key, url string, query map[string]string) ([]cve.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)
+ for key := range query {
+ req = req.Send(fmt.Sprintf("%s=%s", key, query[key])).Type("json")
+ }
+ resp, body, errs = req.End()
+ if len(errs) > 0 || resp.StatusCode != 200 {
+ return 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 {
+ return []cve.CveDetail{}, fmt.Errorf("HTTP Error %s", err)
+ }
+
+ cveDetails := []cve.CveDetail{}
+ if err := json.Unmarshal([]byte(body), &cveDetails); err != nil {
+ return []cve.CveDetail{},
+ fmt.Errorf("Failed to Unmarshall. body: %s, err: %s", body, err)
+ }
+ return cveDetails, nil
+}
diff --git a/db/db.go b/db/db.go
new file mode 100644
index 00000000..7ba2c556
--- /dev/null
+++ b/db/db.go
@@ -0,0 +1,272 @@
+/* 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 db
+
+import (
+ "fmt"
+ "sort"
+ "time"
+
+ "github.com/future-architect/vuls/config"
+ m "github.com/future-architect/vuls/models"
+ "github.com/jinzhu/gorm"
+ cvedb "github.com/kotakanbe/go-cve-dictionary/db"
+ cve "github.com/kotakanbe/go-cve-dictionary/models"
+)
+
+var db *gorm.DB
+
+// OpenDB opens Database
+func OpenDB() (err error) {
+ db, err = gorm.Open("sqlite3", config.Conf.DBPath)
+ if err != nil {
+ err = fmt.Errorf("Failed to open DB. datafile: %s, err: %s", config.Conf.DBPath, err)
+ return
+
+ }
+ db.LogMode(config.Conf.DebugSQL)
+ return
+}
+
+// MigrateDB migrates Database
+func MigrateDB() error {
+ if err := db.AutoMigrate(
+ &m.ScanHistory{},
+ &m.ScanResult{},
+ // &m.NWLink{},
+ &m.CveInfo{},
+ &m.CpeName{},
+ &m.PackageInfo{},
+ &m.DistroAdvisory{},
+ &cve.CveDetail{},
+ &cve.Jvn{},
+ &cve.Nvd{},
+ &cve.Reference{},
+ &cve.Cpe{},
+ ).Error; err != nil {
+ return fmt.Errorf("Failed to migrate. err: %s", err)
+ }
+
+ errMsg := "Failed to create index. err: %s"
+ // if err := db.Model(&m.NWLink{}).
+ // AddIndex("idx_n_w_links_scan_result_id", "scan_result_id").Error; err != nil {
+ // return fmt.Errorf(errMsg, err)
+ // }
+ if err := db.Model(&m.CveInfo{}).
+ AddIndex("idx_cve_infos_scan_result_id", "scan_result_id").Error; err != nil {
+ return fmt.Errorf(errMsg, err)
+ }
+ if err := db.Model(&m.CpeName{}).
+ AddIndex("idx_cpe_names_cve_info_id", "cve_info_id").Error; err != nil {
+ return fmt.Errorf(errMsg, err)
+ }
+ if err := db.Model(&m.PackageInfo{}).
+ AddIndex("idx_package_infos_cve_info_id", "cve_info_id").Error; err != nil {
+ return fmt.Errorf(errMsg, err)
+ }
+ if err := db.Model(&m.DistroAdvisory{}).
+ //TODO check table name
+ AddIndex("idx_distro_advisories_cve_info_id", "cve_info_id").Error; err != nil {
+ return fmt.Errorf(errMsg, err)
+ }
+ if err := db.Model(&cve.CveDetail{}).
+ AddIndex("idx_cve_detail_cve_info_id", "cve_info_id").Error; err != nil {
+ return fmt.Errorf(errMsg, err)
+ }
+ if err := db.Model(&cve.CveDetail{}).
+ AddIndex("idx_cve_detail_cveid", "cve_id").Error; err != nil {
+ return fmt.Errorf(errMsg, err)
+ }
+ if err := db.Model(&cve.Nvd{}).
+ AddIndex("idx_nvds_cve_detail_id", "cve_detail_id").Error; err != nil {
+ return fmt.Errorf(errMsg, err)
+ }
+ if err := db.Model(&cve.Jvn{}).
+ AddIndex("idx_jvns_cve_detail_id", "cve_detail_id").Error; err != nil {
+ return fmt.Errorf(errMsg, err)
+ }
+ if err := db.Model(&cve.Cpe{}).
+ AddIndex("idx_cpes_jvn_id", "jvn_id").Error; err != nil {
+ return fmt.Errorf(errMsg, err)
+ }
+ if err := db.Model(&cve.Reference{}).
+ AddIndex("idx_references_jvn_id", "jvn_id").Error; err != nil {
+ return fmt.Errorf(errMsg, err)
+ }
+ if err := db.Model(&cve.Cpe{}).
+ AddIndex("idx_cpes_nvd_id", "nvd_id").Error; err != nil {
+ return fmt.Errorf(errMsg, err)
+ }
+ if err := db.Model(&cve.Reference{}).
+ AddIndex("idx_references_nvd_id", "nvd_id").Error; err != nil {
+ return fmt.Errorf(errMsg, err)
+ }
+
+ return nil
+}
+
+// Insert inserts scan results into DB
+func Insert(results []m.ScanResult) error {
+ for _, r := range results {
+ r.KnownCves = resetGormIDs(r.KnownCves)
+ r.UnknownCves = resetGormIDs(r.UnknownCves)
+ }
+
+ history := m.ScanHistory{
+ ScanResults: results,
+ ScannedAt: time.Now(),
+ }
+
+ db = db.Set("gorm:save_associations", false)
+ if err := db.Create(&history).Error; err != nil {
+ return err
+ }
+ for _, scanResult := range history.ScanResults {
+ scanResult.ScanHistoryID = history.ID
+ if err := db.Create(&scanResult).Error; err != nil {
+ return err
+ }
+ if err := insertCveInfos(scanResult.ID, scanResult.KnownCves); err != nil {
+ return err
+ }
+ if err := insertCveInfos(scanResult.ID, scanResult.UnknownCves); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func insertCveInfos(scanResultID uint, infos []m.CveInfo) error {
+ for _, cveInfo := range infos {
+ cveInfo.ScanResultID = scanResultID
+ if err := db.Create(&cveInfo).Error; err != nil {
+ return err
+ }
+
+ for _, pack := range cveInfo.Packages {
+ pack.CveInfoID = cveInfo.ID
+ if err := db.Create(&pack).Error; err != nil {
+ return err
+ }
+ }
+
+ for _, distroAdvisory := range cveInfo.DistroAdvisories {
+ distroAdvisory.CveInfoID = cveInfo.ID
+ if err := db.Create(&distroAdvisory).Error; err != nil {
+ return err
+ }
+ }
+
+ for _, cpeName := range cveInfo.CpeNames {
+ cpeName.CveInfoID = cveInfo.ID
+ if err := db.Create(&cpeName).Error; err != nil {
+ return err
+ }
+ }
+
+ db = db.Set("gorm:save_associations", true)
+ cveDetail := cveInfo.CveDetail
+ cveDetail.CveInfoID = cveInfo.ID
+ if err := db.Create(&cveDetail).Error; err != nil {
+ return err
+ }
+ db = db.Set("gorm:save_associations", false)
+ }
+ return nil
+}
+
+func resetGormIDs(infos []m.CveInfo) []m.CveInfo {
+ for i := range infos {
+ infos[i].CveDetail.ID = 0
+ // NVD
+ infos[i].CveDetail.Nvd.ID = 0
+ for j := range infos[i].CveDetail.Nvd.Cpes {
+ infos[i].CveDetail.Nvd.Cpes[j].ID = 0
+ }
+ for j := range infos[i].CveDetail.Nvd.References {
+ infos[i].CveDetail.Nvd.References[j].ID = 0
+ }
+
+ // JVN
+ infos[i].CveDetail.Jvn.ID = 0
+ for j := range infos[i].CveDetail.Jvn.Cpes {
+ infos[i].CveDetail.Jvn.Cpes[j].ID = 0
+ }
+ for j := range infos[i].CveDetail.Jvn.References {
+ infos[i].CveDetail.Jvn.References[j].ID = 0
+ }
+
+ //Packages
+ for j := range infos[i].Packages {
+ infos[i].Packages[j].ID = 0
+ infos[i].Packages[j].CveInfoID = 0
+ }
+ }
+ return infos
+}
+
+// SelectLatestScanHistory select latest scan history from DB
+func SelectLatestScanHistory() (m.ScanHistory, error) {
+ scanHistory := m.ScanHistory{}
+ db.Order("scanned_at desc").First(&scanHistory)
+
+ if scanHistory.ID == 0 {
+ return m.ScanHistory{}, fmt.Errorf("No scanHistory records.")
+ }
+
+ results := []m.ScanResult{}
+ db.Model(&scanHistory).Related(&results, "ScanResults")
+ scanHistory.ScanResults = results
+
+ for i, r := range results {
+ // nw := []m.NWLink{}
+ // db.Model(&r).Related(&nw, "NWLinks")
+ // scanHistory.ScanResults[i].NWLinks = nw
+
+ knownCves := selectCveInfos(&r, "KnownCves")
+ sort.Sort(m.CveInfos(knownCves))
+ scanHistory.ScanResults[i].KnownCves = knownCves
+ }
+ return scanHistory, nil
+}
+
+func selectCveInfos(result *m.ScanResult, fieldName string) []m.CveInfo {
+ cveInfos := []m.CveInfo{}
+ db.Model(&result).Related(&cveInfos, fieldName)
+
+ for i, cveInfo := range cveInfos {
+ cveDetail := cve.CveDetail{}
+ db.Model(&cveInfo).Related(&cveDetail, "CveDetail")
+ id := cveDetail.CveID
+ filledCveDetail := cvedb.Get(id, db)
+ cveInfos[i].CveDetail = filledCveDetail
+
+ packs := []m.PackageInfo{}
+ db.Model(&cveInfo).Related(&packs, "Packages")
+ cveInfos[i].Packages = packs
+
+ advisories := []m.DistroAdvisory{}
+ db.Model(&cveInfo).Related(&advisories, "DistroAdvisories")
+ cveInfos[i].DistroAdvisories = advisories
+
+ names := []m.CpeName{}
+ db.Model(&cveInfo).Related(&names, "CpeNames")
+ cveInfos[i].CpeNames = names
+ }
+ return cveInfos
+}
diff --git a/main.go b/main.go
new file mode 100644
index 00000000..896f762e
--- /dev/null
+++ b/main.go
@@ -0,0 +1,44 @@
+/* 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 main
+
+import (
+ "flag"
+ "os"
+
+ "golang.org/x/net/context"
+
+ "github.com/future-architect/vuls/commands"
+ "github.com/google/subcommands"
+
+ _ "github.com/mattn/go-sqlite3"
+)
+
+func main() {
+ subcommands.Register(subcommands.HelpCommand(), "")
+ subcommands.Register(subcommands.FlagsCommand(), "")
+ subcommands.Register(subcommands.CommandsCommand(), "")
+ subcommands.Register(&commands.DiscoverCmd{}, "discover")
+ subcommands.Register(&commands.TuiCmd{}, "tui")
+ subcommands.Register(&commands.ScanCmd{}, "scan")
+ subcommands.Register(&commands.PrepareCmd{}, "prepare")
+
+ flag.Parse()
+ ctx := context.Background()
+ os.Exit(int(subcommands.Execute(ctx)))
+}
diff --git a/models/models.go b/models/models.go
new file mode 100644
index 00000000..05a99318
--- /dev/null
+++ b/models/models.go
@@ -0,0 +1,244 @@
+/* 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 models
+
+import (
+ "fmt"
+ "sort"
+ "time"
+
+ "github.com/future-architect/vuls/config"
+ "github.com/jinzhu/gorm"
+ cve "github.com/kotakanbe/go-cve-dictionary/models"
+)
+
+// ScanHistory is the history of Scanning.
+type ScanHistory struct {
+ gorm.Model
+ ScanResults []ScanResult
+ ScannedAt time.Time
+}
+
+// ScanResults is slice of ScanResult.
+type ScanResults []ScanResult
+
+// FilterByCvssOver is filter function.
+func (results ScanResults) FilterByCvssOver() (filtered ScanResults) {
+ for _, result := range results {
+ cveInfos := []CveInfo{}
+ for _, cveInfo := range result.KnownCves {
+ if config.Conf.CvssScoreOver < cveInfo.CveDetail.CvssScore(config.Conf.Lang) {
+ cveInfos = append(cveInfos, cveInfo)
+ }
+ }
+ result.KnownCves = cveInfos
+ filtered = append(filtered, result)
+ }
+ return
+}
+
+// ScanResult has the result of scanned CVE information.
+type ScanResult struct {
+ gorm.Model
+ ScanHistoryID uint
+
+ ServerName string // TOML Section key
+ // Hostname string
+ Family string
+ Release string
+ // Fqdn string
+ // NWLinks []NWLink
+ KnownCves []CveInfo
+ UnknownCves []CveInfo
+}
+
+// CveSummary summarize the number of CVEs group by CVSSv2 Severity
+func (r ScanResult) CveSummary() string {
+ var high, middle, low, unknown int
+ cves := append(r.KnownCves, r.UnknownCves...)
+ for _, cveInfo := range cves {
+ score := cveInfo.CveDetail.CvssScore(config.Conf.Lang)
+ switch {
+ case 7.0 < score:
+ high++
+ case 4.0 < score:
+ middle++
+ case 0 < score:
+ low++
+ default:
+ unknown++
+ }
+ }
+ return fmt.Sprintf("Total: %d (High:%d Middle:%d Low:%d ?:%d)",
+ high+middle+low+unknown,
+ high, middle, low, unknown,
+ )
+}
+
+// NWLink has network link information.
+type NWLink struct {
+ gorm.Model
+ ScanResultID uint
+
+ IPAddress string
+ Netmask string
+ DevName string
+ LinkState string
+}
+
+// CveInfos is for sorting
+type CveInfos []CveInfo
+
+func (c CveInfos) Len() int {
+ return len(c)
+}
+
+func (c CveInfos) Swap(i, j int) {
+ c[i], c[j] = c[j], c[i]
+}
+
+func (c CveInfos) Less(i, j int) bool {
+ lang := config.Conf.Lang
+ return c[i].CveDetail.CvssScore(lang) > c[j].CveDetail.CvssScore(lang)
+}
+
+// CveInfo has Cve Information.
+type CveInfo struct {
+ gorm.Model
+ ScanResultID uint
+
+ CveDetail cve.CveDetail
+ Packages []PackageInfo
+ DistroAdvisories []DistroAdvisory
+ CpeNames []CpeName
+}
+
+// CpeName has CPE name
+type CpeName struct {
+ gorm.Model
+ CveInfoID uint
+
+ Name string
+}
+
+// PackageInfoList is slice of PackageInfo
+type PackageInfoList []PackageInfo
+
+// Exists returns true if exists the name
+func (ps PackageInfoList) Exists(name string) bool {
+ for _, p := range ps {
+ if p.Name == name {
+ return true
+ }
+ }
+ return false
+}
+
+// UniqByName be uniq by name.
+func (ps PackageInfoList) UniqByName() (distincted PackageInfoList) {
+ set := make(map[string]PackageInfo)
+ for _, p := range ps {
+ set[p.Name] = p
+ }
+ //sort by key
+ keys := []string{}
+ for key := range set {
+ keys = append(keys, key)
+ }
+ sort.Strings(keys)
+ for _, key := range keys {
+ distincted = append(distincted, set[key])
+ }
+ return
+}
+
+// FindByName search PackageInfo by name
+func (ps PackageInfoList) FindByName(name string) (result PackageInfo, found bool) {
+ for _, p := range ps {
+ if p.Name == name {
+ return p, true
+ }
+ }
+ return PackageInfo{}, false
+}
+
+// Find search PackageInfo by name-version-release
+// func (ps PackageInfoList) find(nameVersionRelease string) (PackageInfo, bool) {
+// for _, p := range ps {
+// joined := p.Name
+// if 0 < len(p.Version) {
+// joined = fmt.Sprintf("%s-%s", joined, p.Version)
+// }
+// if 0 < len(p.Release) {
+// joined = fmt.Sprintf("%s-%s", joined, p.Release)
+// }
+// if joined == nameVersionRelease {
+// return p, true
+// }
+// }
+// return PackageInfo{}, false
+// }
+
+// PackageInfo has installed packages.
+type PackageInfo struct {
+ gorm.Model
+ CveInfoID uint
+
+ Name string
+ Version string
+ Release string
+
+ NewVersion string
+ NewRelease string
+}
+
+// ToStringCurrentVersion returns package name-version-release
+func (p PackageInfo) ToStringCurrentVersion() string {
+ str := p.Name
+ if 0 < len(p.Version) {
+ str = fmt.Sprintf("%s-%s", str, p.Version)
+ }
+ if 0 < len(p.Release) {
+ str = fmt.Sprintf("%s-%s", str, p.Release)
+ }
+ return str
+}
+
+// ToStringNewVersion returns package name-version-release
+func (p PackageInfo) ToStringNewVersion() string {
+ str := p.Name
+ if 0 < len(p.NewVersion) {
+ str = fmt.Sprintf("%s-%s", str, p.NewVersion)
+ }
+ if 0 < len(p.NewRelease) {
+ str = fmt.Sprintf("%s-%s", str, p.NewRelease)
+ }
+ return str
+}
+
+// DistroAdvisory has Amazon Linux AMI Security Advisory information.
+//TODO Rename to DistroAdvisory
+type DistroAdvisory struct {
+ gorm.Model
+ CveInfoID uint
+
+ AdvisoryID string
+ Severity string
+ Issued time.Time
+ Updated time.Time
+}
diff --git a/models/models_test.go b/models/models_test.go
new file mode 100644
index 00000000..a6fa25e6
--- /dev/null
+++ b/models/models_test.go
@@ -0,0 +1,54 @@
+/* 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 models
+
+import "testing"
+
+func TestPackageInfosUniqByName(t *testing.T) {
+ var test = struct {
+ in PackageInfoList
+ out PackageInfoList
+ }{
+ PackageInfoList{
+ {
+ Name: "hoge",
+ },
+ {
+ Name: "fuga",
+ },
+ {
+ Name: "hoge",
+ },
+ },
+ PackageInfoList{
+ {
+ Name: "hoge",
+ },
+ {
+ Name: "fuga",
+ },
+ },
+ }
+
+ actual := test.in.UniqByName()
+ for i, ePack := range test.out {
+ if actual[i].Name == ePack.Name {
+ t.Errorf("expected %#v, actual %#v", ePack.Name, actual[i].Name)
+ }
+ }
+}
diff --git a/report/json.go b/report/json.go
new file mode 100644
index 00000000..218eedf6
--- /dev/null
+++ b/report/json.go
@@ -0,0 +1,37 @@
+/* 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 report
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/future-architect/vuls/models"
+)
+
+// JSONWriter writes report as JSON format
+type JSONWriter struct{}
+
+func (w JSONWriter) Write(scanResults []models.ScanResult) (err error) {
+ var j []byte
+ if j, err = json.MarshalIndent(scanResults, "", " "); err != nil {
+ return
+ }
+ fmt.Println(string(j))
+ return nil
+}
diff --git a/report/logrus.go b/report/logrus.go
new file mode 100644
index 00000000..4a663e98
--- /dev/null
+++ b/report/logrus.go
@@ -0,0 +1,51 @@
+/* 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 report
+
+import (
+ "os"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/future-architect/vuls/models"
+ formatter "github.com/kotakanbe/logrus-prefixed-formatter"
+)
+
+// LogrusWriter write to logfile
+type LogrusWriter struct {
+}
+
+func (w LogrusWriter) Write(scanResults []models.ScanResult) error {
+ path := "/var/log/vuls/report.log"
+ f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
+ if err != nil {
+ return err
+ }
+ log := logrus.New()
+ log.Formatter = &formatter.TextFormatter{}
+ log.Out = f
+ log.Level = logrus.InfoLevel
+
+ for _, s := range scanResults {
+ text, err := toPlainText(s)
+ if err != nil {
+ return err
+ }
+ log.Infof(text)
+ }
+ return nil
+}
diff --git a/report/mail.go b/report/mail.go
new file mode 100644
index 00000000..5ee6793d
--- /dev/null
+++ b/report/mail.go
@@ -0,0 +1,70 @@
+/* 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 report
+
+import (
+ "crypto/tls"
+ "fmt"
+ "strconv"
+
+ "github.com/future-architect/vuls/config"
+ "github.com/future-architect/vuls/models"
+ "gopkg.in/gomail.v2"
+)
+
+// MailWriter send mail
+type MailWriter struct{}
+
+func (w MailWriter) Write(scanResults []models.ScanResult) (err error) {
+ conf := config.Conf
+ for _, s := range scanResults {
+ m := gomail.NewMessage()
+ m.SetHeader("From", conf.Mail.From)
+ m.SetHeader("To", conf.Mail.To...)
+ m.SetHeader("Cc", conf.Mail.Cc...)
+
+ subject := fmt.Sprintf("%s%s %s",
+ conf.Mail.SubjectPrefix,
+ s.ServerName,
+ s.CveSummary(),
+ )
+ m.SetHeader("Subject", subject)
+
+ var body string
+ if body, err = toPlainText(s); err != nil {
+ return err
+ }
+ m.SetBody("text/plain", body)
+ port, _ := strconv.Atoi(conf.Mail.SMTPPort)
+ d := gomail.NewPlainDialer(
+ conf.Mail.SMTPAddr,
+ port,
+ conf.Mail.User,
+ conf.Mail.Password,
+ )
+
+ d.TLSConfig = &tls.Config{
+ InsecureSkipVerify: true,
+ }
+
+ if err := d.DialAndSend(m); err != nil {
+ panic(err)
+ }
+ }
+ return nil
+}
diff --git a/report/slack.go b/report/slack.go
new file mode 100644
index 00000000..6a575b10
--- /dev/null
+++ b/report/slack.go
@@ -0,0 +1,236 @@
+/* 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 report
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+ "time"
+
+ log "github.com/Sirupsen/logrus"
+ "github.com/cenkalti/backoff"
+ "github.com/future-architect/vuls/config"
+ "github.com/future-architect/vuls/models"
+ "github.com/parnurzeal/gorequest"
+)
+
+type field struct {
+ Title string `json:"title"`
+ Value string `json:"value"`
+ Short bool `json:"short"`
+}
+type attachment struct {
+ Title string `json:"title"`
+ TitleLink string `json:"title_link"`
+ Fallback string `json:"fallback"`
+ Text string `json:"text"`
+ Pretext string `json:"pretext"`
+ Color string `json:"color"`
+ Fields []*field `json:"fields"`
+ MrkdwnIn []string `json:"mrkdwn_in"`
+}
+type message struct {
+ Text string `json:"text"`
+ Username string `json:"username"`
+ IconEmoji string `json:"icon_emoji"`
+ Channel string `json:"channel"`
+ Attachments []*attachment `json:"attachments"`
+}
+
+// SlackWriter send report to slack
+type SlackWriter struct{}
+
+func (w SlackWriter) Write(scanResults []models.ScanResult) error {
+ conf := config.Conf.Slack
+ for _, s := range scanResults {
+
+ channel := conf.Channel
+ if channel == "${servername}" {
+ channel = fmt.Sprintf("#%s", s.ServerName)
+ }
+
+ msg := message{
+ Text: msgText(s),
+ Username: conf.AuthUser,
+ IconEmoji: conf.IconEmoji,
+ Channel: channel,
+ Attachments: toSlackAttachments(s),
+ }
+
+ bytes, _ := json.Marshal(msg)
+ jsonBody := string(bytes)
+ f := func() (err error) {
+ resp, body, errs := gorequest.New().Proxy(config.Conf.HTTPProxy).Post(conf.HookURL).
+ Send(string(jsonBody)).End()
+ if resp.StatusCode != 200 {
+ log.Errorf("Resonse body: %s", body)
+ if len(errs) > 0 {
+ return errs[0]
+ }
+ }
+ return nil
+ }
+ notify := func(err error, t time.Duration) {
+ log.Warn("Retrying in ", t)
+ }
+ if err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify); err != nil {
+ return fmt.Errorf("HTTP Error: %s", err)
+ }
+ }
+ return nil
+}
+
+func msgText(r models.ScanResult) string {
+
+ notifyUsers := ""
+ if 0 < len(r.KnownCves) || 0 < len(r.UnknownCves) {
+ notifyUsers = getNotifyUsers(config.Conf.Slack.NotifyUsers)
+ }
+
+ hostinfo := fmt.Sprintf(
+ "*%s* (%s %s)",
+ r.ServerName,
+ r.Family,
+ r.Release,
+ )
+ return fmt.Sprintf("%s\n%s\n>%s", notifyUsers, hostinfo, r.CveSummary())
+}
+
+func toSlackAttachments(scanResult models.ScanResult) (attaches []*attachment) {
+
+ scanResult.KnownCves = append(scanResult.KnownCves, scanResult.UnknownCves...)
+ for _, cveInfo := range scanResult.KnownCves {
+ cveID := cveInfo.CveDetail.CveID
+
+ curentPackages := []string{}
+ for _, p := range cveInfo.Packages {
+ curentPackages = append(curentPackages, p.ToStringCurrentVersion())
+ }
+ for _, cpename := range cveInfo.CpeNames {
+ curentPackages = append(curentPackages, cpename.Name)
+ }
+
+ newPackages := []string{}
+ for _, p := range cveInfo.Packages {
+ newPackages = append(newPackages, p.ToStringNewVersion())
+ }
+
+ a := attachment{
+ Title: cveID,
+ TitleLink: fmt.Sprintf("%s?vulnId=%s", nvdBaseURL, cveID),
+ Text: attachmentText(cveInfo, scanResult.Family),
+ MrkdwnIn: []string{"text", "pretext"},
+ Fields: []*field{
+ {
+ // Title: "Current Package/CPE",
+ Title: "Installed",
+ Value: strings.Join(curentPackages, "\n"),
+ Short: true,
+ },
+ {
+ Title: "Candidate",
+ Value: strings.Join(newPackages, "\n"),
+ Short: true,
+ },
+ },
+ Color: color(cveInfo.CveDetail.CvssScore(config.Conf.Lang)),
+ }
+ attaches = append(attaches, &a)
+ }
+ return
+}
+
+// https://api.slack.com/docs/attachments
+func color(cvssScore float64) string {
+ switch {
+ case 7 <= cvssScore:
+ return "danger"
+ case 4 <= cvssScore && cvssScore < 7:
+ return "warning"
+ case cvssScore < 0:
+ return "#C0C0C0"
+ default:
+ return "good"
+ }
+}
+
+func attachmentText(cveInfo models.CveInfo, osFamily string) string {
+
+ linkText := links(cveInfo, osFamily)
+
+ switch {
+ case config.Conf.Lang == "ja" &&
+ cveInfo.CveDetail.Jvn.ID != 0 &&
+ 0 < cveInfo.CveDetail.CvssScore("ja"):
+
+ jvn := cveInfo.CveDetail.Jvn
+ return fmt.Sprintf("*%4.1f (%s)* <%s|%s>\n%s\n%s",
+ cveInfo.CveDetail.CvssScore(config.Conf.Lang),
+ jvn.Severity,
+ fmt.Sprintf(cvssV2CalcURLTemplate, cveInfo.CveDetail.CveID, jvn.Vector),
+ jvn.Vector,
+ jvn.Title,
+ linkText,
+ )
+
+ case 0 < cveInfo.CveDetail.CvssScore("en"):
+ nvd := cveInfo.CveDetail.Nvd
+ return fmt.Sprintf("*%4.1f (%s)* <%s|%s>\n%s\n%s",
+ cveInfo.CveDetail.CvssScore(config.Conf.Lang),
+ nvd.Severity(),
+ fmt.Sprintf(cvssV2CalcURLTemplate, cveInfo.CveDetail.CveID, nvd.CvssVector()),
+ nvd.CvssVector(),
+ nvd.Summary,
+ linkText,
+ )
+ default:
+ nvd := cveInfo.CveDetail.Nvd
+ return fmt.Sprintf("?\n%s\n%s", nvd.Summary, linkText)
+ }
+}
+
+func links(cveInfo models.CveInfo, osFamily string) string {
+ links := []string{}
+ cveID := cveInfo.CveDetail.CveID
+ if config.Conf.Lang == "ja" && 0 < len(cveInfo.CveDetail.Jvn.Link()) {
+ jvn := fmt.Sprintf("<%s|JVN>", cveInfo.CveDetail.Jvn.Link())
+ links = append(links, jvn)
+ }
+ links = append(links, fmt.Sprintf("<%s|CVEDetails>",
+ fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)))
+ links = append(links, fmt.Sprintf("<%s|MITRE>",
+ fmt.Sprintf("%s%s", mitreBaseURL, cveID)))
+
+ dlinks := distroLinks(cveInfo, osFamily)
+ for _, link := range dlinks {
+ links = append(links,
+ fmt.Sprintf("<%s|%s>", link.url, link.title))
+ }
+
+ return strings.Join(links, " / ")
+}
+
+// See testcase
+func getNotifyUsers(notifyUsers []string) string {
+ slackStyleTexts := []string{}
+ for _, username := range notifyUsers {
+ slackStyleTexts = append(slackStyleTexts, fmt.Sprintf("<%s>", username))
+ }
+ return strings.Join(slackStyleTexts, " ")
+}
diff --git a/report/slack_test.go b/report/slack_test.go
new file mode 100644
index 00000000..0eae9031
--- /dev/null
+++ b/report/slack_test.go
@@ -0,0 +1,23 @@
+package report
+
+import "testing"
+
+func TestGetNotifyUsers(t *testing.T) {
+ var tests = []struct {
+ in []string
+ expected string
+ }{
+ {
+ []string{"@user1", "@user2"},
+ "<@user1> <@user2>",
+ },
+ }
+
+ for _, tt := range tests {
+ actual := getNotifyUsers(tt.in)
+ if tt.expected != actual {
+ t.Errorf("expected %s, actual %s", tt.expected, actual)
+ }
+ }
+
+}
diff --git a/report/stdout.go b/report/stdout.go
new file mode 100644
index 00000000..e8d660ac
--- /dev/null
+++ b/report/stdout.go
@@ -0,0 +1,38 @@
+/* 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 report
+
+import (
+ "fmt"
+
+ "github.com/future-architect/vuls/models"
+)
+
+// TextWriter write to stdout
+type TextWriter struct{}
+
+func (w TextWriter) Write(scanResults []models.ScanResult) error {
+ for _, s := range scanResults {
+ text, err := toPlainText(s)
+ if err != nil {
+ return err
+ }
+ fmt.Println(text)
+ }
+ return nil
+}
diff --git a/report/tui.go b/report/tui.go
new file mode 100644
index 00000000..c836f2a5
--- /dev/null
+++ b/report/tui.go
@@ -0,0 +1,770 @@
+/* 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 report
+
+import (
+ "bytes"
+ "fmt"
+ "strings"
+ "text/template"
+ "time"
+
+ log "github.com/Sirupsen/logrus"
+ "github.com/future-architect/vuls/config"
+ "github.com/future-architect/vuls/db"
+ "github.com/future-architect/vuls/models"
+ "github.com/google/subcommands"
+ "github.com/gosuri/uitable"
+ "github.com/jroimartin/gocui"
+ cve "github.com/kotakanbe/go-cve-dictionary/models"
+)
+
+var scanHistory models.ScanHistory
+var currentScanResult models.ScanResult
+var currentCveInfo int
+var currentDetailLimitY int
+
+// RunTui execute main logic
+func RunTui() subcommands.ExitStatus {
+ var err error
+ scanHistory, err = latestScanHistory()
+ if err != nil {
+ log.Fatal(err)
+ return subcommands.ExitFailure
+ }
+
+ g := gocui.NewGui()
+ if err := g.Init(); err != nil {
+ log.Panicln(err)
+ }
+ defer g.Close()
+
+ g.SetLayout(layout)
+ if err := keybindings(g); err != nil {
+ log.Panicln(err)
+ }
+ g.SelBgColor = gocui.ColorGreen
+ g.SelFgColor = gocui.ColorBlack
+ g.Cursor = true
+
+ if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
+ log.Panicln(err)
+ return subcommands.ExitFailure
+ }
+
+ return subcommands.ExitSuccess
+}
+
+func latestScanHistory() (latest models.ScanHistory, err error) {
+ if err := db.OpenDB(); err != nil {
+ return latest, fmt.Errorf(
+ "Failed to open DB. datafile: %s, err: %s", config.Conf.DBPath, err)
+ }
+ latest, err = db.SelectLatestScanHistory()
+ return
+}
+
+func keybindings(g *gocui.Gui) (err error) {
+ errs := []error{}
+
+ // Move beetween views
+ errs = append(errs, g.SetKeybinding("side", gocui.KeyTab, gocui.ModNone, nextView))
+ // errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlH, gocui.ModNone, previousView))
+ // errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlL, gocui.ModNone, nextView))
+ // errs = append(errs, g.SetKeybinding("side", gocui.KeyArrowRight, gocui.ModAlt, nextView))
+ errs = append(errs, g.SetKeybinding("side", gocui.KeyArrowDown, gocui.ModNone, cursorDown))
+ errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlJ, gocui.ModNone, cursorDown))
+ errs = append(errs, g.SetKeybinding("side", gocui.KeyArrowUp, gocui.ModNone, cursorUp))
+ errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlK, gocui.ModNone, cursorUp))
+ errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlD, gocui.ModNone, cursorPageDown))
+ errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlU, gocui.ModNone, cursorPageUp))
+ errs = append(errs, g.SetKeybinding("side", gocui.KeySpace, gocui.ModNone, cursorPageDown))
+ errs = append(errs, g.SetKeybinding("side", gocui.KeyBackspace, gocui.ModNone, cursorPageUp))
+ errs = append(errs, g.SetKeybinding("side", gocui.KeyBackspace2, gocui.ModNone, cursorPageUp))
+ errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlN, gocui.ModNone, cursorDown))
+ errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlP, gocui.ModNone, cursorUp))
+ errs = append(errs, g.SetKeybinding("side", gocui.KeyEnter, gocui.ModNone, nextView))
+
+ // errs = append(errs, g.SetKeybinding("msg", gocui.KeyEnter, gocui.ModNone, delMsg))
+ // errs = append(errs, g.SetKeybinding("side", gocui.KeyEnter, gocui.ModNone, showMsg))
+
+ // summary
+ errs = append(errs, g.SetKeybinding("summary", gocui.KeyTab, gocui.ModNone, nextView))
+ errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlQ, gocui.ModNone, previousView))
+ errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlH, gocui.ModNone, previousView))
+ // errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlL, gocui.ModNone, nextView))
+ // errs = append(errs, g.SetKeybinding("summary", gocui.KeyArrowLeft, gocui.ModAlt, previousView))
+ // errs = append(errs, g.SetKeybinding("summary", gocui.KeyArrowDown, gocui.ModAlt, nextView))
+ errs = append(errs, g.SetKeybinding("summary", gocui.KeyArrowDown, gocui.ModNone, cursorDown))
+ errs = append(errs, g.SetKeybinding("summary", gocui.KeyArrowUp, gocui.ModNone, cursorUp))
+ errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlJ, gocui.ModNone, cursorDown))
+ errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlK, gocui.ModNone, cursorUp))
+ errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlD, gocui.ModNone, cursorPageDown))
+ errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlU, gocui.ModNone, cursorPageUp))
+ errs = append(errs, g.SetKeybinding("summary", gocui.KeySpace, gocui.ModNone, cursorPageDown))
+ errs = append(errs, g.SetKeybinding("summary", gocui.KeyBackspace, gocui.ModNone, cursorPageUp))
+ errs = append(errs, g.SetKeybinding("summary", gocui.KeyBackspace2, gocui.ModNone, cursorPageUp))
+ // errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlM, gocui.ModNone, cursorMoveMiddle))
+ errs = append(errs, g.SetKeybinding("summary", gocui.KeyEnter, gocui.ModNone, nextView))
+ errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlN, gocui.ModNone, nextSummary))
+ errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlP, gocui.ModNone, previousSummary))
+
+ // detail
+ errs = append(errs, g.SetKeybinding("detail", gocui.KeyTab, gocui.ModNone, nextView))
+ errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlQ, gocui.ModNone, previousView))
+ errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlH, gocui.ModNone, nextView))
+ // errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlL, gocui.ModNone, nextView))
+ // errs = append(errs, g.SetKeybinding("detail", gocui.KeyArrowUp, gocui.ModAlt, previousView))
+ // errs = append(errs, g.SetKeybinding("detail", gocui.KeyArrowLeft, gocui.ModAlt, nextView))
+ errs = append(errs, g.SetKeybinding("detail", gocui.KeyArrowDown, gocui.ModNone, cursorDown))
+ errs = append(errs, g.SetKeybinding("detail", gocui.KeyArrowUp, gocui.ModNone, cursorUp))
+ errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlJ, gocui.ModNone, cursorDown))
+ errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlK, gocui.ModNone, cursorUp))
+ errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlD, gocui.ModNone, cursorPageDown))
+ errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlU, gocui.ModNone, cursorPageUp))
+ errs = append(errs, g.SetKeybinding("detail", gocui.KeySpace, gocui.ModNone, cursorPageDown))
+ errs = append(errs, g.SetKeybinding("detail", gocui.KeyBackspace, gocui.ModNone, cursorPageUp))
+ errs = append(errs, g.SetKeybinding("detail", gocui.KeyBackspace2, gocui.ModNone, cursorPageUp))
+ // errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlM, gocui.ModNone, cursorMoveMiddle))
+ errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlN, gocui.ModNone, nextSummary))
+ errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlP, gocui.ModNone, previousSummary))
+ errs = append(errs, g.SetKeybinding("detail", gocui.KeyEnter, gocui.ModNone, nextView))
+
+ // errs = append(errs, g.SetKeybinding("msg", gocui.KeyEnter, gocui.ModNone, delMsg))
+ // errs = append(errs, g.SetKeybinding("detail", gocui.KeyEnter, gocui.ModNone, showMsg))
+
+ //TODO Help Ctrl-h
+
+ errs = append(errs, g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit))
+ // errs = append(errs, g.SetKeybinding("side", gocui.KeyEnter, gocui.ModNone, getLine))
+ // errs = append(errs, g.SetKeybinding("msg", gocui.KeyEnter, gocui.ModNone, delMsg))
+
+ for _, e := range errs {
+ if e != nil {
+ return e
+ }
+ }
+ return nil
+}
+
+func nextView(g *gocui.Gui, v *gocui.View) error {
+ if v == nil {
+ return g.SetCurrentView("side")
+ }
+ switch v.Name() {
+ case "side":
+ return g.SetCurrentView("summary")
+ case "summary":
+ return g.SetCurrentView("detail")
+ case "detail":
+ return g.SetCurrentView("side")
+ default:
+ return g.SetCurrentView("summary")
+ }
+}
+
+func previousView(g *gocui.Gui, v *gocui.View) error {
+ if v == nil {
+ return g.SetCurrentView("side")
+ }
+ switch v.Name() {
+ case "side":
+ return g.SetCurrentView("side")
+ case "summary":
+ return g.SetCurrentView("side")
+ case "detail":
+ return g.SetCurrentView("summary")
+ default:
+ return g.SetCurrentView("side")
+ }
+}
+
+func movable(v *gocui.View, nextY int) (ok bool, yLimit int) {
+ switch v.Name() {
+ case "side":
+ yLimit = len(scanHistory.ScanResults) - 1
+ if yLimit < nextY {
+ return false, yLimit
+ }
+ return true, yLimit
+ case "summary":
+ yLimit = len(currentScanResult.KnownCves) - 1
+ if yLimit < nextY {
+ return false, yLimit
+ }
+ return true, yLimit
+ case "detail":
+ if currentDetailLimitY < nextY {
+ return false, currentDetailLimitY
+ }
+ return true, currentDetailLimitY
+ default:
+ return true, 0
+ }
+}
+
+func pageUpDownJumpCount(v *gocui.View) int {
+ var jump int
+ switch v.Name() {
+ case "side", "summary":
+ jump = 8
+ case "detail":
+ jump = 30
+ default:
+ jump = 8
+ }
+ return jump
+}
+
+// redraw views
+func onMovingCursorRedrawView(g *gocui.Gui, v *gocui.View) error {
+ switch v.Name() {
+ case "summary":
+ if err := redrawDetail(g); err != nil {
+ return err
+ }
+ case "side":
+ if err := changeHost(g, v); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func cursorDown(g *gocui.Gui, v *gocui.View) error {
+ if v != nil {
+ cx, cy := v.Cursor()
+ ox, oy := v.Origin()
+ // ok, := movable(v, oy+cy+1)
+ // _, maxY := v.Size()
+ ok, _ := movable(v, oy+cy+1)
+ // log.Info(cy, oy, maxY, yLimit)
+ if !ok {
+ return nil
+ }
+ if err := v.SetCursor(cx, cy+1); err != nil {
+ if err := v.SetOrigin(ox, oy+1); err != nil {
+ return err
+ }
+ }
+ onMovingCursorRedrawView(g, v)
+ }
+ return nil
+}
+
+func cursorMoveTop(g *gocui.Gui, v *gocui.View) error {
+ if v != nil {
+ cx, _ := v.Cursor()
+ v.SetCursor(cx, 0)
+ }
+ onMovingCursorRedrawView(g, v)
+ return nil
+}
+
+func cursorMoveBottom(g *gocui.Gui, v *gocui.View) error {
+ if v != nil {
+ _, maxY := v.Size()
+ cx, _ := v.Cursor()
+ v.SetCursor(cx, maxY-1)
+ }
+ onMovingCursorRedrawView(g, v)
+ return nil
+}
+
+func cursorMoveMiddle(g *gocui.Gui, v *gocui.View) error {
+ if v != nil {
+ _, maxY := v.Size()
+ cx, _ := v.Cursor()
+ v.SetCursor(cx, maxY/2)
+ }
+ onMovingCursorRedrawView(g, v)
+ return nil
+}
+
+func cursorPageDown(g *gocui.Gui, v *gocui.View) error {
+ jump := pageUpDownJumpCount(v)
+
+ if v != nil {
+ cx, cy := v.Cursor()
+ ox, oy := v.Origin()
+ ok, yLimit := movable(v, oy+cy+jump)
+ _, maxY := v.Size()
+
+ if !ok {
+ if yLimit < maxY {
+ v.SetCursor(cx, yLimit)
+ } else {
+ v.SetCursor(cx, maxY-1)
+ v.SetOrigin(ox, yLimit-maxY+1)
+ }
+ } else if yLimit < oy+jump+maxY {
+ if yLimit < maxY {
+ v.SetCursor(cx, yLimit)
+ } else {
+ v.SetOrigin(ox, yLimit-maxY+1)
+ v.SetCursor(cx, maxY-1)
+ }
+ } else {
+ v.SetCursor(cx, cy)
+ v.SetOrigin(ox, oy+jump)
+ }
+ onMovingCursorRedrawView(g, v)
+ }
+ return nil
+}
+
+func cursorUp(g *gocui.Gui, v *gocui.View) error {
+ if v != nil {
+ ox, oy := v.Origin()
+ cx, cy := v.Cursor()
+ if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
+ if err := v.SetOrigin(ox, oy-1); err != nil {
+ return err
+ }
+ }
+ }
+ onMovingCursorRedrawView(g, v)
+ return nil
+}
+
+func cursorPageUp(g *gocui.Gui, v *gocui.View) error {
+ jump := pageUpDownJumpCount(v)
+ if v != nil {
+ cx, _ := v.Cursor()
+ ox, oy := v.Origin()
+ if err := v.SetOrigin(ox, oy-jump); err != nil {
+ v.SetOrigin(ox, 0)
+ v.SetCursor(cx, 0)
+
+ }
+ onMovingCursorRedrawView(g, v)
+ }
+ return nil
+}
+
+func previousSummary(g *gocui.Gui, v *gocui.View) error {
+ if v != nil {
+ // cursor to summary
+ if err := g.SetCurrentView("summary"); err != nil {
+ return err
+ }
+ // move next line
+ if err := cursorUp(g, g.CurrentView()); err != nil {
+ return err
+ }
+ // cursor to detail
+ if err := g.SetCurrentView("detail"); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func nextSummary(g *gocui.Gui, v *gocui.View) error {
+ if v != nil {
+ // cursor to summary
+ if err := g.SetCurrentView("summary"); err != nil {
+ return err
+ }
+ // move next line
+ if err := cursorDown(g, g.CurrentView()); err != nil {
+ return err
+ }
+ // cursor to detail
+ if err := g.SetCurrentView("detail"); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func changeHost(g *gocui.Gui, v *gocui.View) error {
+
+ if err := g.DeleteView("summary"); err != nil {
+ return err
+ }
+ if err := g.DeleteView("detail"); err != nil {
+ return err
+ }
+
+ _, cy := v.Cursor()
+ l, err := v.Line(cy)
+ if err != nil {
+ return err
+ }
+ serverName := strings.TrimSpace(l)
+
+ for _, r := range scanHistory.ScanResults {
+ if serverName == r.ServerName {
+ currentScanResult = r
+ break
+ }
+ }
+
+ if err := setSummaryLayout(g); err != nil {
+ return err
+ }
+ if err := setDetailLayout(g); err != nil {
+ return err
+ }
+ return nil
+}
+
+func redrawDetail(g *gocui.Gui) error {
+ if err := g.DeleteView("detail"); err != nil {
+ return err
+ }
+
+ if err := setDetailLayout(g); err != nil {
+ return err
+ }
+ return nil
+}
+
+func getLine(g *gocui.Gui, v *gocui.View) error {
+ var l string
+ var err error
+
+ _, cy := v.Cursor()
+ if l, err = v.Line(cy); err != nil {
+ l = ""
+ }
+
+ maxX, maxY := g.Size()
+ if v, err := g.SetView("msg", maxX/2-30, maxY/2, maxX/2+30, maxY/2+2); err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ fmt.Fprintln(v, l)
+ if err := g.SetCurrentView("msg"); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func showMsg(g *gocui.Gui, v *gocui.View) error {
+ jump := 8
+ _, cy := v.Cursor()
+ _, oy := v.Origin()
+ ok, yLimit := movable(v, oy+cy+jump)
+ // maxX, maxY := v.Size()
+ _, maxY := v.Size()
+
+ l := fmt.Sprintf("cy: %d, oy: %d, maxY: %d, yLimit: %d, curCve %d, ok: %v", cy, oy, maxY, yLimit, currentCveInfo, ok)
+ // if v, err := g.SetView("msg", maxX/2-30, maxY/2, maxX/2+30, maxY/2+2); err != nil {
+ if v, err := g.SetView("msg", 10, maxY/2, 10+50, maxY/2+2); err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ fmt.Fprintln(v, l)
+ if err := g.SetCurrentView("msg"); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func delMsg(g *gocui.Gui, v *gocui.View) error {
+ if err := g.DeleteView("msg"); err != nil {
+ return err
+ }
+ if err := g.SetCurrentView("summary"); err != nil {
+ return err
+ }
+ return nil
+}
+
+func quit(g *gocui.Gui, v *gocui.View) error {
+ return gocui.ErrQuit
+}
+
+func layout(g *gocui.Gui) error {
+ if err := setSideLayout(g); err != nil {
+ return err
+ }
+ if err := setSummaryLayout(g); err != nil {
+ return err
+ }
+ if err := setDetailLayout(g); err != nil {
+ return err
+ }
+ return nil
+}
+
+func setSideLayout(g *gocui.Gui) error {
+ _, maxY := g.Size()
+ if v, err := g.SetView("side", -1, -1, 30, maxY); err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Highlight = true
+
+ for _, result := range scanHistory.ScanResults {
+ fmt.Fprintln(v, result.ServerName)
+ }
+ currentScanResult = scanHistory.ScanResults[0]
+ if err := g.SetCurrentView("side"); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func setSummaryLayout(g *gocui.Gui) error {
+ maxX, maxY := g.Size()
+ if v, err := g.SetView("summary", 30, -1, maxX, int(float64(maxY)*0.2)); err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+
+ lines := summaryLines(currentScanResult)
+ fmt.Fprintf(v, lines)
+
+ v.Highlight = true
+ v.Editable = false
+ v.Wrap = false
+ }
+ return nil
+}
+
+func summaryLines(data models.ScanResult) string {
+ stable := uitable.New()
+ stable.MaxColWidth = 1000
+ stable.Wrap = false
+
+ indexFormat := ""
+ if len(data.KnownCves) < 10 {
+ indexFormat = "[%1d]"
+ } else if len(data.KnownCves) < 100 {
+ indexFormat = "[%2d]"
+ } else {
+ indexFormat = "[%3d]"
+ }
+
+ for i, d := range data.KnownCves {
+ var cols []string
+ // packs := []string{}
+ // for _, pack := range d.Packages {
+ // packs = append(packs, pack.Name)
+ // }
+ if config.Conf.Lang == "ja" && 0 < d.CveDetail.Jvn.CvssScore() {
+ summary := d.CveDetail.Jvn.Title
+ cols = []string{
+ fmt.Sprintf(indexFormat, i+1),
+ d.CveDetail.CveID,
+ fmt.Sprintf("| %-4.1f(%s)",
+ d.CveDetail.CvssScore(config.Conf.Lang),
+ d.CveDetail.Jvn.Severity,
+ ),
+ // strings.Join(packs, ","),
+ summary,
+ }
+ } else {
+ summary := d.CveDetail.Nvd.Summary
+
+ var cvssScore string
+ if d.CveDetail.CvssScore("en") <= 0 {
+ cvssScore = "| ?"
+ } else {
+ cvssScore = fmt.Sprintf("| %-4.1f(%s)",
+ d.CveDetail.CvssScore(config.Conf.Lang),
+ d.CveDetail.Nvd.Severity(),
+ )
+ }
+
+ cols = []string{
+ fmt.Sprintf(indexFormat, i+1),
+ d.CveDetail.CveID,
+ cvssScore,
+ summary,
+ }
+ }
+
+ icols := make([]interface{}, len(cols))
+ for j := range cols {
+ icols[j] = cols[j]
+ }
+ stable.AddRow(icols...)
+ }
+ // ignore UnknownCves
+ return fmt.Sprintf("%s", stable)
+}
+
+func setDetailLayout(g *gocui.Gui) error {
+ maxX, maxY := g.Size()
+
+ summaryView, err := g.View("summary")
+ if err != nil {
+ return err
+ }
+ _, cy := summaryView.Cursor()
+ _, oy := summaryView.Origin()
+ currentCveInfo = cy + oy
+
+ if v, err := g.SetView("detail", 30, int(float64(maxY)*0.2), maxX, maxY); err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ // text := report.ToPlainTextDetailsLangEn(
+ // currentScanResult.KnownCves[currentCveInfo],
+ // currentScanResult.Family)
+
+ //TODO error handling
+ text, err := detailLines()
+ if err != nil {
+ return err
+ }
+ fmt.Fprint(v, text)
+ v.Editable = false
+ v.Wrap = true
+
+ currentDetailLimitY = len(strings.Split(text, "\n")) - 1
+ }
+ return nil
+}
+
+type dataForTmpl struct {
+ CveID string
+ CvssScore string
+ CvssVector string
+ CvssSeverity string
+ Summary string
+ VulnSiteLinks []string
+ References []cve.Reference
+ Packages []string
+ CpeNames []models.CpeName
+ PublishedDate time.Time
+ LastModifiedDate time.Time
+}
+
+func detailLines() (string, error) {
+ cveInfo := currentScanResult.KnownCves[currentCveInfo]
+ cveID := cveInfo.CveDetail.CveID
+
+ tmpl, err := template.New("detail").Parse(detailTemplate())
+ if err != nil {
+ return "", err
+ }
+
+ var cvssSeverity, cvssVector, summary string
+ var refs []cve.Reference
+ switch {
+ case config.Conf.Lang == "ja" &&
+ 0 < cveInfo.CveDetail.Jvn.CvssScore():
+ jvn := cveInfo.CveDetail.Jvn
+ cvssSeverity = jvn.Severity
+ cvssVector = jvn.Vector
+ summary = fmt.Sprintf("%s\n%s", jvn.Title, jvn.Summary)
+ refs = jvn.References
+ default:
+ nvd := cveInfo.CveDetail.Nvd
+ cvssSeverity = nvd.Severity()
+ cvssVector = nvd.CvssVector()
+ summary = nvd.Summary
+ refs = nvd.References
+ }
+
+ links := []string{
+ fmt.Sprintf("[NVD]( %s )", fmt.Sprintf("%s?vulnId=%s", nvdBaseURL, cveID)),
+ fmt.Sprintf("[MITRE]( %s )", fmt.Sprintf("%s%s", mitreBaseURL, cveID)),
+ fmt.Sprintf("[CveDetais]( %s )", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)),
+ fmt.Sprintf("[CVSSv2 Caluclator]( %s )", fmt.Sprintf(cvssV2CalcURLTemplate, cveID, cvssVector)),
+ }
+ dlinks := distroLinks(cveInfo, currentScanResult.Family)
+ for _, link := range dlinks {
+ links = append(links, fmt.Sprintf("[%s]( %s )", link.title, link.url))
+ }
+
+ var cvssScore string
+ if cveInfo.CveDetail.CvssScore(config.Conf.Lang) == -1 {
+ cvssScore = "?"
+ } else {
+ cvssScore = fmt.Sprintf("%4.1f", cveInfo.CveDetail.CvssScore(config.Conf.Lang))
+ }
+
+ packages := []string{}
+ for _, pack := range cveInfo.Packages {
+ packages = append(packages,
+ fmt.Sprintf(
+ "%s -> %s",
+ pack.ToStringCurrentVersion(),
+ pack.ToStringNewVersion()))
+ }
+
+ data := dataForTmpl{
+ CveID: cveID,
+ CvssScore: cvssScore,
+ CvssSeverity: cvssSeverity,
+ CvssVector: cvssVector,
+ Summary: summary,
+ VulnSiteLinks: links,
+ References: refs,
+ Packages: packages,
+ CpeNames: cveInfo.CpeNames,
+ }
+
+ buf := bytes.NewBuffer(nil) // create empty buffer
+ if err := tmpl.Execute(buf, data); err != nil {
+ return "", err
+ }
+
+ return string(buf.Bytes()), nil
+}
+
+// * {{.Name}}-{{.Version}}-{{.Release}}
+
+func detailTemplate() string {
+ return `
+{{.CveID}}
+==============
+
+CVSS Score
+--------------
+
+{{.CvssScore}} ({{.CvssSeverity}}) {{.CvssVector}}
+
+Summary
+--------------
+
+ {{.Summary }}
+
+Package/CPE
+--------------
+
+{{range $pack := .Packages -}}
+* {{$pack}}
+{{end -}}
+{{range .CpeNames -}}
+* {{.Name}}
+{{end}}
+Links
+--------------
+
+{{range $link := .VulnSiteLinks -}}
+* {{$link}}
+{{end}}
+References
+--------------
+
+{{range .References -}}
+* [{{.Source}}]( {{.Link}} )
+{{end}}
+
+`
+}
diff --git a/report/util.go b/report/util.go
new file mode 100644
index 00000000..d3e87639
--- /dev/null
+++ b/report/util.go
@@ -0,0 +1,334 @@
+/* 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 report
+
+import (
+ "bytes"
+ "fmt"
+ "strings"
+
+ "github.com/future-architect/vuls/config"
+ "github.com/future-architect/vuls/models"
+ "github.com/gosuri/uitable"
+)
+
+func toPlainText(scanResult models.ScanResult) (string, error) {
+ hostinfo := fmt.Sprintf(
+ "%s (%s %s)",
+ scanResult.ServerName,
+ scanResult.Family,
+ scanResult.Release,
+ )
+
+ var buffer bytes.Buffer
+ for i := 0; i < len(hostinfo); i++ {
+ buffer.WriteString("=")
+ }
+ header := fmt.Sprintf("%s\n%s", hostinfo, buffer.String())
+
+ if len(scanResult.KnownCves) == 0 && len(scanResult.UnknownCves) == 0 {
+ return fmt.Sprintf(`
+%s
+No unsecure packages.
+`, header), nil
+ }
+
+ summary := ToPlainTextSummary(scanResult)
+ scoredReport, unscoredReport := []string{}, []string{}
+ scoredReport, unscoredReport = toPlainTextDetails(scanResult, scanResult.Family)
+
+ scored := strings.Join(scoredReport, "\n\n")
+ unscored := strings.Join(unscoredReport, "\n\n")
+ detail := fmt.Sprintf(`
+%s
+
+%s
+`,
+ scored,
+ unscored,
+ )
+ text := fmt.Sprintf("%s\n%s\n%s\n", header, summary, detail)
+
+ return text, nil
+}
+
+// ToPlainTextSummary format summary for plain text.
+func ToPlainTextSummary(r models.ScanResult) string {
+ stable := uitable.New()
+ stable.MaxColWidth = 84
+ stable.Wrap = true
+ cves := append(r.KnownCves, r.UnknownCves...)
+ for _, d := range cves {
+ var scols []string
+
+ switch {
+ case config.Conf.Lang == "ja" &&
+ d.CveDetail.Jvn.ID != 0 &&
+ 0 < d.CveDetail.CvssScore("ja"):
+
+ summary := d.CveDetail.Jvn.Title
+ scols = []string{
+ d.CveDetail.CveID,
+ fmt.Sprintf("%-4.1f (%s)",
+ d.CveDetail.CvssScore(config.Conf.Lang),
+ d.CveDetail.Jvn.Severity,
+ ),
+ summary,
+ }
+ case 0 < d.CveDetail.CvssScore("en"):
+ summary := d.CveDetail.Nvd.Summary
+ scols = []string{
+ d.CveDetail.CveID,
+ fmt.Sprintf("%-4.1f",
+ d.CveDetail.CvssScore(config.Conf.Lang),
+ ),
+ summary,
+ }
+ default:
+ scols = []string{
+ d.CveDetail.CveID,
+ "?",
+ d.CveDetail.Nvd.Summary,
+ }
+ }
+
+ cols := make([]interface{}, len(scols))
+ for i := range cols {
+ cols[i] = scols[i]
+ }
+ stable.AddRow(cols...)
+ }
+ return fmt.Sprintf("%s", stable)
+}
+
+//TODO Distro Advisory
+func toPlainTextDetails(data models.ScanResult, osFamily string) (scoredReport, unscoredReport []string) {
+ for _, cve := range data.KnownCves {
+ switch config.Conf.Lang {
+ case "en":
+ if cve.CveDetail.Nvd.ID != 0 {
+ scoredReport = append(
+ scoredReport, toPlainTextDetailsLangEn(cve, osFamily))
+ } else {
+ scoredReport = append(
+ scoredReport, toPlainTextUnknownCve(cve, osFamily))
+ }
+ case "ja":
+ if cve.CveDetail.Jvn.ID != 0 {
+ scoredReport = append(
+ scoredReport, toPlainTextDetailsLangJa(cve, osFamily))
+ } else if cve.CveDetail.Nvd.ID != 0 {
+ scoredReport = append(
+ scoredReport, toPlainTextDetailsLangEn(cve, osFamily))
+ } else {
+ scoredReport = append(
+ scoredReport, toPlainTextUnknownCve(cve, osFamily))
+ }
+ }
+ }
+ for _, cve := range data.UnknownCves {
+ unscoredReport = append(
+ unscoredReport, toPlainTextUnknownCve(cve, osFamily))
+ }
+ return
+}
+
+func toPlainTextUnknownCve(cveInfo models.CveInfo, osFamily string) string {
+ cveID := cveInfo.CveDetail.CveID
+ dtable := uitable.New()
+ dtable.MaxColWidth = 100
+ dtable.Wrap = true
+ dtable.AddRow(cveID)
+ dtable.AddRow("-------------")
+ dtable.AddRow("Score", "?")
+ dtable.AddRow("NVD",
+ fmt.Sprintf("%s?vulnId=%s", nvdBaseURL, cveID))
+ dtable.AddRow("CVE Details",
+ fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID))
+
+ dlinks := distroLinks(cveInfo, osFamily)
+ for _, link := range dlinks {
+ dtable.AddRow(link.title, link.url)
+ }
+
+ return fmt.Sprintf("%s", dtable)
+}
+
+func toPlainTextDetailsLangJa(cveInfo models.CveInfo, osFamily string) string {
+
+ cveDetail := cveInfo.CveDetail
+ cveID := cveDetail.CveID
+ jvn := cveDetail.Jvn
+
+ dtable := uitable.New()
+ //TODO resize
+ dtable.MaxColWidth = 100
+ dtable.Wrap = true
+ dtable.AddRow(cveID)
+ dtable.AddRow("-------------")
+ if score := cveDetail.Jvn.CvssScore(); 0 < score {
+ dtable.AddRow("Score",
+ fmt.Sprintf("%4.1f (%s)",
+ cveDetail.Jvn.CvssScore(),
+ jvn.Severity,
+ ))
+ } else {
+ dtable.AddRow("Score", "?")
+ }
+ dtable.AddRow("Vector", jvn.Vector)
+ dtable.AddRow("Title", jvn.Title)
+ dtable.AddRow("Description", jvn.Summary)
+
+ dtable.AddRow("JVN", jvn.Link())
+ dtable.AddRow("NVD", fmt.Sprintf("%s?vulnId=%s", nvdBaseURL, cveID))
+ dtable.AddRow("MITRE", fmt.Sprintf("%s%s", mitreBaseURL, cveID))
+ dtable.AddRow("CVE Details", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID))
+ dtable.AddRow("CVSS Claculator", cveDetail.CvssV2CalculatorLink("ja"))
+
+ dlinks := distroLinks(cveInfo, osFamily)
+ for _, link := range dlinks {
+ dtable.AddRow(link.title, link.url)
+ }
+
+ dtable = addPackageInfos(dtable, cveInfo.Packages)
+ dtable = addCpeNames(dtable, cveInfo.CpeNames)
+
+ return fmt.Sprintf("%s", dtable)
+}
+
+func toPlainTextDetailsLangEn(d models.CveInfo, osFamily string) string {
+ cveDetail := d.CveDetail
+ cveID := cveDetail.CveID
+ nvd := cveDetail.Nvd
+
+ dtable := uitable.New()
+ //TODO resize
+ dtable.MaxColWidth = 100
+ dtable.Wrap = true
+ dtable.AddRow(cveID)
+ dtable.AddRow("-------------")
+
+ if score := cveDetail.Nvd.CvssScore(); 0 < score {
+ dtable.AddRow("Score",
+ fmt.Sprintf("%4.1f (%s)",
+ cveDetail.Nvd.CvssScore(),
+ nvd.Severity(),
+ ))
+ } else {
+ dtable.AddRow("Score", "?")
+ }
+
+ dtable.AddRow("Vector", nvd.CvssVector())
+ dtable.AddRow("Summary", nvd.Summary)
+ dtable.AddRow("NVD", fmt.Sprintf("%s?vulnId=%s", nvdBaseURL, cveID))
+ dtable.AddRow("MITRE", fmt.Sprintf("%s%s", mitreBaseURL, cveID))
+ dtable.AddRow("CVE Details", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID))
+ dtable.AddRow("CVSS Claculator", cveDetail.CvssV2CalculatorLink("en"))
+
+ links := distroLinks(d, osFamily)
+ for _, link := range links {
+ dtable.AddRow(link.title, link.url)
+ }
+ dtable = addPackageInfos(dtable, d.Packages)
+ dtable = addCpeNames(dtable, d.CpeNames)
+
+ return fmt.Sprintf("%s\n", dtable)
+}
+
+type distroLink struct {
+ title string
+ url string
+}
+
+// addVendorSite add Vendor site of the CVE to table
+func distroLinks(cveInfo models.CveInfo, osFamily string) []distroLink {
+ cveID := cveInfo.CveDetail.CveID
+ switch osFamily {
+ case "rhel", "centos":
+ links := []distroLink{
+ {
+ "RHEL-CVE",
+ fmt.Sprintf("%s/%s", redhatSecurityBaseURL, cveID),
+ },
+ }
+ for _, advisory := range cveInfo.DistroAdvisories {
+ aidURL := strings.Replace(advisory.AdvisoryID, ":", "-", -1)
+ links = append(links, distroLink{
+ // "RHEL-errata",
+ advisory.AdvisoryID,
+ fmt.Sprintf(redhatRHSABaseBaseURL, aidURL),
+ })
+ }
+ return links
+ case "amazon":
+ links := []distroLink{
+ {
+ "RHEL-CVE",
+ fmt.Sprintf("%s/%s", redhatSecurityBaseURL, cveID),
+ },
+ }
+ for _, advisory := range cveInfo.DistroAdvisories {
+ links = append(links, distroLink{
+ // "Amazon-ALAS",
+ advisory.AdvisoryID,
+ fmt.Sprintf(amazonSecurityBaseURL, advisory.AdvisoryID),
+ })
+ }
+ return links
+ case "ubuntu":
+ return []distroLink{
+ {
+ "Ubuntu-CVE",
+ fmt.Sprintf("%s/%s", ubuntuSecurityBaseURL, cveID),
+ },
+ //TODO Ubuntu USN
+ }
+ case "debian":
+ return []distroLink{
+ {
+ "Debian-CVE",
+ fmt.Sprintf("%s/%s", debianTrackerBaseURL, cveID),
+ },
+ // TODO Debian dsa
+ }
+ default:
+ return []distroLink{}
+ }
+}
+
+//TODO
+// addPackageInfos add package information related the CVE to table
+func addPackageInfos(table *uitable.Table, packs []models.PackageInfo) *uitable.Table {
+ for i, p := range packs {
+ var title string
+ if i == 0 {
+ title = "Package/CPE"
+ }
+ ver := fmt.Sprintf(
+ "%s -> %s", p.ToStringCurrentVersion(), p.ToStringNewVersion())
+ table.AddRow(title, ver)
+ }
+ return table
+}
+
+func addCpeNames(table *uitable.Table, names []models.CpeName) *uitable.Table {
+ for _, p := range names {
+ table.AddRow("CPE", fmt.Sprintf("%s", p.Name))
+ }
+ return table
+}
diff --git a/report/writer.go b/report/writer.go
new file mode 100644
index 00000000..d23a390e
--- /dev/null
+++ b/report/writer.go
@@ -0,0 +1,39 @@
+/* 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 report
+
+import "github.com/future-architect/vuls/models"
+
+const (
+ nvdBaseURL = "https://web.nvd.nist.gov/view/vuln/detail"
+ mitreBaseURL = "https://cve.mitre.org/cgi-bin/cvename.cgi?name="
+ cveDetailsBaseURL = "http://www.cvedetails.com/cve"
+ cvssV2CalcURLTemplate = "https://nvd.nist.gov/cvss/v2-calculator?name=%s&vector=%s"
+
+ redhatSecurityBaseURL = "https://access.redhat.com/security/cve"
+ redhatRHSABaseBaseURL = "https://rhn.redhat.com/errata/%s.html"
+ amazonSecurityBaseURL = "https://alas.aws.amazon.com/%s.html"
+
+ ubuntuSecurityBaseURL = "http://people.ubuntu.com/~ubuntu-security/cve"
+ debianTrackerBaseURL = "https://security-tracker.debian.org/tracker"
+)
+
+// ResultWriter Interface
+type ResultWriter interface {
+ Write([]models.ScanResult) error
+}
diff --git a/scan/debian.go b/scan/debian.go
new file mode 100644
index 00000000..3d38f85a
--- /dev/null
+++ b/scan/debian.go
@@ -0,0 +1,655 @@
+/* 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 (
+ "fmt"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/future-architect/vuls/config"
+ "github.com/future-architect/vuls/cveapi"
+ "github.com/future-architect/vuls/models"
+ "github.com/future-architect/vuls/util"
+)
+
+// inherit OsTypeInterface
+type debian struct {
+ linux
+}
+
+// NewDebian is constructor
+func newDebian(c config.ServerInfo) *debian {
+ d := &debian{}
+ d.log = util.NewCustomLogger(c)
+ return d
+}
+
+// Ubuntu, Debian
+// https://github.com/serverspec/specinfra/blob/master/lib/specinfra/helper/detect_os/debian.rb
+func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface) {
+
+ deb = newDebian(c)
+
+ // set sudo option flag
+ c.SudoOpt = config.SudoOption{ExecBySudo: true}
+ deb.setServerInfo(c)
+
+ if r := sshExec(c, "ls /etc/debian_version", noSudo); !r.isSuccess() {
+ Log.Debugf("Not Debian like Linux. Host: %s:%s", c.Host, c.Port)
+ return false, deb
+ }
+
+ if r := sshExec(c, "lsb_release -ir", noSudo); r.isSuccess() {
+ // e.g.
+ // root@fa3ec524be43:/# lsb_release -ir
+ // Distributor ID: Ubuntu
+ // Release: 14.04
+ re, _ := regexp.Compile(
+ `(?s)^Distributor ID:\s*(.+?)\n*Release:\s*(.+?)$`)
+ result := re.FindStringSubmatch(trim(r.Stdout))
+
+ if len(result) == 0 {
+ deb.setDistributionInfo("debian/ubuntu", "unknown")
+ Log.Warnf(
+ "Unknown Debian/Ubuntu version. lsb_release -ir: %s, Host: %s:%s",
+ r.Stdout, c.Host, c.Port)
+ } else {
+ distro := strings.ToLower(trim(result[1]))
+ deb.setDistributionInfo(distro, trim(result[2]))
+ }
+ return true, deb
+ }
+
+ if r := sshExec(c, "cat /etc/lsb-release", noSudo); r.isSuccess() {
+ // e.g.
+ // DISTRIB_ID=Ubuntu
+ // DISTRIB_RELEASE=14.04
+ // DISTRIB_CODENAME=trusty
+ // DISTRIB_DESCRIPTION="Ubuntu 14.04.2 LTS"
+ re, _ := regexp.Compile(
+ `(?s)^DISTRIB_ID=(.+?)\n*DISTRIB_RELEASE=(.+?)\n.*$`)
+ result := re.FindStringSubmatch(trim(r.Stdout))
+ if len(result) == 0 {
+ Log.Warnf(
+ "Unknown Debian/Ubuntu. cat /etc/lsb-release: %s, Host: %s:%s",
+ r.Stdout, c.Host, c.Port)
+ deb.setDistributionInfo("debian/ubuntu", "unknown")
+ } else {
+ distro := strings.ToLower(trim(result[1]))
+ deb.setDistributionInfo(distro, trim(result[2]))
+ }
+ return true, deb
+ }
+
+ // Debian
+ cmd := "cat /etc/debian_version"
+ if r := sshExec(c, cmd, noSudo); r.isSuccess() {
+ deb.setDistributionInfo("debian", trim(r.Stdout))
+ return true, deb
+ }
+
+ Log.Debugf("Not Debian like Linux. Host: %s:%s", c.Host, c.Port)
+ return false, deb
+}
+
+func trim(str string) string {
+ return strings.TrimSpace(str)
+}
+
+func (o *debian) install() error {
+
+ // apt-get update
+ o.log.Infof("apt-get update...")
+ cmd := util.PrependProxyEnv("apt-get update")
+ if r := o.ssh(cmd, sudo); !r.isSuccess() {
+ msg := fmt.Sprintf("Failed to %s. status: %d, stdout: %s, stderr: %s",
+ cmd, r.ExitStatus, r.Stdout, r.Stderr)
+ o.log.Errorf(msg)
+ return fmt.Errorf(msg)
+ }
+
+ if o.Family == "debian" {
+ // install aptitude
+ cmd = util.PrependProxyEnv("apt-get install --force-yes -y aptitude")
+ if r := o.ssh(cmd, sudo); !r.isSuccess() {
+ msg := fmt.Sprintf("Failed to %s. status: %d, stdout: %s, stderr: %s",
+ cmd, r.ExitStatus, r.Stdout, r.Stderr)
+ o.log.Errorf(msg)
+ return fmt.Errorf(msg)
+ }
+ o.log.Infof("Installed: aptitude")
+ }
+
+ // install unattended-upgrades
+ if !config.Conf.UseUnattendedUpgrades {
+ return nil
+ }
+
+ if r := o.ssh("type unattended-upgrade", noSudo); r.isSuccess() {
+ o.log.Infof(
+ "Ignored: unattended-upgrade already installed")
+ return nil
+ }
+
+ cmd = util.PrependProxyEnv(
+ "apt-get install --force-yes -y unattended-upgrades")
+ if r := o.ssh(cmd, sudo); !r.isSuccess() {
+ msg := fmt.Sprintf("Failed to %s. status: %d, stdout: %s, stderr: %s",
+ cmd, r.ExitStatus, r.Stdout, r.Stderr)
+ o.log.Errorf(msg)
+ return fmt.Errorf(msg)
+ }
+
+ o.log.Infof("Installed: unattended-upgrades")
+ return nil
+}
+
+func (o *debian) scanPackages() error {
+ var err error
+ var packs []models.PackageInfo
+ if packs, err = o.scanInstalledPackages(); err != nil {
+ o.log.Errorf("Failed to scan installed packages")
+ return err
+ }
+ o.setPackages(packs)
+
+ var unsecurePacks []CvePacksInfo
+ if unsecurePacks, err = o.scanUnsecurePackages(packs); err != nil {
+ o.log.Errorf("Failed to scan valnerable packages")
+ return err
+ }
+ o.setUnsecurePackages(unsecurePacks)
+ return nil
+}
+
+func (o *debian) scanInstalledPackages() (packs []models.PackageInfo, err error) {
+ r := o.ssh("dpkg-query -W", noSudo)
+ if !r.isSuccess() {
+ return packs, fmt.Errorf(
+ "Failed to scan packages. status: %d, stdout:%s, stderr: %s",
+ r.ExitStatus, r.Stdout, r.Stderr)
+ }
+
+ // e.g.
+ // curl 7.19.7-40.el6_6.4
+ // openldap 2.4.39-8.el6
+ lines := strings.Split(r.Stdout, "\n")
+ for _, line := range lines {
+ if trimmed := strings.TrimSpace(line); len(trimmed) != 0 {
+ name, version, err := o.parseScanedPackagesLine(trimmed)
+ if err != nil {
+ return nil, fmt.Errorf(
+ "Debian: Failed to parse package line: %s", line)
+ }
+ packs = append(packs, models.PackageInfo{
+ Name: name,
+ Version: version,
+ })
+ }
+ }
+ return
+}
+
+func (o *debian) parseScanedPackagesLine(line string) (name, version string, err error) {
+ re, _ := regexp.Compile(`^([^\t']+)\t(.+)$`)
+ result := re.FindStringSubmatch(line)
+ if len(result) == 3 {
+ // remove :amd64, i386...
+ name = regexp.MustCompile(":.+").ReplaceAllString(result[1], "")
+ version = result[2]
+ return
+ }
+
+ return "", "", fmt.Errorf("Unknown format: %s", line)
+}
+
+// unattended-upgrade command need to check security upgrades).
+func (o *debian) checkRequiredPackagesInstalled() error {
+
+ if o.Family == "debian" {
+ if r := o.ssh("test -f /usr/bin/aptitude", sudo); !r.isSuccess() {
+ msg := "aptitude is not installed"
+ o.log.Errorf(msg)
+ return fmt.Errorf(msg)
+ }
+ }
+
+ if !config.Conf.UseUnattendedUpgrades {
+ return nil
+ }
+
+ if r := o.ssh("type unattended-upgrade", noSudo); !r.isSuccess() {
+ msg := "unattended-upgrade is not installed"
+ o.log.Errorf(msg)
+ return fmt.Errorf(msg)
+ }
+ 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")
+ cmd := util.PrependProxyEnv("apt-get update")
+ if r := o.ssh(cmd, sudo); !r.isSuccess() {
+ return nil, fmt.Errorf(
+ "Failed to %s. status: %d, stdout: %s, stderr: %s",
+ cmd, r.ExitStatus, r.Stdout, r.Stderr,
+ )
+ }
+
+ var upgradablePackNames []string
+ var err error
+ if config.Conf.UseUnattendedUpgrades {
+ upgradablePackNames, err = o.GetUnsecurePackNamesUsingUnattendedUpgrades()
+ if err != nil {
+ return []CvePacksInfo{}, err
+ }
+ } else {
+ upgradablePackNames, err = o.GetUpgradablePackNames()
+ if err != nil {
+ return []CvePacksInfo{}, err
+ }
+ }
+
+ // Convert package name to PackageInfo struct
+ var unsecurePacks []models.PackageInfo
+ for _, name := range upgradablePackNames {
+ for _, pack := range packs {
+ if pack.Name == name {
+ unsecurePacks = append(unsecurePacks, pack)
+ break
+ }
+ }
+ }
+
+ unsecurePacks, err = o.fillCandidateVersion(unsecurePacks)
+ if err != nil {
+ return nil, err
+ }
+
+ // Collect CVE information of upgradable packages
+ cvePacksInfos, err := o.scanPackageCveInfos(unsecurePacks)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to scan unsecure packages. err: %s", err)
+ }
+
+ 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
+ }
+ }()
+
+ 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("apt-cache policy %s", p.Name)
+ r := o.ssh(cmd, sudo)
+ if !r.isSuccess() {
+ errChan <- fmt.Errorf(
+ "Failed to %s. status: %d, stdout: %s, stderr: %s",
+ cmd, r.ExitStatus, r.Stdout, r.Stderr)
+ 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)
+ }
+ }
+ }
+
+ 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:
+ return nil, err
+ case <-timeout:
+ return nil, fmt.Errorf("Timeout fillCandidateVersion.")
+ }
+ }
+ return result, nil
+}
+
+func (o *debian) GetUnsecurePackNamesUsingUnattendedUpgrades() (packNames []string, err error) {
+ cmd := util.PrependProxyEnv("unattended-upgrades --dry-run -d 2>&1 ")
+ release, err := strconv.ParseFloat(o.Release, 64)
+ if err != nil {
+ return packNames, fmt.Errorf(
+ "OS Release Version is invalid, %s, %s", o.Family, o.Release)
+ }
+ switch {
+ case release < 12:
+ return packNames, fmt.Errorf(
+ "Support expired. %s, %s", o.Family, o.Release)
+
+ case 12 < release && release < 14:
+ cmd += `| grep 'pkgs that look like they should be upgraded:' |
+ sed -e 's/pkgs that look like they should be upgraded://g'`
+
+ case 14 < release:
+ cmd += `| grep 'Packages that will be upgraded:' |
+ sed -e 's/Packages that will be upgraded://g'`
+
+ default:
+ return packNames, fmt.Errorf(
+ "Not supported yet. %s, %s", o.Family, o.Release)
+ }
+
+ r := o.ssh(cmd, sudo)
+ if r.isSuccess(0, 1) {
+ packNames = strings.Split(strings.TrimSpace(r.Stdout), " ")
+ return packNames, nil
+ }
+
+ return packNames, fmt.Errorf(
+ "Failed to %s. status: %d, stdout: %s, stderr: %s",
+ cmd, r.ExitStatus, r.Stdout, r.Stderr)
+}
+
+func (o *debian) GetUpgradablePackNames() (packNames []string, err error) {
+ cmd := util.PrependProxyEnv("apt-get upgrade --dry-run")
+ r := o.ssh(cmd, sudo)
+ if r.isSuccess(0, 1) {
+ return o.parseAptGetUpgrade(r.Stdout)
+ }
+ return packNames, fmt.Errorf(
+ "Failed to %s. status: %d, stdout: %s, stderr: %s",
+ cmd, r.ExitStatus, r.Stdout, r.Stderr)
+}
+
+func (o *debian) parseAptGetUpgrade(stdout string) (upgradableNames []string, err error) {
+ startRe, _ := regexp.Compile(`The following packages will be upgraded:`)
+ stopRe, _ := regexp.Compile(`^(\d+) upgraded.*`)
+ startLineFound, stopLineFound := false, false
+
+ lines := strings.Split(stdout, "\n")
+ for _, line := range lines {
+ if !startLineFound {
+ if matche := startRe.MatchString(line); matche {
+ startLineFound = true
+ }
+ continue
+ }
+ result := stopRe.FindStringSubmatch(line)
+ if len(result) == 2 {
+ numUpgradablePacks, err := strconv.Atoi(result[1])
+ if err != nil {
+ return nil, fmt.Errorf(
+ "Failed to scan upgradable packages number. line: %s", line)
+ }
+ if numUpgradablePacks != len(upgradableNames) {
+ return nil, fmt.Errorf(
+ "Failed to scan upgradable packages, expected: %s, detected: %d",
+ result[1], len(upgradableNames))
+ }
+ stopLineFound = true
+ o.log.Debugf("Found the stop line. line: %s", line)
+ break
+ }
+ upgradableNames = append(upgradableNames, strings.Fields(line)...)
+ }
+ if !startLineFound {
+ // no upgrades
+ return
+ }
+ if !stopLineFound {
+ // There are upgrades, but not found the stop line.
+ return nil, fmt.Errorf("Failed to scan upgradable packages")
+ }
+ return
+}
+
+func (o *debian) scanPackageCveInfos(unsecurePacks []models.PackageInfo) (cvePacksList CvePacksList, err error) {
+
+ // { CVE ID: [packageInfo] }
+ cvePackages := make(map[string][]models.PackageInfo)
+
+ type strarray []string
+ resChan := make(chan struct {
+ models.PackageInfo
+ strarray
+ }, len(unsecurePacks))
+ errChan := make(chan error, len(unsecurePacks))
+ reqChan := make(chan models.PackageInfo, len(unsecurePacks))
+ defer close(resChan)
+ defer close(errChan)
+ defer close(reqChan)
+
+ go func() {
+ for _, pack := range unsecurePacks {
+ reqChan <- pack
+ }
+ }()
+
+ timeout := time.After(30 * 60 * time.Second)
+
+ concurrency := 10
+ tasks := util.GenWorkers(concurrency)
+ for range unsecurePacks {
+ tasks <- func() {
+ select {
+ case pack := <-reqChan:
+ func(p models.PackageInfo) {
+ if cveIds, err := o.scanPackageCveIds(p); err != nil {
+ errChan <- err
+ } else {
+ resChan <- struct {
+ models.PackageInfo
+ strarray
+ }{p, cveIds}
+ }
+ }(pack)
+ }
+ }
+ }
+
+ for i := 0; i < len(unsecurePacks); i++ {
+ select {
+ case pair := <-resChan:
+ pack := pair.PackageInfo
+ cveIds := pair.strarray
+ for _, cveID := range cveIds {
+ cvePackages[cveID] = appendPackIfMissing(cvePackages[cveID], pack)
+ }
+ o.log.Infof("(%d/%d) Scanned %s-%s : %s",
+ i+1, len(unsecurePacks), pair.Name, pair.PackageInfo.Version, cveIds)
+ case err := <-errChan:
+ if err != nil {
+ return nil, err
+ }
+ case <-timeout:
+ return nil, fmt.Errorf("Timeout scanPackageCveIds.")
+ }
+ }
+
+ var cveIds []string
+ 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...")
+ cveDetails, err := cveapi.CveClient.FetchCveDetails(cveIds)
+ if err != nil {
+ return nil, err
+ }
+ o.log.Info("Done")
+
+ for _, detail := range cveDetails {
+ cvePacksList = append(cvePacksList, CvePacksInfo{
+ CveID: detail.CveID,
+ CveDetail: detail,
+ Packs: cvePackages[detail.CveID],
+ // CvssScore: cinfo.CvssScore(conf.Lang),
+ })
+ }
+ sort.Sort(CvePacksList(cvePacksList))
+ return
+}
+
+func (o *debian) scanPackageCveIds(pack models.PackageInfo) (cveIds []string, err error) {
+ cmd := ""
+ switch o.Family {
+ case "ubuntu":
+ cmd = fmt.Sprintf(`apt-get changelog %s | grep '\(urgency\|CVE\)'`, pack.Name)
+ case "debian":
+ cmd = fmt.Sprintf(`aptitude changelog %s | grep '\(urgency\|CVE\)'`, pack.Name)
+ }
+ cmd = util.PrependProxyEnv(cmd)
+
+ r := o.ssh(cmd, noSudo)
+ if !r.isSuccess() {
+ o.log.Warnf(
+ "Failed to %s. status: %d, stdout: %s, stderr: %s",
+ cmd, r.ExitStatus, r.Stdout, r.Stderr)
+ // Ignore this Error.
+ return nil, nil
+
+ }
+ cveIds, err = o.getCveIDParsingChangelog(r.Stdout, pack.Name, pack.Version)
+ if err != nil {
+ trimUbuntu := strings.Split(pack.Version, "ubuntu")[0]
+ return o.getCveIDParsingChangelog(r.Stdout, pack.Name, trimUbuntu)
+ }
+ return
+}
+
+func (o *debian) getCveIDParsingChangelog(changelog string,
+ packName string, versionOrLater string) (cveIDs []string, err error) {
+
+ cveIDs, err = o.parseChangelog(changelog, packName, versionOrLater)
+ if err == nil {
+ return
+ }
+
+ ver := strings.Split(versionOrLater, "ubuntu")[0]
+ cveIDs, err = o.parseChangelog(changelog, packName, ver)
+ if err == nil {
+ return
+ }
+
+ splittedByColon := strings.Split(versionOrLater, ":")
+ if 1 < len(splittedByColon) {
+ ver = splittedByColon[1]
+ }
+ cveIDs, err = o.parseChangelog(changelog, packName, ver)
+ if err == nil {
+ return
+ }
+
+ //TODO report as unable to parse changelog.
+ o.log.Warn(err)
+ return []string{}, nil
+}
+
+// Collect CVE-IDs included in the changelog.
+// The version which specified in argument(versionOrLater) is excluded.
+func (o *debian) parseChangelog(changelog string,
+ packName string, versionOrLater string) (cveIDs []string, err error) {
+
+ cveRe, _ := regexp.Compile(`(CVE-\d{4}-\d{4})`)
+ stopRe, _ := regexp.Compile(fmt.Sprintf(`\(%s\)`, regexp.QuoteMeta(versionOrLater)))
+ stopLineFound := false
+ lines := strings.Split(changelog, "\n")
+ for _, line := range lines {
+ if matche := stopRe.MatchString(line); matche {
+ o.log.Debugf("Found the stop line. line: %s", line)
+ stopLineFound = true
+ break
+ } else if matches := cveRe.FindAllString(line, -1); len(matches) > 0 {
+ for _, m := range matches {
+ cveIDs = util.AppendIfMissing(cveIDs, m)
+ }
+ }
+ }
+ if !stopLineFound {
+ return []string{}, fmt.Errorf(
+ "Failed to scan CVE IDs. The version is not in changelog. name: %s, version: %s",
+ packName,
+ versionOrLater,
+ )
+ }
+ return
+}
+
+type packCandidateVer struct {
+ Name string
+ Installed string
+ Candidate string
+}
+
+// parseAptCachePolicy the stdout of parse pat-get cache policy
+func (o *debian) parseAptCachePolicy(stdout, name string) (packCandidateVer, error) {
+ ver := packCandidateVer{Name: name}
+ lines := strings.Split(stdout, "\n")
+ for _, line := range lines {
+ fields := strings.Fields(line)
+ if len(fields) != 2 {
+ continue
+ }
+ switch fields[0] {
+ case "Installed:":
+ ver.Installed = fields[1]
+ case "Candidate:":
+ ver.Candidate = fields[1]
+ return ver, nil
+ default:
+ // nop
+ }
+ }
+ return ver, fmt.Errorf("Unknown Format: %s", stdout)
+}
+
+func appendPackIfMissing(slice []models.PackageInfo, s models.PackageInfo) []models.PackageInfo {
+ for _, ele := range slice {
+ if ele.Name == s.Name &&
+ ele.Version == s.Version &&
+ ele.Release == s.Release {
+ return slice
+ }
+ }
+ return append(slice, s)
+}
diff --git a/scan/debian_test.go b/scan/debian_test.go
new file mode 100644
index 00000000..c01d33ca
--- /dev/null
+++ b/scan/debian_test.go
@@ -0,0 +1,601 @@
+/* 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 (
+ "reflect"
+ "testing"
+
+ "github.com/future-architect/vuls/config"
+ "github.com/k0kubun/pp"
+)
+
+func TestParseScanedPackagesLineDebian(t *testing.T) {
+
+ var packagetests = []struct {
+ in string
+ name string
+ version string
+ }{
+ {"base-passwd 3.5.33", "base-passwd", "3.5.33"},
+ {"bzip2 1.0.6-5", "bzip2", "1.0.6-5"},
+ {"adduser 3.113+nmu3ubuntu3", "adduser", "3.113+nmu3ubuntu3"},
+ {"bash 4.3-7ubuntu1.5", "bash", "4.3-7ubuntu1.5"},
+ {"bsdutils 1:2.20.1-5.1ubuntu20.4", "bsdutils", "1:2.20.1-5.1ubuntu20.4"},
+ {"ca-certificates 20141019ubuntu0.14.04.1", "ca-certificates", "20141019ubuntu0.14.04.1"},
+ {"apt 1.0.1ubuntu2.8", "apt", "1.0.1ubuntu2.8"},
+ }
+
+ d := newDebian(config.ServerInfo{})
+ for _, tt := range packagetests {
+ n, v, _ := d.parseScanedPackagesLine(tt.in)
+ if n != tt.name {
+ t.Errorf("name: expected %s, actual %s", tt.name, n)
+ }
+ if v != tt.version {
+ t.Errorf("version: expected %s, actual %s", tt.version, v)
+ }
+ }
+
+}
+
+func TestgetCveIDParsingChangelog(t *testing.T) {
+
+ var tests = []struct {
+ in []string
+ expected []string
+ }{
+ {
+ // verubuntu1
+ []string{
+ "systemd",
+ "228-4ubuntu1",
+ `systemd (229-2) unstable; urgency=medium
+systemd (229-1) unstable; urgency=medium
+systemd (228-6) unstable; urgency=medium
+CVE-2015-2325: heap buffer overflow in compile_branch(). (Closes: #781795)
+CVE-2015-2326: heap buffer overflow in pcre_compile2(). (Closes: #783285)
+CVE-2015-3210: heap buffer overflow in pcre_compile2() /
+systemd (228-5) unstable; urgency=medium
+systemd (228-4) unstable; urgency=medium
+systemd (228-3) unstable; urgency=medium
+systemd (228-2) unstable; urgency=medium
+systemd (228-1) unstable; urgency=medium
+systemd (227-3) unstable; urgency=medium
+systemd (227-2) unstable; urgency=medium
+systemd (227-1) unstable; urgency=medium`,
+ },
+ []string{
+ "CVE-2015-2325",
+ "CVE-2015-2326",
+ "CVE-2015-3210",
+ },
+ },
+
+ {
+ // ver
+ []string{
+ "libpcre3",
+ "2:8.38-1ubuntu1",
+ `pcre3 (2:8.38-2) unstable; urgency=low
+pcre3 (2:8.38-1) unstable; urgency=low
+pcre3 (2:8.35-8) unstable; urgency=low
+pcre3 (2:8.35-7.4) unstable; urgency=medium
+pcre3 (2:8.35-7.3) unstable; urgency=medium
+pcre3 (2:8.35-7.2) unstable; urgency=low
+CVE-2015-2325: heap buffer overflow in compile_branch(). (Closes: #781795)
+CVE-2015-2326: heap buffer overflow in pcre_compile2(). (Closes: #783285)
+CVE-2015-3210: heap buffer overflow in pcre_compile2() /
+pcre3 (2:8.35-7.1) unstable; urgency=medium
+pcre3 (2:8.35-7) unstable; urgency=medium`,
+ },
+ []string{
+ "CVE-2015-2325",
+ "CVE-2015-2326",
+ "CVE-2015-3210",
+ },
+ },
+
+ {
+ // ver-ubuntu3
+ []string{
+ "sysvinit",
+ "2.88dsf-59.2ubuntu3",
+ `sysvinit (2.88dsf-59.3ubuntu1) xenial; urgency=low
+sysvinit (2.88dsf-59.3) unstable; urgency=medium
+CVE-2015-2325: heap buffer overflow in compile_branch(). (Closes: #781795)
+CVE-2015-2326: heap buffer overflow in pcre_compile2(). (Closes: #783285)
+CVE-2015-3210: heap buffer overflow in pcre_compile2() /
+sysvinit (2.88dsf-59.2ubuntu3) xenial; urgency=medium
+sysvinit (2.88dsf-59.2ubuntu2) wily; urgency=medium
+sysvinit (2.88dsf-59.2ubuntu1) wily; urgency=medium
+CVE-2015-2321: heap buffer overflow in pcre_compile2(). (Closes: #783285)
+sysvinit (2.88dsf-59.2) unstable; urgency=medium
+sysvinit (2.88dsf-59.1ubuntu3) wily; urgency=medium
+CVE-2015-2322: heap buffer overflow in pcre_compile2(). (Closes: #783285)
+sysvinit (2.88dsf-59.1ubuntu2) wily; urgency=medium
+sysvinit (2.88dsf-59.1ubuntu1) wily; urgency=medium
+sysvinit (2.88dsf-59.1) unstable; urgency=medium
+CVE-2015-2326: heap buffer overflow in pcre_compile2(). (Closes: #783285)
+sysvinit (2.88dsf-59) unstable; urgency=medium
+sysvinit (2.88dsf-58) unstable; urgency=low
+sysvinit (2.88dsf-57) unstable; urgency=low`,
+ },
+ []string{
+ "CVE-2015-2325",
+ "CVE-2015-2326",
+ "CVE-2015-3210",
+ },
+ },
+ {
+ // 1:ver-ubuntu3
+ []string{
+ "bsdutils",
+ "1:2.27.1-1ubuntu3",
+ ` util-linux (2.27.1-3ubuntu1) xenial; urgency=medium
+util-linux (2.27.1-3) unstable; urgency=medium
+CVE-2015-2325: heap buffer overflow in compile_branch(). (Closes: #781795)
+CVE-2015-2326: heap buffer overflow in pcre_compile2(). (Closes: #783285)
+CVE-2015-3210: heap buffer overflow in pcre_compile2() /
+util-linux (2.27.1-2) unstable; urgency=medium
+util-linux (2.27.1-1ubuntu4) xenial; urgency=medium
+util-linux (2.27.1-1ubuntu3) xenial; urgency=medium
+util-linux (2.27.1-1ubuntu2) xenial; urgency=medium
+util-linux (2.27.1-1ubuntu1) xenial; urgency=medium
+util-linux (2.27.1-1) unstable; urgency=medium
+util-linux (2.27-3ubuntu1) xenial; urgency=medium
+util-linux (2.27-3) unstable; urgency=medium
+util-linux (2.27-2) unstable; urgency=medium
+util-linux (2.27-1) unstable; urgency=medium
+util-linux (2.27~rc2-2) experimental; urgency=medium
+util-linux (2.27~rc2-1) experimental; urgency=medium
+util-linux (2.27~rc1-1) experimental; urgency=medium
+util-linux (2.26.2-9) unstable; urgency=medium
+util-linux (2.26.2-8) experimental; urgency=medium
+util-linux (2.26.2-7) experimental; urgency=medium
+util-linux (2.26.2-6ubuntu3) wily; urgency=medium
+CVE-2015-2329: heap buffer overflow in compile_branch(). (Closes: #781795)
+util-linux (2.26.2-6ubuntu2) wily; urgency=medium
+util-linux (2.26.2-6ubuntu1) wily; urgency=medium
+util-linux (2.26.2-6) unstable; urgency=medium`,
+ },
+ []string{
+ "CVE-2015-2325",
+ "CVE-2015-2326",
+ "CVE-2015-3210",
+ },
+ },
+ }
+
+ d := newDebian(config.ServerInfo{})
+ for _, tt := range tests {
+ actual, _ := d.getCveIDParsingChangelog(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)
+ continue
+ }
+ for i := range tt.expected {
+ if actual[i] != tt.expected[i] {
+ t.Errorf("expected %s, actual %s", tt.expected[i], actual[i])
+ }
+ }
+ }
+
+ 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) {
+
+ var tests = []struct {
+ in string
+ expected []string
+ }{
+ { // Ubuntu 12.04
+ `Reading package lists... Done
+Building dependency tree
+Reading state information... Done
+The following packages will be upgraded:
+ apt ca-certificates cpio dpkg e2fslibs e2fsprogs gnupg gpgv libc-bin libc6 libcomerr2 libpcre3
+ libpng12-0 libss2 libssl1.0.0 libudev0 multiarch-support openssl tzdata udev upstart
+21 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
+Inst dpkg [1.16.1.2ubuntu7.5] (1.16.1.2ubuntu7.7 Ubuntu:12.04/precise-updates [amd64])
+Conf dpkg (1.16.1.2ubuntu7.7 Ubuntu:12.04/precise-updates [amd64])
+Inst upstart [1.5-0ubuntu7.2] (1.5-0ubuntu7.3 Ubuntu:12.04/precise-updates [amd64])
+Inst libc-bin [2.15-0ubuntu10.10] (2.15-0ubuntu10.13 Ubuntu:12.04/precise-updates [amd64]) [libc6:amd64 ]
+Conf libc-bin (2.15-0ubuntu10.13 Ubuntu:12.04/precise-updates [amd64]) [libc6:amd64 ]
+Inst libc6 [2.15-0ubuntu10.10] (2.15-0ubuntu10.13 Ubuntu:12.04/precise-updates [amd64])
+Conf libc6 (2.15-0ubuntu10.13 Ubuntu:12.04/precise-updates [amd64])
+Inst libudev0 [175-0ubuntu9.9] (175-0ubuntu9.10 Ubuntu:12.04/precise-updates [amd64])
+Inst tzdata [2015a-0ubuntu0.12.04] (2015g-0ubuntu0.12.04 Ubuntu:12.04/precise-updates [all])
+Conf tzdata (2015g-0ubuntu0.12.04 Ubuntu:12.04/precise-updates [all])
+Inst e2fslibs [1.42-1ubuntu2] (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64]) [e2fsprogs:amd64 on e2fslibs:amd64] [e2fsprogs:amd64 ]
+Conf e2fslibs (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64]) [e2fsprogs:amd64 ]
+Inst e2fsprogs [1.42-1ubuntu2] (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64])
+Conf e2fsprogs (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64])
+Inst gpgv [1.4.11-3ubuntu2.7] (1.4.11-3ubuntu2.9 Ubuntu:12.04/precise-updates [amd64])
+Conf gpgv (1.4.11-3ubuntu2.9 Ubuntu:12.04/precise-updates [amd64])
+Inst gnupg [1.4.11-3ubuntu2.7] (1.4.11-3ubuntu2.9 Ubuntu:12.04/precise-updates [amd64])
+Conf gnupg (1.4.11-3ubuntu2.9 Ubuntu:12.04/precise-updates [amd64])
+Inst apt [0.8.16~exp12ubuntu10.22] (0.8.16~exp12ubuntu10.26 Ubuntu:12.04/precise-updates [amd64])
+Conf apt (0.8.16~exp12ubuntu10.26 Ubuntu:12.04/precise-updates [amd64])
+Inst libcomerr2 [1.42-1ubuntu2] (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64])
+Conf libcomerr2 (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64])
+Inst libss2 [1.42-1ubuntu2] (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64])
+Conf libss2 (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64])
+Inst libssl1.0.0 [1.0.1-4ubuntu5.21] (1.0.1-4ubuntu5.34 Ubuntu:12.04/precise-updates [amd64])
+Conf libssl1.0.0 (1.0.1-4ubuntu5.34 Ubuntu:12.04/precise-updates [amd64])
+Inst libpcre3 [8.12-4] (8.12-4ubuntu0.1 Ubuntu:12.04/precise-updates [amd64])
+Inst libpng12-0 [1.2.46-3ubuntu4] (1.2.46-3ubuntu4.2 Ubuntu:12.04/precise-updates [amd64])
+Inst multiarch-support [2.15-0ubuntu10.10] (2.15-0ubuntu10.13 Ubuntu:12.04/precise-updates [amd64])
+Conf multiarch-support (2.15-0ubuntu10.13 Ubuntu:12.04/precise-updates [amd64])
+Inst cpio [2.11-7ubuntu3.1] (2.11-7ubuntu3.2 Ubuntu:12.04/precise-updates [amd64])
+Inst udev [175-0ubuntu9.9] (175-0ubuntu9.10 Ubuntu:12.04/precise-updates [amd64])
+Inst openssl [1.0.1-4ubuntu5.33] (1.0.1-4ubuntu5.34 Ubuntu:12.04/precise-updates [amd64])
+Inst ca-certificates [20141019ubuntu0.12.04.1] (20160104ubuntu0.12.04.1 Ubuntu:12.04/precise-updates [all])
+Conf libudev0 (175-0ubuntu9.10 Ubuntu:12.04/precise-updates [amd64])
+Conf upstart (1.5-0ubuntu7.3 Ubuntu:12.04/precise-updates [amd64])
+Conf libpcre3 (8.12-4ubuntu0.1 Ubuntu:12.04/precise-updates [amd64])
+Conf libpng12-0 (1.2.46-3ubuntu4.2 Ubuntu:12.04/precise-updates [amd64])
+Conf cpio (2.11-7ubuntu3.2 Ubuntu:12.04/precise-updates [amd64])
+Conf udev (175-0ubuntu9.10 Ubuntu:12.04/precise-updates [amd64])
+Conf openssl (1.0.1-4ubuntu5.34 Ubuntu:12.04/precise-updates [amd64])
+Conf ca-certificates (20160104ubuntu0.12.04.1 Ubuntu:12.04/precise-updates [all])`,
+ []string{
+ "apt",
+ "ca-certificates",
+ "cpio",
+ "dpkg",
+ "e2fslibs",
+ "e2fsprogs",
+ "gnupg",
+ "gpgv",
+ "libc-bin",
+ "libc6",
+ "libcomerr2",
+ "libpcre3",
+ "libpng12-0",
+ "libss2",
+ "libssl1.0.0",
+ "libudev0",
+ "multiarch-support",
+ "openssl",
+ "tzdata",
+ "udev",
+ "upstart",
+ },
+ },
+ { // Ubuntu 14.04
+ `Reading package lists... Done
+Building dependency tree
+Reading state information... Done
+Calculating upgrade... Done
+The following packages will be upgraded:
+ apt apt-utils base-files bsdutils coreutils cpio dh-python dpkg e2fslibs
+ e2fsprogs gcc-4.8-base gcc-4.9-base gnupg gpgv ifupdown initscripts iproute2
+ isc-dhcp-client isc-dhcp-common libapt-inst1.5 libapt-pkg4.12 libblkid1
+ libc-bin libc6 libcgmanager0 libcomerr2 libdrm2 libexpat1 libffi6 libgcc1
+ libgcrypt11 libgnutls-openssl27 libgnutls26 libmount1 libpcre3 libpng12-0
+ libpython3.4-minimal libpython3.4-stdlib libsqlite3-0 libss2 libssl1.0.0
+ libstdc++6 libtasn1-6 libudev1 libuuid1 login mount multiarch-support
+ ntpdate passwd python3.4 python3.4-minimal rsyslog sudo sysv-rc
+ sysvinit-utils tzdata udev util-linux
+59 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
+Inst base-files [7.2ubuntu5.2] (7.2ubuntu5.4 Ubuntu:14.04/trusty-updates [amd64])
+Conf base-files (7.2ubuntu5.4 Ubuntu:14.04/trusty-updates [amd64])
+Inst coreutils [8.21-1ubuntu5.1] (8.21-1ubuntu5.3 Ubuntu:14.04/trusty-updates [amd64])
+Conf coreutils (8.21-1ubuntu5.3 Ubuntu:14.04/trusty-updates [amd64])
+Inst dpkg [1.17.5ubuntu5.3] (1.17.5ubuntu5.5 Ubuntu:14.04/trusty-updates [amd64])
+Conf dpkg (1.17.5ubuntu5.5 Ubuntu:14.04/trusty-updates [amd64])
+Inst libc-bin [2.19-0ubuntu6.5] (2.19-0ubuntu6.7 Ubuntu:14.04/trusty-updates [amd64])
+Inst libc6 [2.19-0ubuntu6.5] (2.19-0ubuntu6.7 Ubuntu:14.04/trusty-updates [amd64])
+Inst libgcc1 [1:4.9.1-0ubuntu1] (1:4.9.3-0ubuntu4 Ubuntu:14.04/trusty-updates [amd64]) []
+Inst gcc-4.9-base [4.9.1-0ubuntu1] (4.9.3-0ubuntu4 Ubuntu:14.04/trusty-updates [amd64])
+Conf gcc-4.9-base (4.9.3-0ubuntu4 Ubuntu:14.04/trusty-updates [amd64])
+Conf libgcc1 (1:4.9.3-0ubuntu4 Ubuntu:14.04/trusty-updates [amd64])
+Conf libc6 (2.19-0ubuntu6.7 Ubuntu:14.04/trusty-updates [amd64])
+Conf libc-bin (2.19-0ubuntu6.7 Ubuntu:14.04/trusty-updates [amd64])
+Inst e2fslibs [1.42.9-3ubuntu1] (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64]) [e2fsprogs:amd64 on e2fslibs:amd64] [e2fsprogs:amd64 ]
+Conf e2fslibs (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64]) [e2fsprogs:amd64 ]
+Inst e2fsprogs [1.42.9-3ubuntu1] (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64])
+Conf e2fsprogs (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64])
+Inst login [1:4.1.5.1-1ubuntu9] (1:4.1.5.1-1ubuntu9.2 Ubuntu:14.04/trusty-updates [amd64])
+Conf login (1:4.1.5.1-1ubuntu9.2 Ubuntu:14.04/trusty-updates [amd64])
+Inst mount [2.20.1-5.1ubuntu20.4] (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
+Conf mount (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
+Inst tzdata [2015a-0ubuntu0.14.04] (2015g-0ubuntu0.14.04 Ubuntu:14.04/trusty-updates [all])
+Conf tzdata (2015g-0ubuntu0.14.04 Ubuntu:14.04/trusty-updates [all])
+Inst sysvinit-utils [2.88dsf-41ubuntu6] (2.88dsf-41ubuntu6.3 Ubuntu:14.04/trusty-updates [amd64])
+Inst sysv-rc [2.88dsf-41ubuntu6] (2.88dsf-41ubuntu6.3 Ubuntu:14.04/trusty-updates [all])
+Conf sysv-rc (2.88dsf-41ubuntu6.3 Ubuntu:14.04/trusty-updates [all])
+Conf sysvinit-utils (2.88dsf-41ubuntu6.3 Ubuntu:14.04/trusty-updates [amd64])
+Inst util-linux [2.20.1-5.1ubuntu20.4] (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
+Conf util-linux (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
+Inst gcc-4.8-base [4.8.2-19ubuntu1] (4.8.4-2ubuntu1~14.04.1 Ubuntu:14.04/trusty-updates [amd64]) [libstdc++6:amd64 ]
+Conf gcc-4.8-base (4.8.4-2ubuntu1~14.04.1 Ubuntu:14.04/trusty-updates [amd64]) [libstdc++6:amd64 ]
+Inst libstdc++6 [4.8.2-19ubuntu1] (4.8.4-2ubuntu1~14.04.1 Ubuntu:14.04/trusty-updates [amd64])
+Conf libstdc++6 (4.8.4-2ubuntu1~14.04.1 Ubuntu:14.04/trusty-updates [amd64])
+Inst libapt-pkg4.12 [1.0.1ubuntu2.6] (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64])
+Conf libapt-pkg4.12 (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64])
+Inst gpgv [1.4.16-1ubuntu2.1] (1.4.16-1ubuntu2.3 Ubuntu:14.04/trusty-updates [amd64])
+Conf gpgv (1.4.16-1ubuntu2.3 Ubuntu:14.04/trusty-updates [amd64])
+Inst gnupg [1.4.16-1ubuntu2.1] (1.4.16-1ubuntu2.3 Ubuntu:14.04/trusty-updates [amd64])
+Conf gnupg (1.4.16-1ubuntu2.3 Ubuntu:14.04/trusty-updates [amd64])
+Inst apt [1.0.1ubuntu2.6] (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64])
+Conf apt (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64])
+Inst bsdutils [1:2.20.1-5.1ubuntu20.4] (1:2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
+Conf bsdutils (1:2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
+Inst passwd [1:4.1.5.1-1ubuntu9] (1:4.1.5.1-1ubuntu9.2 Ubuntu:14.04/trusty-updates [amd64])
+Conf passwd (1:4.1.5.1-1ubuntu9.2 Ubuntu:14.04/trusty-updates [amd64])
+Inst libuuid1 [2.20.1-5.1ubuntu20.4] (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
+Conf libuuid1 (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
+Inst libblkid1 [2.20.1-5.1ubuntu20.4] (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
+Conf libblkid1 (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
+Inst libcomerr2 [1.42.9-3ubuntu1] (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64])
+Conf libcomerr2 (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64])
+Inst libmount1 [2.20.1-5.1ubuntu20.4] (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
+Conf libmount1 (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
+Inst libpcre3 [1:8.31-2ubuntu2] (1:8.31-2ubuntu2.1 Ubuntu:14.04/trusty-updates [amd64])
+Conf libpcre3 (1:8.31-2ubuntu2.1 Ubuntu:14.04/trusty-updates [amd64])
+Inst libss2 [1.42.9-3ubuntu1] (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64])
+Conf libss2 (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64])
+Inst libapt-inst1.5 [1.0.1ubuntu2.6] (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64])
+Inst libexpat1 [2.1.0-4ubuntu1] (2.1.0-4ubuntu1.1 Ubuntu:14.04/trusty-updates [amd64])
+Inst libffi6 [3.1~rc1+r3.0.13-12] (3.1~rc1+r3.0.13-12ubuntu0.1 Ubuntu:14.04/trusty-updates [amd64])
+Inst libgcrypt11 [1.5.3-2ubuntu4.1] (1.5.3-2ubuntu4.3 Ubuntu:14.04/trusty-updates [amd64])
+Inst libtasn1-6 [3.4-3ubuntu0.1] (3.4-3ubuntu0.3 Ubuntu:14.04/trusty-updates [amd64])
+Inst libgnutls-openssl27 [2.12.23-12ubuntu2.1] (2.12.23-12ubuntu2.4 Ubuntu:14.04/trusty-updates [amd64]) []
+Inst libgnutls26 [2.12.23-12ubuntu2.1] (2.12.23-12ubuntu2.4 Ubuntu:14.04/trusty-updates [amd64])
+Inst libsqlite3-0 [3.8.2-1ubuntu2] (3.8.2-1ubuntu2.1 Ubuntu:14.04/trusty-updates [amd64])
+Inst python3.4 [3.4.0-2ubuntu1] (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64]) []
+Inst libpython3.4-stdlib [3.4.0-2ubuntu1] (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64]) []
+Inst python3.4-minimal [3.4.0-2ubuntu1] (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64]) []
+Inst libssl1.0.0 [1.0.1f-1ubuntu2.8] (1.0.1f-1ubuntu2.16 Ubuntu:14.04/trusty-updates [amd64]) []
+Inst libpython3.4-minimal [3.4.0-2ubuntu1] (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64])
+Inst ntpdate [1:4.2.6.p5+dfsg-3ubuntu2.14.04.2] (1:4.2.6.p5+dfsg-3ubuntu2.14.04.8 Ubuntu:14.04/trusty-updates [amd64])
+Inst libdrm2 [2.4.56-1~ubuntu2] (2.4.64-1~ubuntu14.04.1 Ubuntu:14.04/trusty-updates [amd64])
+Inst libpng12-0 [1.2.50-1ubuntu2] (1.2.50-1ubuntu2.14.04.2 Ubuntu:14.04/trusty-updates [amd64])
+Inst initscripts [2.88dsf-41ubuntu6] (2.88dsf-41ubuntu6.3 Ubuntu:14.04/trusty-updates [amd64])
+Inst libcgmanager0 [0.24-0ubuntu7.3] (0.24-0ubuntu7.5 Ubuntu:14.04/trusty-updates [amd64])
+Inst udev [204-5ubuntu20.10] (204-5ubuntu20.18 Ubuntu:14.04/trusty-updates [amd64]) []
+Inst libudev1 [204-5ubuntu20.10] (204-5ubuntu20.18 Ubuntu:14.04/trusty-updates [amd64])
+Inst multiarch-support [2.19-0ubuntu6.5] (2.19-0ubuntu6.7 Ubuntu:14.04/trusty-updates [amd64])
+Conf multiarch-support (2.19-0ubuntu6.7 Ubuntu:14.04/trusty-updates [amd64])
+Inst apt-utils [1.0.1ubuntu2.6] (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64])
+Inst dh-python [1.20140128-1ubuntu8] (1.20140128-1ubuntu8.2 Ubuntu:14.04/trusty-updates [all])
+Inst iproute2 [3.12.0-2] (3.12.0-2ubuntu1 Ubuntu:14.04/trusty-updates [amd64])
+Inst ifupdown [0.7.47.2ubuntu4.1] (0.7.47.2ubuntu4.3 Ubuntu:14.04/trusty-updates [amd64])
+Inst isc-dhcp-client [4.2.4-7ubuntu12] (4.2.4-7ubuntu12.4 Ubuntu:14.04/trusty-updates [amd64]) []
+Inst isc-dhcp-common [4.2.4-7ubuntu12] (4.2.4-7ubuntu12.4 Ubuntu:14.04/trusty-updates [amd64])
+Inst rsyslog [7.4.4-1ubuntu2.5] (7.4.4-1ubuntu2.6 Ubuntu:14.04/trusty-updates [amd64])
+Inst sudo [1.8.9p5-1ubuntu1] (1.8.9p5-1ubuntu1.2 Ubuntu:14.04/trusty-updates [amd64])
+Inst cpio [2.11+dfsg-1ubuntu1.1] (2.11+dfsg-1ubuntu1.2 Ubuntu:14.04/trusty-updates [amd64])
+Conf libapt-inst1.5 (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64])
+Conf libexpat1 (2.1.0-4ubuntu1.1 Ubuntu:14.04/trusty-updates [amd64])
+Conf libffi6 (3.1~rc1+r3.0.13-12ubuntu0.1 Ubuntu:14.04/trusty-updates [amd64])
+Conf libgcrypt11 (1.5.3-2ubuntu4.3 Ubuntu:14.04/trusty-updates [amd64])
+Conf libtasn1-6 (3.4-3ubuntu0.3 Ubuntu:14.04/trusty-updates [amd64])
+Conf libgnutls26 (2.12.23-12ubuntu2.4 Ubuntu:14.04/trusty-updates [amd64])
+Conf libgnutls-openssl27 (2.12.23-12ubuntu2.4 Ubuntu:14.04/trusty-updates [amd64])
+Conf libsqlite3-0 (3.8.2-1ubuntu2.1 Ubuntu:14.04/trusty-updates [amd64])
+Conf libssl1.0.0 (1.0.1f-1ubuntu2.16 Ubuntu:14.04/trusty-updates [amd64])
+Conf libpython3.4-minimal (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64])
+Conf python3.4-minimal (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64])
+Conf libpython3.4-stdlib (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64])
+Conf python3.4 (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64])
+Conf ntpdate (1:4.2.6.p5+dfsg-3ubuntu2.14.04.8 Ubuntu:14.04/trusty-updates [amd64])
+Conf libdrm2 (2.4.64-1~ubuntu14.04.1 Ubuntu:14.04/trusty-updates [amd64])
+Conf libpng12-0 (1.2.50-1ubuntu2.14.04.2 Ubuntu:14.04/trusty-updates [amd64])
+Conf initscripts (2.88dsf-41ubuntu6.3 Ubuntu:14.04/trusty-updates [amd64])
+Conf libcgmanager0 (0.24-0ubuntu7.5 Ubuntu:14.04/trusty-updates [amd64])
+Conf libudev1 (204-5ubuntu20.18 Ubuntu:14.04/trusty-updates [amd64])
+Conf udev (204-5ubuntu20.18 Ubuntu:14.04/trusty-updates [amd64])
+Conf apt-utils (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64])
+Conf dh-python (1.20140128-1ubuntu8.2 Ubuntu:14.04/trusty-updates [all])
+Conf iproute2 (3.12.0-2ubuntu1 Ubuntu:14.04/trusty-updates [amd64])
+Conf ifupdown (0.7.47.2ubuntu4.3 Ubuntu:14.04/trusty-updates [amd64])
+Conf isc-dhcp-common (4.2.4-7ubuntu12.4 Ubuntu:14.04/trusty-updates [amd64])
+Conf isc-dhcp-client (4.2.4-7ubuntu12.4 Ubuntu:14.04/trusty-updates [amd64])
+Conf rsyslog (7.4.4-1ubuntu2.6 Ubuntu:14.04/trusty-updates [amd64])
+Conf sudo (1.8.9p5-1ubuntu1.2 Ubuntu:14.04/trusty-updates [amd64])
+Conf cpio (2.11+dfsg-1ubuntu1.2 Ubuntu:14.04/trusty-updates [amd64])
+`,
+ []string{
+ "apt",
+ "apt-utils",
+ "base-files",
+ "bsdutils",
+ "coreutils",
+ "cpio",
+ "dh-python",
+ "dpkg",
+ "e2fslibs",
+ "e2fsprogs",
+ "gcc-4.8-base",
+ "gcc-4.9-base",
+ "gnupg",
+ "gpgv",
+ "ifupdown",
+ "initscripts",
+ "iproute2",
+ "isc-dhcp-client",
+ "isc-dhcp-common",
+ "libapt-inst1.5",
+ "libapt-pkg4.12",
+ "libblkid1",
+ "libc-bin",
+ "libc6",
+ "libcgmanager0",
+ "libcomerr2",
+ "libdrm2",
+ "libexpat1",
+ "libffi6",
+ "libgcc1",
+ "libgcrypt11",
+ "libgnutls-openssl27",
+ "libgnutls26",
+ "libmount1",
+ "libpcre3",
+ "libpng12-0",
+ "libpython3.4-minimal",
+ "libpython3.4-stdlib",
+ "libsqlite3-0",
+ "libss2",
+ "libssl1.0.0",
+ "libstdc++6",
+ "libtasn1-6",
+ "libudev1",
+ "libuuid1",
+ "login",
+ "mount",
+ "multiarch-support",
+ "ntpdate",
+ "passwd",
+ "python3.4",
+ "python3.4-minimal",
+ "rsyslog",
+ "sudo",
+ "sysv-rc",
+ "sysvinit-utils",
+ "tzdata",
+ "udev",
+ "util-linux",
+ },
+ },
+ {
+ //Ubuntu12.04
+ `Reading package lists... Done
+Building dependency tree
+Reading state information... Done
+0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.`,
+ []string{},
+ },
+ {
+ //Ubuntu14.04
+ `Reading package lists... Done
+Building dependency tree
+Reading state information... Done
+Calculating upgrade... Done
+0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.`,
+ []string{},
+ },
+ }
+
+ d := newDebian(config.ServerInfo{})
+ for _, tt := range tests {
+ actual, err := d.parseAptGetUpgrade(tt.in)
+ if err != nil {
+ t.Errorf("Returning error is unexpected.")
+ }
+ if len(tt.expected) != len(actual) {
+ t.Errorf("Result length is not as same as expected. expected: %d, actual: %d", len(tt.expected), len(actual))
+ pp.Println(tt.expected)
+ pp.Println(actual)
+ return
+ }
+ for i := range tt.expected {
+ if tt.expected[i] != actual[i] {
+ t.Errorf("[%d] expected %s, actual %s", i, tt.expected[i], actual[i])
+ }
+ }
+ }
+}
+
+func TestParseAptCachePolicy(t *testing.T) {
+
+ var tests = []struct {
+ stdout string
+ name string
+ expected packCandidateVer
+ }{
+ {
+ // Ubuntu 16.04
+ `openssl:
+ Installed: 1.0.2f-2ubuntu1
+ Candidate: 1.0.2g-1ubuntu2
+ Version table:
+ 1.0.2g-1ubuntu2 500
+ 500 http://archive.ubuntu.com/ubuntu xenial/main amd64 Packages
+ *** 1.0.2f-2ubuntu1 100
+ 100 /var/lib/dpkg/status`,
+ "openssl",
+ packCandidateVer{
+ Name: "openssl",
+ Installed: "1.0.2f-2ubuntu1",
+ Candidate: "1.0.2g-1ubuntu2",
+ },
+ },
+ {
+ // Ubuntu 14.04
+ `openssl:
+ Installed: 1.0.1f-1ubuntu2.16
+ Candidate: 1.0.1f-1ubuntu2.17
+ Version table:
+ 1.0.1f-1ubuntu2.17 0
+ 500 http://archive.ubuntu.com/ubuntu/ trusty-updates/main amd64 Packages
+ 500 http://archive.ubuntu.com/ubuntu/ trusty-security/main amd64 Packages
+ *** 1.0.1f-1ubuntu2.16 0
+ 100 /var/lib/dpkg/status
+ 1.0.1f-1ubuntu2 0
+ 500 http://archive.ubuntu.com/ubuntu/ trusty/main amd64 Packages`,
+ "openssl",
+ packCandidateVer{
+ Name: "openssl",
+ Installed: "1.0.1f-1ubuntu2.16",
+ Candidate: "1.0.1f-1ubuntu2.17",
+ },
+ },
+ {
+ // Ubuntu 12.04
+ `openssl:
+ Installed: 1.0.1-4ubuntu5.33
+ Candidate: 1.0.1-4ubuntu5.34
+ Version table:
+ 1.0.1-4ubuntu5.34 0
+ 500 http://archive.ubuntu.com/ubuntu/ precise-updates/main amd64 Packages
+ 500 http://archive.ubuntu.com/ubuntu/ precise-security/main amd64 Packages
+ *** 1.0.1-4ubuntu5.33 0
+ 100 /var/lib/dpkg/status
+ 1.0.1-4ubuntu3 0
+ 500 http://archive.ubuntu.com/ubuntu/ precise/main amd64 Packages`,
+ "openssl",
+ packCandidateVer{
+ Name: "openssl",
+ Installed: "1.0.1-4ubuntu5.33",
+ Candidate: "1.0.1-4ubuntu5.34",
+ },
+ },
+ }
+
+ d := newDebian(config.ServerInfo{})
+ for _, tt := range tests {
+ actual, err := d.parseAptCachePolicy(tt.stdout, tt.name)
+ if err != nil {
+ t.Errorf("Error has occurred: %s, actual: %#v", err, actual)
+ }
+ 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)
+ }
+ }
+}
diff --git a/scan/linux.go b/scan/linux.go
new file mode 100644
index 00000000..4002caf8
--- /dev/null
+++ b/scan/linux.go
@@ -0,0 +1,129 @@
+/* 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 (
+ "sort"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/future-architect/vuls/config"
+ "github.com/future-architect/vuls/cveapi"
+ "github.com/future-architect/vuls/models"
+)
+
+type linux struct {
+ ServerInfo config.ServerInfo
+
+ Family string
+ Release string
+ osPackages
+ log *logrus.Entry
+}
+
+func (l *linux) ssh(cmd string, sudo bool) sshResult {
+ return sshExec(l.ServerInfo, cmd, sudo, l.log)
+}
+
+func (l *linux) setServerInfo(c config.ServerInfo) {
+ l.ServerInfo = c
+}
+
+func (l *linux) getServerInfo() config.ServerInfo {
+ return l.ServerInfo
+}
+
+func (l *linux) setDistributionInfo(fam, rel string) {
+ l.Family = fam
+ l.Release = rel
+}
+
+func (l *linux) convertToModel() (models.ScanResult, error) {
+ var cves, unknownScoreCves []models.CveInfo
+ for _, p := range l.UnsecurePackages {
+ if p.CveDetail.CvssScore(config.Conf.Lang) < 0 {
+ unknownScoreCves = append(unknownScoreCves, models.CveInfo{
+ CveDetail: p.CveDetail,
+ Packages: p.Packs,
+ DistroAdvisories: p.DistroAdvisories, // only Amazon Linux
+ })
+ continue
+ }
+
+ cpenames := []models.CpeName{}
+ for _, cpename := range p.CpeNames {
+ cpenames = append(cpenames,
+ models.CpeName{Name: cpename})
+ }
+
+ cve := models.CveInfo{
+ CveDetail: p.CveDetail,
+ Packages: p.Packs,
+ DistroAdvisories: p.DistroAdvisories, // only Amazon Linux
+ CpeNames: cpenames,
+ }
+ cves = append(cves, cve)
+ }
+
+ return models.ScanResult{
+ ServerName: l.ServerInfo.ServerName,
+ Family: l.Family,
+ Release: l.Release,
+ KnownCves: cves,
+ UnknownCves: unknownScoreCves,
+ }, nil
+}
+
+// scanVulnByCpeName search vulnerabilities that specified in config file.
+func (l *linux) scanVulnByCpeName() error {
+ unsecurePacks := CvePacksList{}
+
+ serverInfo := l.getServerInfo()
+ cpeNames := serverInfo.CpeNames
+
+ // remove duplicate
+ set := map[string]CvePacksInfo{}
+
+ for _, name := range cpeNames {
+ details, err := cveapi.CveClient.FetchCveDetailsByCpeName(name)
+ if err != nil {
+ return err
+ }
+ for _, detail := range details {
+ if val, ok := set[detail.CveID]; ok {
+ names := val.CpeNames
+ names = append(names, name)
+ val.CpeNames = names
+ set[detail.CveID] = val
+ } else {
+ set[detail.CveID] = CvePacksInfo{
+ CveID: detail.CveID,
+ CveDetail: detail,
+ CpeNames: []string{name},
+ }
+ }
+ }
+ }
+
+ for key := range set {
+ unsecurePacks = append(unsecurePacks, set[key])
+ }
+ unsecurePacks = append(unsecurePacks, l.UnsecurePackages...)
+ sort.Sort(CvePacksList(unsecurePacks))
+ l.setUnsecurePackages(unsecurePacks)
+ return nil
+}
diff --git a/scan/linux_test.go b/scan/linux_test.go
new file mode 100644
index 00000000..ef421884
--- /dev/null
+++ b/scan/linux_test.go
@@ -0,0 +1,18 @@
+/* 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
diff --git a/scan/redhat.go b/scan/redhat.go
new file mode 100644
index 00000000..6c8d4402
--- /dev/null
+++ b/scan/redhat.go
@@ -0,0 +1,861 @@
+/* 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 (
+ "fmt"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/future-architect/vuls/config"
+ "github.com/future-architect/vuls/cveapi"
+ "github.com/future-architect/vuls/models"
+ "github.com/future-architect/vuls/util"
+
+ "github.com/k0kubun/pp"
+)
+
+// inherit OsTypeInterface
+type redhat struct {
+ linux
+}
+
+// NewRedhat is constructor
+func newRedhat(c config.ServerInfo) *redhat {
+ r := &redhat{}
+ r.log = util.NewCustomLogger(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)
+
+ // set sudo option flag
+ c.SudoOpt = config.SudoOption{ExecBySudoSh: true}
+ red.setServerInfo(c)
+
+ if r := sshExec(c, "ls /etc/fedora-release", noSudo); r.isSuccess() {
+ red.setDistributionInfo("fedora", "unknown")
+ Log.Warn("Fedora not tested yet. Host: %s:%s", c.Host, c.Port)
+ return true, red
+ }
+
+ if r := sshExec(c, "ls /etc/redhat-release", noSudo); r.isSuccess() {
+ // https://www.rackaid.com/blog/how-to-determine-centos-or-red-hat-version/
+ // e.g.
+ // $ cat /etc/redhat-release
+ // CentOS release 6.5 (Final)
+ if r := sshExec(c, "cat /etc/redhat-release", noSudo); r.isSuccess() {
+ re, _ := regexp.Compile(`(.*) release (\d[\d.]*)`)
+ result := re.FindStringSubmatch(strings.TrimSpace(r.Stdout))
+ if len(result) != 3 {
+ Log.Warn(
+ "Failed to parse RedHat/CentOS version. stdout: %s, Host: %s:%s",
+ r.Stdout, c.Host, c.Port)
+ return true, red
+ }
+
+ release := result[2]
+ switch strings.ToLower(result[1]) {
+ case "centos", "centos linux":
+ red.setDistributionInfo("centos", release)
+ default:
+ red.setDistributionInfo("rhel", release)
+ }
+ return true, red
+ }
+ return true, red
+ }
+
+ if r := sshExec(c, "ls /etc/system-release", noSudo); r.isSuccess() {
+ family := "amazon"
+ release := "unknown"
+ if r := sshExec(c, "cat /etc/system-release", noSudo); r.isSuccess() {
+ fields := strings.Fields(r.Stdout)
+ if len(fields) == 5 {
+ release = fields[4]
+ }
+ }
+ red.setDistributionInfo(family, release)
+ return true, red
+ }
+
+ Log.Debugf("Not RedHat like Linux. Host: %s:%s", c.Host, c.Port)
+ return false, red
+}
+
+// CentOS 5 ... yum-plugin-security, yum-changelog
+// CentOS 6 ... yum-plugin-security, yum-plugin-changelog
+// CentOS 7 ... yum-plugin-security, yum-plugin-changelog
+// RHEL, Amazon ... no additinal packages needed
+func (o *redhat) install() error {
+
+ switch o.Family {
+ case "rhel", "amazon":
+ o.log.Infof("Nothing to do")
+ return nil
+ }
+
+ if err := o.installYumPluginSecurity(); err != nil {
+ return err
+ }
+ return o.installYumChangelog()
+}
+
+func (o *redhat) installYumPluginSecurity() error {
+
+ if r := o.ssh("rpm -q yum-plugin-security", noSudo); r.isSuccess() {
+ o.log.Infof("Ignored: yum-plugin-security already installed")
+ return nil
+ }
+
+ cmd := util.PrependProxyEnv("yum install -y yum-plugin-security")
+ if r := o.ssh(cmd, sudo); !r.isSuccess() {
+ return fmt.Errorf(
+ "Failed to %s. status: %d, stdout: %s, stderr: %s",
+ cmd, r.ExitStatus, r.Stdout, r.Stderr)
+ }
+ return nil
+}
+
+func (o *redhat) installYumChangelog() error {
+ o.log.Info("Installing yum-plugin-security...")
+
+ if o.Family == "centos" {
+ var majorVersion int
+ if 0 < len(o.Release) {
+ majorVersion, _ = strconv.Atoi(strings.Split(o.Release, ".")[0])
+ } else {
+ return fmt.Errorf(
+ "Not implemented yet. family: %s, release: %s",
+ o.Family, o.Release)
+ }
+
+ var packName = ""
+ if majorVersion < 6 {
+ packName = "yum-changelog"
+ } else {
+ packName = "yum-plugin-changelog"
+ }
+
+ cmd := "rpm -q " + packName
+ if r := o.ssh(cmd, noSudo); r.isSuccess() {
+ o.log.Infof("Ignored: %s already installed.", packName)
+ return nil
+ }
+
+ cmd = util.PrependProxyEnv("yum install -y " + packName)
+ if r := o.ssh(cmd, sudo); !r.isSuccess() {
+ return fmt.Errorf(
+ "Failed to install %s. status: %d, stdout: %s, stderr: %s",
+ packName, r.ExitStatus, r.Stdout, r.Stderr)
+ }
+ o.log.Infof("Installed: %s.", packName)
+ }
+ return nil
+}
+
+func (o *redhat) checkRequiredPackagesInstalled() error {
+ if config.Conf.UseYumPluginSecurity {
+ // check if yum-plugin-security is installed.
+ // Amazon Linux, REHL can execute 'yum updateinfo --security updates' without yum-plugin-security
+ cmd := "rpm -q yum-plugin-security"
+ if o.Family == "centos" {
+ if r := o.ssh(cmd, noSudo); !r.isSuccess() {
+ msg := "yum-plugin-security is not installed"
+ o.log.Errorf(msg)
+ return fmt.Errorf(msg)
+ }
+ }
+ return nil
+ }
+
+ if o.Family == "centos" {
+ var majorVersion int
+ if 0 < len(o.Release) {
+ majorVersion, _ = strconv.Atoi(strings.Split(o.Release, ".")[0])
+ } else {
+ msg := fmt.Sprintf("Not implemented yet. family: %s, release: %s", o.Family, o.Release)
+ o.log.Errorf(msg)
+ return fmt.Errorf(msg)
+ }
+
+ var packName = ""
+ if majorVersion < 6 {
+ packName = "yum-changelog"
+ } else {
+ packName = "yum-plugin-changelog"
+ }
+
+ cmd := "rpm -q " + packName
+ if r := o.ssh(cmd, noSudo); !r.isSuccess() {
+ msg := fmt.Sprintf("%s is not installed", packName)
+ o.log.Errorf(msg)
+ return fmt.Errorf(msg)
+ }
+ }
+ return nil
+}
+
+func (o *redhat) scanPackages() error {
+ var err error
+ var packs []models.PackageInfo
+ if packs, err = o.scanInstalledPackages(); err != nil {
+ o.log.Errorf("Failed to scan installed packages")
+ return err
+ }
+ o.setPackages(packs)
+
+ var unsecurePacks []CvePacksInfo
+ if unsecurePacks, err = o.scanUnsecurePackages(); err != nil {
+ o.log.Errorf("Failed to scan valnerable packages")
+ return err
+ }
+ o.setUnsecurePackages(unsecurePacks)
+ return nil
+}
+
+func (o *redhat) scanInstalledPackages() (installedPackages models.PackageInfoList, err error) {
+ cmd := "rpm -qa --queryformat '%{NAME}\t%{VERSION}\t%{RELEASE}\n'"
+ r := o.ssh(cmd, noSudo)
+ if r.isSuccess() {
+ // e.g.
+ // openssl 1.0.1e 30.el6.11
+ lines := strings.Split(r.Stdout, "\n")
+ for _, line := range lines {
+ if trimed := strings.TrimSpace(line); len(trimed) != 0 {
+ var packinfo models.PackageInfo
+ if packinfo, err = o.parseScanedPackagesLine(line); err != nil {
+ return
+ }
+ installedPackages = append(installedPackages, packinfo)
+ }
+ }
+ return
+ }
+
+ return installedPackages, fmt.Errorf(
+ "Scan packages failed. status: %d, stdout: %s, stderr: %s",
+ r.ExitStatus, r.Stdout, r.Stderr)
+}
+
+func (o *redhat) parseScanedPackagesLine(line string) (pack models.PackageInfo, err error) {
+ re, _ := regexp.Compile(`^([^\t']+)\t([^\t]+)\t(.+)$`)
+ result := re.FindStringSubmatch(line)
+ if len(result) == 4 {
+ pack.Name = result[1]
+ pack.Version = result[2]
+ pack.Release = strings.TrimSpace(result[3])
+ } else {
+ err = fmt.Errorf("redhat: Failed to parse package line: %s", line)
+ }
+ return
+}
+
+func (o *redhat) scanUnsecurePackages() ([]CvePacksInfo, error) {
+ if o.Family != "centos" || config.Conf.UseYumPluginSecurity {
+ // Amazon, RHEL has yum updateinfo as default
+ // yum updateinfo can collenct vendor advisory information.
+ return o.scanUnsecurePackagesUsingYumPluginSecurity()
+ }
+ // CentOS does not have security channel...
+ // So, yum check-update then parse chnagelog.
+ return o.scanUnsecurePackagesUsingYumCheckUpdate()
+}
+
+//TODO return whether already expired.
+func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (CvePacksList, error) {
+
+ cmd := "yum check-update"
+ r := o.ssh(util.PrependProxyEnv(cmd), sudo)
+ if !r.isSuccess(0, 100) {
+ //returns an exit code of 100 if there are available updates.
+ return nil, fmt.Errorf(
+ "Failed to %s. status: %d, stdout: %s, stderr: %s",
+ cmd, r.ExitStatus, r.Stdout, r.Stderr)
+ }
+
+ // get Updateble package name, installed, candidate version.
+ packInfoList, err := o.parseYumCheckUpdateLines(r.Stdout)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to parse %s. err: %s", cmd, err)
+ }
+ o.log.Debugf("%s", pp.Sprintf("%s", packInfoList))
+
+ // Collect CVE-IDs in changelog
+ type PackInfoCveIDs struct {
+ PackInfo models.PackageInfo
+ CveIDs []string
+ }
+ var results []PackInfoCveIDs
+ for i, packInfo := range packInfoList {
+ changelog, err := o.getChangelog(packInfo.Name)
+ if err != nil {
+ o.log.Errorf("Failed to collect CVE. err: %s", err)
+ return nil, err
+ }
+
+ // Collect unique set of CVE-ID in each changelog
+ uniqueCveIDMap := make(map[string]bool)
+ lines := strings.Split(changelog, "\n")
+ for _, line := range lines {
+ cveIDs := o.parseYumUpdateinfoLineToGetCveIDs(line)
+ for _, c := range cveIDs {
+ uniqueCveIDMap[c] = true
+ }
+ }
+
+ // keys
+ var cveIDs []string
+ for k := range uniqueCveIDMap {
+ cveIDs = append(cveIDs, k)
+ }
+ p := PackInfoCveIDs{
+ PackInfo: packInfo,
+ CveIDs: cveIDs,
+ }
+ results = append(results, p)
+
+ o.log.Infof("(%d/%d) Scanned %s-%s-%s -> %s-%s : %s",
+ i+1,
+ len(packInfoList),
+ p.PackInfo.Name,
+ p.PackInfo.Version,
+ p.PackInfo.Release,
+ p.PackInfo.NewVersion,
+ p.PackInfo.NewRelease,
+ p.CveIDs)
+ }
+
+ // transform datastructure
+ // - From
+ // [
+ // {
+ // PackInfo: models.PackageInfo,
+ // CveIDs: []string,
+ // },
+ // ]
+ // - To
+ // map {
+ // CveID: []models.PackageInfo
+ // }
+ cveIDPackInfoMap := make(map[string][]models.PackageInfo)
+ for _, res := range results {
+ for _, cveID := range res.CveIDs {
+ // packInfo, found := o.Packages.FindByName(res.Packname)
+ // if !found {
+ // return CvePacksList{}, fmt.Errorf(
+ // "Faild to transform data structure: %v", res.Packname)
+ // }
+ cveIDPackInfoMap[cveID] = append(cveIDPackInfoMap[cveID], res.PackInfo)
+ }
+ }
+
+ var uniqueCveIDs []string
+ for cveID := range cveIDPackInfoMap {
+ uniqueCveIDs = append(uniqueCveIDs, cveID)
+ }
+
+ // cveIDs => []cve.CveInfo
+ o.log.Info("Fetching CVE details...")
+ cveDetails, err := cveapi.CveClient.FetchCveDetails(uniqueCveIDs)
+ if err != nil {
+ return nil, err
+ }
+ o.log.Info("Done")
+
+ cvePacksList := []CvePacksInfo{}
+ for _, detail := range cveDetails {
+ // Amazon, RHEL do not use this method, so VendorAdvisory do not set.
+ cvePacksList = append(cvePacksList, CvePacksInfo{
+ CveID: detail.CveID,
+ CveDetail: detail,
+ Packs: cveIDPackInfoMap[detail.CveID],
+ // CvssScore: cinfo.CvssScore(conf.Lang),
+ })
+ }
+ sort.Sort(CvePacksList(cvePacksList))
+ return cvePacksList, nil
+}
+
+// parseYumCheckUpdateLines parse yum check-update to get package name, candidate version
+func (o *redhat) parseYumCheckUpdateLines(stdout string) (results models.PackageInfoList, err error) {
+ needToParse := false
+ lines := strings.Split(stdout, "\n")
+ for _, line := range lines {
+ // update information of packages begin after blank line.
+ if trimed := strings.TrimSpace(line); len(trimed) == 0 {
+ needToParse = true
+ continue
+ }
+ if needToParse {
+ candidate, err := o.parseYumCheckUpdateLine(line)
+ if err != nil {
+ return models.PackageInfoList{}, err
+ }
+
+ installed, found := o.Packages.FindByName(candidate.Name)
+ if !found {
+ return models.PackageInfoList{}, fmt.Errorf(
+ "Failed to parse yum check update line: %s-%s-%s",
+ candidate.Name, candidate.Version, candidate.Release)
+ }
+ installed.NewVersion = candidate.NewVersion
+ installed.NewRelease = candidate.NewRelease
+ results = append(results, installed)
+ }
+ }
+ return
+}
+
+func (o *redhat) parseYumCheckUpdateLine(line string) (models.PackageInfo, error) {
+ fields := strings.Fields(line)
+ if len(fields) != 3 {
+ return models.PackageInfo{}, fmt.Errorf("Unknown format: %s", line)
+ }
+ splitted := strings.Split(fields[0], ".")
+ packName := ""
+ if len(splitted) == 1 {
+ packName = fields[0]
+ } else {
+ packName = strings.Join(strings.Split(fields[0], ".")[0:(len(splitted)-1)], ".")
+ }
+
+ fields = strings.Split(fields[1], "-")
+ if len(fields) != 2 {
+ return models.PackageInfo{}, fmt.Errorf("Unknown format: %s", line)
+ }
+ version := fields[0]
+ release := fields[1]
+ return models.PackageInfo{
+ Name: packName,
+ NewVersion: version,
+ NewRelease: release,
+ }, nil
+}
+
+func (o *redhat) getChangelog(packageNames string) (stdout string, err error) {
+ command := "echo N | "
+ if 0 < len(config.Conf.HTTPProxy) {
+ command += util.ProxyEnv()
+ }
+ command += fmt.Sprintf(" yum update --changelog %s | grep CVE", packageNames)
+
+ r := o.ssh(command, sudo)
+ if !r.isSuccess(0, 1) {
+ return "", fmt.Errorf(
+ "Failed to get changelog. status: %d, stdout: %s, stderr: %s",
+ r.ExitStatus, r.Stdout, r.Stderr)
+ }
+ return r.Stdout, nil
+}
+
+type distroAdvisoryCveIDs struct {
+ DistroAdvisory models.DistroAdvisory
+ CveIDs []string
+}
+
+// Scaning unsecure packages using yum-plugin-security.
+//TODO return whether already expired.
+func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (CvePacksList, error) {
+ if o.Family == "centos" {
+ // CentOS has no security channel.
+ // So use yum check-update && parse changelog
+ return CvePacksList{}, fmt.Errorf(
+ "yum updateinfo is not suppported on CentOS")
+ }
+
+ cmd := "yum repolist"
+ r := o.ssh(util.PrependProxyEnv(cmd), sudo)
+ if !r.isSuccess() {
+ return nil, fmt.Errorf(
+ "Failed to %s. status: %d, stdout: %s, stderr: %s",
+ cmd, r.ExitStatus, r.Stdout, r.Stderr)
+ }
+
+ // get advisoryID(RHSA, ALAS) - package name,version
+ cmd = "yum updateinfo list available --security"
+ r = o.ssh(util.PrependProxyEnv(cmd), sudo)
+ if !r.isSuccess() {
+ return nil, fmt.Errorf(
+ "Failed to %s. status: %d, stdout: %s, stderr: %s",
+ cmd, r.ExitStatus, r.Stdout, r.Stderr)
+ }
+ advIDPackNamesList, err := o.parseYumUpdateinfoListAvailable(r.Stdout)
+
+ // get package name, version, rel to be upgrade.
+ cmd = "yum check-update --security"
+ r = o.ssh(util.PrependProxyEnv(cmd), sudo)
+ if !r.isSuccess(0, 100) {
+ //returns an exit code of 100 if there are available updates.
+ return nil, fmt.Errorf(
+ "Failed to %s. status: %d, stdout: %s, stderr: %s",
+ cmd, r.ExitStatus, r.Stdout, r.Stderr)
+ }
+ vulnerablePackInfoList, err := o.parseYumCheckUpdateLines(r.Stdout)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to parse %s. err: %s", cmd, err)
+ }
+ o.log.Debugf("%s", pp.Sprintf("%s", vulnerablePackInfoList))
+ for i, packInfo := range vulnerablePackInfoList {
+ installedPack, found := o.Packages.FindByName(packInfo.Name)
+ if !found {
+ return nil, fmt.Errorf(
+ "Parsed package not found. packInfo: %#v", packInfo)
+ }
+ vulnerablePackInfoList[i].Version = installedPack.Version
+ vulnerablePackInfoList[i].Release = installedPack.Release
+ }
+
+ dict := map[string][]models.PackageInfo{}
+ for _, advIDPackNames := range advIDPackNamesList {
+ packInfoList := models.PackageInfoList{}
+ for _, packName := range advIDPackNames.PackNames {
+ packInfo, found := vulnerablePackInfoList.FindByName(packName)
+ if !found {
+ return nil, fmt.Errorf(
+ "PackInfo not found. packInfo: %#v", packName)
+ }
+ packInfoList = append(packInfoList, packInfo)
+ continue
+ }
+ dict[advIDPackNames.AdvisoryID] = packInfoList
+ }
+
+ // get advisoryID(RHSA, ALAS) - CVE IDs
+ cmd = "yum updateinfo --security update"
+ r = o.ssh(util.PrependProxyEnv(cmd), noSudo)
+ if !r.isSuccess() {
+ return nil, fmt.Errorf(
+ "Failed to %s. status: %d, stdout: %s, stderr: %s",
+ cmd, r.ExitStatus, r.Stdout, r.Stderr)
+ }
+ advisoryCveIDsList, err := o.parseYumUpdateinfo(r.Stdout)
+ if err != nil {
+ return CvePacksList{}, err
+ }
+ // pp.Println(advisoryCveIDsList)
+
+ // All information collected.
+ // Convert to CvePacksList.
+ o.log.Info("Fetching CVE details...")
+ result := CvePacksList{}
+ for _, advIDCveIDs := range advisoryCveIDsList {
+ cveDetails, err :=
+ cveapi.CveClient.FetchCveDetails(advIDCveIDs.CveIDs)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, cveDetail := range cveDetails {
+ found := false
+ for i, p := range result {
+ if cveDetail.CveID == p.CveID {
+ advAppended := append(p.DistroAdvisories, advIDCveIDs.DistroAdvisory)
+ result[i].DistroAdvisories = advAppended
+
+ packs := dict[advIDCveIDs.DistroAdvisory.AdvisoryID]
+ result[i].Packs = append(result[i].Packs, packs...)
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ cpinfo := CvePacksInfo{
+ CveID: cveDetail.CveID,
+ CveDetail: cveDetail,
+ DistroAdvisories: []models.DistroAdvisory{advIDCveIDs.DistroAdvisory},
+ Packs: dict[advIDCveIDs.DistroAdvisory.AdvisoryID],
+ }
+ result = append(result, cpinfo)
+ }
+ }
+ }
+ o.log.Info("Done")
+ return result, nil
+}
+
+func (o *redhat) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveIDs, err error) {
+ sectionState := Outside
+ lines := strings.Split(stdout, "\n")
+ lines = append(lines, "=============")
+
+ // Amazon Linux AMI Security Information
+ advisory := models.DistroAdvisory{}
+
+ cveIDsSetInThisSection := make(map[string]bool)
+
+ // use this flag to Collect CVE IDs in CVEs field.
+ var inDesctiption = false
+
+ for _, line := range lines {
+ line = strings.TrimSpace(line)
+
+ // find the new section pattern
+ if match, _ := o.isHorizontalRule(line); match {
+
+ // set previous section's result to return-variable
+ if sectionState == Content {
+
+ foundCveIDs := []string{}
+ for cveID := range cveIDsSetInThisSection {
+ foundCveIDs = append(foundCveIDs, cveID)
+ }
+ sort.Strings(foundCveIDs)
+ result = append(result, distroAdvisoryCveIDs{
+ DistroAdvisory: advisory,
+ CveIDs: foundCveIDs,
+ })
+
+ // reset for next section.
+ cveIDsSetInThisSection = make(map[string]bool)
+ inDesctiption = false
+ }
+
+ // Go to next section
+ sectionState = o.changeSectionState(sectionState)
+ continue
+ }
+
+ switch sectionState {
+ case Header:
+ switch o.Family {
+ case "centos":
+ // CentOS has no security channel.
+ // So use yum check-update && parse changelog
+ return result, fmt.Errorf(
+ "yum updateinfo is not suppported on CentOS")
+ case "rhel", "amazon":
+ // nop
+ }
+
+ case Content:
+ if found := o.isDescriptionLine(line); found {
+ inDesctiption = true
+ }
+
+ // severity
+ severity, found := o.parseYumUpdateinfoToGetSeverity(line)
+ if found {
+ advisory.Severity = severity
+ }
+
+ // No need to parse in description except severity
+ if inDesctiption {
+ continue
+ }
+
+ cveIDs := o.parseYumUpdateinfoLineToGetCveIDs(line)
+ for _, cveID := range cveIDs {
+ cveIDsSetInThisSection[cveID] = true
+ }
+
+ advisoryID, found := o.parseYumUpdateinfoToGetAdvisoryID(line)
+ if found {
+ advisory.AdvisoryID = advisoryID
+ }
+
+ issued, found := o.parseYumUpdateinfoLineToGetIssued(line)
+ if found {
+ advisory.Issued = issued
+ }
+
+ updated, found := o.parseYumUpdateinfoLineToGetUpdated(line)
+ if found {
+ advisory.Updated = updated
+ }
+ }
+ }
+ return
+}
+
+// state
+const (
+ Outside = iota
+ Header = iota
+ Content = iota
+)
+
+func (o *redhat) changeSectionState(state int) (newState int) {
+ switch state {
+ case Outside, Content:
+ newState = Header
+ case Header:
+ newState = Content
+ }
+ return newState
+}
+
+func (o *redhat) isHorizontalRule(line string) (bool, error) {
+ return regexp.MatchString("^=+$", line)
+}
+
+// see test case
+func (o *redhat) parseYumUpdateinfoHeaderCentOS(line string) (packs []models.PackageInfo, err error) {
+ pkgs := strings.Split(strings.TrimSpace(line), ",")
+ for _, pkg := range pkgs {
+ packs = append(packs, models.PackageInfo{})
+ s := strings.Split(pkg, "-")
+ if len(s) == 3 {
+ packs[len(packs)-1].Name = s[0]
+ packs[len(packs)-1].Version = s[1]
+ packs[len(packs)-1].Release = s[2]
+ } else {
+ return packs, fmt.Errorf("CentOS: Unknown Header format: %s", line)
+ }
+ }
+ return
+}
+
+func (o *redhat) parseYumUpdateinfoHeaderAmazon(line string) (a models.DistroAdvisory, names []string, err error) {
+ re, _ := regexp.Compile(`(ALAS-.+): (.+) priority package update for (.+)$`)
+ result := re.FindStringSubmatch(line)
+ if len(result) == 4 {
+ a.AdvisoryID = result[1]
+ a.Severity = result[2]
+ spaceSeparatedPacknames := result[3]
+ names = strings.Fields(spaceSeparatedPacknames)
+ return
+ }
+ err = fmt.Errorf("Amazon Linux: Unknown Header Format. %s", line)
+ return
+}
+
+func (o *redhat) parseYumUpdateinfoLineToGetCveIDs(line string) []string {
+ re, _ := regexp.Compile(`(CVE-\d{4}-\d{4})`)
+ return re.FindAllString(line, -1)
+}
+
+func (o *redhat) parseYumUpdateinfoToGetAdvisoryID(line string) (advisoryID string, found bool) {
+ re, _ := regexp.Compile(`^ *Update ID : (.*)$`)
+ result := re.FindStringSubmatch(line)
+ if len(result) != 2 {
+ return "", false
+ }
+ return strings.TrimSpace(result[1]), true
+}
+
+func (o *redhat) parseYumUpdateinfoLineToGetIssued(line string) (date time.Time, found bool) {
+ return o.parseYumUpdateinfoLineToGetDate(line, `^\s*Issued : (\d{4}-\d{2}-\d{2})`)
+}
+
+func (o *redhat) parseYumUpdateinfoLineToGetUpdated(line string) (date time.Time, found bool) {
+ return o.parseYumUpdateinfoLineToGetDate(line, `^\s*Updated : (\d{4}-\d{2}-\d{2})`)
+}
+
+func (o *redhat) parseYumUpdateinfoLineToGetDate(line, regexpFormat string) (date time.Time, found bool) {
+ re, _ := regexp.Compile(regexpFormat)
+ result := re.FindStringSubmatch(line)
+ if len(result) != 2 {
+ return date, false
+ }
+ t, err := time.Parse("2006-01-02", result[1])
+ if err != nil {
+ return date, false
+ }
+ return t, true
+}
+
+func (o *redhat) isDescriptionLine(line string) bool {
+ re, _ := regexp.Compile(`^\s*Description : `)
+ return re.MatchString(line)
+}
+
+func (o *redhat) parseYumUpdateinfoToGetSeverity(line string) (severity string, found bool) {
+ re, _ := regexp.Compile(`^ *Severity : (.*)$`)
+ result := re.FindStringSubmatch(line)
+ if len(result) != 2 {
+ return "", false
+ }
+ return strings.TrimSpace(result[1]), true
+}
+
+type advisoryIDPacks struct {
+ AdvisoryID string
+ PackNames []string
+}
+
+type advisoryIDPacksList []advisoryIDPacks
+
+func (list advisoryIDPacksList) find(advisoryID string) (advisoryIDPacks, bool) {
+ for _, a := range list {
+ if a.AdvisoryID == advisoryID {
+ return a, true
+ }
+ }
+ return advisoryIDPacks{}, false
+}
+func (o *redhat) extractPackNameVerRel(nameVerRel string) (name, ver, rel string) {
+ fields := strings.Split(nameVerRel, ".")
+ archTrimed := strings.Join(fields[0:len(fields)-1], ".")
+
+ fields = strings.Split(archTrimed, "-")
+ rel = fields[len(fields)-1]
+ ver = fields[len(fields)-2]
+ name = strings.Join(fields[0:(len(fields)-2)], "-")
+ return
+}
+
+// parseYumUpdateinfoListAvailable collect AdvisorID(RHSA, ALAS), packages
+func (o *redhat) parseYumUpdateinfoListAvailable(stdout string) (advisoryIDPacksList, error) {
+
+ result := []advisoryIDPacks{}
+ lines := strings.Split(stdout, "\n")
+ for _, line := range lines {
+
+ if !(strings.HasPrefix(line, "RHSA") ||
+ strings.HasPrefix(line, "ALAS")) {
+ continue
+ }
+
+ fields := strings.Fields(line)
+ if len(fields) != 3 {
+ return []advisoryIDPacks{}, fmt.Errorf(
+ "Unknown format. line: %s", line)
+ }
+
+ // extract fields
+ advisoryID := fields[0]
+ packVersion := fields[2]
+ packName, _, _ := o.extractPackNameVerRel(packVersion)
+
+ found := false
+ for i, s := range result {
+ if s.AdvisoryID == advisoryID {
+ names := s.PackNames
+ names = append(names, packName)
+ result[i].PackNames = names
+ found = true
+ break
+ }
+ }
+ if !found {
+ result = append(result, advisoryIDPacks{
+ AdvisoryID: advisoryID,
+ PackNames: []string{packName},
+ })
+ }
+ }
+ return result, nil
+}
diff --git a/scan/redhat_test.go b/scan/redhat_test.go
new file mode 100644
index 00000000..ce54d5fb
--- /dev/null
+++ b/scan/redhat_test.go
@@ -0,0 +1,843 @@
+/* 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 (
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/future-architect/vuls/config"
+ "github.com/future-architect/vuls/models"
+ "github.com/k0kubun/pp"
+)
+
+// func unixtimeNoerr(s string) time.Time {
+// t, _ := unixtime(s)
+// return t
+// }
+
+func TestParseScanedPackagesLineRedhat(t *testing.T) {
+
+ r := newRedhat(config.ServerInfo{})
+
+ var packagetests = []struct {
+ in string
+ pack models.PackageInfo
+ }{
+ {
+ "openssl 1.0.1e 30.el6.11",
+ models.PackageInfo{
+ Name: "openssl",
+ Version: "1.0.1e",
+ Release: "30.el6.11",
+ },
+ },
+ }
+
+ for _, tt := range packagetests {
+ p, _ := r.parseScanedPackagesLine(tt.in)
+ if p.Name != tt.pack.Name {
+ t.Errorf("name: expected %s, actual %s", tt.pack.Name, p.Name)
+ }
+ if p.Version != tt.pack.Version {
+ t.Errorf("version: expected %s, actual %s", tt.pack.Version, p.Version)
+ }
+ if p.Release != tt.pack.Release {
+ t.Errorf("release: expected %s, actual %s", tt.pack.Release, p.Release)
+ }
+ }
+
+}
+
+func TestChangeSectionState(t *testing.T) {
+ r := newRedhat(config.ServerInfo{})
+ var tests = []struct {
+ oldState int
+ newState int
+ }{
+ {Outside, Header},
+ {Header, Content},
+ {Content, Header},
+ }
+
+ for _, tt := range tests {
+ if n := r.changeSectionState(tt.oldState); n != tt.newState {
+ t.Errorf("expected %d, actual %d", tt.newState, n)
+ }
+ }
+}
+
+func TestParseYumUpdateinfoHeader(t *testing.T) {
+ r := newRedhat(config.ServerInfo{})
+ var tests = []struct {
+ in string
+ out []models.PackageInfo
+ }{
+ {
+ " nodejs-0.10.36-3.el6,libuv-0.10.34-1.el6,v8-3.14.5.10-17.el6 ",
+ []models.PackageInfo{
+ {
+ Name: "nodejs",
+ Version: "0.10.36",
+ Release: "3.el6",
+ },
+ {
+ Name: "libuv",
+ Version: "0.10.34",
+ Release: "1.el6",
+ },
+ {
+ Name: "v8",
+ Version: "3.14.5.10",
+ Release: "17.el6",
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ if a, err := r.parseYumUpdateinfoHeaderCentOS(tt.in); err != nil {
+ t.Errorf("err: %s", err)
+ } else {
+ if !reflect.DeepEqual(a, tt.out) {
+ e := pp.Sprintf("%#v", tt.out)
+ a := pp.Sprintf("%#v", a)
+ t.Errorf("expected %s, actual %s", e, a)
+ }
+ }
+ }
+}
+
+func TestParseYumUpdateinfoLineToGetCveIDs(t *testing.T) {
+ r := newRedhat(config.ServerInfo{})
+ var tests = []struct {
+ in string
+ out []string
+ }{
+ {
+ "Bugs : 1194651 - CVE-2015-0278 libuv:",
+ []string{"CVE-2015-0278"},
+ },
+ {
+ ": 1195457 - nodejs-0.10.35 causes undefined symbolsCVE-2015-0278, CVE-2015-0278, CVE-2015-0277",
+ []string{
+ "CVE-2015-0278",
+ "CVE-2015-0278",
+ "CVE-2015-0277",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ act := r.parseYumUpdateinfoLineToGetCveIDs(tt.in)
+ for i, s := range act {
+ if s != tt.out[i] {
+ t.Errorf("expected %s, actual %s", tt.out[i], s)
+ }
+ }
+ }
+}
+
+func TestParseYumUpdateinfoToGetAdvisoryID(t *testing.T) {
+ r := newRedhat(config.ServerInfo{})
+ var tests = []struct {
+ in string
+ out string
+ found bool
+ }{
+ {
+ "Update ID : RHSA-2015:2315",
+ "RHSA-2015:2315",
+ true,
+ },
+ {
+ "Update ID : ALAS-2015-620",
+ "ALAS-2015-620",
+ true,
+ },
+ {
+ "Issued : 2015-11-19 00:00:00",
+ "",
+ false,
+ },
+ }
+
+ for _, tt := range tests {
+ advisoryID, found := r.parseYumUpdateinfoToGetAdvisoryID(tt.in)
+ if tt.out != advisoryID {
+ t.Errorf("expected %s, actual %s", tt.out, advisoryID)
+ }
+ if tt.found != found {
+ t.Errorf("expected %t, actual %t", tt.found, found)
+ }
+ }
+}
+
+func TestParseYumUpdateinfoLineToGetIssued(t *testing.T) {
+
+ date, _ := time.Parse("2006-01-02", "2015-12-15")
+
+ r := newRedhat(config.ServerInfo{})
+ var tests = []struct {
+ in string
+ out time.Time
+ found bool
+ }{
+ {
+ "Issued : 2015-12-15 00:00:00",
+ date,
+ true,
+ },
+ {
+ " Issued : 2015-12-15 00:00:00 ",
+ date,
+ true,
+ },
+ {
+ "Type : security",
+ time.Time{},
+ false,
+ },
+ }
+
+ for i, tt := range tests {
+ d, found := r.parseYumUpdateinfoLineToGetIssued(tt.in)
+ if tt.found != found {
+ t.Errorf("[%d] line: %s, expected %t, actual %t", i, tt.in, tt.found, found)
+ }
+ if tt.out != d {
+ t.Errorf("[%d] line: %s, expected %v, actual %v", i, tt.in, tt.out, d)
+ }
+ }
+}
+
+func TestParseYumUpdateinfoLineToGetUpdated(t *testing.T) {
+
+ date, _ := time.Parse("2006-01-02", "2015-12-15")
+
+ r := newRedhat(config.ServerInfo{})
+ var tests = []struct {
+ in string
+ out time.Time
+ found bool
+ }{
+ {
+ "Updated : 2015-12-15 00:00:00 Bugs : 1286966 - CVE-2015-8370 grub2: buffer overflow when checking password entered during bootup",
+ date,
+ true,
+ },
+ {
+
+ "Updated : 2015-12-15 14:16 CVEs : CVE-2015-7981",
+ date,
+ true,
+ },
+ {
+ "Type : security",
+ time.Time{},
+ false,
+ },
+ }
+
+ for i, tt := range tests {
+ d, found := r.parseYumUpdateinfoLineToGetUpdated(tt.in)
+ if tt.found != found {
+ t.Errorf("[%d] line: %s, expected %t, actual %t", i, tt.in, tt.found, found)
+ }
+ if tt.out != d {
+ t.Errorf("[%d] line: %s, expected %v, actual %v", i, tt.in, tt.out, d)
+ }
+ }
+}
+
+func TestIsDescriptionLine(t *testing.T) {
+ r := newRedhat(config.ServerInfo{})
+ var tests = []struct {
+ in string
+ found bool
+ }{
+ {
+ "Description : Package updates are available for Amazon Linux AMI that fix the",
+ true,
+ },
+ {
+ " Description : Package updates are available for Amazon Linux AMI that fix the",
+ true,
+ },
+ {
+ "Status : final",
+ false,
+ },
+ }
+
+ for i, tt := range tests {
+ found := r.isDescriptionLine(tt.in)
+ if tt.found != found {
+ t.Errorf("[%d] line: %s, expected %t, actual %t", i, tt.in, tt.found, found)
+ }
+ }
+}
+
+func TestParseYumUpdateinfoToGetSeverity(t *testing.T) {
+ r := newRedhat(config.ServerInfo{})
+ var tests = []struct {
+ in string
+ out string
+ found bool
+ }{
+ {
+ "Severity : Moderate",
+ "Moderate",
+ true,
+ },
+ {
+ " Severity : medium",
+ "medium",
+ true,
+ },
+ {
+ "Status : final",
+ "",
+ false,
+ },
+ }
+
+ for i, tt := range tests {
+ out, found := r.parseYumUpdateinfoToGetSeverity(tt.in)
+ if tt.found != found {
+ t.Errorf("[%d] line: %s, expected %t, actual %t", i, tt.in, tt.found, found)
+ }
+ if tt.out != out {
+ t.Errorf("[%d] line: %s, expected %v, actual %v", i, tt.in, tt.out, out)
+ }
+ }
+}
+
+func TestParseYumUpdateinfoRHEL(t *testing.T) {
+
+ stdout := `===============================================================================
+ Important: bind security update
+===============================================================================
+ Update ID : RHSA-2015:1705
+ Release :
+ Type : security
+ Status : final
+ Issued : 2015-09-03 00:00:00
+ Bugs : 1259087 - CVE-2015-5722 bind: malformed DNSSEC key failed assertion denial of service
+ CVEs : CVE-2015-5722
+Description : The Berkeley Internet Name Domain (BIND) is an implementation of
+ : the Domain Name System (DNS) protocols. BIND
+ : includes a DNS server (named); a resolver library
+ : (routines for applications to use when interfacing
+ : with DNS); and tools for verifying that the DNS
+ : server is operating correctly.
+ :
+ Severity : Important
+
+===============================================================================
+ Important: bind security update
+===============================================================================
+ Update ID : RHSA-2015:2655
+ Release :
+ Type : security
+ Status : final
+ Issued : 2015-09-03 01:00:00
+ Updated : 2015-09-04 00:00:00
+ Bugs : 1291176 - CVE-2015-8000 bind: responses with a malformed class attribute can trigger an assertion failure in db.c
+ CVEs : CVE-2015-8000
+ : CVE-2015-8001
+Description : The Berkeley Internet Name Domain (BIND) is an implementation of
+ : the Domain Name System (DNS) protocols. BIND
+ : includes a DNS server (named); a resolver library
+ : (routines for applications to use when interfacing
+ : with DNS); and tools for verifying that the DNS
+ : server is operating correctly.
+ :
+ Severity : Low
+
+===============================================================================
+ Moderate: bind security update
+===============================================================================
+ Update ID : RHSA-2016:0073
+ Release :
+ Type : security
+ Status : final
+ Issued : 2015-09-03 02:00:00
+ Bugs : 1299364 - CVE-2015-8704 bind: specific APL data could trigger an INSIST in apl_42.c CVEs : CVE-2015-8704
+ : CVE-2015-8705
+Description : The Berkeley Internet Name Domain (BIND) is an implementation of
+ : the Domain Name System (DNS) protocols. BIND
+ : includes a DNS server (named); a resolver library
+ : (routines for applications to use when interfacing
+ : with DNS); and tools for verifying that the DNS
+ : server is operating correctly.
+ :
+ Severity : Moderate
+
+ `
+ issued, _ := time.Parse("2006-01-02", "2015-09-03")
+ updated, _ := time.Parse("2006-01-02", "2015-09-04")
+
+ r := newRedhat(config.ServerInfo{})
+ r.Family = "redhat"
+
+ var tests = []struct {
+ in string
+ out []distroAdvisoryCveIDs
+ }{
+ {
+ stdout,
+ []distroAdvisoryCveIDs{
+ {
+ DistroAdvisory: models.DistroAdvisory{
+ AdvisoryID: "RHSA-2015:1705",
+ Severity: "Important",
+ Issued: issued,
+ },
+ CveIDs: []string{"CVE-2015-5722"},
+ },
+ {
+ DistroAdvisory: models.DistroAdvisory{
+ AdvisoryID: "RHSA-2015:2655",
+ Severity: "Low",
+ Issued: issued,
+ Updated: updated,
+ },
+ CveIDs: []string{
+ "CVE-2015-8000",
+ "CVE-2015-8001",
+ },
+ },
+ {
+ DistroAdvisory: models.DistroAdvisory{
+ AdvisoryID: "RHSA-2016:0073",
+ Severity: "Moderate",
+ Issued: issued,
+ Updated: updated,
+ },
+ CveIDs: []string{
+ "CVE-2015-8704",
+ "CVE-2015-8705",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ actual, _ := r.parseYumUpdateinfo(tt.in)
+ for i, advisoryCveIDs := range actual {
+ if !reflect.DeepEqual(tt.out[i], advisoryCveIDs) {
+ e := pp.Sprintf("%v", tt.out[i])
+ a := pp.Sprintf("%v", advisoryCveIDs)
+ t.Errorf("[%d] Alas is not same. \nexpected: %s\nactual: %s",
+ i, e, a)
+ }
+ }
+ }
+}
+
+func TestParseYumUpdateinfoAmazon(t *testing.T) {
+
+ r := newRedhat(config.ServerInfo{})
+ r.Family = "amazon"
+
+ issued, _ := time.Parse("2006-01-02", "2015-12-15")
+ updated, _ := time.Parse("2006-01-02", "2015-12-16")
+
+ var tests = []struct {
+ in string
+ out []distroAdvisoryCveIDs
+ }{
+ {
+ `===============================================================================
+ Amazon Linux AMI 2014.03 - ALAS-2016-644: medium priority package update for python-rsa
+===============================================================================
+ Update ID : ALAS-2016-644
+ Release :
+ Type : security
+ Status : final
+ Issued : 2015-12-15 13:30
+ CVEs : CVE-2016-1494
+Description : Package updates are available for Amazon Linux AMI that fix the
+ : following vulnerabilities: CVE-2016-1494:
+ : 1295869:
+ : CVE-2016-1494 python-rsa: Signature forgery using
+ : Bleichenbacher'06 attack
+ Severity : medium
+
+===============================================================================
+ Amazon Linux AMI 2014.03 - ALAS-2015-614: medium priority package update for openssl
+===============================================================================
+ Update ID : ALAS-2015-614
+ Release :
+ Type : security
+ Status : final
+ Issued : 2015-12-15 10:00
+ Updated : 2015-12-16 14:15 CVEs : CVE-2015-3194
+ : CVE-2015-3195
+ : CVE-2015-3196
+Description : Package updates are available for Amazon Linux AMI that fix the
+ : following vulnerabilities: CVE-2015-3196:
+ : 1288326:
+ : CVE-2015-3196 OpenSSL: Race condition handling PSK
+ : identify hint A race condition flaw, leading to a
+ : double free, was found in the way OpenSSL handled
+ : pre-shared keys (PSKs). A remote attacker could
+ : use this flaw to crash a multi-threaded SSL/TLS
+ : client.
+ :
+ Severity : medium`,
+
+ []distroAdvisoryCveIDs{
+ {
+ DistroAdvisory: models.DistroAdvisory{
+ AdvisoryID: "ALAS-2016-644",
+ Severity: "medium",
+ Issued: issued,
+ },
+ CveIDs: []string{"CVE-2016-1494"},
+ },
+ {
+ DistroAdvisory: models.DistroAdvisory{
+ AdvisoryID: "ALAS-2015-614",
+ Severity: "medium",
+ Issued: issued,
+ Updated: updated,
+ },
+ CveIDs: []string{
+ "CVE-2015-3194",
+ "CVE-2015-3195",
+ "CVE-2015-3196",
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ actual, _ := r.parseYumUpdateinfo(tt.in)
+ for i, advisoryCveIDs := range actual {
+ if !reflect.DeepEqual(tt.out[i], advisoryCveIDs) {
+ e := pp.Sprintf("%v", tt.out[i])
+ a := pp.Sprintf("%v", advisoryCveIDs)
+ t.Errorf("[%d] Alas is not same. expected %s, actual %s",
+ i, e, a)
+ }
+ }
+ }
+}
+
+func TestParseYumCheckUpdateLines(t *testing.T) {
+ r := newRedhat(config.ServerInfo{})
+ r.Family = "centos"
+ stdout := `Loaded plugins: changelog, fastestmirror, keys, protect-packages, protectbase, security
+Loading mirror speeds from cached hostfile
+ * base: mirror.fairway.ne.jp
+ * epel: epel.mirror.srv.co.ge
+ * extras: mirror.fairway.ne.jp
+ * updates: mirror.fairway.ne.jp
+0 packages excluded due to repository protections
+
+audit-libs.x86_64 2.3.7-5.el6 base
+bash.x86_64 4.1.2-33.el6_7.1 updates
+ `
+ r.Packages = []models.PackageInfo{
+ {
+ Name: "audit-libs",
+ Version: "2.3.6",
+ Release: "4.el6",
+ },
+ {
+ Name: "bash",
+ Version: "4.1.1",
+ Release: "33",
+ },
+ }
+ var tests = []struct {
+ in string
+ out models.PackageInfoList
+ }{
+ {
+ stdout,
+ models.PackageInfoList{
+ {
+ Name: "audit-libs",
+ Version: "2.3.6",
+ Release: "4.el6",
+ NewVersion: "2.3.7",
+ NewRelease: "5.el6",
+ },
+ {
+ Name: "bash",
+ Version: "4.1.1",
+ Release: "33",
+ NewVersion: "4.1.2",
+ NewRelease: "33.el6_7.1",
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ packInfoList, err := r.parseYumCheckUpdateLines(tt.in)
+ if err != nil {
+ t.Errorf("Error has occurred, err: %s\ntt.in: %v", err, tt.in)
+ return
+ }
+ for i, ePackInfo := range tt.out {
+ if !reflect.DeepEqual(ePackInfo, packInfoList[i]) {
+ e := pp.Sprintf("%v", ePackInfo)
+ a := pp.Sprintf("%v", packInfoList[i])
+ t.Errorf("[%d] expected %s, actual %s", i, e, a)
+ }
+ }
+ }
+}
+
+func TestParseYumCheckUpdateLinesAmazon(t *testing.T) {
+ r := newRedhat(config.ServerInfo{})
+ r.Family = "amzon"
+ stdout := `Loaded plugins: priorities, update-motd, upgrade-helper
+34 package(s) needed for security, out of 71 available
+
+bind-libs.x86_64 32:9.8.2-0.37.rc1.45.amzn1 amzn-main
+java-1.7.0-openjdk.x86_64 1:1.7.0.95-2.6.4.0.65.amzn1 amzn-main
+if-not-architecture 100-200 amzn-main
+`
+ r.Packages = []models.PackageInfo{
+ {
+ Name: "bind-libs",
+ Version: "32:9.8.0",
+ Release: "0.33.rc1.45.amzn1",
+ },
+ {
+ Name: "java-1.7.0-openjdk",
+ Version: "1:1.7.0.0",
+ Release: "2.6.4.0.0.amzn1",
+ },
+ {
+ Name: "if-not-architecture",
+ Version: "10",
+ Release: "20",
+ },
+ }
+ var tests = []struct {
+ in string
+ out models.PackageInfoList
+ }{
+ {
+ stdout,
+ models.PackageInfoList{
+ {
+ Name: "bind-libs",
+ Version: "32:9.8.0",
+ Release: "0.33.rc1.45.amzn1",
+ NewVersion: "32:9.8.2",
+ NewRelease: "0.37.rc1.45.amzn1",
+ },
+ {
+ Name: "java-1.7.0-openjdk",
+ Version: "1:1.7.0.0",
+ Release: "2.6.4.0.0.amzn1",
+ NewVersion: "1:1.7.0.95",
+ NewRelease: "2.6.4.0.65.amzn1",
+ },
+ {
+ Name: "if-not-architecture",
+ Version: "10",
+ Release: "20",
+ NewVersion: "100",
+ NewRelease: "200",
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ packInfoList, err := r.parseYumCheckUpdateLines(tt.in)
+ if err != nil {
+ t.Errorf("Error has occurred, err: %s\ntt.in: %v", err, tt.in)
+ return
+ }
+ for i, ePackInfo := range tt.out {
+ if !reflect.DeepEqual(ePackInfo, packInfoList[i]) {
+ e := pp.Sprintf("%v", ePackInfo)
+ a := pp.Sprintf("%v", packInfoList[i])
+ t.Errorf("[%d] expected %s, actual %s", i, e, a)
+ }
+ }
+ }
+}
+
+func TestParseYumUpdateinfoAmazonLinuxHeader(t *testing.T) {
+ r := newRedhat(config.ServerInfo{})
+ var tests = []struct {
+ in string
+ out models.DistroAdvisory
+ }{
+ {
+ "Amazon Linux AMI 2014.03 - ALAS-2015-598: low priority package update for grep",
+ models.DistroAdvisory{
+ AdvisoryID: "ALAS-2015-598",
+ Severity: "low",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ a, _, _ := r.parseYumUpdateinfoHeaderAmazon(tt.in)
+ if !reflect.DeepEqual(a, tt.out) {
+ e := pp.Sprintf("%v", tt.out)
+ a := pp.Sprintf("%v", a)
+ t.Errorf("expected %s, actual %s", e, a)
+ }
+ }
+}
+
+func TestParseYumUpdateinfoListAvailable(t *testing.T) {
+ r := newRedhat(config.ServerInfo{})
+ rhelStdout := `RHSA-2015:2315 Moderate/Sec. NetworkManager-1:1.0.6-27.el7.x86_64
+RHSA-2015:2315 Moderate/Sec. NetworkManager-config-server-1:1.0.6-27.el7.x86_64
+RHSA-2015:1705 Important/Sec. bind-libs-lite-32:9.9.4-18.el7_1.5.x86_64
+RHSA-2016:0176 Critical/Sec. glibc-2.17-106.el7_2.4.x86_64
+RHSA-2015:2401 Low/Sec. grub2-1:2.02-0.29.el7.x86_64
+RHSA-2015:2401 Low/Sec. grub2-tools-1:2.02-0.29.el7.x86_64
+updateinfo list done`
+
+ var tests = []struct {
+ in string
+ out []advisoryIDPacks
+ }{
+ {
+ rhelStdout,
+ []advisoryIDPacks{
+ {
+ AdvisoryID: "RHSA-2015:2315",
+ PackNames: []string{
+ "NetworkManager",
+ "NetworkManager-config-server",
+ },
+ },
+ {
+ AdvisoryID: "RHSA-2015:1705",
+ PackNames: []string{
+ "bind-libs-lite",
+ },
+ },
+ {
+ AdvisoryID: "RHSA-2016:0176",
+ PackNames: []string{
+ "glibc",
+ },
+ },
+ {
+ AdvisoryID: "RHSA-2015:2401",
+ PackNames: []string{
+ "grub2",
+ "grub2-tools",
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ actual, err := r.parseYumUpdateinfoListAvailable(tt.in)
+ if err != nil {
+ t.Errorf("Error has occurred: %s", err)
+ return
+ }
+
+ for i := range actual {
+ if !reflect.DeepEqual(actual[i], tt.out[i]) {
+ e := pp.Sprintf("%v", tt.out)
+ a := pp.Sprintf("%v", actual)
+ t.Errorf("[%d] expected: %s\nactual: %s", i, e, a)
+ }
+ }
+ }
+}
+
+func TestParseYumUpdateinfoToGetUpdateID(t *testing.T) {
+
+ r := newRedhat(config.ServerInfo{})
+
+ var packagetests = []struct {
+ in string
+ pack models.PackageInfo
+ }{
+ {
+ "openssl 1.0.1e 30.el6.11",
+ models.PackageInfo{
+ Name: "openssl",
+ Version: "1.0.1e",
+ Release: "30.el6.11",
+ },
+ },
+ }
+
+ for _, tt := range packagetests {
+ p, _ := r.parseScanedPackagesLine(tt.in)
+ if p.Name != tt.pack.Name {
+ t.Errorf("name: expected %s, actual %s", tt.pack.Name, p.Name)
+ }
+ if p.Version != tt.pack.Version {
+ t.Errorf("version: expected %s, actual %s", tt.pack.Version, p.Version)
+ }
+ if p.Release != tt.pack.Release {
+ t.Errorf("release: expected %s, actual %s", tt.pack.Release, p.Release)
+ }
+ }
+
+}
+
+func TestExtractPackNameVerRel(t *testing.T) {
+ r := newRedhat(config.ServerInfo{})
+ var tests = []struct {
+ in string
+ out []string
+ }{
+ {
+ "openssh-server-6.2p2-8.45.amzn1.x86_64",
+ []string{"openssh-server", "6.2p2", "8.45.amzn1"},
+ },
+ {
+ "bind-libs-lite-32:9.9.4-29.el7_2.1.x86_64",
+ []string{"bind-libs-lite", "32:9.9.4", "29.el7_2.1"},
+ },
+ {
+ "glibc-2.17-106.el7_2.1.x86_64",
+ []string{"glibc", "2.17", "106.el7_2.1"},
+ },
+ }
+
+ for _, tt := range tests {
+ name, ver, rel := r.extractPackNameVerRel(tt.in)
+ if tt.out[0] != name {
+ t.Errorf("name: expected %s, actual %s", tt.out[0], name)
+ }
+ if tt.out[1] != ver {
+ t.Errorf("ver: expected %s, actual %s", tt.out[1], ver)
+ }
+ if tt.out[2] != rel {
+ t.Errorf("ver: expected %s, actual %s", tt.out[2], rel)
+ }
+ }
+
+}
diff --git a/scan/serverapi.go b/scan/serverapi.go
new file mode 100644
index 00000000..72e55851
--- /dev/null
+++ b/scan/serverapi.go
@@ -0,0 +1,209 @@
+package scan
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/future-architect/vuls/config"
+ "github.com/future-architect/vuls/models"
+ "github.com/k0kubun/pp"
+ cve "github.com/kotakanbe/go-cve-dictionary/models"
+)
+
+// Log for localhsot
+var Log *logrus.Entry
+
+var servers []osTypeInterface
+
+// Base Interface of redhat, debian
+type osTypeInterface interface {
+ setServerInfo(config.ServerInfo)
+ getServerInfo() config.ServerInfo
+ setDistributionInfo(string, string)
+ checkRequiredPackagesInstalled() error
+ scanPackages() error
+ scanVulnByCpeName() error
+ install() error
+ convertToModel() (models.ScanResult, error)
+}
+
+// osPackages included by linux struct
+type osPackages struct {
+ // installed packages
+ Packages models.PackageInfoList
+
+ // unsecure packages
+ UnsecurePackages CvePacksList
+}
+
+func (p *osPackages) setPackages(pi models.PackageInfoList) {
+ p.Packages = pi
+}
+
+func (p *osPackages) setUnsecurePackages(pi []CvePacksInfo) {
+ p.UnsecurePackages = pi
+}
+
+// CvePacksList have CvePacksInfo list, getter/setter, sortable methods.
+type CvePacksList []CvePacksInfo
+
+// CvePacksInfo hold the CVE information.
+type CvePacksInfo struct {
+ CveID string
+ CveDetail cve.CveDetail
+ Packs []models.PackageInfo
+ DistroAdvisories []models.DistroAdvisory // for Aamazon, RHEL
+ CpeNames []string
+ // CvssScore float64
+}
+
+// FindByCveID find by CVEID
+func (s CvePacksList) FindByCveID(cveID string) (pi CvePacksInfo, found bool) {
+ for _, p := range s {
+ if cveID == p.CveID {
+ return p, true
+ }
+ }
+ return CvePacksInfo{CveID: cveID}, false
+}
+
+// immutable
+func (s CvePacksList) set(cveID string, cvePacksInfo CvePacksInfo) CvePacksList {
+ for i, p := range s {
+ if cveID == p.CveID {
+ s[i] = cvePacksInfo
+ return s
+ }
+ }
+ return append(s, cvePacksInfo)
+}
+
+// Len implement Sort Interface
+func (s CvePacksList) Len() int {
+ return len(s)
+}
+
+// Swap implement Sort Interface
+func (s CvePacksList) Swap(i, j int) {
+ s[i], s[j] = s[j], s[i]
+}
+
+// Less implement Sort Interface
+func (s CvePacksList) Less(i, j int) bool {
+ return s[i].CveDetail.CvssScore("en") > s[j].CveDetail.CvssScore("en")
+}
+
+func detectOs(c config.ServerInfo) (osType osTypeInterface) {
+ var itsMe bool
+ itsMe, osType = detectDebian(c)
+ if itsMe {
+ return
+ }
+ itsMe, osType = detectRedhat(c)
+ return
+}
+
+// InitServers detect the kind of OS distribution of target servers
+func InitServers(localLogger *logrus.Entry) (err error) {
+ Log = localLogger
+ if servers, err = detectServersOS(); err != nil {
+ err = fmt.Errorf("Failed to detect OS")
+ } else {
+ Log.Debugf("%s", pp.Sprintf("%s", servers))
+ }
+ return
+}
+
+func detectServersOS() (osi []osTypeInterface, err error) {
+ osTypeChan := make(chan osTypeInterface, len(config.Conf.Servers))
+ defer close(osTypeChan)
+ for _, s := range config.Conf.Servers {
+ go func(s config.ServerInfo) {
+ osTypeChan <- detectOs(s)
+ }(s)
+ }
+
+ timeout := time.After(60 * time.Second)
+ for i := 0; i < len(config.Conf.Servers); i++ {
+ select {
+ case res := <-osTypeChan:
+ osi = append(osi, res)
+ case <-timeout:
+ Log.Error("Timeout Occured while detecting OS.")
+ err = fmt.Errorf("Timeout!")
+ return
+ }
+ }
+ return
+}
+
+// Prepare installs requred packages to scan vulnerabilities.
+func Prepare() []error {
+ return parallelSSHExec(func(o osTypeInterface) error {
+ if err := o.install(); err != nil {
+ return err
+ }
+ return nil
+ })
+}
+
+// Scan scan
+func Scan() []error {
+ if len(servers) == 0 {
+ return []error{fmt.Errorf("Not initialize yet.")}
+ }
+
+ Log.Info("Check required packages for scanning...")
+ if errs := checkRequiredPackagesInstalled(); errs != nil {
+ Log.Error("Please execute with [prepare] subcommand to install required packages before scanning")
+ return errs
+ }
+
+ Log.Info("Scanning vuluneable OS packages...")
+ if errs := scanPackages(); errs != nil {
+ return errs
+ }
+
+ Log.Info("Scanning vulnerable software specified in CPE...")
+ if errs := scanVulnByCpeName(); errs != nil {
+ return errs
+ }
+ return nil
+}
+
+func checkRequiredPackagesInstalled() []error {
+ timeoutSec := 30 * 60
+ return parallelSSHExec(func(o osTypeInterface) error {
+ return o.checkRequiredPackagesInstalled()
+ }, timeoutSec)
+}
+
+func scanPackages() []error {
+ timeoutSec := 30 * 60
+ return parallelSSHExec(func(o osTypeInterface) error {
+ return o.scanPackages()
+ }, timeoutSec)
+
+}
+
+// scanVulnByCpeName search vulnerabilities that specified in config file.
+func scanVulnByCpeName() []error {
+ timeoutSec := 30 * 60
+ return parallelSSHExec(func(o osTypeInterface) error {
+ return o.scanVulnByCpeName()
+ }, timeoutSec)
+
+}
+
+// GetScanResults returns Scan Resutls
+func GetScanResults() (results models.ScanResults, err error) {
+ for _, s := range servers {
+ r, err := s.convertToModel()
+ if err != nil {
+ return results, fmt.Errorf("Failed converting to model: %s.", err)
+ }
+ results = append(results, r)
+ }
+ return
+}
diff --git a/scan/serverapi_test.go b/scan/serverapi_test.go
new file mode 100644
index 00000000..59825b12
--- /dev/null
+++ b/scan/serverapi_test.go
@@ -0,0 +1,47 @@
+package scan
+
+import "testing"
+
+func TestPackageCveInfosSetGet(t *testing.T) {
+ var test = struct {
+ in []string
+ out []string
+ }{
+ []string{
+ "CVE1",
+ "CVE2",
+ "CVE3",
+ "CVE1",
+ "CVE1",
+ "CVE2",
+ "CVE3",
+ },
+ []string{
+ "CVE1",
+ "CVE2",
+ "CVE3",
+ },
+ }
+
+ // var ps packageCveInfos
+ var ps CvePacksList
+ for _, cid := range test.in {
+ ps = ps.set(cid, CvePacksInfo{CveID: cid})
+ }
+
+ if len(test.out) != len(ps) {
+ t.Errorf("length: expected %d, actual %d", len(test.out), len(ps))
+ }
+
+ for i, expectedCid := range test.out {
+ if expectedCid != ps[i].CveID {
+ t.Errorf("expected %s, actual %s", expectedCid, ps[i].CveID)
+ }
+ }
+ for _, cid := range test.in {
+ p, _ := ps.FindByCveID(cid)
+ if p.CveID != cid {
+ t.Errorf("expected %s, actual %s", cid, p.CveID)
+ }
+ }
+}
diff --git a/scan/sshutil.go b/scan/sshutil.go
new file mode 100644
index 00000000..3e8e6820
--- /dev/null
+++ b/scan/sshutil.go
@@ -0,0 +1,311 @@
+/* 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 (
+ "bytes"
+ "crypto/x509"
+ "encoding/pem"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "os"
+ "strings"
+ "time"
+
+ "golang.org/x/crypto/ssh"
+ "golang.org/x/crypto/ssh/agent"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/cenkalti/backoff"
+ conf "github.com/future-architect/vuls/config"
+ "github.com/k0kubun/pp"
+)
+
+type sshResult struct {
+ Host string
+ Port string
+ Stdout string
+ Stderr string
+ ExitStatus int
+}
+
+func (s sshResult) isSuccess(expectedStatusCodes ...int) bool {
+ if len(expectedStatusCodes) == 0 {
+ return s.ExitStatus == 0
+ }
+ for _, code := range expectedStatusCodes {
+ if code == s.ExitStatus {
+ return true
+ }
+ }
+ return false
+}
+
+// Sudo is Const value for sudo mode
+const sudo = true
+
+// NoSudo is Const value for normal user mode
+const noSudo = false
+
+func parallelSSHExec(fn func(osTypeInterface) error, timeoutSec ...int) (errs []error) {
+ errChan := make(chan error, len(servers))
+ defer close(errChan)
+ for _, s := range servers {
+ go func(s osTypeInterface) {
+ if err := fn(s); err != nil {
+ errChan <- fmt.Errorf("%s@%s:%s: %s",
+ s.getServerInfo().User,
+ s.getServerInfo().Host,
+ s.getServerInfo().Port,
+ err,
+ )
+ } else {
+ errChan <- nil
+ }
+ }(s)
+ }
+
+ var timeout int
+ if len(timeoutSec) == 0 {
+ timeout = 10 * 60
+ } else {
+ timeout = timeoutSec[0]
+ }
+
+ for i := 0; i < len(servers); i++ {
+ select {
+ case err := <-errChan:
+ if err != nil {
+ errs = append(errs, err)
+ } else {
+ logrus.Debug("Parallel SSH Success.")
+ }
+ case <-time.After(time.Duration(timeout) * time.Second):
+ logrus.Errorf("Parallel SSH Timeout.")
+ errs = append(errs, fmt.Errorf("Timed out!"))
+ }
+ }
+ return
+}
+
+func sshExec(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (result sshResult) {
+ // Setup Logger
+ var logger *logrus.Entry
+ if len(log) == 0 {
+ level := logrus.InfoLevel
+ if conf.Conf.Debug == true {
+ level = logrus.DebugLevel
+ }
+ l := &logrus.Logger{
+ Out: os.Stderr,
+ Formatter: new(logrus.TextFormatter),
+ Hooks: make(logrus.LevelHooks),
+ Level: level,
+ }
+ logger = logrus.NewEntry(l)
+ } else {
+ logger = log[0]
+ }
+
+ var err error
+ if sudo && c.User != "root" {
+ switch {
+ case c.SudoOpt.ExecBySudo:
+ cmd = fmt.Sprintf("echo %s | sudo -S %s", c.Password, cmd)
+ case c.SudoOpt.ExecBySudoSh:
+ cmd = fmt.Sprintf("echo %s | sudo sh -c '%s'", c.Password, cmd)
+ default:
+ logger.Panicf("sudoOpt is invalid. SudoOpt: %v", c.SudoOpt)
+ }
+ }
+ // set pipefail option.
+ // http://unix.stackexchange.com/questions/14270/get-exit-status-of-process-thats-piped-to-another
+ cmd = fmt.Sprintf("set -o pipefail; %s", cmd)
+ logger.Debugf("Command: %s", strings.Replace(cmd, "\n", "", -1))
+
+ var client *ssh.Client
+ client, err = sshConnect(c)
+ defer client.Close()
+
+ var session *ssh.Session
+ if session, err = client.NewSession(); err != nil {
+ logger.Errorf("Failed to new session. err: %s, c: %s",
+ err,
+ pp.Sprintf("%v", c))
+ result.ExitStatus = 999
+ return
+ }
+ defer session.Close()
+
+ // http://blog.ralch.com/tutorial/golang-ssh-connection/
+ modes := ssh.TerminalModes{
+ ssh.ECHO: 0, // disable echoing
+ ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
+ ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
+ }
+ if err = session.RequestPty("xterm", 400, 120, modes); err != nil {
+ logger.Errorf("Failed to request for pseudo terminal. err: %s, c: %s",
+ err,
+ pp.Sprintf("%v", c))
+
+ result.ExitStatus = 999
+ return
+ }
+
+ var stdoutBuf, stderrBuf bytes.Buffer
+ session.Stdout = &stdoutBuf
+ session.Stderr = &stderrBuf
+
+ if err := session.Run(cmd); err != nil {
+ if exitErr, ok := err.(*ssh.ExitError); ok {
+ result.ExitStatus = exitErr.ExitStatus()
+ } else {
+ result.ExitStatus = 999
+ }
+ } else {
+ result.ExitStatus = 0
+ }
+
+ result.Stdout = stdoutBuf.String()
+ result.Stderr = stderrBuf.String()
+ result.Host = c.Host
+ result.Port = c.Port
+
+ logger.Debugf(
+ "SSH executed. cmd: %s, status: %#v\nstdout: \n%s\nstderr: \n%s",
+ cmd, err, result.Stdout, result.Stderr)
+
+ return
+}
+
+func getAgentAuth() (auth ssh.AuthMethod, ok bool) {
+ if sock := os.Getenv("SSH_AUTH_SOCK"); len(sock) > 0 {
+ if agconn, err := net.Dial("unix", sock); err == nil {
+ ag := agent.NewClient(agconn)
+ auth = ssh.PublicKeysCallback(ag.Signers)
+ ok = true
+ }
+ }
+ return
+}
+
+func tryAgentConnect(c conf.ServerInfo) *ssh.Client {
+ if auth, ok := getAgentAuth(); ok {
+ config := &ssh.ClientConfig{
+ User: c.User,
+ Auth: []ssh.AuthMethod{auth},
+ }
+ client, _ := ssh.Dial("tcp", c.Host+":"+c.Port, config)
+ return client
+ }
+ return nil
+}
+
+func sshConnect(c conf.ServerInfo) (client *ssh.Client, err error) {
+
+ if client = tryAgentConnect(c); client != nil {
+ return client, nil
+ }
+
+ var auths = []ssh.AuthMethod{}
+ if auths, err = addKeyAuth(auths, c.KeyPath, c.KeyPassword); err != nil {
+ logrus.Fatalf("Faild to add keyAuth. err: %s", err)
+ }
+
+ if c.Password != "" {
+ auths = append(auths, ssh.Password(c.Password))
+ }
+
+ // http://blog.ralch.com/tutorial/golang-ssh-connection/
+ config := &ssh.ClientConfig{
+ User: c.User,
+ Auth: auths,
+ }
+ // log.Debugf("config: %s", pp.Sprintf("%v", config))
+
+ notifyFunc := func(e error, t time.Duration) {
+ logrus.Warnf("Faild to ssh %s@%s:%s. err: %s, Retrying in %s...",
+ c.User, c.Host, c.Port, e, t)
+ logrus.Debugf("sshConInfo: %s", pp.Sprintf("%v", c))
+ }
+ err = backoff.RetryNotify(func() error {
+ if client, err = ssh.Dial("tcp", c.Host+":"+c.Port, config); err != nil {
+ return err
+ }
+ return nil
+ }, backoff.NewExponentialBackOff(), notifyFunc)
+
+ return
+}
+
+// https://github.com/rapidloop/rtop/blob/ba5b35e964135d50e0babedf0bd69b2fcb5dbcb4/src/sshhelper.go#L100
+func addKeyAuth(auths []ssh.AuthMethod, keypath string, keypassword string) ([]ssh.AuthMethod, error) {
+ if len(keypath) == 0 {
+ return auths, nil
+ }
+
+ // read the file
+ pemBytes, err := ioutil.ReadFile(keypath)
+ if err != nil {
+ return auths, err
+ }
+
+ // get first pem block
+ block, _ := pem.Decode(pemBytes)
+ if block == nil {
+ return auths, fmt.Errorf("no key found in %s", keypath)
+ }
+
+ // handle plain and encrypted keyfiles
+ if x509.IsEncryptedPEMBlock(block) {
+ block.Bytes, err = x509.DecryptPEMBlock(block, []byte(keypassword))
+ if err != nil {
+ return auths, err
+ }
+ key, err := parsePemBlock(block)
+ if err != nil {
+ return auths, err
+ }
+ signer, err := ssh.NewSignerFromKey(key)
+ if err != nil {
+ return auths, err
+ }
+ return append(auths, ssh.PublicKeys(signer)), nil
+ }
+
+ signer, err := ssh.ParsePrivateKey(pemBytes)
+ if err != nil {
+ return auths, err
+ }
+ return append(auths, ssh.PublicKeys(signer)), nil
+}
+
+// ref golang.org/x/crypto/ssh/keys.go#ParseRawPrivateKey.
+func parsePemBlock(block *pem.Block) (interface{}, error) {
+ switch block.Type {
+ case "RSA PRIVATE KEY":
+ return x509.ParsePKCS1PrivateKey(block.Bytes)
+ case "EC PRIVATE KEY":
+ return x509.ParseECPrivateKey(block.Bytes)
+ case "DSA PRIVATE KEY":
+ return ssh.ParseDSAPrivateKey(block.Bytes)
+ default:
+ return nil, fmt.Errorf("rtop: unsupported key type %q", block.Type)
+ }
+}
diff --git a/util/logutil.go b/util/logutil.go
new file mode 100644
index 00000000..008e65f7
--- /dev/null
+++ b/util/logutil.go
@@ -0,0 +1,68 @@
+/* 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 util
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/rifflock/lfshook"
+
+ "github.com/future-architect/vuls/config"
+ formatter "github.com/kotakanbe/logrus-prefixed-formatter"
+)
+
+// NewCustomLogger creates logrus
+func NewCustomLogger(c config.ServerInfo) *logrus.Entry {
+ log := logrus.New()
+ log.Formatter = &formatter.TextFormatter{MsgAnsiColor: c.LogMsgAnsiColor}
+ log.Out = os.Stderr
+ log.Level = logrus.InfoLevel
+ if config.Conf.Debug {
+ log.Level = logrus.DebugLevel
+ }
+
+ // File output
+ logDir := "/var/log/vuls"
+ if _, err := os.Stat(logDir); os.IsNotExist(err) {
+ if err := os.Mkdir(logDir, 0666); err != nil {
+ logrus.Errorf("Failed to create log directory: %s", err)
+ }
+ }
+
+ whereami := "localhost"
+ if 0 < len(c.ServerName) {
+ whereami = fmt.Sprintf("%s:%s", c.ServerName, c.Port)
+
+ }
+ if _, err := os.Stat(logDir); err == nil {
+ path := fmt.Sprintf("%s/%s.log", logDir, whereami)
+ log.Hooks.Add(lfshook.NewHook(lfshook.PathMap{
+ logrus.DebugLevel: path,
+ logrus.InfoLevel: path,
+ logrus.WarnLevel: path,
+ logrus.ErrorLevel: path,
+ logrus.FatalLevel: path,
+ logrus.PanicLevel: path,
+ }))
+ }
+
+ fields := logrus.Fields{"prefix": whereami}
+ return log.WithFields(fields)
+}
diff --git a/util/util.go b/util/util.go
new file mode 100644
index 00000000..21f9b218
--- /dev/null
+++ b/util/util.go
@@ -0,0 +1,120 @@
+/* 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 util
+
+import (
+ "fmt"
+ "net/url"
+ "strings"
+
+ "github.com/future-architect/vuls/config"
+)
+
+// GenWorkers generates goroutine
+// http://qiita.com/na-o-ys/items/65373132b1c5bc973cca
+func GenWorkers(num int) chan<- func() {
+ tasks := make(chan func())
+ for i := 0; i < num; i++ {
+ go func() {
+ for f := range tasks {
+ f()
+ }
+ }()
+ }
+ return tasks
+}
+
+// AppendIfMissing append to the slice if missing
+func AppendIfMissing(slice []string, s string) []string {
+ for _, ele := range slice {
+ if ele == s {
+ return slice
+ }
+ }
+ return append(slice, s)
+}
+
+// URLPathJoin make URL
+func URLPathJoin(baseURL string, paths ...string) (string, error) {
+ baseURL = strings.TrimSuffix(baseURL, "/")
+ trimedPaths := []string{}
+ for _, path := range paths {
+ trimed := strings.Trim(path, " /")
+ if len(trimed) != 0 {
+ trimedPaths = append(trimedPaths, trimed)
+ }
+ }
+ var url *url.URL
+ url, err := url.Parse(baseURL)
+ if err != nil {
+ return "", err
+ }
+ url.Path += strings.Join(trimedPaths, "/")
+ return url.String(), nil
+}
+
+// URLPathParamJoin make URL
+func URLPathParamJoin(baseURL string, paths []string, params map[string]string) (string, error) {
+ urlPath, err := URLPathJoin(baseURL, paths...)
+ if err != nil {
+ return "", err
+ }
+ u, err := url.Parse(urlPath)
+ if err != nil {
+ return "", err
+ }
+
+ parameters := url.Values{}
+ for key := range params {
+ parameters.Add(key, params[key])
+ }
+ u.RawQuery = parameters.Encode()
+ return u.String(), nil
+}
+
+// ProxyEnv returns shell environment variables to set proxy
+func ProxyEnv() string {
+ httpProxyEnv := "env"
+ keys := []string{
+ "http_proxy",
+ "https_proxy",
+ "HTTP_PROXY",
+ "HTTPS_PROXY",
+ }
+ for _, key := range keys {
+ httpProxyEnv += fmt.Sprintf(
+ ` %s="%s"`, key, config.Conf.HTTPProxy)
+ }
+ return httpProxyEnv
+}
+
+// PrependProxyEnv prepends proxy enviroment variable
+func PrependProxyEnv(cmd string) string {
+ if config.Conf.HTTPProxy == "" {
+ return cmd
+ }
+ return fmt.Sprintf("%s %s", ProxyEnv(), cmd)
+}
+
+// func unixtime(s string) (time.Time, error) {
+// i, err := strconv.ParseInt(s, 10, 64)
+// if err != nil {
+// return time.Time{}, err
+// }
+// return time.Unix(i, 0), nil
+// }
diff --git a/util/util_test.go b/util/util_test.go
new file mode 100644
index 00000000..50035170
--- /dev/null
+++ b/util/util_test.go
@@ -0,0 +1,133 @@
+/* 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 util
+
+import (
+ "testing"
+
+ "github.com/future-architect/vuls/config"
+)
+
+func TestUrlJoin(t *testing.T) {
+ var tests = []struct {
+ in []string
+ out string
+ }{
+ {
+ []string{
+ "http://hoge.com:8080",
+ "status",
+ },
+ "http://hoge.com:8080/status",
+ },
+ {
+ []string{
+ "http://hoge.com:8080/",
+ "status",
+ },
+ "http://hoge.com:8080/status",
+ },
+ {
+ []string{
+ "http://hoge.com:8080",
+ "/status",
+ },
+ "http://hoge.com:8080/status",
+ },
+ {
+ []string{
+ "http://hoge.com:8080/",
+ "/status",
+ },
+ "http://hoge.com:8080/status",
+ },
+ {
+ []string{
+ "http://hoge.com:8080/",
+ "/status",
+ },
+ "http://hoge.com:8080/status",
+ },
+ {
+ []string{
+ "http://hoge.com:8080/",
+ "",
+ "hega",
+ "",
+ },
+ "http://hoge.com:8080/hega",
+ },
+ {
+ []string{
+ "http://hoge.com:8080/",
+ "status",
+ "/fuga",
+ },
+ "http://hoge.com:8080/status/fuga",
+ },
+ }
+ for _, tt := range tests {
+ baseurl := tt.in[0]
+ paths := tt.in[1:]
+ actual, err := URLPathJoin(baseurl, paths...)
+ if err != nil {
+ t.Errorf("\nunexpected error occurred, err: %s,\ninput:%#v\nexpected: %s\n actual: %s", err, tt.in, tt.out, actual)
+ }
+ if actual != tt.out {
+ t.Errorf("\ninput:%#v\nexpected: %s\n actual: %s", tt.in, tt.out, actual)
+ }
+ }
+
+}
+
+func TestPrependHTTPProxyEnv(t *testing.T) {
+ var tests = []struct {
+ in []string
+ out string
+ }{
+ {
+ []string{
+ "http://proxy.co.jp:8080",
+ "yum check-update",
+ },
+ `env http_proxy="http://proxy.co.jp:8080" https_proxy="http://proxy.co.jp:8080" HTTP_PROXY="http://proxy.co.jp:8080" HTTPS_PROXY="http://proxy.co.jp:8080" yum check-update`,
+ },
+ {
+ []string{
+ "http://proxy.co.jp:8080",
+ "",
+ },
+ `env http_proxy="http://proxy.co.jp:8080" https_proxy="http://proxy.co.jp:8080" HTTP_PROXY="http://proxy.co.jp:8080" HTTPS_PROXY="http://proxy.co.jp:8080" `,
+ },
+ {
+ []string{
+ "",
+ "yum check-update",
+ },
+ `yum check-update`,
+ },
+ }
+ for _, tt := range tests {
+ config.Conf.HTTPProxy = tt.in[0]
+ actual := PrependProxyEnv(tt.in[1])
+ if actual != tt.out {
+ t.Errorf("\nexpected: %s\n actual: %s", tt.out, actual)
+ }
+ }
+
+}
diff --git a/version.go b/version.go
new file mode 100644
index 00000000..581e9e6d
--- /dev/null
+++ b/version.go
@@ -0,0 +1,24 @@
+/* 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 main
+
+// Name.
+const Name string = "vuls"
+
+// Version.
+const Version string = "0.1.0"