/* 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 report import ( "encoding/json" "fmt" "sort" "strings" "time" log "github.com/Sirupsen/logrus" "github.com/cenkalti/backoff" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" "github.com/parnurzeal/gorequest" ) type field struct { Title string `json:"title"` 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"` } type message struct { Text string `json:"text"` Username string `json:"username"` IconEmoji string `json:"icon_emoji"` Channel string `json:"channel"` Attachments []*attachment `json:"attachments"` } // SlackWriter send report to slack type SlackWriter struct{} func (w SlackWriter) Write(rs ...models.ScanResult) error { conf := config.Conf.Slack channel := conf.Channel for _, r := range rs { if channel == "${servername}" { channel = fmt.Sprintf("#%s", r.ServerName) } if 0 < len(r.Errors) { serverInfo := fmt.Sprintf("*%s*", r.ServerInfo()) notifyUsers := getNotifyUsers(config.Conf.Slack.NotifyUsers) txt := fmt.Sprintf("%s\n%s\nError: %s", notifyUsers, serverInfo, r.Errors) msg := message{ Text: txt, Username: conf.AuthUser, IconEmoji: conf.IconEmoji, Channel: channel, } if err := send(msg); err != nil { return err } continue } // A maximum of 100 attachments are allowed on a message. // Split into chunks with 100 elements // https://api.slack.com/methods/chat.postMessage maxAttachments := 100 m := map[int][]*attachment{} for i, a := range toSlackAttachments(r) { m[i/maxAttachments] = append(m[i/maxAttachments], a) } chunkKeys := []int{} for k := range m { chunkKeys = append(chunkKeys, k) } sort.Ints(chunkKeys) 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 } func send(msg message) error { conf := config.Conf.Slack count, retryMax := 0, 10 bytes, _ := json.Marshal(msg) jsonBody := string(bytes) f := func() (err error) { resp, body, errs := gorequest.New().Proxy(config.Conf.HTTPProxy).Post(conf.HookURL).Send(string(jsonBody)).End() if 0 < len(errs) || resp == nil || resp.StatusCode != 200 { count++ if count == retryMax { return nil } return fmt.Errorf( "HTTP POST error: %v, url: %s, resp: %v, body: %s", errs, conf.HookURL, resp, body) } return nil } notify := func(err error, t time.Duration) { log.Warnf("Error %s", err) log.Warn("Retrying in ", t) } boff := backoff.NewExponentialBackOff() if err := backoff.RetryNotify(f, boff, notify); err != nil { return fmt.Errorf("HTTP error: %s", err) } if count == retryMax { return fmt.Errorf("Retry count exceeded") } return nil } func msgText(r models.ScanResult) string { notifyUsers := "" if 0 < len(r.KnownCves) || 0 < len(r.UnknownCves) { notifyUsers = getNotifyUsers(config.Conf.Slack.NotifyUsers) } serverInfo := fmt.Sprintf("*%s*", r.ServerInfo()) return fmt.Sprintf("%s\n%s\n>%s", notifyUsers, serverInfo, r.CveSummary()) } func toSlackAttachments(scanResult models.ScanResult) (attaches []*attachment) { cves := scanResult.KnownCves if !config.Conf.IgnoreUnscoredCves { cves = append(cves, scanResult.UnknownCves...) } for _, cveInfo := range cves { cveID := cveInfo.VulnInfo.CveID curentPackages := []string{} for _, p := range cveInfo.Packages { curentPackages = append(curentPackages, p.FormatCurrentVer()) } for _, n := range cveInfo.CpeNames { curentPackages = append(curentPackages, n) } newPackages := []string{} for _, p := range cveInfo.Packages { newPackages = append(newPackages, p.FormatNewVer()) } a := attachment{ Title: cveID, TitleLink: fmt.Sprintf("%s/%s", nvdBaseURL, cveID), Text: attachmentText(cveInfo, scanResult.Family), MrkdwnIn: []string{"text", "pretext"}, Fields: []*field{ { // Title: "Current Package/CPE", Title: "Installed", Value: strings.Join(curentPackages, "\n"), Short: true, }, { Title: "Candidate", Value: strings.Join(newPackages, "\n"), Short: true, }, }, Color: color(cveInfo.CvssV2Score()), } attaches = append(attaches, &a) } return } // https://api.slack.com/docs/attachments func color(cvssScore float64) string { switch { case 7 <= cvssScore: return "danger" case 4 <= cvssScore && cvssScore < 7: return "warning" case cvssScore < 0: return "#C0C0C0" default: return "good" } } func attachmentText(cveInfo models.CveInfo, osFamily string) string { // linkText := links(cveInfo, osFamily) //TODO return "" // switch { // case config.Conf.Lang == "ja" && // 0 < cveInfo.CveDetail.Jvn.CvssScore(): // jvn := cveInfo.CveDetail.Jvn // return fmt.Sprintf("*%4.1f (%s)* <%s|%s>\n%s\n%s\n*Confidence:* %v", // cveInfo.CveDetail.CvssScore(config.Conf.Lang), // jvn.CvssSeverity(), // fmt.Sprintf(cvssV2CalcBaseURL, cveInfo.CveDetail.CveID), // jvn.CvssVector(), // jvn.CveTitle(), // linkText, // cveInfo.VulnInfo.Confidence, // ) // case 0 < cveInfo.CveDetail.CvssScore("en"): // nvd := cveInfo.CveDetail.Nvd // return fmt.Sprintf("*%4.1f (%s)* <%s|%s>\n%s\n%s\n*Confidence:* %v", // cveInfo.CveDetail.CvssScore(config.Conf.Lang), // nvd.CvssSeverity(), // fmt.Sprintf(cvssV2CalcBaseURL, cveInfo.CveDetail.CveID), // nvd.CvssVector(), // nvd.CveSummary(), // linkText, // cveInfo.VulnInfo.Confidence, // ) // default: // nvd := cveInfo.CveDetail.Nvd // return fmt.Sprintf("?\n%s\n%s\n*Confidence:* %v", // nvd.CveSummary(), linkText, cveInfo.VulnInfo.Confidence) // } } func links(cveInfo models.CveInfo, osFamily string) string { links := []string{} //TODO // cweID := cveInfo.CveDetail.CweID() // if 0 < len(cweID) { // links = append(links, fmt.Sprintf("<%s|%s>", // cweURL(cweID), cweID)) // if config.Conf.Lang == "ja" { // links = append(links, fmt.Sprintf("<%s|%s(JVN)>", // cweJvnURL(cweID), cweID)) // } // } cveID := cveInfo.VulnInfo.CveID //TODO // if config.Conf.Lang == "ja" && 0 < len(cveInfo.CveDetail.Jvn.Link()) { // jvn := fmt.Sprintf("<%s|JVN>", cveInfo.CveDetail.Jvn.Link()) // links = append(links, jvn) // } dlinks := distroLinks(cveInfo, osFamily) for _, link := range dlinks { links = append(links, fmt.Sprintf("<%s|%s>", link.url, link.title)) } links = append(links, fmt.Sprintf("<%s|MITRE>", fmt.Sprintf("%s%s", mitreBaseURL, cveID))) links = append(links, fmt.Sprintf("<%s|CVEDetails>", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID))) return strings.Join(links, " / ") } // See testcase func getNotifyUsers(notifyUsers []string) string { slackStyleTexts := []string{} for _, username := range notifyUsers { slackStyleTexts = append(slackStyleTexts, fmt.Sprintf("<%s>", username)) } return strings.Join(slackStyleTexts, " ") }