Skip to content

Commit 19a3f40

Browse files
committed
Slack works like Discord with threads and details
1 parent 55dd91b commit 19a3f40

File tree

16 files changed

+961
-337
lines changed

16 files changed

+961
-337
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(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: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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+
type IssueData struct {
22+
Description string
23+
Title string
24+
FindingID uint64
25+
FindingLink string
26+
TargetName string
27+
TargetLink string
28+
IsRepository bool
29+
IsPurl bool
30+
Confidence string
31+
CWE string
32+
CWELink string
33+
CVE string
34+
Tool string
35+
RunName string
36+
RunLink string
37+
FindingPath string
38+
FindingStartLine uint32
39+
FindingEndLine uint32
40+
Reference string
41+
}
42+
43+
func (r slackReporter) getMsgs(findings []*vf.VulnerabilityFinding) ([]string, error) {
44+
tpl, err := template.New("issue").Parse(issueTpl)
45+
if err != nil {
46+
return nil, errors.Errorf("could not parse thread template: %w", err)
47+
}
48+
49+
const (
50+
unknownValue = "unknown"
51+
unsetValue = "-"
52+
)
53+
54+
var msgs []string
55+
for _, finding := range findings {
56+
var (
57+
findingPath, targetName, targetLink, reference, confidence = unknownValue, unknownValue, unknownValue, unknownValue, unknownValue
58+
findingStartLine, findingEndLine uint32
59+
isRepository, isPurl bool
60+
)
61+
62+
if finding.Finding.GetConfidence() != "" {
63+
confidence = r.getConfidence(finding.Finding.GetConfidenceId())
64+
}
65+
66+
if len(finding.Finding.GetFindingInfo().DataSources) > 0 {
67+
var dataSource ocsffindinginfo.DataSource
68+
if err := protojson.Unmarshal([]byte(finding.Finding.GetFindingInfo().DataSources[0]), &dataSource); err != nil {
69+
return nil, errors.Errorf("could not unmarshal finding data source from finding info: %w", err)
70+
}
71+
72+
if dataSource.GetTargetType() == ocsffindinginfo.DataSource_TARGET_TYPE_REPOSITORY {
73+
reference = dataSource.GetSourceCodeMetadata().GetReference()
74+
75+
switch dataSource.GetUri().GetUriSchema() {
76+
case ocsffindinginfo.DataSource_URI_SCHEMA_FILE:
77+
isRepository = true
78+
findingStartLine = dataSource.GetFileFindingLocationData().GetStartLine()
79+
findingEndLine = dataSource.GetFileFindingLocationData().GetEndLine()
80+
findingPath = dataSource.GetUri().GetPath()
81+
targetName = dataSource.GetSourceCodeMetadata().GetRepositoryUrl()
82+
var err error
83+
targetLink, err = util.MakeRepositoryLink(&dataSource)
84+
if err != nil {
85+
return nil, errors.Errorf("could not get repo target link: %w", err)
86+
}
87+
case ocsffindinginfo.DataSource_URI_SCHEMA_PURL:
88+
isPurl = true
89+
targetName = dataSource.GetPurlFindingLocationData().String()
90+
targetLink = dataSource.GetPurlFindingLocationData().String()
91+
findingPath = dataSource.GetPurlFindingLocationData().String()
92+
}
93+
}
94+
}
95+
96+
targetName = strings.TrimPrefix(targetLink, "https://")
97+
98+
for _, f := range finding.Finding.GetVulnerabilities() {
99+
var (
100+
findingLink = util.MakeFindingLink(
101+
r.conf.SmithyDashURL.Host,
102+
finding.ID,
103+
)
104+
runLink = util.MakeRunLink(
105+
r.conf.SmithyDashURL.Host,
106+
r.conf.SmithyInstanceID,
107+
)
108+
tool, cwe, cweLink, cve = unknownValue, unsetValue, unsetValue, unsetValue
109+
)
110+
111+
if f.GetVendorName() != "" {
112+
tool = f.GetVendorName()
113+
}
114+
115+
if f.GetCwe().GetCaption() != "" {
116+
cwe = f.GetCwe().GetCaption()
117+
}
118+
119+
if f.GetCwe().GetSrcUrl() != "" {
120+
cweLink = f.GetCwe().GetSrcUrl()
121+
}
122+
123+
if f.GetCve().GetUid() != "" {
124+
cve = f.GetCve().GetUid()
125+
}
126+
127+
var buf bytes.Buffer
128+
if err := tpl.Execute(
129+
&buf,
130+
IssueData{
131+
Title: f.GetTitle(),
132+
Description: f.GetDesc(),
133+
FindingID: finding.ID,
134+
FindingLink: findingLink,
135+
Tool: tool,
136+
TargetLink: targetLink,
137+
TargetName: targetName,
138+
IsRepository: isRepository,
139+
IsPurl: isPurl,
140+
Confidence: confidence,
141+
CWE: cwe,
142+
CWELink: cweLink,
143+
CVE: cve,
144+
RunName: r.conf.SmithyInstanceName,
145+
RunLink: runLink,
146+
FindingPath: findingPath,
147+
FindingStartLine: findingStartLine,
148+
FindingEndLine: findingEndLine,
149+
Reference: reference,
150+
},
151+
); err != nil {
152+
return nil, errors.Errorf("could not execute issue description template: %w", err)
153+
}
154+
155+
msgs = append(msgs, buf.String())
156+
}
157+
}
158+
159+
return msgs, nil
160+
}
161+
162+
func (r slackReporter) getPriority(severity string) string {
163+
switch severity {
164+
case ocsf.VulnerabilityFinding_SEVERITY_ID_LOW.String(),
165+
ocsf.VulnerabilityFinding_SEVERITY_ID_INFORMATIONAL.String(),
166+
ocsf.VulnerabilityFinding_SEVERITY_ID_OTHER.String():
167+
return "Low"
168+
case ocsf.VulnerabilityFinding_SEVERITY_ID_MEDIUM.String():
169+
return "Medium"
170+
case ocsf.VulnerabilityFinding_SEVERITY_ID_HIGH.String():
171+
return "High"
172+
case ocsf.VulnerabilityFinding_SEVERITY_ID_FATAL.String(),
173+
ocsf.VulnerabilityFinding_SEVERITY_ID_CRITICAL.String():
174+
return "Highest"
175+
}
176+
return severity
177+
}
178+
179+
func (r slackReporter) getConfidence(confidence ocsf.VulnerabilityFinding_ConfidenceId) string {
180+
switch confidence {
181+
case ocsf.VulnerabilityFinding_CONFIDENCE_ID_LOW,
182+
ocsf.VulnerabilityFinding_CONFIDENCE_ID_OTHER:
183+
return "Low"
184+
case ocsf.VulnerabilityFinding_CONFIDENCE_ID_MEDIUM:
185+
return "Medium"
186+
case ocsf.VulnerabilityFinding_CONFIDENCE_ID_HIGH:
187+
return "High"
188+
}
189+
return "Unknown"
190+
}
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)