/* 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 ( "context" "flag" "fmt" "io/ioutil" "os" "path/filepath" "strings" "time" "github.com/Sirupsen/logrus" c "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/cveapi" "github.com/future-architect/vuls/report" "github.com/future-architect/vuls/scan" "github.com/future-architect/vuls/util" "github.com/google/subcommands" "github.com/k0kubun/pp" ) // ScanCmd is Subcommand of host discovery mode type ScanCmd struct { lang string debug bool debugSQL bool configPath string resultsDir string cvedbtype string cvedbpath string cveDictionaryURL string cacheDBPath string cvssScoreOver float64 ignoreUnscoredCves bool httpProxy string askSudoPassword bool askKeyPassword bool containersOnly bool skipBroken bool // reporting reportSlack bool reportMail bool reportJSON bool reportText bool reportS3 bool reportAzureBlob bool reportXML bool awsProfile string awsS3Bucket string awsRegion string azureAccount string azureKey string azureContainer string sshExternal 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] [-results-dir=/path/to/results] [-cve-dictionary-dbtype=sqlite3|mysql] [-cve-dictionary-dbpath=/path/to/cve.sqlite3 or mysql connection string] [-cve-dictionary-url=http://127.0.0.1:1323] [-cache-dbpath=/path/to/cache.db] [-cvss-over=7] [-ignore-unscored-cves] [-ssh-external] [-containers-only] [-skip-broken] [-report-azure-blob] [-report-json] [-report-mail] [-report-s3] [-report-slack] [-report-text] [-report-xml] [-http-proxy=http://192.168.0.1:8080] [-ask-key-password] [-debug] [-debug-sql] [-aws-profile=default] [-aws-region=us-west-2] [-aws-s3-bucket=bucket_name] [-azure-account=accout] [-azure-key=key] [-azure-container=container] [SERVER]... ` } // 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") wd, _ := os.Getwd() defaultConfPath := filepath.Join(wd, "config.toml") f.StringVar(&p.configPath, "config", defaultConfPath, "/path/to/toml") defaultResultsDir := filepath.Join(wd, "results") f.StringVar(&p.resultsDir, "results-dir", defaultResultsDir, "/path/to/results") f.StringVar( &p.cvedbtype, "cve-dictionary-dbtype", "sqlite3", "DB type for fetching CVE dictionary (sqlite3 or mysql)") f.StringVar( &p.cvedbpath, "cve-dictionary-dbpath", "", "/path/to/sqlite3 (For get cve detail from cve.sqlite3)") defaultURL := "http://127.0.0.1:1323" f.StringVar( &p.cveDictionaryURL, "cve-dictionary-url", defaultURL, "http://CVE.Dictionary") defaultCacheDBPath := filepath.Join(wd, "cache.db") f.StringVar( &p.cacheDBPath, "cache-dbpath", defaultCacheDBPath, "/path/to/cache.db (local cache of changelog for Ubuntu/Debian)") f.Float64Var( &p.cvssScoreOver, "cvss-over", 0, "-cvss-over=6.5 means reporting CVSS Score 6.5 and over (default: 0 (means report all))") f.BoolVar( &p.ignoreUnscoredCves, "ignore-unscored-cves", false, "Don't report the unscored CVEs") f.BoolVar( &p.sshExternal, "ssh-external", false, "Use external ssh command. Default: Use the Go native implementation") f.BoolVar( &p.containersOnly, "containers-only", false, "Scan containers only. Default: Scan both of hosts and containers") f.BoolVar( &p.skipBroken, "skip-broken", false, "[For CentOS] yum update changelog with --skip-broken option") f.StringVar( &p.httpProxy, "http-proxy", "", "http://proxy-url:port (default: empty)", ) f.BoolVar(&p.reportSlack, "report-slack", false, "Send report via Slack") f.BoolVar(&p.reportMail, "report-mail", false, "Send report via Email") f.BoolVar(&p.reportJSON, "report-json", false, fmt.Sprintf("Write report to JSON files (%s/results/current)", wd), ) f.BoolVar(&p.reportText, "report-text", false, fmt.Sprintf("Write report to text files (%s/results/current)", wd), ) f.BoolVar(&p.reportXML, "report-xml", false, fmt.Sprintf("Write report to XML files (%s/results/current)", wd), ) f.BoolVar(&p.reportS3, "report-s3", false, "Write report to S3 (bucket/yyyyMMdd_HHmm/servername.json)", ) f.StringVar(&p.awsProfile, "aws-profile", "default", "AWS profile to use") f.StringVar(&p.awsRegion, "aws-region", "us-east-1", "AWS region to use") f.StringVar(&p.awsS3Bucket, "aws-s3-bucket", "", "S3 bucket name") f.BoolVar(&p.reportAzureBlob, "report-azure-blob", false, "Write report to Azure Storage blob (container/yyyyMMdd_HHmm/servername.json)", ) f.StringVar(&p.azureAccount, "azure-account", "", "Azure account name to use. AZURE_STORAGE_ACCOUNT environment variable is used if not specified") f.StringVar(&p.azureKey, "azure-key", "", "Azure account key to use. AZURE_STORAGE_ACCESS_KEY environment variable is used if not specified") f.StringVar(&p.azureContainer, "azure-container", "", "Azure storage container name") f.BoolVar( &p.askKeyPassword, "ask-key-password", false, "Ask ssh privatekey password before scanning", ) f.BoolVar( &p.askSudoPassword, "ask-sudo-password", false, "[Deprecated] THIS OPTION WAS REMOVED FOR SECURITY REASONS. Define NOPASSWD in /etc/sudoers on target servers and use SSH key-based authentication", ) } // Execute execute func (p *ScanCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { var keyPass string var err error if p.askKeyPassword { prompt := "SSH key password: " if keyPass, err = getPasswd(prompt); err != nil { logrus.Error(err) return subcommands.ExitFailure } } if p.askSudoPassword { logrus.Errorf("[Deprecated] -ask-sudo-password WAS REMOVED FOR SECURITY REASONS. Define NOPASSWD in /etc/sudoers on target servers and use SSH key-based authentication") return subcommands.ExitFailure } c.Conf.Debug = p.debug err = c.Load(p.configPath, keyPass) if err != nil { logrus.Errorf("Error loading %s, %s", p.configPath, err) return subcommands.ExitUsageError } logrus.Info("Start scanning") logrus.Infof("config: %s", p.configPath) if p.cvedbpath != "" { if p.cvedbtype == "sqlite3" { logrus.Infof("cve-dictionary: %s", p.cvedbpath) } } else { logrus.Infof("cve-dictionary: %s", p.cveDictionaryURL) } var servernames []string if 0 < len(f.Args()) { servernames = f.Args() } else { stat, _ := os.Stdin.Stat() if (stat.Mode() & os.ModeCharDevice) == 0 { bytes, err := ioutil.ReadAll(os.Stdin) if err != nil { logrus.Errorf("Failed to read stdin: %s", err) return subcommands.ExitFailure } fields := strings.Fields(string(bytes)) if 0 < len(fields) { servernames = fields } } } target := make(map[string]c.ServerInfo) for _, arg := range servernames { 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(servernames) { c.Conf.Servers = target } logrus.Debugf("%s", pp.Sprintf("%v", target)) c.Conf.Lang = p.lang c.Conf.DebugSQL = p.debugSQL // logger Log := util.NewCustomLogger(c.ServerInfo{}) scannedAt := time.Now() // report reports := []report.ResultWriter{ report.StdoutWriter{}, report.LogrusWriter{}, } if p.reportSlack { reports = append(reports, report.SlackWriter{}) } if p.reportMail { reports = append(reports, report.MailWriter{}) } if p.reportJSON { reports = append(reports, report.JSONWriter{ScannedAt: scannedAt}) } if p.reportText { reports = append(reports, report.TextFileWriter{ScannedAt: scannedAt}) } if p.reportXML { reports = append(reports, report.XMLWriter{ScannedAt: scannedAt}) } if p.reportS3 { c.Conf.AwsRegion = p.awsRegion c.Conf.AwsProfile = p.awsProfile c.Conf.S3Bucket = p.awsS3Bucket if err := report.CheckIfBucketExists(); err != nil { Log.Errorf("Failed to access to the S3 bucket. err: %s", err) Log.Error("Ensure the bucket or check AWS config before scanning") return subcommands.ExitUsageError } reports = append(reports, report.S3Writer{}) } if p.reportAzureBlob { c.Conf.AzureAccount = p.azureAccount if len(c.Conf.AzureAccount) == 0 { c.Conf.AzureAccount = os.Getenv("AZURE_STORAGE_ACCOUNT") } c.Conf.AzureKey = p.azureKey if len(c.Conf.AzureKey) == 0 { c.Conf.AzureKey = os.Getenv("AZURE_STORAGE_ACCESS_KEY") } c.Conf.AzureContainer = p.azureContainer if len(c.Conf.AzureContainer) == 0 { Log.Error("Azure storage container name is requied with --azure-container option") return subcommands.ExitUsageError } if err := report.CheckIfAzureContainerExists(); err != nil { Log.Errorf("Failed to access to the Azure Blob container. err: %s", err) Log.Error("Ensure the container or check Azure config before scanning") return subcommands.ExitUsageError } reports = append(reports, report.AzureBlobWriter{}) } c.Conf.ResultsDir = p.resultsDir c.Conf.CveDBType = p.cvedbtype c.Conf.CveDBPath = p.cvedbpath c.Conf.CveDictionaryURL = p.cveDictionaryURL c.Conf.CacheDBPath = p.cacheDBPath c.Conf.CvssScoreOver = p.cvssScoreOver c.Conf.IgnoreUnscoredCves = p.ignoreUnscoredCves c.Conf.SSHExternal = p.sshExternal c.Conf.HTTPProxy = p.httpProxy c.Conf.ContainersOnly = p.containersOnly c.Conf.SkipBroken = p.skipBroken 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. err: %s", err) Log.Errorf("Run go-cve-dictionary as server mode or specify -cve-dictionary-dbpath option") return subcommands.ExitFailure } Log.Info("Detecting Server/Contianer OS... ") if err := scan.InitServers(Log); err != nil { Log.Errorf("Failed to init servers: %s", err) return subcommands.ExitFailure } Log.Info("Checking sudo configuration... ") if err := scan.CheckIfSudoNoPasswd(Log); err != nil { Log.Errorf("Failed to sudo with nopassword via SSH. Define NOPASSWD in /etc/sudoers on target servers") return subcommands.ExitFailure } Log.Info("Detecting Platforms... ") scan.DetectPlatforms(Log) 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 report, err: %s", err) return subcommands.ExitFailure } } return subcommands.ExitSuccess }