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
This commit is contained in:
Shigechika AIKAWA
2021-07-02 05:32:00 +09:00
committed by GitHub
parent 0e0e5ce4be
commit 1c8e074c9d
5 changed files with 170 additions and 19 deletions

View File

@@ -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,

32
config/googlechatconf.go Normal file
View File

@@ -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
}

102
reporter/googlechat.go Normal file
View File

@@ -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)
}

View File

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

View File

@@ -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})
}