Skip to content

Commit d9614d2

Browse files
committed
Slack works like Discord with threads and details
1 parent 0feab9d commit d9614d2

File tree

20 files changed

+1685
-328
lines changed

20 files changed

+1685
-328
lines changed

components/reporters/slack/README.md

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# slack
22

33
This component implements a [reporter](https://github.com/smithy-security/smithy/blob/main/sdk/component/component.go)
4-
that sends a summary of results to slack.
4+
that sends a summary of results to slack and optionally creates detailed vulnerability threads.
55

66
## Environment variables
77

@@ -13,4 +13,55 @@ as well as the following:
1313

1414
| Environment Variable | Type | Required | Default | Description |
1515
|----------------------------|--------|----------|---------|-------------------------------------------------------------------------|
16-
| SLACK\_WEBHOOK | string | yes | - | The slack webhook to POST results to|
16+
| SLACK\_TOKEN | string | no | - | The slack bot token (required for thread creation mode)|
17+
| SLACK\_CHANNEL | string | no | - | The slack channel ID (required for thread creation mode)|
18+
| SLACK\_DEBUG | bool | no | false | Whether to enable debug logging for the slack client|
19+
20+
## Operation
21+
22+
* Uses `SLACK_TOKEN` and `SLACK_CHANNEL` for Web API access
23+
* Creates threads with detailed vulnerability information
24+
* Requires bot setup with appropriate permissions -- read on for details
25+
* Sends both summary and detailed findings
26+
27+
## Bot Setup for Thread Creation
28+
29+
To use thread creation mode, you need to:
30+
31+
1. Create a Slack app in your workspace
32+
2. Add the following bot token scopes:
33+
* `chat:write` - Send messages to channels
34+
* `channels:read` - Read channel information
35+
3. Install the app to your workspace
36+
4. Invite the bot to the target channel
37+
5. Use the bot token as `SLACK_TOKEN`
38+
6. Use the channel ID as `SLACK_CHANNEL`
39+
40+
## Example Configuration
41+
42+
```yaml
43+
parameters:
44+
- name: "slack_token"
45+
type: "string"
46+
value: "xoxb-your-bot-token"
47+
- name: "slack_channel"
48+
type: "string"
49+
value: "C1234567890"
50+
- name: "create_threads"
51+
type: "bool"
52+
value: "true"
53+
```
54+
55+
## FAQ
56+
57+
* Why do I need a bot token?
58+
* The bot token is required for thread creation and sending messages to channels. It allows the app to interact with Slack's Web API.
59+
* Why do I need a channel ID?
60+
* The channel ID is required to specify which channel the bot will send messages to. It ensures that the messages are delivered to the correct location.
61+
* You can find the channel ID by right-clicking on the channel name in Slack and selecting "Copy Link". The ID is the part after `/archives/` in the URL.
62+
* If you are using the Slack APP, the channel ID is located at the very bottom in the channel details pane.
63+
* Help, I created a token but it doesn't work!
64+
* Make sure you have invited the bot to the channel you want to post in. The bot needs to be a member of the channel to send messages.
65+
* Ensure that the bot has the necessary permissions (scopes) to send messages and create threads.
66+
* Check that you are using the correct token and channel ID in your configuration.
67+
* If you didn't add the correct permissions when creating the bot then you need to recreate the bot token and re-invite the bot to the channel. Slack docs do not mention this at the time of writing.

components/reporters/slack/cmd/main.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ package main
33
import (
44
"context"
55
"log"
6-
"net/http"
76
"time"
87

98
"github.com/go-errors/errors"
9+
"github.com/smithy-security/pkg/retry"
1010
"github.com/smithy-security/smithy/sdk/component"
1111

12+
componentlogger "github.com/smithy-security/smithy/sdk/logger"
13+
1214
"github.com/smithy-security/smithy/components/reporters/slack/internal/reporter"
15+
"github.com/smithy-security/smithy/components/reporters/slack/internal/reporter/slack"
1316
)
1417

1518
func main() {
@@ -25,16 +28,30 @@ func Main(ctx context.Context, opts ...component.RunnerOption) error {
2528
opts = append(opts, component.RunnerWithComponentName("slack"))
2629
config, err := reporter.NewConf(nil)
2730
if err != nil {
28-
return err
31+
return errors.Errorf("failed to get config: %w", err)
32+
}
33+
34+
config.SlackClientConfig.BaseClient, err = retry.NewClient(
35+
retry.Config{
36+
Logger: componentlogger.LoggerFromContext(ctx),
37+
},
38+
)
39+
if err != nil {
40+
return errors.Errorf("failed to create retry client: %w", err)
2941
}
30-
c := http.Client{}
31-
slackLogger, err := reporter.NewSlackLogger(config, &c)
42+
43+
sl, err := slack.NewClient(ctx, config.SlackClientConfig)
44+
if err != nil {
45+
return errors.Errorf("failed to create slack client: %w", err)
46+
}
47+
slackReporter, err := reporter.NewSlackReporter(config, sl)
3248
if err != nil {
33-
return err
49+
return errors.Errorf("failed to create slack reporter: %w", err)
3450
}
51+
3552
if err := component.RunReporter(
3653
ctx,
37-
slackLogger,
54+
slackReporter,
3855
opts...,
3956
); err != nil {
4057
return errors.Errorf("could not run reporter: %w", err)

components/reporters/slack/component.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,21 @@ parameters:
55
- name: "slack_webhook"
66
type: "string"
77
value: ""
8+
- name: "slack_token"
9+
type: "string"
10+
value: ""
11+
- name: "slack_channel"
12+
type: "string"
13+
value: ""
14+
- name: "debug"
15+
type: "string"
16+
value: "false"
817
steps:
918
- name: "slack"
1019
image: "components/reporters/slack"
1120
executable: "/bin/app"
1221
env_vars:
1322
SLACK_WEBHOOK: "{{ .parameters.slack_webhook }}"
23+
SLACK_TOKEN: "{{ .parameters.slack_token }}"
24+
SLACK_CHANNEL: "{{ .parameters.slack_channel }}"
25+
SLACK_DEBUG: "{{ .parameters.debug }}"
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
package reporter
2+
3+
import (
4+
"bytes"
5+
_ "embed"
6+
"strings"
7+
"text/template"
8+
9+
"github.com/go-errors/errors"
10+
vf "github.com/smithy-security/smithy/sdk/component/vulnerability-finding"
11+
ocsffindinginfo "github.com/smithy-security/smithy/sdk/gen/ocsf_ext/finding_info/v1"
12+
ocsf "github.com/smithy-security/smithy/sdk/gen/ocsf_schema/v1"
13+
"google.golang.org/protobuf/encoding/protojson"
14+
15+
"github.com/smithy-security/smithy/components/reporters/slack/internal/reporter/util"
16+
)
17+
18+
//go:embed issue.tpl
19+
var issueTpl string
20+
21+
const (
22+
unknownValue = "unknown"
23+
unsetValue = "-"
24+
)
25+
26+
// IssueData holds all the information needed to create a Slack issue message from a VulnerabilityFinding and Vulnerability instance.
27+
// It is designed to be used with text/template to generate formatted messages.
28+
type IssueData struct {
29+
Description string
30+
Title string
31+
FindingID uint64
32+
FindingLink string
33+
TargetName string
34+
TargetLink string
35+
IsRepository bool
36+
IsPurl bool
37+
Confidence string
38+
Priority string
39+
CWE string
40+
CWELink string
41+
CVE string
42+
Tool string
43+
RunName string
44+
RunLink string
45+
FindingPath string
46+
FindingStartLine uint32
47+
FindingEndLine uint32
48+
Reference string
49+
}
50+
51+
// NewIssueData creates a new IssueData struct from a VulnerabilityFinding and configuration.
52+
// It sets default values for most of the IssueData fields, and extracts relevant information for elements that are common across vulns from a single scan (e.g. time started or runID)
53+
func NewIssueData(finding *vf.VulnerabilityFinding, conf *Conf) (*IssueData, error) {
54+
if finding == nil || finding.Finding == nil {
55+
return nil, errors.New("finding or finding.Finding is nil")
56+
}
57+
58+
if conf == nil {
59+
return nil, errors.New("configuration is nil")
60+
}
61+
62+
var (
63+
findingPath, targetLink, reference, confidence, severity, title, description = unknownValue, unknownValue, unknownValue, unknownValue, unknownValue, unsetValue, unsetValue
64+
findingStartLine, findingEndLine uint32
65+
isRepository, isPurl bool
66+
findingLink = util.MakeFindingLink(conf.SmithyDashURL.Host, finding.ID)
67+
runLink = util.MakeRunLink(conf.SmithyDashURL.Host, conf.SmithyInstanceID)
68+
)
69+
70+
if len(finding.Finding.GetFindingInfo().DataSources) > 0 {
71+
var dataSource ocsffindinginfo.DataSource
72+
if err := protojson.Unmarshal([]byte(finding.Finding.GetFindingInfo().DataSources[0]), &dataSource); err != nil {
73+
return nil, errors.Errorf("could not unmarshal finding data source from finding info: %w", err)
74+
}
75+
76+
if dataSource.GetTargetType() == ocsffindinginfo.DataSource_TARGET_TYPE_REPOSITORY {
77+
reference = dataSource.GetSourceCodeMetadata().GetReference()
78+
79+
switch dataSource.GetUri().GetUriSchema() {
80+
case ocsffindinginfo.DataSource_URI_SCHEMA_FILE:
81+
isRepository = true
82+
findingStartLine = dataSource.GetFileFindingLocationData().GetStartLine()
83+
findingEndLine = dataSource.GetFileFindingLocationData().GetEndLine()
84+
findingPath = dataSource.GetUri().GetPath()
85+
var err error
86+
targetLink, err = util.MakeRepositoryLink(&dataSource)
87+
if err != nil {
88+
return nil, errors.Errorf("could not get repo target link: %w", err)
89+
}
90+
case ocsffindinginfo.DataSource_URI_SCHEMA_PURL:
91+
isPurl = true
92+
targetLink = dataSource.GetPurlFindingLocationData().String()
93+
findingPath = dataSource.GetPurlFindingLocationData().String()
94+
default:
95+
return nil, errors.Errorf("unsupported data source uri schema: %s", dataSource.GetUri().GetUriSchema().String())
96+
}
97+
98+
}
99+
}
100+
101+
issueData := &IssueData{
102+
FindingID: finding.ID,
103+
FindingLink: findingLink,
104+
TargetLink: targetLink,
105+
TargetName: strings.TrimPrefix(targetLink, "https://"),
106+
IsRepository: isRepository,
107+
IsPurl: isPurl,
108+
Confidence: confidence,
109+
RunName: conf.SmithyInstanceName,
110+
RunLink: runLink,
111+
FindingPath: findingPath,
112+
FindingStartLine: findingStartLine,
113+
FindingEndLine: findingEndLine,
114+
Reference: reference,
115+
Priority: severity,
116+
Title: title,
117+
Description: description,
118+
}
119+
120+
if finding.Finding.GetConfidence() != "" {
121+
issueData.Confidence = issueData.getConfidence(finding.Finding.GetConfidenceId())
122+
}
123+
124+
// Set default, overridable values for Priority and Description
125+
if finding.Finding.GetSeverity() != "" {
126+
issueData.Priority = issueData.getPriority(finding.Finding.GetSeverity())
127+
}
128+
129+
if finding.Finding.GetMessage() != "" {
130+
issueData.Description = finding.Finding.GetMessage()
131+
}
132+
133+
return issueData, nil
134+
}
135+
136+
// NewVulnerability enriches the current IssueData with a new Vulnerability instance from the IssueData.
137+
// It sets default values for Tool, CWE, CWELink, and CVE, and extracts relevant information from the Vulnerability instance to populate the IssueData fields.
138+
// It overrides Title, Description, and Priority fields if the Vulnerability instance provides values for them.
139+
func (i IssueData) EnrichWithNewVulnerability(vulnerability *ocsf.Vulnerability) (*IssueData, error) {
140+
141+
i.Tool, i.CWE, i.CWELink, i.CVE = unknownValue, unsetValue, unsetValue, unsetValue
142+
143+
if vulnerability.GetVendorName() != "" {
144+
i.Tool = vulnerability.GetVendorName()
145+
}
146+
147+
if vulnerability.GetCwe().GetCaption() != "" {
148+
i.CWE = vulnerability.GetCwe().GetCaption()
149+
}
150+
151+
if vulnerability.GetCwe().GetSrcUrl() != "" {
152+
i.CWELink = vulnerability.GetCwe().GetSrcUrl()
153+
}
154+
155+
if vulnerability.GetCve().GetUid() != "" {
156+
i.CVE = vulnerability.GetCve().GetUid()
157+
}
158+
159+
// Override Title, Description, and Priority if the vulnerability provides them
160+
if vulnerability.GetSeverity() != "" {
161+
i.Priority = i.getPriority(vulnerability.GetSeverity())
162+
}
163+
164+
if vulnerability.GetDesc() != "" {
165+
i.Description = vulnerability.GetDesc()
166+
}
167+
168+
if vulnerability.GetTitle() != "" {
169+
i.Title = vulnerability.GetTitle()
170+
}
171+
172+
return &i, nil
173+
}
174+
175+
// String executes the provided template with the IssueData and returns the resulting string.
176+
func (i IssueData) String(tpl *template.Template) (string, error) {
177+
var buf bytes.Buffer
178+
if err := tpl.Execute(&buf, i); err != nil {
179+
return "", errors.Errorf("could not execute issue description template: %w", err)
180+
}
181+
return buf.String(), nil
182+
}
183+
184+
func (i IssueData) getPriority(severity string) string {
185+
switch severity {
186+
case ocsf.VulnerabilityFinding_SEVERITY_ID_LOW.String(),
187+
ocsf.VulnerabilityFinding_SEVERITY_ID_INFORMATIONAL.String(),
188+
ocsf.VulnerabilityFinding_SEVERITY_ID_OTHER.String():
189+
return "Low"
190+
case ocsf.VulnerabilityFinding_SEVERITY_ID_MEDIUM.String():
191+
return "Medium"
192+
case ocsf.VulnerabilityFinding_SEVERITY_ID_HIGH.String():
193+
return "High"
194+
case ocsf.VulnerabilityFinding_SEVERITY_ID_FATAL.String(),
195+
ocsf.VulnerabilityFinding_SEVERITY_ID_CRITICAL.String():
196+
return "Highest"
197+
}
198+
return severity
199+
}
200+
201+
func (i IssueData) getConfidence(confidence ocsf.VulnerabilityFinding_ConfidenceId) string {
202+
switch confidence {
203+
case ocsf.VulnerabilityFinding_CONFIDENCE_ID_LOW,
204+
ocsf.VulnerabilityFinding_CONFIDENCE_ID_OTHER:
205+
return "Low"
206+
case ocsf.VulnerabilityFinding_CONFIDENCE_ID_MEDIUM:
207+
return "Medium"
208+
case ocsf.VulnerabilityFinding_CONFIDENCE_ID_HIGH:
209+
return "High"
210+
}
211+
return "Unknown"
212+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Smithy detected a vulnerability in *[{{ .TargetName }}]({{ .TargetLink }})*.
2+
3+
{{ if ne .Title "" }}*{{ .Title }}:*{{ end }}
4+
{{ if ne .Description "" }}{{ .Description }}.{{ end }}
5+
6+
{{ if .IsRepository }}
7+
*Location:* *{{ .FindingPath }}* between line {{ .FindingStartLine }} and {{ .FindingEndLine }} on branch *{{ .Reference }}*.
8+
{{ else if .IsPurl }}
9+
*Location:* *{{ .FindingPath }}*.
10+
{{ end }}
11+
12+
*Finding info:*
13+
- *ID:* [{{ .FindingID }}]({{ .FindingLink }})
14+
- *Confidence:* {{ .Confidence }}
15+
- *CWE:* [{{ .CWE }}]({{ .CWELink }})
16+
- *CVE:* {{ .CVE }}
17+
- *Reporting Tool:* {{ .Tool }}
18+
- *Detected by Run:* [{{ .RunName }}]({{ .RunLink }})

0 commit comments

Comments
 (0)