From eadd127e1c48e5327c4618daa1733a4fc08d1536 Mon Sep 17 00:00:00 2001 From: Alex Flores Date: Fri, 29 May 2020 07:47:52 -0400 Subject: [PATCH] custom webhooks and other webhook modifications (#43) * 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 --- README.md | 5 +- config.yaml | 10 +- core/config.go | 179 ++++++++++++------------- core/github.go | 2 +- core/log.go | 18 ++- core/options.go | 108 +++++++-------- main.go | 344 ++++++++++++++++++++++++------------------------ 7 files changed, 341 insertions(+), 325 deletions(-) diff --git a/README.md b/README.md index 4150e5e..5a63c0c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/config.yaml b/config.yaml index 6831549..fa02ee0 100644 --- a/config.yaml +++ b/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 diff --git a/core/config.go b/core/config.go index cb400f7..a90c13a 100644 --- a/core/config.go +++ b/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 +} diff --git a/core/github.go b/core/github.go index a5d5fde..3ea8860 100644 --- a/core/github.go +++ b/core/github.go @@ -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 { diff --git a/core/log.go b/core/log.go index 8aa6331..edd3077 100644 --- a/core/log.go +++ b/core/log.go @@ -1,11 +1,11 @@ package core import ( - "bytes" - "encoding/json" "fmt" "net/http" "os" + "regexp" + "strings" "sync" "github.com/fatih/color" @@ -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 { @@ -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, "") +} diff --git a/core/options.go b/core/options.go index 31f2737..3ec6eb3 100644 --- a/core/options.go +++ b/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 +} diff --git a/main.go b/main.go index 58d34b8..7175c20 100644 --- a/main.go +++ b/main.go @@ -1,172 +1,172 @@ -package main - -import ( - "bufio" - "bytes" - "os" - "regexp" - "strings" - - "github.com/eth0izzle/shhgit/core" - "github.com/fatih/color" -) - -var session = core.GetSession() - -func ProcessRepositories() { - threadNum := *session.Options.Threads - - for i := 0; i < threadNum; i++ { - go func(tid int) { - - for { - repositoryId := <-session.Repositories - repo, err := core.GetRepository(session, repositoryId) - - if err != nil { - session.Log.Warn("Failed to retrieve repository %d: %s", repositoryId, err) - continue - } - - if repo.GetPermissions()["pull"] && - uint(repo.GetStargazersCount()) >= *session.Options.MinimumStars && - uint(repo.GetSize()) < *session.Options.MaximumRepositorySize { - - processRepositoryOrGist(repo.GetCloneURL()) - } - } - }(i) - } -} - -func ProcessGists() { - threadNum := *session.Options.Threads - - for i := 0; i < threadNum; i++ { - go func(tid int) { - for { - gistUrl := <-session.Gists - processRepositoryOrGist(gistUrl) - } - }(i) - } -} - -func processRepositoryOrGist(url string) { - var ( - matchedAny bool = false - ) - - dir := core.GetTempDir(core.GetHash(url)) - _, err := core.CloneRepository(session, url, dir) - - if err != nil { - session.Log.Debug("[%s] Cloning failed: %s", url, err.Error()) - os.RemoveAll(dir) - return - } - - session.Log.Debug("[%s] Cloning in to %s", url, strings.Replace(dir, *session.Options.TempDirectory, "", -1)) - matchedAny = checkSignatures(dir, url) - if !matchedAny { - os.RemoveAll(dir) - } -} - -func checkSignatures(dir string, url string) (matchedAny bool) { - for _, file := range core.GetMatchingFiles(dir) { - var ( - matches []string - relativeFileName string - ) - if strings.Contains(dir, *session.Options.TempDirectory) { - relativeFileName = strings.Replace(file.Path, *session.Options.TempDirectory, "", -1) - } else { - relativeFileName = strings.Replace(file.Path, dir, "", -1) - } - - if *session.Options.SearchQuery != "" { - queryRegex := regexp.MustCompile(*session.Options.SearchQuery) - for _, match := range queryRegex.FindAllSubmatch(file.Contents, -1) { - matches = append(matches, string(match[0])) - } - - if matches != nil { - count := len(matches) - m := strings.Join(matches, ", ") - session.Log.Important("[%s] %d %s for %s in file %s: %s", url, count, core.Pluralize(count, "match", "matches"), color.GreenString("Search Query"), relativeFileName, color.YellowString(m)) - session.WriteToCsv([]string{url, "Search Query", relativeFileName, m}) - } - } else { - for _, signature := range session.Signatures { - if matched, part := signature.Match(file); matched { - matchedAny = true - - if part == core.PartContents { - if matches = signature.GetContentsMatches(file); matches != nil { - count := len(matches) - m := strings.Join(matches, ", ") - session.Log.Important("[%s] %d %s for %s in file %s: %s", url, count, core.Pluralize(count, "match", "matches"), color.GreenString(signature.Name()), relativeFileName, color.YellowString(m)) - session.WriteToCsv([]string{url, signature.Name(), relativeFileName, m}) - } - } else { - if *session.Options.PathChecks { - session.Log.Important("[%s] Matching file %s for %s", url, color.YellowString(relativeFileName), color.GreenString(signature.Name())) - session.WriteToCsv([]string{url, signature.Name(), relativeFileName, ""}) - } - - if *session.Options.EntropyThreshold > 0 && file.CanCheckEntropy() { - scanner := bufio.NewScanner(bytes.NewReader(file.Contents)) - - for scanner.Scan() { - line := scanner.Text() - - if len(line) > 6 && len(line) < 100 { - entropy := core.GetEntropy(scanner.Text()) - - if entropy >= *session.Options.EntropyThreshold { - session.Log.Important("[%s] Potential secret in %s = %s", url, color.YellowString(relativeFileName), color.GreenString(scanner.Text())) - session.WriteToCsv([]string{url, signature.Name(), relativeFileName, scanner.Text()}) - } - } - } - } - } - } - } - } - - if !matchedAny { - os.Remove(file.Path) - } - } - return -} - -func main() { - if session.Options.LocalRun { - session.Log.Info("Scanning local dir %s with %s v%s. Loaded %d signatures.", *session.Options.Local, core.Name, core.Version, len(session.Signatures)) - rc := 0 - if checkSignatures(*session.Options.Local, *session.Options.Local) { - rc = 1 - } - os.Exit(rc) - } else { - session.Log.Info("%s v%s started. Loaded %d signatures. Using %d GitHub tokens and %d threads. Work dir: %s", core.Name, core.Version, len(session.Signatures), len(session.Clients), *session.Options.Threads, *session.Options.TempDirectory) - - if *session.Options.SearchQuery != "" { - session.Log.Important("Search Query '%s' given. Only returning matching results.", *session.Options.SearchQuery) - } - - go core.GetRepositories(session) - go ProcessRepositories() - - if *session.Options.ProcessGists { - go core.GetGists(session) - go ProcessGists() - } - - session.Log.Info("Press Ctrl+C to stop and exit.\n") - select {} - } -} +package main + +import ( + "bufio" + "bytes" + "os" + "regexp" + "strings" + + "github.com/eth0izzle/shhgit/core" + "github.com/fatih/color" +) + +var session = core.GetSession() + +func ProcessRepositories() { + threadNum := *session.Options.Threads + + for i := 0; i < threadNum; i++ { + go func(tid int) { + + for { + repositoryId := <-session.Repositories + repo, err := core.GetRepository(session, repositoryId) + + if err != nil { + session.Log.Warn("Failed to retrieve repository %d: %s", repositoryId, err) + continue + } + + if repo.GetPermissions()["pull"] && + uint(repo.GetStargazersCount()) >= *session.Options.MinimumStars && + uint(repo.GetSize()) < *session.Options.MaximumRepositorySize { + + processRepositoryOrGist(repo.GetCloneURL()) + } + } + }(i) + } +} + +func ProcessGists() { + threadNum := *session.Options.Threads + + for i := 0; i < threadNum; i++ { + go func(tid int) { + for { + gistUrl := <-session.Gists + processRepositoryOrGist(gistUrl) + } + }(i) + } +} + +func processRepositoryOrGist(url string) { + var ( + matchedAny bool = false + ) + + dir := core.GetTempDir(core.GetHash(url)) + _, err := core.CloneRepository(session, url, dir) + + if err != nil { + session.Log.Debug("[%s] Cloning failed: %s", url, err.Error()) + os.RemoveAll(dir) + return + } + + session.Log.Debug("[%s] Cloning in to %s", url, strings.Replace(dir, *session.Options.TempDirectory, "", -1)) + matchedAny = checkSignatures(dir, url) + if !matchedAny { + os.RemoveAll(dir) + } +} + +func checkSignatures(dir string, url string) (matchedAny bool) { + for _, file := range core.GetMatchingFiles(dir) { + var ( + matches []string + relativeFileName string + ) + if strings.Contains(dir, *session.Options.TempDirectory) { + relativeFileName = strings.Replace(file.Path, *session.Options.TempDirectory, "", -1) + } else { + relativeFileName = strings.Replace(file.Path, dir, "", -1) + } + + if *session.Options.SearchQuery != "" { + queryRegex := regexp.MustCompile(*session.Options.SearchQuery) + for _, match := range queryRegex.FindAllSubmatch(file.Contents, -1) { + matches = append(matches, string(match[0])) + } + + if matches != nil { + count := len(matches) + m := strings.Join(matches, ", ") + session.Log.Important("[%s] %d %s for %s in file %s: %s", url, count, core.Pluralize(count, "match", "matches"), color.GreenString("Search Query"), relativeFileName, color.YellowString(m)) + session.WriteToCsv([]string{url, "Search Query", relativeFileName, m}) + } + } else { + for _, signature := range session.Signatures { + if matched, part := signature.Match(file); matched { + matchedAny = true + + if part == core.PartContents { + if matches = signature.GetContentsMatches(file); matches != nil { + count := len(matches) + m := strings.Join(matches, ", ") + session.Log.Important("[%s] %d %s for %s in file %s: %s", url, count, core.Pluralize(count, "match", "matches"), color.GreenString(signature.Name()), relativeFileName, color.YellowString(m)) + session.WriteToCsv([]string{url, signature.Name(), relativeFileName, m}) + } + } else { + if *session.Options.PathChecks { + session.Log.Important("[%s] Matching file %s for %s", url, color.YellowString(relativeFileName), color.GreenString(signature.Name())) + session.WriteToCsv([]string{url, signature.Name(), relativeFileName, ""}) + } + + if *session.Options.EntropyThreshold > 0 && file.CanCheckEntropy() { + scanner := bufio.NewScanner(bytes.NewReader(file.Contents)) + + for scanner.Scan() { + line := scanner.Text() + + if len(line) > 6 && len(line) < 100 { + entropy := core.GetEntropy(scanner.Text()) + + if entropy >= *session.Options.EntropyThreshold { + session.Log.Important("[%s] Potential secret in %s = %s", url, color.YellowString(relativeFileName), color.GreenString(scanner.Text())) + session.WriteToCsv([]string{url, signature.Name(), relativeFileName, scanner.Text()}) + } + } + } + } + } + } + } + } + + if !matchedAny { + os.Remove(file.Path) + } + } + return +} + +func main() { + if session.Options.LocalRun { + session.Log.Info("Scanning local dir %s with %s v%s. Loaded %d signatures.", *session.Options.Local, core.Name, core.Version, len(session.Signatures)) + rc := 0 + if checkSignatures(*session.Options.Local, *session.Options.Local) { + rc = 1 + } + os.Exit(rc) + } else { + session.Log.Info("%s v%s started. Loaded %d signatures. Using %d GitHub tokens and %d threads. Work dir: %s", core.Name, core.Version, len(session.Signatures), len(session.Clients), *session.Options.Threads, *session.Options.TempDirectory) + + if *session.Options.SearchQuery != "" { + session.Log.Important("Search Query '%s' given. Only returning matching results.", *session.Options.SearchQuery) + } + + go core.GetRepositories(session) + go ProcessRepositories() + + if *session.Options.ProcessGists { + go core.GetGists(session) + go ProcessGists() + } + + session.Log.Info("Press Ctrl+C to stop and exit.\n") + select {} + } +}