Files
vuls/commands/scan.go
2016-11-07 21:22:38 +09:00

452 lines
12 KiB
Go

/* 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 <http://www.gnu.org/licenses/>.
*/
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
}