diff --git a/config/config.go b/config/config.go index d65ef294..2edc9a14 100644 --- a/config/config.go +++ b/config/config.go @@ -42,16 +42,17 @@ type Config struct { Exploit ExploitConf `json:"exploit,omitempty"` Metasploit MetasploitConf `json:"metasploit,omitempty"` - Slack SlackConf `json:"-"` - EMail SMTPConf `json:"-"` - HTTP HTTPConf `json:"-"` - Syslog SyslogConf `json:"-"` - AWS AWSConf `json:"-"` - Azure AzureConf `json:"-"` - ChatWork ChatWorkConf `json:"-"` - Telegram TelegramConf `json:"-"` - WpScan WpScanConf `json:"-"` - Saas SaasConf `json:"-"` + Slack SlackConf `json:"-"` + EMail SMTPConf `json:"-"` + HTTP HTTPConf `json:"-"` + Syslog SyslogConf `json:"-"` + AWS AWSConf `json:"-"` + Azure AzureConf `json:"-"` + ChatWork ChatWorkConf `json:"-"` + GoogleChat GoogleChatConf `json:"-"` + Telegram TelegramConf `json:"-"` + WpScan WpScanConf `json:"-"` + Saas SaasConf `json:"-"` ReportOpts } @@ -157,6 +158,7 @@ func (c *Config) ValidateOnReport() bool { &c.EMail, &c.Slack, &c.ChatWork, + &c.GoogleChat, &c.Telegram, &c.Syslog, &c.HTTP, diff --git a/config/googlechatconf.go b/config/googlechatconf.go new file mode 100644 index 00000000..5c5bad50 --- /dev/null +++ b/config/googlechatconf.go @@ -0,0 +1,32 @@ +package config + +import ( + "github.com/asaskevich/govalidator" + "golang.org/x/xerrors" +) + +// GoogleChatConf is GoogleChat config +type GoogleChatConf struct { + WebHookURL string `valid:"url" json:"-" toml:"webHookURL,omitempty"` + SkipIfNoCve bool `valid:"type(bool)" json:"-" toml:"skipIfNoCve"` + ServerNameRegexp string `valid:"type(string)" json:"-" toml:"serverNameRegexp,omitempty"` + Enabled bool `valid:"type(bool)" json:"-" toml:"-"` +} + +// Validate validates configuration +func (c *GoogleChatConf) Validate() (errs []error) { + if !c.Enabled { + return + } + if len(c.WebHookURL) == 0 { + errs = append(errs, xerrors.New("googleChatConf.webHookURL must not be empty")) + } + if !govalidator.IsRegex(c.ServerNameRegexp) { + errs = append(errs, xerrors.New("googleChatConf.serverNameRegexp must be regex")) + } + _, err := govalidator.ValidateStruct(c) + if err != nil { + errs = append(errs, err) + } + return +} diff --git a/constant/constant.go b/constant/constant.go index f394aad3..e0d2e1f0 100644 --- a/constant/constant.go +++ b/constant/constant.go @@ -17,6 +17,9 @@ const ( // CentOS is CentOS = "centos" + // Rocky is + Rocky = "Rocky" + // Fedora is // Fedora = "fedora" diff --git a/go.mod b/go.mod index 4d7022ce..26699019 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( github.com/spf13/cobra v1.1.3 github.com/spf13/viper v1.8.1 // indirect github.com/takuzoo3868/go-msfdb v0.1.5 - github.com/vulsio/go-exploitdb v0.1.7 + github.com/vulsio/go-exploitdb v0.1.8-0.20210625021845-e5081ca67229 go.opentelemetry.io/otel/internal/metric v0.21.0 // indirect golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect diff --git a/go.sum b/go.sum index d115305f..d680fbe9 100644 --- a/go.sum +++ b/go.sum @@ -1417,8 +1417,8 @@ github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6Ac github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= -github.com/vulsio/go-exploitdb v0.1.7 h1:wdq+6H/PvGGnUiyAaLQ3DtczsLy3rrBQgmNOiXH62z0= -github.com/vulsio/go-exploitdb v0.1.7/go.mod h1:4strSWuNtCTz76QB8RuxpMQmYifArGKiHKBFCMOTxY4= +github.com/vulsio/go-exploitdb v0.1.8-0.20210625021845-e5081ca67229 h1:fgwhSbKAPf0wnGwmrkjWyfUi48lMJhS6y8rqGPyHyJE= +github.com/vulsio/go-exploitdb v0.1.8-0.20210625021845-e5081ca67229/go.mod h1:4strSWuNtCTz76QB8RuxpMQmYifArGKiHKBFCMOTxY4= github.com/wasmerio/go-ext-wasm v0.3.1/go.mod h1:VGyarTzasuS7k5KhSIGpM3tciSZlkP31Mp9VJTHMMeI= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= diff --git a/gost/gost.go b/gost/gost.go index d7313e35..7d6edc6c 100644 --- a/gost/gost.go +++ b/gost/gost.go @@ -74,7 +74,7 @@ func NewClient(cnf config.GostConf, family string) (Client, error) { case constant.Windows: return Microsoft{Base{DBDriver: driver}}, nil default: - return Pseudo{}, nil + return Pseudo{Base{DBDriver: driver}}, nil } } diff --git a/oval/debian.go b/oval/debian.go index c0926a23..d461336b 100644 --- a/oval/debian.go +++ b/oval/debian.go @@ -342,6 +342,47 @@ func (o Ubuntu) FillWithOval(r *models.ScanResult) (nCVEs int, err error) { "linux", } return o.fillWithOval(r, kernelNamesInOval) + case "21": + kernelNamesInOval := []string{ + "linux-aws", + "linux-base-sgx", + "linux-base", + "linux-cloud-tools-common", + "linux-cloud-tools-generic", + "linux-cloud-tools-lowlatency", + "linux-cloud-tools-virtual", + "linux-gcp", + "linux-generic", + "linux-gke", + "linux-headers-aws", + "linux-headers-gcp", + "linux-headers-gke", + "linux-headers-oracle", + "linux-image-aws", + "linux-image-extra-virtual", + "linux-image-gcp", + "linux-image-generic", + "linux-image-gke", + "linux-image-lowlatency", + "linux-image-oracle", + "linux-image-virtual", + "linux-lowlatency", + "linux-modules-extra-aws", + "linux-modules-extra-gcp", + "linux-modules-extra-gke", + "linux-oracle", + "linux-tools-aws", + "linux-tools-common", + "linux-tools-gcp", + "linux-tools-generic", + "linux-tools-gke", + "linux-tools-host", + "linux-tools-lowlatency", + "linux-tools-oracle", + "linux-tools-virtual", + "linux-virtual", + } + return o.fillWithOval(r, kernelNamesInOval) } return 0, fmt.Errorf("Ubuntu %s is not support for now", r.Release) } diff --git a/reporter/googlechat.go b/reporter/googlechat.go new file mode 100644 index 00000000..2c9fc680 --- /dev/null +++ b/reporter/googlechat.go @@ -0,0 +1,102 @@ +package reporter + +import ( + "bytes" + "context" + "fmt" + "net/http" + "regexp" + "strings" + "time" + + "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/models" + "github.com/future-architect/vuls/util" + "golang.org/x/xerrors" +) + +// GoogleChatWriter send report to GoogleChat +type GoogleChatWriter struct { + Cnf config.GoogleChatConf + Proxy string +} + +func (w GoogleChatWriter) Write(rs ...models.ScanResult) (err error) { + re := regexp.MustCompile(w.Cnf.ServerNameRegexp) + + for _, r := range rs { + if re.Match([]byte(r.FormatServerName())) { + continue + } + msgs := []string{fmt.Sprintf("*%s*\n%s\t%s\t%s", + r.ServerInfo(), + r.ScannedCves.FormatCveSummary(), + r.ScannedCves.FormatFixedStatus(r.Packages), + r.FormatUpdatablePkgsSummary())} + for _, vinfo := range r.ScannedCves.ToSortedSlice() { + max := vinfo.MaxCvssScore().Value.Score + + exploits := "" + if 0 < len(vinfo.Exploits) || 0 < len(vinfo.Metasploits) { + exploits = "*PoC*" + } + + link := "" + if strings.HasPrefix(vinfo.CveID, "CVE-") { + link = fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", vinfo.CveID) + } else if strings.HasPrefix(vinfo.CveID, "WPVDBID-") { + link = fmt.Sprintf("https://wpscan.com/vulnerabilities/%s", strings.TrimPrefix(vinfo.CveID, "WPVDBID-")) + } + + msgs = append(msgs, fmt.Sprintf(`%s %s %4.1f %5s %s`, + vinfo.CveIDDiffFormat(), + link, + max, + vinfo.AttackVector(), + exploits)) + if len(msgs) == 50 { + msgs = append(msgs, "(The rest is omitted.)") + break + } + } + if len(msgs) == 1 && w.Cnf.SkipIfNoCve { + msgs = []string{} + } + if len(msgs) != 0 { + if err = w.postMessage(strings.Join(msgs, "\n")); err != nil { + return err + } + } + } + return nil +} + +func (w GoogleChatWriter) postMessage(message string) error { + uri := fmt.Sprintf("%s", w.Cnf.WebHookURL) + payload := `{"text": "` + message + `" }` + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri, bytes.NewBuffer([]byte(payload))) + defer cancel() + if err != nil { + return err + } + req.Header.Add("Content-Type", "application/json; charset=utf-8") + client, err := util.GetHTTPClient(w.Proxy) + if err != nil { + return err + } + resp, err := client.Do(req) + if checkResponse(resp) != nil && err != nil { + return err + } + defer resp.Body.Close() + return nil +} + +func (w GoogleChatWriter) checkResponse(r *http.Response) error { + if c := r.StatusCode; 200 <= c && c <= 299 { + return nil + } + return xerrors.Errorf("API call to %s failed: %s", r.Request.URL.String(), r.Status) +} diff --git a/scanner/redhatbase.go b/scanner/redhatbase.go index b467f523..60d481f6 100644 --- a/scanner/redhatbase.go +++ b/scanner/redhatbase.go @@ -64,6 +64,27 @@ func detectRedhat(c config.ServerInfo) (bool, osTypeInterface) { } } + if r := exec(c, "ls /etc/rocky-release", noSudo); r.isSuccess() { + if r := exec(c, "cat /etc/rocky-release", noSudo); r.isSuccess() { + re := regexp.MustCompile(`(.*) release (\d[\d\.]*)`) + result := re.FindStringSubmatch(strings.TrimSpace(r.Stdout)) + if len(result) != 3 { + logging.Log.Warnf("Failed to parse Rocky version: %s", r) + return true, newRocky(c) + } + + release := result[2] + switch strings.ToLower(result[1]) { + case "rocky", "rocky linux": + rocky := newRocky(c) + rocky.setDistro(constant.Rocky, release) + return true, rocky + default: + logging.Log.Warnf("Failed to parse Rocky: %s", r) + } + } + } + if r := exec(c, "ls /etc/redhat-release", noSudo); r.isSuccess() { // https://www.rackaid.com/blog/how-to-determine-centos-or-red-hat-version/ // e.g. diff --git a/scanner/rocky.go b/scanner/rocky.go new file mode 100644 index 00000000..922ff71b --- /dev/null +++ b/scanner/rocky.go @@ -0,0 +1,118 @@ +package scanner + +import ( + "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/logging" + "github.com/future-architect/vuls/models" +) + +// inherit OsTypeInterface +type rocky struct { + redhatBase +} + +// NewAmazon is constructor +func newRocky(c config.ServerInfo) *rocky { + r := &rocky{ + redhatBase{ + base: base{ + osPackages: osPackages{ + Packages: models.Packages{}, + VulnInfos: models.VulnInfos{}, + }, + }, + sudo: rootPrivRocky{}, + }, + } + r.log = logging.NewNormalLogger() + r.setServerInfo(c) + return r +} + +func (o *rocky) checkScanMode() error { + return nil +} + +func (o *rocky) checkDeps() error { + if o.getServerInfo().Mode.IsFast() { + return o.execCheckDeps(o.depsFast()) + } else if o.getServerInfo().Mode.IsFastRoot() { + return o.execCheckDeps(o.depsFastRoot()) + } else { + return o.execCheckDeps(o.depsDeep()) + } +} + +func (o *rocky) depsFast() []string { + if o.getServerInfo().Mode.IsOffline() { + return []string{} + } + + // repoquery + // `rpm -qa` shows dnf-utils as yum-utils on RHEL8, CentOS8, Rocky + return []string{"yum-utils"} +} + +func (o *rocky) depsFastRoot() []string { + if o.getServerInfo().Mode.IsOffline() { + return []string{} + } + + // repoquery + // `rpm -qa` shows dnf-utils as yum-utils on RHEL8, CentOS8, Rocky + return []string{"yum-utils"} +} + +func (o *rocky) depsDeep() []string { + return o.depsFastRoot() +} + +func (o *rocky) checkIfSudoNoPasswd() error { + if o.getServerInfo().Mode.IsFast() { + return o.execCheckIfSudoNoPasswd(o.sudoNoPasswdCmdsFast()) + } else if o.getServerInfo().Mode.IsFastRoot() { + return o.execCheckIfSudoNoPasswd(o.sudoNoPasswdCmdsFastRoot()) + } else { + return o.execCheckIfSudoNoPasswd(o.sudoNoPasswdCmdsDeep()) + } +} + +func (o *rocky) sudoNoPasswdCmdsFast() []cmd { + return []cmd{} +} + +func (o *rocky) sudoNoPasswdCmdsFastRoot() []cmd { + if !o.ServerInfo.IsContainer() { + return []cmd{ + {"repoquery -h", exitStatusZero}, + {"needs-restarting", exitStatusZero}, + {"which which", exitStatusZero}, + {"stat /proc/1/exe", exitStatusZero}, + {"ls -l /proc/1/exe", exitStatusZero}, + {"cat /proc/1/maps", exitStatusZero}, + {"lsof -i -P", exitStatusZero}, + } + } + return []cmd{ + {"repoquery -h", exitStatusZero}, + {"needs-restarting", exitStatusZero}, + } +} + +func (o *rocky) sudoNoPasswdCmdsDeep() []cmd { + return o.sudoNoPasswdCmdsFastRoot() +} + +type rootPrivRocky struct{} + +func (o rootPrivRocky) repoquery() bool { + return false +} + +func (o rootPrivRocky) yumMakeCache() bool { + return false +} + +func (o rootPrivRocky) yumPS() bool { + return false +} diff --git a/subcmds/discover.go b/subcmds/discover.go index 7268e415..3d2b350a 100644 --- a/subcmds/discover.go +++ b/subcmds/discover.go @@ -157,6 +157,13 @@ func printConfigToml(ips []string) (err error) { #room = "xxxxxxxxxxx" #apiToken = "xxxxxxxxxxxxxxxxxx" +# https://vuls.io/docs/en/config.toml.html#googlechat-section +#[googlechat] +#webHookURL = "https://chat.googleapis.com/v1/spaces/xxxxxxxxxx/messages?key=yyyyyyyyyy&token=zzzzzzzzzz%3D" +#skipIfNoCve = false +#serverNameRegexp = "^(\\[Reboot Required\\] )?((spam|ham).*|.*(egg)$)" # include spamonigiri, hamburger, boiledegg +#serverNameRegexp = "^(\\[Reboot Required\\] )?(?:(spam|ham).*|.*(?:egg)$)" # exclude spamonigiri, hamburger, boiledegg + # https://vuls.io/docs/en/config.toml.html#telegram-section #[telegram] #chatID = "xxxxxxxxxxx" diff --git a/subcmds/report.go b/subcmds/report.go index 3a0818f2..671aebd4 100644 --- a/subcmds/report.go +++ b/subcmds/report.go @@ -30,15 +30,16 @@ type ReportCmd struct { formatList bool gzip bool - toSlack bool - toChatWork bool - toTelegram bool - toEmail bool - toSyslog bool - toLocalFile bool - toS3 bool - toAzureBlob bool - toHTTP bool + toSlack bool + toChatWork bool + toGoogleChat bool + toTelegram bool + toEmail bool + toSyslog bool + toLocalFile bool + toS3 bool + toAzureBlob bool + toHTTP bool } // Name return subcommand name @@ -67,6 +68,7 @@ func (*ReportCmd) Usage() string { [-to-http] [-to-slack] [-to-chatwork] + [-to-googlechat] [-to-telegram] [-to-localfile] [-to-s3] @@ -146,6 +148,7 @@ func (p *ReportCmd) SetFlags(f *flag.FlagSet) { f.BoolVar(&p.toSlack, "to-slack", false, "Send report via Slack") f.BoolVar(&p.toChatWork, "to-chatwork", false, "Send report via chatwork") + f.BoolVar(&p.toGoogleChat, "to-googlechat", false, "Send report via Google Chat") f.BoolVar(&p.toTelegram, "to-telegram", false, "Send report via Telegram") f.BoolVar(&p.toEmail, "to-email", false, "Send report via Email") f.BoolVar(&p.toSyslog, "to-syslog", false, "Send report via Syslog") @@ -173,6 +176,7 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} } config.Conf.Slack.Enabled = p.toSlack config.Conf.ChatWork.Enabled = p.toChatWork + config.Conf.GoogleChat.Enabled = p.toGoogleChat config.Conf.Telegram.Enabled = p.toTelegram config.Conf.EMail.Enabled = p.toEmail config.Conf.Syslog.Enabled = p.toSyslog @@ -261,6 +265,10 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} reports = append(reports, reporter.ChatWorkWriter{Cnf: config.Conf.ChatWork, Proxy: config.Conf.HTTPProxy}) } + if p.toGoogleChat { + reports = append(reports, reporter.GoogleChatWriter{Cnf: config.Conf.GoogleChat, Proxy: config.Conf.HTTPProxy}) + } + if p.toTelegram { reports = append(reports, reporter.TelegramWriter{Cnf: config.Conf.Telegram}) }