From eb2acaff22395a63d5df673ba65621991ca10bf9 Mon Sep 17 00:00:00 2001 From: sadayuki-matsuno Date: Thu, 26 Oct 2017 13:30:01 +0900 Subject: [PATCH] send slack msg by api (#525) --- Gopkg.lock | 10 ++++- README.ja.md | 5 ++- README.md | 18 ++++++++- config/config.go | 9 +++-- report/slack.go | 99 +++++++++++++++++++++++++++++------------------- 5 files changed, 93 insertions(+), 48 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index c87f8d2f..d5520f1c 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -198,6 +198,12 @@ packages = ["."] revision = "9ac6cf4d929b2fa8fd2d2e6dec5bb0feb4f4911d" +[[projects]] + name = "github.com/nlopes/slack" + packages = ["."] + revision = "c86337c0ef2486a15edd804355d9c73d2f2caed1" + version = "v0.1.0" + [[projects]] branch = "master" name = "github.com/nsf/termbox-go" @@ -249,7 +255,7 @@ [[projects]] branch = "master" name = "golang.org/x/net" - packages = ["context","idna","publicsuffix"] + packages = ["context","idna","publicsuffix","websocket"] revision = "0a9397675ba34b2845f758fe3cd68828369c6517" [[projects]] @@ -267,6 +273,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "36d700add80d36c56484ed310b9a7e622b3e308ab22eb42bdfb02fd8f5c90407" + inputs-digest = "a0cf09ec0aec110a8743e9c6085f35052dcb9f1c5c9174efd89f179f5df64dfc" solver-name = "gps-cdcl" solver-version = 1 diff --git a/README.ja.md b/README.ja.md index b15ade46..edc4481f 100644 --- a/README.ja.md +++ b/README.ja.md @@ -732,6 +732,7 @@ host = "172.31.4.82" ``` [slack] hookURL = "https://hooks.slack.com/services/abc123/defghijklmnopqrstuvwxyz" + #legacyToken = "xoxp-11111111111-222222222222-3333333333" channel = "#channel-name" #channel = "${servername}" iconEmoji = ":ghost:" @@ -739,10 +740,12 @@ host = "172.31.4.82" notifyUsers = ["@username"] ``` - - hookURL : Incoming webhook's URL + - hookURL : Incoming webhook's URL (legacyTokenが設定されている場合、hookURLは無視される。) + - legacyToken : slack legacy token (https://api.slack.com/custom-integrations/legacy-tokens) - channel : channel name. channelに`${servername}`を指定すると、結果レポートをサーバごとに別チャネルにすることが出来る。 以下のサンプルでは、`#server1`チャネルと`#server2`チャネルに送信される。スキャン前にチャネルを作成する必要がある。 + **legacyTokenが設定されている場合、channelは実在するchannelでなければならない。** ``` [slack] channel = "${servername}" diff --git a/README.md b/README.md index 721b7284..e84519ec 100644 --- a/README.md +++ b/README.md @@ -748,6 +748,7 @@ You can customize your configuration using this template. ``` [slack] hookURL = "https://hooks.slack.com/services/abc123/defghijklmnopqrstuvwxyz" + #legacyToken = "xoxp-11111111111-222222222222-3333333333" channel = "#channel-name" #channel = "${servername}" iconEmoji = ":ghost:" @@ -755,11 +756,13 @@ You can customize your configuration using this template. notifyUsers = ["@username"] ``` - - hookURL : Incoming webhook's URL - - channel : channel name. + - hookURL : Incoming webhook's URL (hookURL is ignored when legacyToken is set.) + - legacyToken : slack legacy token (https://api.slack.com/custom-integrations/legacy-tokens) + - channel : channel name. If you set `${servername}` to channel, the report will be sent to each channel. In the following example, the report will be sent to the `#server1` and `#server2`. Be sure to create these channels before scanning. + **if legacyToken is set, you must set up an existing channel** ``` [slack] channel = "${servername}" @@ -1383,6 +1386,17 @@ With this sample command, it will .. - Only Report CVEs that CVSS score is over 7 +``` +$ vuls report \ + -to-slack \ + -cvss-over=7 \ + -cvedb-path=$PWD/cve.sqlite3 +``` +With this sample command, it will .. +- Send scan results to slack +- Only Report CVEs that CVSS score is over 7 + + ## Example: Put results in S3 bucket To put results in S3 bucket, configure following settings in AWS before reporting. - Create S3 bucket. see [Creating a Bucket](http://docs.aws.amazon.com/AmazonS3/latest/UG/CreatingaBucket.html) diff --git a/config/config.go b/config/config.go index 15cf6e36..e8f9c7e8 100644 --- a/config/config.go +++ b/config/config.go @@ -379,10 +379,11 @@ func (c *SMTPConf) Validate() (errs []error) { // SlackConf is slack config type SlackConf struct { - HookURL string `valid:"url" json:"-"` - Channel string `json:"channel"` - IconEmoji string `json:"icon_emoji"` - AuthUser string `json:"username"` + 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"` diff --git a/report/slack.go b/report/slack.go index e3990eed..89a9d164 100644 --- a/report/slack.go +++ b/report/slack.go @@ -27,6 +27,7 @@ import ( "github.com/cenkalti/backoff" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" + "github.com/nlopes/slack" "github.com/parnurzeal/gorequest" log "github.com/sirupsen/logrus" ) @@ -36,31 +37,23 @@ type field struct { Value string `json:"value"` Short bool `json:"short"` } -type attachment struct { - Title string `json:"title"` - TitleLink string `json:"title_link"` - Fallback string `json:"fallback"` - Text string `json:"text"` - Pretext string `json:"pretext"` - Color string `json:"color"` - Fields []*field `json:"fields"` - MrkdwnIn []string `json:"mrkdwn_in"` - Footer string `json:"footer"` -} + type message struct { - Text string `json:"text"` - Username string `json:"username"` - IconEmoji string `json:"icon_emoji"` - Channel string `json:"channel"` - Attachments []*attachment `json:"attachments"` + Text string `json:"text"` + Username string `json:"username"` + IconEmoji string `json:"icon_emoji"` + Channel string `json:"channel"` + ThreadTimeStamp string `json:"thread_ts"` + Attachments []slack.Attachment `json:"attachments"` } // SlackWriter send report to slack type SlackWriter struct{} -func (w SlackWriter) Write(rs ...models.ScanResult) error { +func (w SlackWriter) Write(rs ...models.ScanResult) (err error) { conf := config.Conf.Slack channel := conf.Channel + token := conf.LegacyToken for _, r := range rs { if channel == "${servername}" { @@ -78,7 +71,7 @@ func (w SlackWriter) Write(rs ...models.ScanResult) error { IconEmoji: conf.IconEmoji, Channel: channel, } - if err := send(msg); err != nil { + if err = send(msg); err != nil { return err } continue @@ -88,7 +81,7 @@ func (w SlackWriter) Write(rs ...models.ScanResult) error { // Split into chunks with 100 elements // https://api.slack.com/methods/chat.postMessage maxAttachments := 100 - m := map[int][]*attachment{} + m := map[int][]slack.Attachment{} for i, a := range toSlackAttachments(r) { m[i/maxAttachments] = append(m[i/maxAttachments], a) } @@ -98,21 +91,49 @@ func (w SlackWriter) Write(rs ...models.ScanResult) error { } sort.Ints(chunkKeys) - for i, k := range chunkKeys { - txt := "" - if i == 0 { - txt = msgText(r) + // Send slack by API + if 0 < len(token) { + api := slack.New(token) + ParentMsg := slack.PostMessageParameters{ + // Text: msgText(r), + Username: conf.AuthUser, + IconEmoji: conf.IconEmoji, } - msg := message{ - Text: txt, - Username: conf.AuthUser, - IconEmoji: conf.IconEmoji, - Channel: channel, - Attachments: m[k], - } - if err := send(msg); err != nil { + + var ts string + if _, ts, err = api.PostMessage(channel, msgText(r), ParentMsg); err != nil { return err } + + for _, k := range chunkKeys { + params := slack.PostMessageParameters{ + // Text: msgText(r), + Username: conf.AuthUser, + IconEmoji: conf.IconEmoji, + Attachments: m[k], + ThreadTimestamp: ts, + } + if _, _, err = api.PostMessage(channel, msgText(r), params); err != nil { + return err + } + } + } else { + for i, k := range chunkKeys { + txt := "" + if i == 0 { + txt = msgText(r) + } + msg := message{ + Text: txt, + Username: conf.AuthUser, + IconEmoji: conf.IconEmoji, + Channel: channel, + Attachments: m[k], + } + if err = send(msg); err != nil { + return err + } + } } } return nil @@ -164,7 +185,7 @@ func msgText(r models.ScanResult) string { r.ScannedCves.FormatCveSummary()) } -func toSlackAttachments(r models.ScanResult) (attaches []*attachment) { +func toSlackAttachments(r models.ScanResult) (attaches []slack.Attachment) { vinfos := r.ScannedCves.ToSortedSlice() for _, vinfo := range vinfos { curent := []string{} @@ -196,12 +217,12 @@ func toSlackAttachments(r models.ScanResult) (attaches []*attachment) { new = append(new, "?") } - a := attachment{ - Title: vinfo.CveID, - TitleLink: "https://nvd.nist.gov/vuln/detail/" + vinfo.CveID, - Text: attachmentText(vinfo, r.Family), - MrkdwnIn: []string{"text", "pretext"}, - Fields: []*field{ + a := slack.Attachment{ + Title: vinfo.CveID, + TitleLink: "https://nvd.nist.gov/vuln/detail/" + vinfo.CveID, + Text: attachmentText(vinfo, r.Family), + MarkdownIn: []string{"text", "pretext"}, + Fields: []slack.AttachmentField{ { // Title: "Current Package/CPE", Title: "Installed", @@ -216,7 +237,7 @@ func toSlackAttachments(r models.ScanResult) (attaches []*attachment) { }, Color: color(vinfo.MaxCvssScore().Value.Score), } - attaches = append(attaches, &a) + attaches = append(attaches, a) } return }