Completed
Pull Request — master (#805)
by kota
05:47
created

report/slack.go   D

Size/Duplication

Total Lines 343
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
cc 58
eloc 238
dl 0
loc 343
rs 4.5599
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
C report.send 0 32 9
B report.cvssColor 0 10 6
A report.getNotifyUsers 0 6 2
F report.SlackWriter.Write 0 92 16
C report.toSlackAttachments 0 66 10
A report.cweIDs 0 13 3
D report.attachmentText 0 73 12
1
/* Vuls - Vulnerability Scanner
2
Copyright (C) 2016  Future Corporation , Japan.
3
4
This program is free software: you can redistribute it and/or modify
5
it under the terms of the GNU General Public License as published by
6
the Free Software Foundation, either version 3 of the License, or
7
(at your option) any later version.
8
9
This program is distributed in the hope that it will be useful,
10
but WITHOUT ANY WARRANTY; without even the implied warranty of
11
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
GNU General Public License for more details.
13
14
You should have received a copy of the GNU General Public License
15
along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
*/
17
18
package report
19
20
import (
21
	"encoding/json"
22
	"fmt"
23
	"sort"
24
	"strings"
25
	"time"
26
27
	"github.com/cenkalti/backoff"
28
	"github.com/future-architect/vuls/config"
29
	"github.com/future-architect/vuls/models"
30
	"github.com/nlopes/slack"
31
	"github.com/parnurzeal/gorequest"
32
	log "github.com/sirupsen/logrus"
33
	"golang.org/x/xerrors"
34
)
35
36
type field struct {
37
	Title string `json:"title"`
38
	Value string `json:"value"`
39
	Short bool   `json:"short"`
40
}
41
42
type message struct {
43
	Text        string             `json:"text"`
44
	Username    string             `json:"username"`
45
	IconEmoji   string             `json:"icon_emoji"`
46
	Channel     string             `json:"channel"`
47
	Attachments []slack.Attachment `json:"attachments"`
48
}
49
50
// SlackWriter send report to slack
51
type SlackWriter struct{}
52
53
func (w SlackWriter) Write(rs ...models.ScanResult) (err error) {
54
	conf := config.Conf.Slack
55
	channel := conf.Channel
56
	token := conf.LegacyToken
57
58
	for _, r := range rs {
59
		if channel == "${servername}" {
60
			channel = fmt.Sprintf("#%s", r.ServerName)
61
		}
62
63
		// A maximum of 100 attachments are allowed on a message.
64
		// Split into chunks with 100 elements
65
		// https://api.slack.com/methods/chat.postMessage
66
		maxAttachments := 100
67
		m := map[int][]slack.Attachment{}
68
		for i, a := range toSlackAttachments(r) {
69
			m[i/maxAttachments] = append(m[i/maxAttachments], a)
70
		}
71
		chunkKeys := []int{}
72
		for k := range m {
73
			chunkKeys = append(chunkKeys, k)
74
		}
75
		sort.Ints(chunkKeys)
76
77
		summary := fmt.Sprintf("%s\n%s",
78
			getNotifyUsers(config.Conf.Slack.NotifyUsers),
79
			formatOneLineSummary(r))
80
81
		// Send slack by API
82
		if 0 < len(token) {
83
			api := slack.New(token)
84
			msgPrms := slack.PostMessageParameters{
85
				Username:  conf.AuthUser,
86
				IconEmoji: conf.IconEmoji,
87
			}
88
89
			var ts string
90
			if _, ts, err = api.PostMessage(channel,
91
				summary, msgPrms); err != nil {
92
				return err
93
			}
94
95
			if config.Conf.FormatOneLineText || 0 < len(r.Errors) {
96
				continue
97
			}
98
99
			for _, k := range chunkKeys {
100
				params := slack.PostMessageParameters{
101
					Username:        conf.AuthUser,
102
					IconEmoji:       conf.IconEmoji,
103
					Attachments:     m[k],
104
					ThreadTimestamp: ts,
105
				}
106
				if _, _, err = api.PostMessage(channel, "", params); err != nil {
107
					return err
108
				}
109
			}
110
		} else {
111
			msg := message{
112
				Text:      summary,
113
				Username:  conf.AuthUser,
114
				IconEmoji: conf.IconEmoji,
115
				Channel:   channel,
116
			}
117
			if err := send(msg); err != nil {
118
				return err
119
			}
120
121
			if config.Conf.FormatOneLineText || 0 < len(r.Errors) {
122
				continue
123
			}
124
125
			for _, k := range chunkKeys {
126
				txt := fmt.Sprintf("%d/%d for %s",
127
					k+1,
128
					len(chunkKeys),
129
					r.FormatServerName())
130
131
				msg := message{
132
					Text:        txt,
133
					Username:    conf.AuthUser,
134
					IconEmoji:   conf.IconEmoji,
135
					Channel:     channel,
136
					Attachments: m[k],
137
				}
138
				if err = send(msg); err != nil {
139
					return err
140
				}
141
			}
142
		}
143
	}
144
	return nil
145
}
146
147
func send(msg message) error {
148
	conf := config.Conf.Slack
149
	count, retryMax := 0, 10
150
151
	bytes, _ := json.Marshal(msg)
152
	jsonBody := string(bytes)
153
154
	f := func() (err error) {
155
		resp, body, errs := gorequest.New().Proxy(config.Conf.HTTPProxy).Post(conf.HookURL).Send(string(jsonBody)).End()
156
		if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
157
			count++
158
			if count == retryMax {
159
				return nil
160
			}
161
			return xerrors.Errorf(
0 ignored issues
show
introduced by
unrecognized printf verb 'w'
Loading history...
162
				"HTTP POST error. url: %s, resp: %v, body: %s, err: %w",
163
				conf.HookURL, resp, body, errs)
164
		}
165
		return nil
166
	}
167
	notify := func(err error, t time.Duration) {
168
		log.Warnf("Error %s", err)
169
		log.Warn("Retrying in ", t)
170
	}
171
	boff := backoff.NewExponentialBackOff()
172
	if err := backoff.RetryNotify(f, boff, notify); err != nil {
173
		return xerrors.Errorf("HTTP error: %w", err)
0 ignored issues
show
introduced by
unrecognized printf verb 'w'
Loading history...
174
	}
175
	if count == retryMax {
176
		return xerrors.New("Retry count exceeded")
177
	}
178
	return nil
179
}
180
181
func toSlackAttachments(r models.ScanResult) (attaches []slack.Attachment) {
182
	vinfos := r.ScannedCves.ToSortedSlice()
183
	for _, vinfo := range vinfos {
184
185
		installed, candidate := []string{}, []string{}
186
		for _, affected := range vinfo.AffectedPackages {
187
			if p, ok := r.Packages[affected.Name]; ok {
188
				installed = append(installed,
189
					fmt.Sprintf("%s-%s", p.Name, p.FormatVer()))
190
			} else {
191
				installed = append(installed, affected.Name)
192
			}
193
194
			if p, ok := r.Packages[affected.Name]; ok {
195
				if affected.NotFixedYet {
196
					candidate = append(candidate, "Not Fixed Yet")
197
				} else {
198
					candidate = append(candidate, p.FormatNewVer())
199
				}
200
			} else {
201
				candidate = append(candidate, "?")
202
			}
203
		}
204
205
		for _, n := range vinfo.CpeURIs {
206
			installed = append(installed, n)
207
			candidate = append(candidate, "?")
208
		}
209
		for _, n := range vinfo.GitHubSecurityAlerts {
210
			installed = append(installed, n.PackageName)
211
			candidate = append(candidate, "?")
212
		}
213
214
		for _, wp := range vinfo.WpPackageFixStats {
215
			if p, ok := r.WordPressPackages.Find(wp.Name); ok {
216
				installed = append(installed, fmt.Sprintf("%s-%s", wp.Name, p.Version))
217
				candidate = append(candidate, wp.FixedIn)
218
			} else {
219
				installed = append(installed, wp.Name)
220
				candidate = append(candidate, "?")
221
			}
222
		}
223
224
		a := slack.Attachment{
225
			Title:      vinfo.CveID,
226
			TitleLink:  "https://nvd.nist.gov/vuln/detail/" + vinfo.CveID,
227
			Text:       attachmentText(vinfo, r.Family, r.CweDict, r.Packages),
228
			MarkdownIn: []string{"text", "pretext"},
229
			Fields: []slack.AttachmentField{
230
				{
231
					// Title: "Current Package/CPE",
232
					Title: "Installed",
233
					Value: strings.Join(installed, "\n"),
234
					Short: true,
235
				},
236
				{
237
					Title: "Candidate",
238
					Value: strings.Join(candidate, "\n"),
239
					Short: true,
240
				},
241
			},
242
			Color: cvssColor(vinfo.MaxCvssScore().Value.Score),
243
		}
244
		attaches = append(attaches, a)
245
	}
246
	return
247
}
248
249
// https://api.slack.com/docs/attachments
250
func cvssColor(cvssScore float64) string {
251
	switch {
252
	case 7 <= cvssScore:
253
		return "danger"
254
	case 4 <= cvssScore && cvssScore < 7:
255
		return "warning"
256
	case cvssScore == 0:
257
		return "#C0C0C0"
258
	default:
259
		return "good"
260
	}
261
}
262
263
func attachmentText(vinfo models.VulnInfo, osFamily string, cweDict map[string]models.CweDictEntry, packs models.Packages) string {
264
	maxCvss := vinfo.MaxCvssScore()
265
	vectors := []string{}
266
267
	scores := append(vinfo.Cvss3Scores(), vinfo.Cvss2Scores(osFamily)...)
268
	for _, cvss := range scores {
269
		if cvss.Value.Severity == "" {
270
			continue
271
		}
272
		calcURL := ""
273
		switch cvss.Value.Type {
274
		case models.CVSS2:
275
			calcURL = fmt.Sprintf(
276
				"https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?name=%s",
277
				vinfo.CveID)
278
		case models.CVSS3:
279
			calcURL = fmt.Sprintf(
280
				"https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?name=%s",
281
				vinfo.CveID)
282
		}
283
284
		if cont, ok := vinfo.CveContents[cvss.Type]; ok {
285
			v := fmt.Sprintf("<%s|%s> %s (<%s|%s>)",
286
				calcURL,
287
				fmt.Sprintf("%3.1f/%s", cvss.Value.Score, cvss.Value.Vector),
288
				cvss.Value.Severity,
289
				cont.SourceLink,
290
				cvss.Type)
291
			vectors = append(vectors, v)
292
293
		} else {
294
			if 0 < len(vinfo.DistroAdvisories) {
295
				links := []string{}
296
				for k, v := range vinfo.VendorLinks(osFamily) {
297
					links = append(links, fmt.Sprintf("<%s|%s>",
298
						v, k))
299
				}
300
301
				v := fmt.Sprintf("<%s|%s> %s (%s)",
302
					calcURL,
303
					fmt.Sprintf("%3.1f/%s", cvss.Value.Score, cvss.Value.Vector),
304
					cvss.Value.Severity,
305
					strings.Join(links, ", "))
306
				vectors = append(vectors, v)
307
			}
308
		}
309
	}
310
311
	severity := strings.ToUpper(maxCvss.Value.Severity)
312
	if severity == "" {
313
		severity = "?"
314
	}
315
316
	nwvec := vinfo.AttackVector()
317
	if nwvec == "Network" || nwvec == "remote" {
318
		nwvec = fmt.Sprintf("*%s*", nwvec)
319
	}
320
321
	mitigation := ""
322
	if vinfo.Mitigations(osFamily)[0].Type != models.Unknown {
323
		mitigation = fmt.Sprintf("\nMitigation:\n```%s```\n",
324
			vinfo.Mitigations(osFamily)[0].Value)
325
	}
326
327
	return fmt.Sprintf("*%4.1f (%s)* %s %s\n%s\n```\n%s\n```%s\n%s\n",
328
		maxCvss.Value.Score,
329
		severity,
330
		nwvec,
331
		vinfo.PatchStatus(packs),
332
		strings.Join(vectors, "\n"),
333
		vinfo.Summaries(config.Conf.Lang, osFamily)[0].Value,
334
		mitigation,
335
		cweIDs(vinfo, osFamily, cweDict),
336
	)
337
}
338
339
func cweIDs(vinfo models.VulnInfo, osFamily string, cweDict models.CweDict) string {
340
	links := []string{}
341
	for _, c := range vinfo.CveContents.UniqCweIDs(osFamily) {
342
		name, url, top10Rank, top10URL := cweDict.Get(c.Value, osFamily)
343
		line := ""
344
		if top10Rank != "" {
345
			line = fmt.Sprintf("<%s|[OWASP Top %s]>",
346
				top10URL, top10Rank)
347
		}
348
		links = append(links, fmt.Sprintf("%s <%s|%s>: %s",
349
			line, url, c.Value, name))
350
	}
351
	return strings.Join(links, "\n")
352
}
353
354
// See testcase
355
func getNotifyUsers(notifyUsers []string) string {
356
	slackStyleTexts := []string{}
357
	for _, username := range notifyUsers {
358
		slackStyleTexts = append(slackStyleTexts, fmt.Sprintf("<%s>", username))
359
	}
360
	return strings.Join(slackStyleTexts, " ")
361
}
362