diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5f21936..467d13d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,13 @@ updates: time: '04:00' open-pull-requests-limit: 10 target-branch: dev + - package-ecosystem: docker + directory: "/" + schedule: + interval: daily + time: '04:00' + open-pull-requests-limit: 10 + target-branch: dev - package-ecosystem: github-actions directory: "/" schedule: diff --git a/Dockerfile b/Dockerfile index 4be7f68..bbf3d23 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,4 @@ + FROM alpine:latest AS builder # add ca certificates and timezone data files @@ -31,4 +32,4 @@ COPY --chown=app css /app USER 1000 # entrypoint -ENTRYPOINT ["/app"] +ENTRYPOINT ["/app"] \ No newline at end of file diff --git a/Makefile b/Makefile index 76fd047..2b32cae 100644 --- a/Makefile +++ b/Makefile @@ -6,4 +6,3 @@ clean: build: go build -o slacker ./cmd/ chmod +x slacker - diff --git a/README.md b/README.md index cee4c23..5428443 100644 --- a/README.md +++ b/README.md @@ -15,33 +15,56 @@ slack: # slack bot token token: "XXX" # Slack user that receives messages if the user is not found - fallback_user: "security@mycompany.com" + security_user: "security@mycompany.com" falcon: clientid: "XXX" secret: "XXX" cloud_region: "eu-1" + # skip vulnerabilities without patches available + skip_no_mitigation: true email: # email domain domain: "mycompany" # what is sent to the user in Go templating -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? - - {{ range $device := .User.Devices }} - :computer: {{ $device.MachineName }} - {{ range $vuln := $device.Findings }} - `{{ $vuln.ProductName }}` - {{ end }} - {{ end }} - - Please update them as soon as possible. In case of any issues, hop into *#security*. - Thank you! :wave: +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? + + {{ range $device := .User.Devices }} + :computer: {{ $device.MachineName }} + {{ range $vuln := $device.Findings }} + `{{ $vuln.ProductName }}` + {{ end }} + {{ end }} + + Please update them 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 }} + :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 }} + {{ end }} + + {{ if .Errors }} + :warning: *Errors:* + {{ range $err := .Errors }} + - {{ $err }} + {{ end }} + {{ end }} ``` 4. Run `css -config=your-config.yml`. 5. See it popup in Slack! diff --git a/cmd/main.go b/cmd/main.go index bcc684f..b4f3b08 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,105 +1,26 @@ package main import ( - "bytes" "context" - "crypto/sha1" - "encoding/hex" - "encoding/json" "flag" - "fmt" - "github.com/crowdstrike/gofalcon/falcon" - "github.com/crowdstrike/gofalcon/falcon/client/spotlight_vulnerabilities" - "github.com/crowdstrike/gofalcon/falcon/models" - config2 "github.com/hazcod/crowdstrike-spotlight-slacker/config" + "github.com/hazcod/crowdstrike-spotlight-slacker/pkg/overview/security" + "github.com/hazcod/crowdstrike-spotlight-slacker/pkg/overview/user" "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "github.com/slack-go/slack" - "log" "os" "strings" - "text/template" -) -const ( - tagEmailPrefix = "email/" - tagFalconPrefix = "FalconGroupingTags/" -) - -var ( - falconAPIMaxRecords = int64(400) + config2 "github.com/hazcod/crowdstrike-spotlight-slacker/config" + "github.com/hazcod/crowdstrike-spotlight-slacker/pkg/falcon" + "github.com/sirupsen/logrus" + "github.com/slack-go/slack" ) -type DeviceUser struct { - Email string - Devices []UserDevice -} - -type UserDevice struct { - MachineName string - Tags []string - Findings []UserDeviceFinding -} - -type UserDeviceFinding struct { - ProductName string - CveID string - CveSeverity string - MitigationAvailable bool - TimestampFound string -} - -func getUniqueDeviceID(hostInfo models.DomainAPIVulnerabilityHostInfoV2) (string, error) { - b, err := json.Marshal(&hostInfo) - if err != nil { return "", err } - hasher := sha1.New() - if _, err := hasher.Write(b); err != nil { - return "", err - } - return hex.EncodeToString(hasher.Sum(nil)), nil -} - -func findEmailTag(tags []string, emailHost string) (email string, err error) { - for _, tag := range tags { - tag = strings.ToLower(tag) - tag = strings.TrimLeft(tag, strings.ToLower(tagFalconPrefix)) - - logrus.WithField("tag", tag).Debug("looking at falcon tag") - - if !strings.HasPrefix(tag, tagEmailPrefix) { - continue - } - - if email != "" { - logrus. - WithField("tag", tag).WithField("email", email). - WithField("prefix", tagEmailPrefix). - Warn("multiple user tags found") - } - - email = strings.TrimLeft(tag, tagEmailPrefix) - } - - if email == "" { - return "", errors.New("email tag not found") - } - - email = strings.ToLower(email) - email = strings.Replace(email, fmt.Sprintf("/%s", emailHost), fmt.Sprintf("@%s", emailHost), 1) - email = strings.ReplaceAll(email, "/", ".") - - if !strings.Contains(email, "@") || !strings.Contains(email, "."){ - return "", errors.New("invalid email address: " + email) - } - - return email, nil -} - func main() { ctx := context.Background() configPath := flag.String("config", "", "Path to your config file.") - logLevelStr:= flag.String("log", "info", "Log level.") + logLevelStr := flag.String("log", "info", "Log level.") + dryMode := flag.Bool("dry", false, "whether we run in dry-run mode and send nothing to the users.") flag.Parse() logLevel, err := logrus.ParseLevel(*logLevelStr) @@ -108,222 +29,123 @@ func main() { } logrus.SetLevel(logLevel) - config, err := config2.LoadConfig(*configPath) + if *dryMode { + logrus.Warn("running in dry mode, nothing will be sent to the users") + } + + config, err := config2.LoadConfig(logrus.StandardLogger(), *configPath) if err != nil { - log.Fatalf("could not load configuration: %s", err) + logrus.Fatalf("could not load configuration: %s", err) } if err := config.Validate(); err != nil { logrus.WithError(err).Fatal("invalid configuration") } - client, err := falcon.NewClient(&falcon.ApiConfig{ - ClientId: config.Falcon.ClientID, - ClientSecret: config.Falcon.Secret, - Cloud: falcon.Cloud(config.Falcon.CloudRegion), - Context: ctx, - }) - if err != nil { - logrus.WithError(err).Fatal("could not init falcon client") - } - - queryResult, err := client.SpotlightVulnerabilities.QueryVulnerabilities( - &spotlight_vulnerabilities.QueryVulnerabilitiesParams{ - Context: context.Background(), - Filter: "status:'open',remediation.ids:'*'", - Limit: &falconAPIMaxRecords, - }, - ) + falconMessages, err := falcon.GetMessages(config, ctx) if err != nil { - logrus. - WithField("error", fmt.Sprintf("%+v", err)). - Fatal("could not query vulnerabilities") - } - - if queryResult == nil { - logrus.Fatal("result is nil") + logrus.WithError(err).Fatal("could not get falcon messages") } - var vulnIDs []string - vulnIDs = append(vulnIDs, queryResult.GetPayload().Resources...) + // --- - if len(vulnIDs) == 0 { - logrus.Println("no vulnerabilities found") - os.Exit(0) - } - - logrus.WithField("vulns", len(vulnIDs)).Info("found vulnerabilities") + slackClient := slack.New(config.Slack.Token) - getResult, err := client.SpotlightVulnerabilities.GetVulnerabilities( - &spotlight_vulnerabilities.GetVulnerabilitiesParams{ - Ids: vulnIDs, - Context: context.Background(), - }, - ) + logrus.Debug("fetching slack users") + slackUsers, err := slackClient.GetUsers() if err != nil { - logrus. - WithField("error", err.Error()). - Fatal("could not query vulnerabilities") - } - - if len(getResult.GetPayload().Resources) != len(vulnIDs) { - logrus.Warn("result payload not as large as vuln list") + logrus.WithError(err).Fatal("could not fetch slack users") } - var hostTags []string - devices := map[string]UserDevice{} - - for _, vuln := range getResult.GetPayload().Resources { - if len(vuln.Remediation.Ids) == 0 { - //logrus.WithField("app", *vuln.App.ProductNameVersion).Warn("skipping vulnerability without remediation") - continue - } - - uniqueDeviceID, err := getUniqueDeviceID(*vuln.HostInfo) - if err != nil { - logrus.WithError(err).Error("could not calculate unique device id") - continue - } - - deviceFinding := UserDeviceFinding{ - ProductName: *vuln.App.ProductNameVersion, - CveID: *vuln.Cve.ID, - CveSeverity: *vuln.Cve.Severity, - MitigationAvailable: true, - TimestampFound: *vuln.CreatedTimestamp, - } - - if _, ok := devices[uniqueDeviceID]; !ok { - devices[uniqueDeviceID] = UserDevice{ - MachineName: fmt.Sprintf( - "%s %s", - *vuln.HostInfo.OsVersion, - *vuln.HostInfo.Hostname, - ), - Tags: vuln.HostInfo.Tags, - Findings: []UserDeviceFinding{}, - } + securityUserID := "" + for _, slackUser := range slackUsers { + if strings.EqualFold(slackUser.Profile.Email, config.Slack.SecurityUser) { + securityUserID = slackUser.ID + break } - - device := devices[uniqueDeviceID] - - found := false - for _, finding := range device.Findings { - if strings.EqualFold(finding.ProductName, deviceFinding.ProductName) { - found = true - break - } - } - - if !found { - device.Findings = append(device.Findings, deviceFinding) - devices[uniqueDeviceID] = device - } - - hostTags = append(hostTags, device.Tags...) } - if len(devices) == 0 { - logrus.Println("no vulnerabilities found with mitigations") - os.Exit(0) - } - - if len(hostTags) == 0 { - logrus.Fatal("no tags found on hosts") + if securityUserID == "" { + logrus.WithField("fallback_user", config.Slack.SecurityUser). + Fatal("could not find fallback user on Slack") } - logrus.WithField("devices", len(devices)).Info("found vulnerable devices") - - users := map[string]DeviceUser{} - - for _, device := range devices { - userEmail, err := findEmailTag(device.Tags, config.Email.Domain) - if err != nil { - logrus. - WithError(err). - WithField("tags", device.Tags). - WithField("prefix", tagEmailPrefix). - WithField("device", device.MachineName). - Warn("could extract user email tag, using fallback Slack user") - - userEmail = config.Slack.FallbackUser - } + logrus.WithField("users", len(slackUsers)).Info("found Slack users") - user, ok := users[userEmail] - if !ok { - users[userEmail] = DeviceUser{ - Email: userEmail, - Devices: []UserDevice{}, - } - } + var errorsToReport []error - user.Devices = append(user.Devices, device) - user.Email = userEmail - users[userEmail] = user - } - - logrus.Debugf("%+v", users) - - slackClient := slack.New(config.Slack.Token) - - logrus.Info("fetching slack users") - slackUsers, err := slackClient.GetUsers() - if err != nil { - logrus.WithError(err).Fatal("could not fetch slack users") - } - - for _, user := range users { - logrus.WithField("user", user.Email).Debug("handling user at risk") + for userEmail, falconResult := range falconMessages { + logrus.WithField("user", userEmail).Debug("handling user at risk") var theSlackUser slack.User for _, slackUser := range slackUsers { - if ! strings.EqualFold(user.Email, slackUser.Profile.Email) { + if !strings.EqualFold(userEmail, slackUser.Profile.Email) { continue } theSlackUser = slackUser } if theSlackUser.Name == "" { - logrus.WithField("user", user.Email).Error("slack user not found") + logrus.WithField("user", userEmail).Error("slack user not found") + errorsToReport = append(errorsToReport, errors.New("User not found on Slack: " + userEmail)) continue } if theSlackUser.IsBot { - logrus.WithField("user", user.Email).Error("user is a Slack bot") + logrus.WithField("user", userEmail).Error("user is a Slack bot") continue } - messageTemplate, err := template.New("message").Parse(config.Message) + slackMessage, err := user.BuildUserOverviewMessage(logrus.StandardLogger(), config, theSlackUser, falconMessages[falconResult.Email]) if err != nil { - logrus.WithError(err).Fatal("unable to parse message") - } - - variables := struct { - Slack slack.User - User DeviceUser - }{ - Slack: theSlackUser, - User: user, - } - - var buffer bytes.Buffer - if err := messageTemplate.Execute(&buffer, &variables); err != nil { - logrus.WithError(err).Fatal("could not parse message") + logrus.WithError(err).WithField("user", theSlackUser.Profile.Email).Error("could not generate user message") + continue } - if _, _, _, err := slackClient.SendMessage( - theSlackUser.ID, - slack.MsgOptionText(buffer.String(), false), - slack.MsgOptionAsUser(true), - ); err != nil { - logrus.WithError(err). - WithField("user", theSlackUser.Profile.Email). - Error("could not send slack message") - continue + if !*dryMode { + if _, _, _, err := slackClient.SendMessage( + theSlackUser.ID, + slack.MsgOptionText(slackMessage, false), + slack.MsgOptionAsUser(true), + ); err != nil { + logrus.WithError(err). + WithField("user", theSlackUser.Profile.Email). + Error("could not send slack message") + continue + } } logrus. - WithField("user", user.Email).WithField("devices", len(user.Devices)). + WithField("user", falconResult.Email).WithField("devices", len(falconResult.Devices)). Info("sent reminder on Slack") } + + /* + if len(falconMessages) == 0 { + logrus.Info("nothing to report, exiting") + os.Exit(0) + } + */ + + if config.Templates.SecurityOverviewMessage == "" { + logrus.Info("not sending a security overview") + os.Exit(0) + } + + overviewText, err := security.BuildSecurityOverviewMessage(logrus.StandardLogger(), *config, falconMessages, errorsToReport) + if err != nil { + logrus.WithError(err).Fatal("could not generate security overview") + } + + logrus.WithField("email", config.Slack.SecurityUser). + Debug("sending security report to security user") + + if _, _, _, err := slackClient.SendMessage( + securityUserID, slack.MsgOptionText(overviewText, false), slack.MsgOptionAsUser(true), + ); err != nil { + logrus.WithField("email", config.Slack.SecurityUser).WithError(err). + Fatal("could not send security overview to security user") + } + + logrus.WithField("email", config.Slack.SecurityUser).Info("sent security overview to security user") } diff --git a/config/config.go b/config/config.go index fb8721c..d4324b5 100644 --- a/config/config.go +++ b/config/config.go @@ -3,9 +3,9 @@ package config import ( "github.com/kelseyhightower/envconfig" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "gopkg.in/yaml.v2" "io/ioutil" - "log" ) const ( @@ -14,24 +14,29 @@ const ( type Config struct { Slack struct { - Token string `yaml:"token" env:"SLACK_TOKEN"` - FallbackUser string `yaml:"fallback_user" emv:"SLACK_FALLBACK_USER"` + Token string `yaml:"token" env:"SLACK_TOKEN"` + SecurityUser string `yaml:"security_user" emv:"SLACK_SECURITY_USER"` } `yaml:"slack"` Falcon struct { ClientID string `yaml:"clientid" env:"FALCON_CLIENT_ID"` Secret string `yaml:"secret" env:"FALCON_SECRET"` CloudRegion string `yaml:"cloud_region" env:"FALCON_CLOUD_REGION"` + + SkipNoMitigation bool `yaml:"skip_no_mitigation" env:"FALCON_SKIP_NO_MITIGATION"` } `yaml:"falcon"` Email struct { Domain string `yaml:"domain" env:"DOMAIN"` } `yaml:"email"` - Message string `yaml:"message" env:"MESSAGE"` + Templates struct { + UserMessage string `yaml:"user_message" env:"USER_MESSAGE"` + SecurityOverviewMessage string `yaml:"security_overview_message" env:"SECURITY_OVERVIEW_MESSAGE"` + } `yaml:"templates"` } -func LoadConfig(path string) (*Config, error) { +func LoadConfig(logger *logrus.Logger, path string) (*Config, error) { var config Config if path != "" { @@ -44,7 +49,7 @@ func LoadConfig(path string) (*Config, error) { return nil, errors.Wrap(err, "could not parse configuration file") } - log.Println("loaded configuration from " + path) + logger.Info("loaded configuration from " + path) } if err := envconfig.Process(appEnvPrefix, &config); err != nil { @@ -75,7 +80,7 @@ func (c *Config) Validate() error { return errors.New("missing email domain") } - if c.Message == "" { + if c.Templates.UserMessage == "" { return errors.New("missing message") } diff --git a/pkg/falcon/extractor.go b/pkg/falcon/extractor.go new file mode 100644 index 0000000..16eebdd --- /dev/null +++ b/pkg/falcon/extractor.go @@ -0,0 +1,249 @@ +package falcon + +import ( + "context" + "crypto/sha1" + "encoding/hex" + "encoding/json" + "fmt" + "github.com/pkg/errors" + "strings" + + "github.com/crowdstrike/gofalcon/falcon" + "github.com/crowdstrike/gofalcon/falcon/client/spotlight_vulnerabilities" + "github.com/crowdstrike/gofalcon/falcon/models" + "github.com/hazcod/crowdstrike-spotlight-slacker/config" + "github.com/sirupsen/logrus" +) + +const ( + tagEmailPrefix = "email/" + tagFalconPrefix = "FalconGroupingTags/" +) + +type FalconResult struct { + Email string + Devices []UserDevice +} + +type UserDevice struct { + MachineName string + Tags []string + Findings []UserDeviceFinding +} + +type UserDeviceFinding struct { + ProductName string + CveID string + CveSeverity string + MitigationAvailable bool + TimestampFound string +} + +func getUniqueDeviceID(hostInfo models.DomainAPIVulnerabilityHostInfoV2) (string, error) { + b, err := json.Marshal(&hostInfo) + if err != nil { + return "", err + } + hasher := sha1.New() + if _, err := hasher.Write(b); err != nil { + return "", err + } + return hex.EncodeToString(hasher.Sum(nil)), nil +} + +func findEmailTag(tags []string, emailHost string) (email string, err error) { + theTag := "" + + for _, tag := range tags { + tag = strings.ToLower(tag) + tag = strings.TrimLeft(tag, strings.ToLower(tagFalconPrefix)) + + logrus.WithField("tag", tag).Trace("looking at falcon tag") + + if !strings.HasPrefix(tag, tagEmailPrefix) { + continue + } + + if theTag != "" { + logrus. + WithField("tag", tag).WithField("email", theTag). + WithField("prefix", tagEmailPrefix). + Warn("multiple user tags found") + } + + theTag = strings.TrimLeft(tag, tagEmailPrefix) + } + + if theTag == "" { + 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, "/", ".") + + if !strings.Contains(email, "@") || !strings.Contains(email, ".") { + return "", errors.New("invalid email address: " + email) + } + + logrus.WithField("tag", theTag).WithField("email", email).Debug("converted tag to email") + + return email, nil +} + +func GetMessages(config *config.Config, ctx context.Context) (results map[string]FalconResult, err error) { + falconAPIMaxRecords := int64(400) + + results = map[string]FalconResult{} + + client, err := falcon.NewClient(&falcon.ApiConfig{ + ClientId: config.Falcon.ClientID, + ClientSecret: config.Falcon.Secret, + Cloud: falcon.Cloud(config.Falcon.CloudRegion), + Context: ctx, + }) + if err != nil { + return nil, errors.Wrap(err, "could not initialize Falcon client") + } + + queryResult, err := client.SpotlightVulnerabilities.QueryVulnerabilities( + &spotlight_vulnerabilities.QueryVulnerabilitiesParams{ + Context: context.Background(), + Filter: "status:'open',remediation.ids:'*'", + Limit: &falconAPIMaxRecords, + }, + ) + if err != nil { + return nil, errors.Wrap(err, "could not query vulnerabilities") + } + + if queryResult == nil { + return nil, errors.New("QueryVulnerabilities result was nil") + } + + var vulnIDs []string + vulnIDs = append(vulnIDs, queryResult.GetPayload().Resources...) + + if len(vulnIDs) == 0 { + return results, nil + } + + logrus.WithField("vulns", len(vulnIDs)).Info("found vulnerabilities") + + getResult, err := client.SpotlightVulnerabilities.GetVulnerabilities( + &spotlight_vulnerabilities.GetVulnerabilitiesParams{ + Ids: vulnIDs, + Context: context.Background(), + }, + ) + if err != nil || getResult == nil { + return nil, errors.Wrap(err, "could not get Falcon vulnerabilities") + } + + if len(getResult.GetPayload().Resources) != len(vulnIDs) { + logrus.Warn("result payload not as large as vuln list") + } + + var hostTags []string + devices := map[string]UserDevice{} + + for _, vuln := range getResult.GetPayload().Resources { + + if len(vuln.Remediation.Ids) == 0 && config.Falcon.SkipNoMitigation { + logrus.WithField("app", *vuln.App.ProductNameVersion).Debug("skipping vulnerability without remediation") + + continue + } + + uniqueDeviceID, err := getUniqueDeviceID(*vuln.HostInfo) + if err != nil { + logrus.WithError(err).Error("could not calculate unique device id") + + continue + } + + deviceFinding := UserDeviceFinding{ + ProductName: *vuln.App.ProductNameVersion, + CveID: *vuln.Cve.ID, + CveSeverity: *vuln.Cve.Severity, + MitigationAvailable: true, + TimestampFound: *vuln.CreatedTimestamp, + } + + logrus.Warnf("%+v", vuln.HostInfo.Tags) + + if _, ok := devices[uniqueDeviceID]; !ok { + devices[uniqueDeviceID] = UserDevice{ + MachineName: fmt.Sprintf( + "%s %s", + *vuln.HostInfo.OsVersion, + *vuln.HostInfo.Hostname, + ), + Tags: vuln.HostInfo.Tags, + Findings: []UserDeviceFinding{}, + } + } + + device := devices[uniqueDeviceID] + + findingExists := false + + for _, finding := range device.Findings { + if strings.EqualFold(finding.ProductName, deviceFinding.ProductName) { + findingExists = true + break + } + } + + if !findingExists { + device.Findings = append(device.Findings, deviceFinding) + } + + device.Tags = append(device.Tags, vuln.HostInfo.Tags...) + + devices[uniqueDeviceID] = device + + hostTags = append(hostTags, device.Tags...) + } + + if len(devices) == 0 { + return results, nil + } + + if len(hostTags) == 0 { + return nil, errors.New("no tags found on decices") + } + + logrus.WithField("devices", len(devices)).Info("found vulnerable devices") + + for _, device := range devices { + userEmail, err := findEmailTag(device.Tags, config.Email.Domain) + if err != nil { + logrus. + WithError(err). + WithField("tags", device.Tags). + WithField("prefix", tagEmailPrefix). + WithField("device", device.MachineName). + Warn("could extract user email tag, using fallback Slack user") + + userEmail = config.Slack.SecurityUser + } + + user, ok := results[userEmail] + if !ok { + results[userEmail] = FalconResult{ + Email: userEmail, + Devices: []UserDevice{}, + } + } + + user.Devices = append(user.Devices, device) + user.Email = userEmail + results[userEmail] = user + } + + logrus.Debugf("%+v", results) + + return results, nil +} diff --git a/pkg/overview/security/builder.go b/pkg/overview/security/builder.go new file mode 100644 index 0000000..01f2d50 --- /dev/null +++ b/pkg/overview/security/builder.go @@ -0,0 +1,37 @@ +package security + +import ( + "bytes" + "github.com/hazcod/crowdstrike-spotlight-slacker/config" + "github.com/hazcod/crowdstrike-spotlight-slacker/pkg/falcon" + "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) { + messageTemplate, err := template.New("message").Parse(config.Templates.SecurityOverviewMessage) + if err != nil { + return "", errors.Wrap(err, "unable to parse message") + } + + variables := struct { + Results map[string]falcon.FalconResult + Date time.Time + Errors []error + }{ + Date: time.Now(), + Results: falconResults, + Errors: reportedErrors, + } + + var buffer bytes.Buffer + if err := messageTemplate.Execute(&buffer, &variables); err != nil { + return "", errors.Wrap(err, "could not parse security overview") + } + + logrus.WithField("message", buffer.String()).Debug("built security overview message") + + return buffer.String(), nil +} diff --git a/pkg/overview/user/builder.go b/pkg/overview/user/builder.go new file mode 100644 index 0000000..9ed77e9 --- /dev/null +++ b/pkg/overview/user/builder.go @@ -0,0 +1,39 @@ +package user + +import ( + "bytes" + "github.com/hazcod/crowdstrike-spotlight-slacker/config" + "github.com/hazcod/crowdstrike-spotlight-slacker/pkg/falcon" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/slack-go/slack" + "html/template" +) + +func BuildUserOverviewMessage(logger *logrus.Logger, config *config.Config, slackUser slack.User, falconResult falcon.FalconResult) (string, error) { + if config.Templates.UserMessage == "" { + return "", errors.New("no user message template defined") + } + + messageTemplate, err := template.New("message").Parse(config.Templates.UserMessage) + if err != nil { + logrus.WithError(err).Fatal("unable to parse message") + } + + variables := struct { + Slack slack.User + User falcon.FalconResult + }{ + Slack: slackUser, + User: falconResult, + } + + var buffer bytes.Buffer + if err := messageTemplate.Execute(&buffer, &variables); err != nil { + logrus.WithError(err).Fatal("could not parse user message") + } + + logrus.WithField("message", buffer.String()).Debug("built user overview message") + + return buffer.String(), nil +} diff --git a/pkg/slack/message.go b/pkg/slack/message.go new file mode 100644 index 0000000..067b7bf --- /dev/null +++ b/pkg/slack/message.go @@ -0,0 +1,6 @@ +package slack + +type Message struct { + UserEmail string + Message string +}