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"