report.msgText   A
last analyzed

Complexity

Conditions 3

Size

Total Lines 22
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 19
dl 0
loc 22
rs 9.45
c 0
b 0
f 0
nop 1
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
		// Send slack by API
78
		if 0 < len(token) {
79
			api := slack.New(token)
80
			ParentMsg := slack.PostMessageParameters{
81
				//			Text:      msgText(r),
82
				Username:  conf.AuthUser,
83
				IconEmoji: conf.IconEmoji,
84
			}
85
86
			if config.Conf.FormatOneLineText {
87
				if _, _, err = api.PostMessage(channel, formatOneLineSummary(r), ParentMsg); err != nil {
88
					return err
89
				}
90
				continue
91
			}
92
93
			var ts string
94
			if _, ts, err = api.PostMessage(channel, msgText(r), ParentMsg); err != nil {
95
				return err
96
			}
97
98
			for _, k := range chunkKeys {
99
				params := slack.PostMessageParameters{
100
					//		Text:            msgText(r),
101
					Username:        conf.AuthUser,
102
					IconEmoji:       conf.IconEmoji,
103
					Attachments:     m[k],
104
					ThreadTimestamp: ts,
105
				}
106
				if _, _, err = api.PostMessage(channel, msgText(r), params); err != nil {
107
					return err
108
				}
109
			}
110
		} else {
111
			if config.Conf.FormatOneLineText {
112
				msg := message{
113
					Text:      formatOneLineSummary(r),
114
					Username:  conf.AuthUser,
115
					IconEmoji: conf.IconEmoji,
116
					Channel:   channel,
117
				}
118
				if err := send(msg); err != nil {
119
					return err
120
				}
121
				continue
122
			}
123
124
			for i, k := range chunkKeys {
125
				txt := ""
126
				if i == 0 {
127
					txt = msgText(r)
128
				}
129
				msg := message{
130
					Text:        txt,
131
					Username:    conf.AuthUser,
132
					IconEmoji:   conf.IconEmoji,
133
					Channel:     channel,
134
					Attachments: m[k],
135
				}
136
				if err = send(msg); err != nil {
137
					return err
138
				}
139
			}
140
		}
141
	}
142
	return nil
143
}
144
145
func send(msg message) error {
146
	conf := config.Conf.Slack
147
	count, retryMax := 0, 10
148
149
	bytes, _ := json.Marshal(msg)
150
	jsonBody := string(bytes)
151
152
	f := func() (err error) {
153
		resp, body, errs := gorequest.New().Proxy(config.Conf.HTTPProxy).Post(conf.HookURL).Send(string(jsonBody)).End()
154
		if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
155
			count++
156
			if count == retryMax {
157
				return nil
158
			}
159
			return xerrors.Errorf(
0 ignored issues
show
introduced by
unrecognized printf verb 'w'
Loading history...
160
				"HTTP POST error. url: %s, resp: %v, body: %s, err: %w",
161
				conf.HookURL, resp, body, errs)
162
		}
163
		return nil
164
	}
165
	notify := func(err error, t time.Duration) {
166
		log.Warnf("Error %s", err)
167
		log.Warn("Retrying in ", t)
168
	}
169
	boff := backoff.NewExponentialBackOff()
170
	if err := backoff.RetryNotify(f, boff, notify); err != nil {
171
		return xerrors.Errorf("HTTP error: %w", err)
0 ignored issues
show
introduced by
unrecognized printf verb 'w'
Loading history...
172
	}
173
	if count == retryMax {
174
		return xerrors.New("Retry count exceeded")
175
	}
176
	return nil
