/* 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 (
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"

	"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
	cvedbpath        string
	cveDictionaryURL string

	cvssScoreOver      float64
	ignoreUnscoredCves bool

	httpProxy       string
	askSudoPassword bool
	askKeyPassword  bool

	// reporting
	reportSlack     bool
	reportMail      bool
	reportJSON      bool
	reportText      bool
	reportS3        bool
	reportAzureBlob bool

	awsProfile  string
	awsS3Bucket string
	awsRegion   string

	azureAccount   string
	azureKey       string
	azureContainer string

	useYumPluginSecurity  bool
	useUnattendedUpgrades bool

	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]
		[-dbpath=/path/to/vuls.sqlite3]
		[-cve-dictionary-dbpath=/path/to/cve.sqlite3]
		[-cve-dictionary-url=http://127.0.0.1:1323]
		[-cvss-over=7]
		[-ignore-unscored-cves]
		[-ssh-external]
		[-report-azure-blob]
		[-report-json]
		[-report-mail]
		[-report-s3]
		[-report-slack]
		[-report-text]
		[-http-proxy=http://192.168.0.1:8080]
		[-ask-sudo-password]
		[-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")

	defaultDBPath := filepath.Join(wd, "vuls.sqlite3")
	f.StringVar(&p.dbpath, "dbpath", defaultDBPath, "/path/to/sqlite3")

	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")

	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.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.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 S3 (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,
		"Ask sudo password of target servers before scanning",
	)

	f.BoolVar(
		&p.useYumPluginSecurity,
		"use-yum-plugin-security",
		false,
		"[Deprecated] For CentOS 5. Scan by yum-plugin-security or not (use yum check-update by default)",
	)

	f.BoolVar(
		&p.useUnattendedUpgrades,
		"use-unattended-upgrades",
		false,
		"[Deprecated] 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 {
	var keyPass, sudoPass 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 {
		prompt := "sudo password: "
		if sudoPass, err = getPasswd(prompt); err != nil {
			logrus.Error(err)
			return subcommands.ExitFailure
		}
	}

	err = c.Load(p.configPath, keyPass, sudoPass)
	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 != "" {
		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
	}

	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.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{})
	}
	if p.reportText {
		reports = append(reports, report.TextFileWriter{})
	}
	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 c.Conf.AzureAccount == "" {
			c.Conf.AzureAccount = os.Getenv("AZURE_STORAGE_ACCOUNT")
		}

		c.Conf.AzureKey = p.azureKey
		if c.Conf.AzureKey == "" {
			c.Conf.AzureKey = os.Getenv("AZURE_STORAGE_ACCESS_KEY")
		}

		c.Conf.AzureContainer = p.azureContainer
		if c.Conf.AzureContainer == "" {
			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.DBPath = p.dbpath
	c.Conf.CveDBPath = p.cvedbpath
	c.Conf.CveDictionaryURL = p.cveDictionaryURL
	c.Conf.CvssScoreOver = p.cvssScoreOver
	c.Conf.IgnoreUnscoredCves = p.ignoreUnscoredCves
	c.Conf.SSHExternal = p.sshExternal
	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. 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... ")
	scan.InitServers(Log)

	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("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
	}

	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
}
