Skip to content

Commit

Permalink
custom webhooks and other webhook modifications (#43)
Browse files Browse the repository at this point in the history
* tune for optimal webhook behaviour

  * raises loglevel threshold for webhook notifications
  * remove ANSI color codes from HTTP payloads for cleaner notifications
  * modify docs/code to reflect generic capabilities of webhooks

* allow for custom webhook payloads

* match newlines with rest of project
  • Loading branch information
audibleblink committed May 29, 2020
1 parent 71f4b5f commit eadd127
Show file tree
Hide file tree
Showing 7 changed files with 341 additions and 325 deletions.
5 changes: 3 additions & 2 deletions README.md
Expand Up @@ -91,13 +91,14 @@ Local running is enabled with flag `--local`. It value should be directory to sc

### Config

The `config.yaml` file has 6 elements. A [default is provided](https://github.com/eth0izzle/shhgit/blob/master/config.yaml).
The `config.yaml` file has 7 elements. A [default is provided](https://github.com/eth0izzle/shhgit/blob/master/config.yaml).

```
github_access_tokens: # provide at least one token
- 'token one'
- 'token two'
slack_webhook: '' # url to your slack webhook. Found secrets will be sent here
webhook: '' # URL to a POST webhook.
webhook_payload: '' # Payload to POST to the webhook URL
blacklisted_extensions: [] # list of extensions to ignore
blacklisted_paths: [] # list of paths to ignore
blacklisted_entropy_extensions: [] # additional extensions to ignore for entropy checks
Expand Down
10 changes: 9 additions & 1 deletion config.yaml
@@ -1,6 +1,14 @@
github_access_tokens:
- ''
slack_webhook: ''
webhook: '' # URL to which the payload is POSTed

# This default payload will work for Slack and MatterMost.
# Consult your webhook API for additional configurations.
webhook_payload: |
{
"text": "%s"
}
blacklisted_extensions: [".exe", ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif", ".psd", ".xcf", ".zip", ".tar.gz", ".ttf", ".lock"]
blacklisted_paths: ["node_modules{sep}", "vendor{sep}bundle", "vendor{sep}cache"] # use {sep} for the OS' path seperator (i.e. / or \)
blacklisted_entropy_extensions: [".pem", "id_rsa", ".asc", ".ovpn", ".sqlite", ".sqlite3"] # additional extensions to skip entropy checks
Expand Down
179 changes: 90 additions & 89 deletions core/config.go
@@ -1,89 +1,90 @@
package core

import (
"errors"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"

"gopkg.in/yaml.v3"
)

type Config struct {
GitHubAccessTokens []string `yaml:"github_access_tokens"`
SlackWebhook string `yaml:"slack_webhook,omitempty"`
BlacklistedExtensions []string `yaml:"blacklisted_extensions"`
BlacklistedPaths []string `yaml:"blacklisted_paths"`
BlacklistedEntropyExtensions []string `yaml:"blacklisted_entropy_extensions"`
Signatures []ConfigSignature `yaml:"signatures"`
}

type ConfigSignature struct {
Name string `yaml:"name"`
Part string `yaml:"part"`
Match string `yaml:"match,omitempty"`
Regex string `yaml:"regex,omitempty"`
Verifier string `yaml:"verifier,omitempty"`
}

func ParseConfig(options *Options) (*Config, error) {
config := &Config{}
var (
data []byte
err error
)

if len(*options.ConfigPath) > 0 {
data, err = ioutil.ReadFile(path.Join(*options.ConfigPath, "config.yaml"))
if err != nil {
return config, err
}
} else {
// Trying to first find the configuration next to executable
// Helps e.g. with Drone where workdir is different than shhgit dir
ex, err := os.Executable()
dir := filepath.Dir(ex)
data, err = ioutil.ReadFile(path.Join(dir, "config.yaml"))
if err != nil {
dir, _ = os.Getwd()
data, err = ioutil.ReadFile(path.Join(dir, "config.yaml"))
if err != nil {
return config, err
}
}
}

err = yaml.Unmarshal(data, config)
if err != nil {
return config, err
}

if !options.LocalRun && (len(config.GitHubAccessTokens) < 1 || strings.TrimSpace(strings.Join(config.GitHubAccessTokens, "")) == "") {
return config, errors.New("You need to provide at least one GitHub Access Token. See https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line")
}

for i := 0; i < len(config.GitHubAccessTokens); i++ {
config.GitHubAccessTokens[i] = os.ExpandEnv(config.GitHubAccessTokens[i])
}

if len(config.SlackWebhook) > 0 {
config.SlackWebhook = os.ExpandEnv(config.SlackWebhook)
}

return config, nil
}

func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = Config{}
type plain Config

err := unmarshal((*plain)(c))

if err != nil {
return err
}

return nil
}
package core

import (
"errors"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"

"gopkg.in/yaml.v3"
)

type Config struct {
GitHubAccessTokens []string `yaml:"github_access_tokens"`
Webhook string `yaml:"webhook,omitempty"`
WebhookPayload string `yaml:"webhook_payload,omitempty"`
BlacklistedExtensions []string `yaml:"blacklisted_extensions"`
BlacklistedPaths []string `yaml:"blacklisted_paths"`
BlacklistedEntropyExtensions []string `yaml:"blacklisted_entropy_extensions"`
Signatures []ConfigSignature `yaml:"signatures"`
}

type ConfigSignature struct {
Name string `yaml:"name"`
Part string `yaml:"part"`
Match string `yaml:"match,omitempty"`
Regex string `yaml:"regex,omitempty"`
Verifier string `yaml:"verifier,omitempty"`
}

func ParseConfig(options *Options) (*Config, error) {
config := &Config{}
var (
data []byte
err error
)

if len(*options.ConfigPath) > 0 {
data, err = ioutil.ReadFile(path.Join(*options.ConfigPath, "config.yaml"))
if err != nil {
return config, err
}
} else {
// Trying to first find the configuration next to executable
// Helps e.g. with Drone where workdir is different than shhgit dir
ex, err := os.Executable()
dir := filepath.Dir(ex)
data, err = ioutil.ReadFile(path.Join(dir, "config.yaml"))
if err != nil {
dir, _ = os.Getwd()
data, err = ioutil.ReadFile(path.Join(dir, "config.yaml"))
if err != nil {
return config, err
}
}
}

err = yaml.Unmarshal(data, config)
if err != nil {
return config, err
}

if !options.LocalRun && (len(config.GitHubAccessTokens) < 1 || strings.TrimSpace(strings.Join(config.GitHubAccessTokens, "")) == "") {
return config, errors.New("You need to provide at least one GitHub Access Token. See https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line")
}

for i := 0; i < len(config.GitHubAccessTokens); i++ {
config.GitHubAccessTokens[i] = os.ExpandEnv(config.GitHubAccessTokens[i])
}

if len(config.Webhook) > 0 {
config.Webhook = os.ExpandEnv(config.Webhook)
}

return config, nil
}

func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = Config{}
type plain Config

err := unmarshal((*plain)(c))

if err != nil {
return err
}

return nil
}
2 changes: 1 addition & 1 deletion core/github.go
Expand Up @@ -40,7 +40,7 @@ func GetRepositories(session *Session) {
GetSession().Log.Fatal("GitHub API abused detected. Quitting...")
}

GetSession().Log.Important("Error getting GitHub events... trying again", err)
GetSession().Log.Warn("Error getting GitHub events... trying again", err)
}

if opt.Page == 0 {
Expand Down
18 changes: 12 additions & 6 deletions core/log.go
@@ -1,11 +1,11 @@
package core

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"regexp"
"strings"
"sync"

"github.com/fatih/color"
Expand Down Expand Up @@ -61,10 +61,10 @@ func (l *Logger) Log(level int, format string, args ...interface{}) {
fmt.Printf(format+"\n", args...)
}

if level > INFO && session.Config.SlackWebhook != "" {
values := map[string]string{"text": fmt.Sprintf(format+"\n", args...)}
jsonValue, _ := json.Marshal(values)
http.Post(session.Config.SlackWebhook, "application/json", bytes.NewBuffer(jsonValue))
if level > WARN && session.Config.Webhook != "" {
text := colorStrip(fmt.Sprintf(format, args...))
payload := fmt.Sprintf(session.Config.WebhookPayload, text)
http.Post(session.Config.Webhook, "application/json", strings.NewReader(payload))
}

if level == FATAL {
Expand Down Expand Up @@ -95,3 +95,9 @@ func (l *Logger) Info(format string, args ...interface{}) {
func (l *Logger) Debug(format string, args ...interface{}) {
l.Log(DEBUG, format, args...)
}

func colorStrip(str string) string {
ansi := "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
re := regexp.MustCompile(ansi)
return re.ReplaceAllString(str, "")
}
108 changes: 54 additions & 54 deletions core/options.go
@@ -1,54 +1,54 @@
package core

import (
"flag"
"os"
"path/filepath"
)

type Options struct {
Threads *int
Silent *bool
Debug *bool
MaximumRepositorySize *uint
MaximumFileSize *uint
CloneRepositoryTimeout *uint
EntropyThreshold *float64
MinimumStars *uint
PathChecks *bool
ProcessGists *bool
TempDirectory *string
CsvPath *string
SearchQuery *string
Local *string
LocalRun bool
ConfigPath *string
}

func ParseOptions() (*Options, error) {
options := &Options{
Threads: flag.Int("threads", 0, "Number of concurrent threads (default number of logical CPUs)"),
Silent: flag.Bool("silent", false, "Suppress all output except for errors"),
Debug: flag.Bool("debug", false, "Print debugging information"),
MaximumRepositorySize: flag.Uint("maximum-repository-size", 5120, "Maximum repository size to process in KB"),
MaximumFileSize: flag.Uint("maximum-file-size", 512, "Maximum file size to process in KB"),
CloneRepositoryTimeout: flag.Uint("clone-repository-timeout", 10, "Maximum time it should take to clone a repository in seconds. Increase this if you have a slower connection"),
EntropyThreshold: flag.Float64("entropy-threshold", 5.0, "Set to 0 to disable entropy checks"),
MinimumStars: flag.Uint("minimum-stars", 0, "Only process repositories with this many stars. Default 0 will ignore star count"),
PathChecks: flag.Bool("path-checks", true, "Set to false to disable checking of filepaths, i.e. just match regex patterns of file contents"),
ProcessGists: flag.Bool("process-gists", true, "Will watch and process Gists. Set to false to disable."),
TempDirectory: flag.String("temp-directory", filepath.Join(os.TempDir(), Name), "Directory to process and store repositories/matches"),
CsvPath: flag.String("csv-path", "", "CSV file path to log found secrets to. Leave blank to disable"),
SearchQuery: flag.String("search-query", "", "Specify a search string to ignore signatures and filter on files containing this string (regex compatible)"),
Local: flag.String("local", "", "Specify local directory (absolute path) which to scan. Scans only given directory recursively. No need to have Githib tokens with local run."),
ConfigPath: flag.String("config-path", "", "Searches for config.yaml from given directory. If not set, tries to find if from shhgit binary's and current directory"),
}

flag.Parse()

if len(*options.Local) > 0 {
options.LocalRun = true
}

return options, nil
}
package core

import (
"flag"
"os"
"path/filepath"
)

type Options struct {
Threads *int
Silent *bool
Debug *bool
MaximumRepositorySize *uint
MaximumFileSize *uint
CloneRepositoryTimeout *uint
EntropyThreshold *float64
MinimumStars *uint
PathChecks *bool
ProcessGists *bool
TempDirectory *string
CsvPath *string
SearchQuery *string
Local *string
LocalRun bool
ConfigPath *string
}

func ParseOptions() (*Options, error) {
options := &Options{
Threads: flag.Int("threads", 0, "Number of concurrent threads (default number of logical CPUs)"),
Silent: flag.Bool("silent", false, "Suppress all output except for errors"),
Debug: flag.Bool("debug", false, "Print debugging information"),
MaximumRepositorySize: flag.Uint("maximum-repository-size", 5120, "Maximum repository size to process in KB"),
MaximumFileSize: flag.Uint("maximum-file-size", 512, "Maximum file size to process in KB"),
CloneRepositoryTimeout: flag.Uint("clone-repository-timeout", 10, "Maximum time it should take to clone a repository in seconds. Increase this if you have a slower connection"),
EntropyThreshold: flag.Float64("entropy-threshold", 5.0, "Set to 0 to disable entropy checks"),
MinimumStars: flag.Uint("minimum-stars", 0, "Only process repositories with this many stars. Default 0 will ignore star count"),
PathChecks: flag.Bool("path-checks", true, "Set to false to disable checking of filepaths, i.e. just match regex patterns of file contents"),
ProcessGists: flag.Bool("process-gists", true, "Will watch and process Gists. Set to false to disable."),
TempDirectory: flag.String("temp-directory", filepath.Join(os.TempDir(), Name), "Directory to process and store repositories/matches"),
CsvPath: flag.String("csv-path", "", "CSV file path to log found secrets to. Leave blank to disable"),
SearchQuery: flag.String("search-query", "", "Specify a search string to ignore signatures and filter on files containing this string (regex compatible)"),
Local: flag.String("local", "", "Specify local directory (absolute path) which to scan. Scans only given directory recursively. No need to have Githib tokens with local run."),
ConfigPath: flag.String("config-path", "", "Searches for config.yaml from given directory. If not set, tries to find if from shhgit binary's and current directory"),
}

flag.Parse()

if len(*options.Local) > 0 {
options.LocalRun = true
}

return options, nil
}

0 comments on commit eadd127

Please sign in to comment.