177
}
178
179
func msgText(r models.ScanResult) string {
180
	notifyUsers := ""
181
	if 0 < len(r.ScannedCves) {
182
		notifyUsers = getNotifyUsers(config.Conf.Slack.NotifyUsers)
183
	}
184
	serverInfo := fmt.Sprintf("*%s*", r.ServerInfo())
185
186
	if 0 < len(r.Errors) {
187
		return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\nError: %s",
188
			notifyUsers,
189
			serverInfo,
190
			r.ScannedCves.FormatCveSummary(),
191
			r.ScannedCves.FormatFixedStatus(r.Packages),
192
			r.FormatUpdatablePacksSummary(),
193
			r.Errors)
194
	}
195
	return fmt.Sprintf("%s\n%s\n%s\n%s\n%s",
196
		notifyUsers,
197
		serverInfo,
198
		r.ScannedCves.FormatCveSummary(),
199
		r.ScannedCves.FormatFixedStatus(r.Packages),
200
		r.FormatUpdatablePacksSummary())
201
}
202
203
func toSlackAttachments(r models.ScanResult) (attaches []slack.Attachment) {
204
	vinfos := r.ScannedCves.ToSortedSlice()
205
	for _, vinfo := range vinfos {
206
207
		installed, candidate := []string{}, []string{}
208
		for _, affected := range vinfo.AffectedPackages {
209
			if p, ok := r.Packages[affected.Name]; ok {
210
				installed = append(installed,
211
					fmt.Sprintf("%s-%s", p.Name, p.FormatVer()))
212
			} else {
213
				installed = append(installed, affected.Name)
214
			}
215
216
			if p, ok := r.Packages[affected.Name]; ok {
217
				if affected.NotFixedYet {
218
					candidate = append(candidate, "Not Fixed Yet")
219
				} else {
220
					candidate = append(candidate, p.FormatNewVer())
221
				}
222
			} else {
223
				candidate = append(candidate, "?")
224
			}
225
		}
226
227
		for _, n := range vinfo.CpeURIs {
228
			installed = append(installed, n)
229
			candidate = append(candidate, "?")
230
		}
231
		for _, n := range vinfo.GitHubSecurityAlerts {
232
			installed = append(installed, n.PackageName)
233
			candidate = append(candidate, "?")
234
		}
235
236
		for _, wp := range vinfo.WpPackageFixStats {
237
			if p, ok := r.WordPressPackages.Find(wp.Name); ok {
238
				installed = append(installed, fmt.Sprintf("%s-%s", wp.Name, p.Version))
239
				candidate = append(candidate, wp.FixedIn)
240
			} else {
241
				installed = append(installed, wp.Name)
242
				candidate = append(candidate, "?")
243
			}
244
		}
245
246
		a := slack.Attachment{
247
			Title:      vinfo.CveID,
248
			TitleLink:  "https://nvd.nist.gov/vuln/detail/" + vinfo.CveID,
249
			Text:       attachmentText(vinfo, r.Family, r.CweDict, r.Packages),
250
			MarkdownIn: []string{"text", "pretext"},
251
			Fields: []slack.AttachmentField{
252
				{
253
					// Title: "Current Package/CPE",
254
					Title: "Installed",
255
					Value: strings.Join(installed, "\n"),
256
					Short: true,
257
				},
258
				{
259
					Title: "Candidate",
260
					Value: strings.Join(candidate, "\n"),
261
					Short: true,
262
				},
263
			},
264
			Color: cvssColor(vinfo.MaxCvssScore().Value.Score),
265
		}
266
		attaches = append(attaches, a)
267
	}
268
	return
269
}
270
271
// https://api.slack.com/docs/attachments
272
func cvssColor(cvssScore float64) string {
273
	switch {
274
	case 7 <= cvssScore:
275
		return "danger"
276
	case 4 <= cvssScore && cvssScore < 7:
277
		return "warning"
278
	case cvssScore == 0:
279
		return "#C0C0C0"
280
	default:
281
		return "good"
282
	}
283
}
284
285
func attachmentText(vinfo models.VulnInfo, osFamily string, cweDict map[string]models.CweDictEntry, packs models.Packages) string {
286
	maxCvss := vinfo.MaxCvssScore()
287
	vectors := []string{}
288
289
	scores := append(vinfo.Cvss3Scores(), vinfo.Cvss2Scores(osFamily)...)
290
	for _, cvss := range scores {
291
		if cvss.Value.Severity == "" {
292
			continue
293
		}
294
		calcURL := ""
295
		switch cvss.Value.Type {
296
		case models.CVSS2:
297
			calcURL = fmt.Sprintf(
298
				"https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?name=%s",
299
				vinfo.CveID)
300
		case models.CVSS3:
301
			calcURL = fmt.Sprintf(
302
				"https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?name=%s",
303
				vinfo.CveID)
304
		}
305
306
		if cont, ok := vinfo.CveContents[cvss.Type]; ok {
307
			v := fmt.Sprintf("<%s|%s> %s (<%s|%s>)",
308
				calcURL,
309
				fmt.Sprintf("%3.1f/%s", cvss.Value.Score, cvss.Value.Vector),
310
				cvss.Value.Severity,
311
				cont.SourceLink,
312
				cvss.Type)
313
			vectors = append(vectors, v)
314
315
		} else {
316
			if 0 < len(vinfo.DistroAdvisories) {
317
				links := []string{}
318
				for k, v := range vinfo.VendorLinks(osFamily) {
319
					links = append(links, fmt.Sprintf("<%s|%s>",
320
						v, k))
321
				}
322
323
				v := fmt.Sprintf("<%s|%s> %s (%s)",
324
					calcURL,
325
					fmt.Sprintf("%3.1f/%s", cvss.Value.Score, cvss.Value.Vector),
326
					cvss.Value.Severity,
327
					strings.Join(links, ", "))
328
				vectors = append(vectors, v)
329
			}
330
		}
331
	}
332
333
	severity := strings.ToUpper(maxCvss.Value.Severity)
334
	if severity == "" {
335
		severity = "?"
336
	}
337
338
	nwvec := vinfo.AttackVector()
339
	if nwvec == "Network" || nwvec == "remote" {
340
		nwvec = fmt.Sprintf("*%s*", nwvec)
341
	}
342
343
	mitigation := ""
344
	if vinfo.Mitigations(osFamily)[0].Type != models.Unknown {
345
		mitigation = fmt.Sprintf("\nMitigation:\n```%s```\n",
346
			vinfo.Mitigations(osFamily)[0].Value)
347
	}
348
349
	return fmt.Sprintf("*%4.1f (%s)* %s %s\n%s\n```\n%s\n```%s\n%s\n",
350
		maxCvss.Value.Score,
351
		severity,
352
		nwvec,
353
		vinfo.PatchStatus(packs),
354
		strings.Join(vectors, "\n"),
355
		vinfo.Summaries(config.Conf.Lang, osFamily)[0].Value,
356
		mitigation,
357
		cweIDs(vinfo, osFamily, cweDict),
358
	)
359
}
360
361
func cweIDs(vinfo models.VulnInfo, osFamily string, cweDict models.CweDict) string {
362
	links := []string{}
363
	for _, c := range vinfo.CveContents.UniqCweIDs(osFamily) {
364
		name, url, top10Rank, top10URL := cweDict.Get(c.Value, osFamily)
365
		line := ""
366
		if top10Rank != "" {
367
			line = fmt.Sprintf("<%s|[OWASP Top %s]>",
368
				top10URL, top10Rank)
369
		}
370
		links = append(links, fmt.Sprintf("%s <%s|%s>: %s",
371
			line, url, c.Value, name))
372
	}
373
	return strings.Join(links, "\n")
374
}
375
376
// See testcase
377
func getNotifyUsers(notifyUsers []string) string {
378
	slackStyleTexts := []string{}
379
	for _, username := range notifyUsers {
380
		slackStyleTexts = append(slackStyleTexts, fmt.Sprintf("<%s>", username))
381
	}
382
	return strings.Join(slackStyleTexts, " ")
383
}
384