From 1c8e074c9d2dbfe8ce0196084ffa609c5cd58b99 Mon Sep 17 00:00:00 2001 From: Shigechika AIKAWA Date: Fri, 2 Jul 2021 05:32:00 +0900 Subject: [PATCH] Feat report googlechat (#1257) (#1258) * feat: Support Ubuntu21 * feat(report): Send report via Google Chat * feat(report): Send report via Google Chat * Snip too long message as (The rest is omitted). * sorry for mixed feat-ubuntu21 branch. exlucded it * append diff, attack vector and exploits info * add ServerName filter by regexp * rename variables and rewrite validators * fix renaming miss * fix renaming miss, again --- config/config.go | 22 +++++---- config/googlechatconf.go | 32 ++++++++++++ reporter/googlechat.go | 102 +++++++++++++++++++++++++++++++++++++++ subcmds/discover.go | 7 +++ subcmds/report.go | 26 ++++++---- 5 files changed, 170 insertions(+), 19 deletions(-) create mode 100644 config/googlechatconf.go create mode 100644 reporter/googlechat.go 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/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/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}) }