Skip to content

Commit

Permalink
Merge pull request #9 from hazcod/feat/overview
Browse files Browse the repository at this point in the history
Feature: Workspace ONE support
  • Loading branch information
hazcod authored Jul 12, 2021
2 parents 1924304 + dff349b commit dae0c92
Show file tree
Hide file tree
Showing 9 changed files with 395 additions and 65 deletions.
Binary file modified .github/readme/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
82 changes: 61 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,63 +1,102 @@
# crowdstrike-spotlight-slacker
Nags users on Slack about outstanding application vulnerabilities found by Crowdstrike Spotlight so they patch their software.
# 🤖 security-slacker
Nags users on Slack about outstanding risks found by Crowdstrike Spotlight or vmware Workspace ONE so they can secure their own endpoint.

Self-service security culture! :partying_face:

![slack example](.github/readme/screenshot.png)

## Instructions

1. Tag your Falcon hosts with `email/user/company/com` if their email is `[email protected]`.
2. Fetch a binary release or Docker image from [Releases](https://github.com/hazcod/crowdstrike-spotlight-slacker/releases).
3. Create a Falcon API token to use in `API Clients and Keys` with `Read` permission to `Hosts` and `Spotlight`.
4. Create a Slack app and get the bot token.
5. Create a configuration file:

2. Assign compliance policies to your devies in Workspace ONE.
3. Fetch a binary release or Docker image from [Releases](https://github.com/hazcod/crowdstrike-spotlight-slacker/releases).
4. Create a Falcon API token to use in `API Clients and Keys` with `Read` permission to `Hosts` and `Spotlight`.
5. Create a Workspace ONE API token and user to use.
6. Create a configuration file:
```yaml
slack:
# slack bot token
token: "XXX"
# Slack user that receives messages if the user is not found
security_user: "[email protected]"
# skip sending a security overview if there is nothing to mention
skip_no_report: true

# falcon crowdstrike
falcon:
# falcon api credentials
clientid: "XXX"
secret: "XXX"
# your falcon SaaS cloud region
cloud_region: "eu-1"
# skip vulnerabilities without patches available
# skip vulnerabilities without available patches
skip_no_mitigation: true

email:
# email domain
domain: "mycompany"
# vmware workspace one
ws1:
api_url: "https://xxx.awmdm.com/api/"
api_key: "XXX"
user: "XXX"
password: "XXX"

# email domains used in your Slack workspace for filtering
# e.g. for a Slack account [email protected]
email.domains: ["mycompany.com"]

# what is sent to the user in Go templating
templates:
user_message: |
*:warning: We found security vulnerabilities on your device(s)*
Hi {{ .Slack.Profile.FirstName }} {{ .Slack.Profile.LastName }}! One or more of your devices seem to be vulnerable.
Luckily we noticed there are patches available. :tada:
Can you please update following software as soon as possible?
*:warning: We detected security issues on your device(s)*
Hi {{ .Slack.Profile.FirstName }} {{ .Slack.Profile.LastName }}!
{{ range $device := .User.Devices }}
{{ if not (eq (len .Falcon.Devices) 0) }}
One or more of your devices seem to be vulnerable.
Luckily we noticed there are patches available. Please install following patches:
{{ range $device := .Falcon.Devices }}
:computer: {{ $device.MachineName }}
{{ range $vuln := $device.Findings }}
`{{ $vuln.ProductName }}`
{{ end }}
{{ end }}
{{ end }}
{{ if not (eq (len .WS1.Devices) 0) }}
One or more of your devices seem to be misconfigured in an insecure way.
Please check the below policies which are violated:
{{ range $device := .WS1.Devices }}
:computer: {{ $device.MachineName }}
{{ range $finding := $device.Findings }}
- :warning: {{ $finding.ComplianceName }}
{{ end }}
{{ end }}
{{ end }}
Please update them as soon as possible. In case of any issues, hop into *#security*.
Please resolve those issues as soon as possible. In case of any issues, hop into *#security*.
Thank you! :wave:
security_overview_message: |
:information_source: *Device Posture overview* {{ .Date.Format "Jan 02, 2006 15:04:05 UTC" }}
{{ if not .Results }}Nothing to report! :white_check_mark: {{ else }}
{{ range $result := .Results }}
{{ if and (not .Falcon) (not .WS1) }}Nothing to report! :white_check_mark: {{ else }}
{{ range $result := .Falcon }}
:man-surfing: *{{ $result.Email }}*
{{ range $device := $result.Devices }}
:computer: {{ $device.MachineName}}
{{ range $vuln := $device.Findings }}- {{ $vuln.ProductName }} ({{ $vuln.CveSeverity }}) ({{ $vuln.TimestampFound }}) ({{ $vuln.CveID }}){{ end }}
{{ end }}
{{ end }}
{{ range $result := .WS1 }}
:man-surfing: *{{ $result.Email }}*
{{ range $device := $result.Devices }}
:computer: {{ $device.MachineName }}
Compromised: {{ $device.Compromised }}
Last seen: {{ $device.LastSeen.Format "Jan 02, 2006 15:04:05 UTC" }}
{{ range $finding := $device.Findings }}- :warning: {{ $finding.ComplianceName }}{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ if .Errors }}
Expand All @@ -67,5 +106,6 @@ templates:
{{ end }}
{{ end }}
```
4. Run `css -config=your-config.yml`.
5. See it popup in Slack!
4. Run `css -config=your-config.yml -log=debug -dry` to test.
5. See the security overview popup to you in Slack!
6. Now run it for real with `css -config=your-config.yml`.
65 changes: 35 additions & 30 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"flag"
"github.com/hazcod/crowdstrike-spotlight-slacker/pkg/overview/security"
"github.com/hazcod/crowdstrike-spotlight-slacker/pkg/overview/user"
"github.com/pkg/errors"
"github.com/hazcod/crowdstrike-spotlight-slacker/pkg/ws1"
"os"
"strings"

Expand Down Expand Up @@ -42,11 +42,18 @@ func main() {
logrus.WithError(err).Fatal("invalid configuration")
}

// ---

falconMessages, err := falcon.GetMessages(config, ctx)
if err != nil {
logrus.WithError(err).Fatal("could not get falcon messages")
}

ws1Messages, err := ws1.GetMessages(config, ctx)
if err != nil {
logrus.WithError(err).Fatal("could not get WS1 messages")
}

// ---

slackClient := slack.New(config.Slack.Token)
Expand Down Expand Up @@ -74,65 +81,63 @@ func main() {

var errorsToReport []error

for userEmail, falconResult := range falconMessages {
logrus.WithField("user", userEmail).Debug("handling user at risk")
for _, slackUser := range slackUsers {
userEmail := strings.ToLower(slackUser.Profile.Email)

var theSlackUser slack.User
for _, slackUser := range slackUsers {
if !strings.EqualFold(userEmail, slackUser.Profile.Email) {
continue
}
theSlackUser = slackUser
if slackUser.IsBot {
continue
}

if theSlackUser.Name == "" {
logrus.WithField("user", userEmail).Error("slack user not found")
errorsToReport = append(errorsToReport, errors.New("User not found on Slack: " + userEmail))
userFalconMsg := falconMessages[userEmail]

userWS1Msg := ws1Messages[userEmail]

if len(userFalconMsg.Devices) == 0 && len(userWS1Msg.Devices) == 0 {
continue
}

if theSlackUser.IsBot {
logrus.WithField("user", userEmail).Error("user is a Slack bot")
logrus.WithField("falcon", userFalconMsg).WithField("ws1", userWS1Msg).WithField("email", userEmail).
Debug("found messages")

slackMessage, err := user.BuildUserOverviewMessage(logrus.StandardLogger(), config, slackUser, falconMessages[userEmail], ws1Messages[userEmail])
if err != nil {
logrus.WithError(err).WithField("user", slackUser.Profile.Email).Error("could not generate user message")
continue
}

slackMessage, err := user.BuildUserOverviewMessage(logrus.StandardLogger(), config, theSlackUser, falconMessages[falconResult.Email])
if err != nil {
logrus.WithError(err).WithField("user", theSlackUser.Profile.Email).Error("could not generate user message")
if slackMessage == "" {
continue
}

if !*dryMode {
if _, _, _, err := slackClient.SendMessage(
theSlackUser.ID,
slackUser.ID,
slack.MsgOptionText(slackMessage, false),
slack.MsgOptionAsUser(true),
); err != nil {
logrus.WithError(err).
WithField("user", theSlackUser.Profile.Email).
WithField("user", slackUser.Profile.Email).
Error("could not send slack message")
continue
}
}

logrus.
WithField("user", falconResult.Email).WithField("devices", len(falconResult.Devices)).
Info("sent reminder on Slack")
logrus.WithField("user", userEmail).Info("sent notice on Slack")
}

/*
if len(falconMessages) == 0 {
logrus.Info("nothing to report, exiting")
if config.Templates.SecurityOverviewMessage == "" {
logrus.Warn("not sending a security overview since template is empty")
os.Exit(0)
}
*/

if config.Templates.SecurityOverviewMessage == "" {
logrus.Info("not sending a security overview")
os.Exit(0)
if config.Slack.SkipNoReport {
if len(falconMessages) == 0 && len(ws1Messages) == 0 {
logrus.Info("nothing to report, exiting")
os.Exit(0)
}
}

overviewText, err := security.BuildSecurityOverviewMessage(logrus.StandardLogger(), *config, falconMessages, errorsToReport)
overviewText, err := security.BuildSecurityOverviewMessage(logrus.StandardLogger(), *config, falconMessages, ws1Messages, errorsToReport)
if err != nil {
logrus.WithError(err).Fatal("could not generate security overview")
}
Expand Down
15 changes: 12 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type Config struct {
Slack struct {
Token string `yaml:"token" env:"SLACK_TOKEN"`
SecurityUser string `yaml:"security_user" emv:"SLACK_SECURITY_USER"`

SkipNoReport bool `yaml:"skip_no_report" env:"SLACK_SKIP_NO_REPORT"`
} `yaml:"slack"`

Falcon struct {
Expand All @@ -26,8 +28,15 @@ type Config struct {
SkipNoMitigation bool `yaml:"skip_no_mitigation" env:"FALCON_SKIP_NO_MITIGATION"`
} `yaml:"falcon"`

WS1 struct {
Endpoint string `yaml:"api_url" env:"WS1_API_URL"`
APIKey string `yaml:"api_key" env:"WS1_API_KEY"`
User string `yaml:"user" env:"WS1_USER"`
Password string `yaml:"password" env:"WS1_PASSWORD"`
} `yaml:"ws1"`

Email struct {
Domain string `yaml:"domain" env:"DOMAIN"`
Domains []string `yaml:"domains" env:"DOMAINS"`
} `yaml:"email"`

Templates struct {
Expand Down Expand Up @@ -76,8 +85,8 @@ func (c *Config) Validate() error {
return errors.New("missing falcon cloud region")
}

if c.Email.Domain == "" {
return errors.New("missing email domain")
if len(c.Email.Domains) == 0 {
return errors.New("missing email domain(s)")
}

if c.Templates.UserMessage == "" {
Expand Down
20 changes: 15 additions & 5 deletions pkg/falcon/extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func getUniqueDeviceID(hostInfo models.DomainAPIVulnerabilityHostInfoV2) (string
return hex.EncodeToString(hasher.Sum(nil)), nil
}

func findEmailTag(tags []string, emailHost string) (email string, err error) {
func findEmailTag(tags []string, emailDomains []string) (email string, err error) {
theTag := ""

for _, tag := range tags {
Expand All @@ -79,9 +79,19 @@ func findEmailTag(tags []string, emailHost string) (email string, err error) {
return "", errors.New("email tag not found")
}

email = strings.ToLower(theTag)
email = strings.Replace(email, fmt.Sprintf("/%s", emailHost), fmt.Sprintf("@%s", emailHost), 1)
email = strings.ReplaceAll(email, "/", ".")
theTag = strings.ToLower(theTag)

for _, domain := range emailDomains {
if ! strings.Contains(theTag ,strings.ToLower(domain)) {
continue
}

email = theTag
email = strings.Replace(email, fmt.Sprintf("/%s", domain), fmt.Sprintf("@%s", domain), 1)
email = strings.ReplaceAll(email, "/", ".")

break
}

if !strings.Contains(email, "@") || !strings.Contains(email, ".") {
return "", errors.New("invalid email address: " + email)
Expand Down Expand Up @@ -218,7 +228,7 @@ func GetMessages(config *config.Config, ctx context.Context) (results map[string
logrus.WithField("devices", len(devices)).Info("found vulnerable devices")

for _, device := range devices {
userEmail, err := findEmailTag(device.Tags, config.Email.Domain)
userEmail, err := findEmailTag(device.Tags, config.Email.Domains)
if err != nil {
logrus.
WithError(err).
Expand Down
17 changes: 14 additions & 3 deletions pkg/overview/security/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,36 @@ import (
"bytes"
"github.com/hazcod/crowdstrike-spotlight-slacker/config"
"github.com/hazcod/crowdstrike-spotlight-slacker/pkg/falcon"
"github.com/hazcod/crowdstrike-spotlight-slacker/pkg/ws1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"html/template"
"time"
)

func BuildSecurityOverviewMessage(logger *logrus.Logger, config config.Config, falconResults map[string]falcon.FalconResult, reportedErrors []error) (string, error) {
func BuildSecurityOverviewMessage(logger *logrus.Logger, config config.Config, falconResults map[string]falcon.FalconResult, ws1Results map[string]ws1.WS1Result, reportedErrors []error) (string, error) {
messageTemplate, err := template.New("message").Parse(config.Templates.SecurityOverviewMessage)
if err != nil {
return "", errors.Wrap(err, "unable to parse message")
}

var allFalcon []falcon.FalconResult
for _, f := range falconResults { allFalcon = append(allFalcon, f) }

var allWS1 []ws1.WS1Result
for _, w := range ws1Results { allWS1 = append(allWS1, w) }

logrus.Debugf("falcon: %d ws1: %d", len(allFalcon), len(allWS1))

variables := struct {
Results map[string]falcon.FalconResult
Falcon []falcon.FalconResult
WS1 []ws1.WS1Result
Date time.Time
Errors []error
}{
Date: time.Now(),
Results: falconResults,
Falcon: allFalcon,
WS1: allWS1,
Errors: reportedErrors,
}

Expand Down
Loading

0 comments on commit dae0c92

Please sign in to comment.