Skip to content

Commit

Permalink
feat: add Slack plugin (#70)
Browse files Browse the repository at this point in the history
Close: #32

```
❯ go run . help slack
Scan Slack team for sensitive information.

Usage:
  2ms slack --token TOKEN --team TEAM [flags]

Flags:
      --channel stringArray   Slack channels to scan
      --duration duration     Slack backward duration for messages (ex: 24h, 7d, 1M, 1y) (default 336h0m0s)
  -h, --help                  help for slack
      --messages-count int    Slack messages count to scan (0 = all messages)
      --team string           Slack team name or ID [required]
      --token string          Slack token [required]

Global Flags:
      --all                scan all plugins (default true)
      --log-level string   log level (trace, debug, info, warn, error, fatal) (default "info")
      --tags strings       select rules to be applied (default [all])
```

Like in Discord, more knowledge is required to integrate this plugin
into an E2E system. For example, except for retrieving the token (from
**OAuth & Permissions** in the Slack App page), you have to add your app
to each **channel** you want to read.


![image](https://github.com/Checkmarx/2ms/assets/17686879/0db4994a-7304-4e70-a268-fff242f6ca35)

---------

Co-authored-by: Jossef Harush Kadouri <jossef12@gmail.com>
  • Loading branch information
Baruch Odem (Rothkoff) and jossef authored May 18, 2023
1 parent 48adfd8 commit 0c9e041
Show file tree
Hide file tree
Showing 5 changed files with 406 additions and 2 deletions.
4 changes: 3 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package cmd

import (
"fmt"
"github.com/checkmarx/2ms/config"
"os"
"path/filepath"
"strings"

"github.com/checkmarx/2ms/config"

"sync"
"time"

Expand Down Expand Up @@ -43,6 +44,7 @@ var allPlugins = []plugins.IPlugin{
&plugins.ConfluencePlugin{},
&plugins.DiscordPlugin{},
&plugins.RepositoryPlugin{},
&plugins.SlackPlugin{},
}

var channels = plugins.Channels{
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ require (
github.com/fatih/semgroup v1.2.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gitleaks/go-gitdiff v0.8.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/h2non/filetype v1.1.3 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
Expand All @@ -35,6 +35,7 @@ require (
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/slack-go/slack v0.12.2 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ github.com/gitleaks/go-gitdiff v0.8.0/go.mod h1:pKz0X4YzCKZs30BL+weqBIG7mx0jl4tF
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
Expand Down Expand Up @@ -110,6 +111,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
Expand All @@ -131,6 +133,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
Expand Down Expand Up @@ -192,6 +196,8 @@ github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ=
github.com/slack-go/slack v0.12.2/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
Expand Down
211 changes: 211 additions & 0 deletions plugins/slack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package plugins

import (
"fmt"
"strconv"
"time"

"github.com/rs/zerolog/log"
"github.com/slack-go/slack"
"github.com/spf13/cobra"
)

const (
slackTokenFlag = "token"
slackTeamFlag = "team"
slackChannelFlag = "channel"
slackBackwardDurationFlag = "duration"
slackMessagesCountFlag = "messages-count"
)

const slackDefaultDateFrom = time.Hour * 24 * 14

type SlackPlugin struct {
Plugin
Channels
Token string
}

func (p *SlackPlugin) GetName() string {
return "slack"
}

var (
tokenArg string
teamArg string
channelsArg []string
backwardDurationArg time.Duration
messagesCountArg int
)

func (p *SlackPlugin) DefineCommand(channels Channels) (*cobra.Command, error) {
p.Channels = channels

command := &cobra.Command{
Use: fmt.Sprintf("%s --%s TOKEN --%s TEAM", p.GetName(), slackTokenFlag, slackTeamFlag),
Short: "Scan Slack team",
Long: "Scan Slack team for sensitive information.",
Run: func(cmd *cobra.Command, args []string) {
p.getItems()
},
}

command.Flags().StringVar(&tokenArg, slackTokenFlag, "", "Slack token [required]")
err := command.MarkFlagRequired(slackTokenFlag)
if err != nil {
return nil, fmt.Errorf("error while marking flag %s as required: %w", slackTokenFlag, err)
}
command.Flags().StringVar(&teamArg, slackTeamFlag, "", "Slack team name or ID [required]")
err = command.MarkFlagRequired(slackTeamFlag)
if err != nil {
return nil, fmt.Errorf("error while marking flag %s as required: %w", slackTeamFlag, err)
}
command.Flags().StringArrayVar(&channelsArg, slackChannelFlag, []string{}, "Slack channels to scan")
command.Flags().DurationVar(&backwardDurationArg, slackBackwardDurationFlag, slackDefaultDateFrom, "Slack backward duration for messages (ex: 24h, 7d, 1M, 1y)")
command.Flags().IntVar(&messagesCountArg, slackMessagesCountFlag, 0, "Slack messages count to scan (0 = all messages)")

return command, nil
}

func (p *SlackPlugin) getItems() {
slackApi := slack.New(tokenArg)

team, err := getTeam(slackApi, teamArg)
if err != nil {
p.Errors <- fmt.Errorf("error while getting team: %w", err)
return
}

channels, err := getChannels(slackApi, team.ID, channelsArg)
if err != nil {
p.Errors <- fmt.Errorf("error while getting channels for team %s: %w", team.Name, err)
return
}
if len(*channels) == 0 {
log.Warn().Msgf("No channels found for team %s", team.Name)
return
}

log.Info().Msgf("Found %d channels for team %s", len(*channels), team.Name)
p.WaitGroup.Add(len(*channels))
for _, channel := range *channels {
go p.getItemsFromChannel(slackApi, channel)
}
}

func (p *SlackPlugin) getItemsFromChannel(slackApi *slack.Client, channel slack.Channel) {
defer p.WaitGroup.Done()
log.Info().Msgf("Getting items from channel %s", channel.Name)

cursor := ""
counter := 0
for {
history, err := slackApi.GetConversationHistory(&slack.GetConversationHistoryParameters{
Cursor: cursor,
ChannelID: channel.ID,
})
if err != nil {
p.Errors <- fmt.Errorf("error while getting history for channel %s: %w", channel.Name, err)
return
}
for _, message := range history.Messages {
outOfRange, err := isMessageOutOfRange(message, backwardDurationArg, counter, messagesCountArg)
if err != nil {
p.Errors <- fmt.Errorf("error while checking message: %w", err)
return
}
if outOfRange {
break
}
if message.Text != "" {
p.Items <- Item{
Content: message.Text,
Source: channel.Name,
ID: message.Timestamp,
}
}
counter++
}
if history.ResponseMetaData.NextCursor == "" {
break
}
cursor = history.ResponseMetaData.NextCursor
}
}

// Declare it to be consistent with all comparaisons
var timeNow = time.Now()

func isMessageOutOfRange(message slack.Message, backwardDuration time.Duration, currentMessagesCount int, limitMessagesCount int) (bool, error) {
if backwardDuration != 0 {
timestamp, err := strconv.ParseFloat(message.Timestamp, 64)
if err != nil {
return true, fmt.Errorf("error while parsing timestamp: %w", err)
}
messageDate := time.Unix(int64(timestamp), 0)
if messageDate.Before(timeNow.Add(-backwardDuration)) {
return true, nil
}
}
if limitMessagesCount != 0 && currentMessagesCount >= limitMessagesCount {
return true, nil
}
return false, nil
}

type ISlackClient interface {
GetConversations(*slack.GetConversationsParameters) ([]slack.Channel, string, error)
ListTeams(slack.ListTeamsParameters) ([]slack.Team, string, error)
}

func getTeam(slackApi ISlackClient, teamName string) (*slack.Team, error) {
cursorHolder := ""
for {
teams, cursor, err := slackApi.ListTeams(slack.ListTeamsParameters{Cursor: cursorHolder})
if err != nil {
return nil, fmt.Errorf("error while getting teams: %w", err)
}
for _, team := range teams {
if team.Name == teamName || team.ID == teamName {
return &team, nil
}
}
if cursor == "" {
break
}
cursorHolder = cursor
}
return nil, fmt.Errorf("team '%s' not found", teamName)
}

func getChannels(slackApi ISlackClient, teamId string, wantedChannels []string) (*[]slack.Channel, error) {
cursorHolder := ""
selectedChannels := []slack.Channel{}
for {
channels, cursor, err := slackApi.GetConversations(&slack.GetConversationsParameters{
Cursor: cursorHolder,
TeamID: teamId,
})
if err != nil {
return nil, fmt.Errorf("error while getting channels: %w", err)
}
if len(wantedChannels) == 0 {
selectedChannels = append(selectedChannels, channels...)
} else {
for _, channel := range wantedChannels {
for _, c := range channels {
if c.Name == channel || c.ID == channel {
selectedChannels = append(selectedChannels, c)
}
}
}
if len(selectedChannels) == len(wantedChannels) {
return &selectedChannels, nil
}
}
if cursor == "" {
return &selectedChannels, nil
}
cursorHolder = cursor
}
}
Loading

0 comments on commit 0c9e041

Please sign in to comment.