/* 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 ( "errors" "fmt" "log/syslog" "os" "runtime" "strconv" "strings" valid "github.com/asaskevich/govalidator" log "github.com/sirupsen/logrus" ) // Conf has Configuration var Conf Config const ( // RedHat is RedHat = "redhat" // Debian is Debian = "debian" // Ubuntu is Ubuntu = "ubuntu" // CentOS is CentOS = "centos" // Fedora is Fedora = "fedora" // Amazon is Amazon = "amazon" // Oracle is Oracle = "oracle" // FreeBSD is FreeBSD = "freebsd" // Raspbian is Raspbian = "raspbian" // Windows is Windows = "windows" // OpenSUSE is OpenSUSE = "opensuse" // OpenSUSELeap is OpenSUSELeap = "opensuse.leap" // SUSEEnterpriseServer is SUSEEnterpriseServer = "suse.linux.enterprise.server" // SUSEEnterpriseDesktop is SUSEEnterpriseDesktop = "suse.linux.enterprise.desktop" // SUSEOpenstackCloud is SUSEOpenstackCloud = "suse.openstack.cloud" // Alpine is Alpine = "alpine" ) const ( // ServerTypePseudo is used for ServerInfo.Type ServerTypePseudo = "pseudo" ) //Config is struct of Configuration type Config struct { Debug bool DebugSQL bool Lang string EMail SMTPConf Slack SlackConf Stride StrideConf HipChat HipChatConf ChatWork ChatWorkConf Syslog SyslogConf Default ServerInfo Servers map[string]ServerInfo CvssScoreOver float64 IgnoreUnscoredCves bool IgnoreUnfixed bool SSHNative bool ContainersOnly bool Fast bool Offline bool Deep bool SkipBroken bool HTTPProxy string `valid:"url"` LogDir string ResultsDir string CveDBType string CveDBPath string CveDBURL string OvalDBType string OvalDBPath string OvalDBURL string CacheDBPath string RefreshCve bool ToSlack bool ToStride bool ToHipChat bool ToChatWork bool ToEmail bool ToSyslog bool ToLocalFile bool ToS3 bool ToAzureBlob bool FormatXML bool FormatJSON bool FormatOneEMail bool FormatOneLineText bool FormatShortText bool FormatFullText bool GZIP bool AwsProfile string AwsRegion string S3Bucket string S3ResultsDir string S3ServerSideEncryption string AzureAccount string AzureKey string `json:"-"` AzureContainer string Pipe bool Vvv bool Diff bool } // ValidateOnConfigtest validates func (c Config) ValidateOnConfigtest() bool { errs := []error{} if runtime.GOOS == "windows" && !c.SSHNative { errs = append(errs, fmt.Errorf("-ssh-native-insecure is needed on windows")) } _, err := valid.ValidateStruct(c) if err != nil { errs = append(errs, err) } for _, err := range errs { log.Error(err) } return len(errs) == 0 } // ValidateOnPrepare validates configuration func (c Config) ValidateOnPrepare() bool { return c.ValidateOnConfigtest() } // ValidateOnScan validates configuration func (c Config) ValidateOnScan() bool { errs := []error{} if len(c.ResultsDir) != 0 { if ok, _ := valid.IsFilePath(c.ResultsDir); !ok { errs = append(errs, fmt.Errorf( "JSON base directory must be a *Absolute* file path. -results-dir: %s", c.ResultsDir)) } } if runtime.GOOS == "windows" && !c.SSHNative { errs = append(errs, fmt.Errorf("-ssh-native-insecure is needed on windows")) } if len(c.ResultsDir) != 0 { if ok, _ := valid.IsFilePath(c.ResultsDir); !ok { errs = append(errs, fmt.Errorf( "JSON base directory must be a *Absolute* file path. -results-dir: %s", c.ResultsDir)) } } if len(c.CacheDBPath) != 0 { if ok, _ := valid.IsFilePath(c.CacheDBPath); !ok { errs = append(errs, fmt.Errorf( "Cache DB path must be a *Absolute* file path. -cache-dbpath: %s", c.CacheDBPath)) } } numTrue := 0 for _, b := range []bool{c.Fast, c.Offline, c.Deep} { if b { numTrue++ } } if numTrue != 1 { errs = append(errs, fmt.Errorf("Specify only one of -fast, -fast-offline, -deep")) } _, err := valid.ValidateStruct(c) if err != nil { errs = append(errs, err) } for _, err := range errs { log.Error(err) } return len(errs) == 0 } // ValidateOnReport validates configuration func (c Config) ValidateOnReport() bool { errs := []error{} if len(c.ResultsDir) != 0 { if ok, _ := valid.IsFilePath(c.ResultsDir); !ok { errs = append(errs, fmt.Errorf( "JSON base directory must be a *Absolute* file path. -results-dir: %s", c.ResultsDir)) } } if err := validateDB("cvedb", c.CveDBType, c.CveDBPath, c.CveDBURL); err != nil { errs = append(errs, err) } if c.CveDBType == "sqlite3" { if _, err := os.Stat(c.CveDBPath); os.IsNotExist(err) { errs = append(errs, fmt.Errorf("SQLite3 DB path (%s) is not exist: %s", "cvedb", c.CveDBPath)) } } if err := validateDB("ovaldb", c.OvalDBType, c.OvalDBPath, c.OvalDBURL); err != nil { errs = append(errs, err) } _, err := valid.ValidateStruct(c) if err != nil { errs = append(errs, err) } if mailerrs := c.EMail.Validate(); 0 < len(mailerrs) { errs = append(errs, mailerrs...) } if slackerrs := c.Slack.Validate(); 0 < len(slackerrs) { errs = append(errs, slackerrs...) } if hipchaterrs := c.HipChat.Validate(); 0 < len(hipchaterrs) { errs = append(errs, hipchaterrs...) } if chatworkerrs := c.ChatWork.Validate(); 0 < len(chatworkerrs) { errs = append(errs, chatworkerrs...) } if strideerrs := c.Stride.Validate(); 0 < len(strideerrs) { errs = append(errs, strideerrs...) } if syslogerrs := c.Syslog.Validate(); 0 < len(syslogerrs) { errs = append(errs, syslogerrs...) } for _, err := range errs { log.Error(err) } return len(errs) == 0 } // ValidateOnTui validates configuration func (c Config) ValidateOnTui() bool { errs := []error{} if len(c.ResultsDir) != 0 { if ok, _ := valid.IsFilePath(c.ResultsDir); !ok { errs = append(errs, fmt.Errorf( "JSON base directory must be a *Absolute* file path. -results-dir: %s", c.ResultsDir)) } } if err := validateDB("cvedb", c.CveDBType, c.CveDBPath, c.CveDBURL); err != nil { errs = append(errs, err) } if c.CveDBType == "sqlite3" { if _, err := os.Stat(c.CveDBPath); os.IsNotExist(err) { errs = append(errs, fmt.Errorf("SQLite3 DB path (%s) is not exist: %s", "cvedb", c.CveDBPath)) } } for _, err := range errs { log.Error(err) } return len(errs) == 0 } // validateDB validates configuration // dictionaryDB name is 'cvedb' or 'ovaldb' func validateDB(dictionaryDBName, dbType, dbPath, dbURL string) error { switch dbType { case "sqlite3": if ok, _ := valid.IsFilePath(dbPath); !ok { return fmt.Errorf( "SQLite3 DB path (%s) must be a *Absolute* file path. -%s-path: %s", dictionaryDBName, dictionaryDBName, dbPath) } case "mysql": if dbURL == "" { return fmt.Errorf( `MySQL connection string is needed. -%s-url="user:pass@tcp(localhost:3306)/dbname"`, dictionaryDBName) } case "postgres": if dbURL == "" { return fmt.Errorf( `PostgreSQL connection string is needed. -%s-url="host=myhost user=user dbname=dbname sslmode=disable password=password"`, dictionaryDBName) } case "redis": if dbURL == "" { return fmt.Errorf( `Redis connection string is needed. -%s-url="redis://localhost/0"`, dictionaryDBName) } default: return fmt.Errorf( "%s type must be either 'sqlite3', 'mysql', 'postgres' or 'redis'. -%s-type: %s", dictionaryDBName, dictionaryDBName, dbType) } return nil } // SMTPConf is smtp config type SMTPConf struct { SMTPAddr string SMTPPort string `valid:"port"` User string Password string `json:"-"` From string To []string Cc []string SubjectPrefix string } 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 !Conf.ToEmail { 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("email.smtpAddr must not be empty")) } if len(c.SMTPPort) == 0 { errs = append(errs, fmt.Errorf("email.smtpPort must not be empty")) } if len(c.To) == 0 { errs = append(errs, fmt.Errorf("email.To required at least one address")) } if len(c.From) == 0 { errs = append(errs, fmt.Errorf("email.From required at least one address")) } _, err := valid.ValidateStruct(c) if err != nil { errs = append(errs, err) } return } // StrideConf is stride config type StrideConf struct { HookURL string `json:"hook_url"` AuthToken string `json:"AuthToken"` } // Validate validates configuration func (c *StrideConf) Validate() (errs []error) { if !Conf.ToStride { return } if len(c.HookURL) == 0 { errs = append(errs, fmt.Errorf("stride.HookURL must not be empty")) } if len(c.AuthToken) == 0 { errs = append(errs, fmt.Errorf("stride.AuthToken must not be empty")) } _, err := valid.ValidateStruct(c) if err != nil { errs = append(errs, err) } return } // SlackConf is slack config type SlackConf struct { HookURL string `valid:"url" json:"-"` LegacyToken string `json:"token" toml:"legacyToken,omitempty"` Channel string `json:"channel"` IconEmoji string `json:"icon_emoji"` AuthUser string `json:"username"` NotifyUsers []string Text string `json:"text"` } // Validate validates configuration func (c *SlackConf) Validate() (errs []error) { if !Conf.ToSlack { return } if len(c.HookURL) == 0 && len(c.LegacyToken) == 0 { errs = append(errs, fmt.Errorf("slack.hookURL or slack.LegacyToken must not be empty")) } if len(c.Channel) == 0 { errs = append(errs, fmt.Errorf("slack.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("slack.authUser must not be empty")) } _, err := valid.ValidateStruct(c) if err != nil { errs = append(errs, err) } return } // HipChatConf is HipChat config type HipChatConf struct { AuthToken string `json:"AuthToken"` Room string `json:"Room"` } // Validate validates configuration func (c *HipChatConf) Validate() (errs []error) { if !Conf.ToHipChat { return } if len(c.Room) == 0 { errs = append(errs, fmt.Errorf("hipcaht.room must not be empty")) } if len(c.AuthToken) == 0 { errs = append(errs, fmt.Errorf("hipcaht.AuthToken must not be empty")) } _, err := valid.ValidateStruct(c) if err != nil { errs = append(errs, err) } return } // ChatWorkConf is ChatWork config type ChatWorkConf struct { APIToken string `json:"ApiToken"` Room string `json:"Room"` } // Validate validates configuration func (c *ChatWorkConf) Validate() (errs []error) { if !Conf.ToChatWork { return } if len(c.Room) == 0 { errs = append(errs, fmt.Errorf("chatworkcaht.room must not be empty")) } if len(c.APIToken) == 0 { errs = append(errs, fmt.Errorf("chatworkcaht.ApiToken must not be empty")) } _, err := valid.ValidateStruct(c) if err != nil { errs = append(errs, err) } return } // SyslogConf is syslog config type SyslogConf struct { Protocol string Host string `valid:"host"` Port string `valid:"port"` Severity string Facility string Tag string Verbose bool } // Validate validates configuration func (c *SyslogConf) Validate() (errs []error) { if !Conf.ToSyslog { return nil } // If protocol is empty, it will connect to the local syslog server. if len(c.Protocol) > 0 && c.Protocol != "tcp" && c.Protocol != "udp" { errs = append(errs, errors.New(`syslog.protocol must be "tcp" or "udp"`)) } // Default port: 514 if c.Port == "" { c.Port = "514" } if _, err := c.GetSeverity(); err != nil { errs = append(errs, err) } if _, err := c.GetFacility(); err != nil { errs = append(errs, err) } if _, err := valid.ValidateStruct(c); err != nil { errs = append(errs, err) } return errs } // GetSeverity gets severity func (c *SyslogConf) GetSeverity() (syslog.Priority, error) { if c.Severity == "" { return syslog.LOG_INFO, nil } switch c.Severity { case "emerg": return syslog.LOG_EMERG, nil case "alert": return syslog.LOG_ALERT, nil case "crit": return syslog.LOG_CRIT, nil case "err": return syslog.LOG_ERR, nil case "warning": return syslog.LOG_WARNING, nil case "notice": return syslog.LOG_NOTICE, nil case "info": return syslog.LOG_INFO, nil case "debug": return syslog.LOG_DEBUG, nil default: return -1, fmt.Errorf("Invalid severity: %s", c.Severity) } } // GetFacility gets facility func (c *SyslogConf) GetFacility() (syslog.Priority, error) { if c.Facility == "" { return syslog.LOG_AUTH, nil } switch c.Facility { case "kern": return syslog.LOG_KERN, nil case "user": return syslog.LOG_USER, nil case "mail": return syslog.LOG_MAIL, nil case "daemon": return syslog.LOG_DAEMON, nil case "auth": return syslog.LOG_AUTH, nil case "syslog": return syslog.LOG_SYSLOG, nil case "lpr": return syslog.LOG_LPR, nil case "news": return syslog.LOG_NEWS, nil case "uucp": return syslog.LOG_UUCP, nil case "cron": return syslog.LOG_CRON, nil case "authpriv": return syslog.LOG_AUTHPRIV, nil case "ftp": return syslog.LOG_FTP, nil case "local0": return syslog.LOG_LOCAL0, nil case "local1": return syslog.LOG_LOCAL1, nil case "local2": return syslog.LOG_LOCAL2, nil case "local3": return syslog.LOG_LOCAL3, nil case "local4": return syslog.LOG_LOCAL4, nil case "local5": return syslog.LOG_LOCAL5, nil case "local6": return syslog.LOG_LOCAL6, nil case "local7": return syslog.LOG_LOCAL7, nil default: return -1, fmt.Errorf("Invalid facility: %s", c.Facility) } } // ServerInfo has SSH Info, additional CPE packages to scan. type ServerInfo struct { ServerName string User string Host string Port string KeyPath string KeyPassword string `json:"-"` CpeNames []string DependencyCheckXMLPath string // Container Names or IDs Containers Containers IgnoreCves []string // Optional key-value set that will be outputted to JSON Optional [][]interface{} // For CentOS, RHEL, Amazon Enablerepo []string // "pseudo" or "" Type string // used internal LogMsgAnsiColor string // DebugLog Color Container Container Distro Distro // IP addresses IPv4Addrs []string IPv6Addrs []string } // GetServerName returns ServerName if this serverInfo is about host. // If this serverInfo is abount a container, returns containerID@ServerName func (s ServerInfo) GetServerName() string { if len(s.Container.ContainerID) == 0 { return s.ServerName } return fmt.Sprintf("%s@%s", s.Container.Name, s.ServerName) } // Distro has distribution info type Distro struct { Family string Release string } func (l Distro) String() string { return fmt.Sprintf("%s %s", l.Family, l.Release) } // MajorVersion returns Major version func (l Distro) MajorVersion() (ver int, err error) { if l.Family == Amazon { ss := strings.Fields(l.Release) if len(ss) == 1 { return 1, nil } ver, err = strconv.Atoi(ss[0]) return } if 0 < len(l.Release) { ver, err = strconv.Atoi(strings.Split(l.Release, ".")[0]) } else { err = fmt.Errorf("Release is empty") } return } // IsContainer returns whether this ServerInfo is about container func (s ServerInfo) IsContainer() bool { return 0 < len(s.Container.ContainerID) } // SetContainer set container func (s *ServerInfo) SetContainer(d Container) { s.Container = d } // Containers has Containers information. type Containers struct { Type string Includes []string Excludes []string } // Container has Container information. type Container struct { ContainerID string Name string Image string }