diff --git a/README.md b/README.md index 2ea9a6d..8bf3b9a 100644 --- a/README.md +++ b/README.md @@ -30,19 +30,19 @@ go get -u github.com/sudosammy/knary ![knary go-ing](https://github.com/sudosammy/knary/raw/master/screenshots/run.png "knary go-ing") ## Testing -* HTTP(S) - `curl http://test.mycanary.com` & `curl https://test.mycanary.com` -* DNS - `dig test.dns.mycanary.com` +See & run `test_knary.sh` ## Blacklisting matches You might find systems that spam your knary even long after an engagement has ended. To stop these from cluttering your Slack channel knary supports a blacklist (location specified in `.env`). Add the offending subdomains or IP addresses separated by a newline: ``` +mycanary.com www.mycanary.com -dns.mycanary.com 171.244.140.247 ``` -This would stop knary from alerting on `www.mycanary.com` but not `another.www.mycanary.com`. Changes to this file will come into effect immediately without requiring a knary restart. +This would stop knary from alerting on `www.mycanary.com` but not `another.www.mycanary.com`. Changes to this file will require a knary restart. ## Config Options +Example config files can be found in `examples/` * `DNS` Enable/Disable the DNS canary * `HTTP` Enable/Disable the HTTP(S) canary * `BIND_ADDR` The IP address you want knary to listen on. Example input: `0.0.0.0` to bind to all addresses available @@ -53,6 +53,7 @@ This would stop knary from alerting on `www.mycanary.com` but not `another.www.m * `DNS_SERVER` __Optional__ The DNS server to use when asking `dns.{CANARY_DOMAIN}.`. This option is obsolete if `EXT_IP` is set. Default is Google's nameserver: `8.8.8.8` * `LOG_FILE` __Optional__ Location for a file that knary will log timestamped matches and some errors. Example input: `/home/me/knary.log` * `BLACKLIST_FILE` __Optional__ Location for a file containing subdomains (separated by newlines) that should be ignored by knary and not logged or posted to Slack. Example input: `blacklist.txt` +* `BLACKLIST_ALERTING` __Optional__ Disable alerting on items in the blacklist that haven't triggered in over a month. Set to `false` to disable this behaviour * `TIMEOUT` __Optional__ The timeout for reading the HTTP(S) request. Default is 2 seconds. Example input: `1` ### Webhooks diff --git a/VERSION b/VERSION index 359a5b9..50aea0e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.0 \ No newline at end of file +2.1.0 \ No newline at end of file diff --git a/libknary/util.go b/libknary/util.go index cd6c141..58519ac 100644 --- a/libknary/util.go +++ b/libknary/util.go @@ -4,12 +4,14 @@ import ( "bufio" "net/http" "os" + "strconv" "strings" + "time" "github.com/blang/semver" ) -func stringContains(stringA string, stringB string) bool { +func stringContains(stringA string, stringB string) bool { // this runs once a day return strings.Contains( strings.ToLower(stringA), strings.ToLower(stringB), @@ -23,6 +25,7 @@ func CheckUpdate(version string, githubVersion string, githubURL string) bool { updFail := "Could not check for updates: " + err.Error() Printy(updFail, 2) logger(updFail) + go sendMsg(":warning: " + updMsg) return false } @@ -32,6 +35,7 @@ func CheckUpdate(version string, githubVersion string, githubURL string) bool { updFail := "Could not check for updates: " + err.Error() Printy(updFail, 2) logger(updFail) + go sendMsg(":warning: " + updMsg) return false } @@ -49,10 +53,10 @@ func CheckUpdate(version string, githubVersion string, githubURL string) bool { } if running.Compare(current) != 0 { - updMsg := ":warning: Your version of knary is *" + version + "* & the latest is *" + current.String() + "* - upgrade your binary here: " + githubURL + updMsg := "Your version of knary is *" + version + "* & the latest is *" + current.String() + "* - upgrade your binary here: " + githubURL Printy(updMsg, 2) logger(updMsg) - go sendMsg(updMsg) + go sendMsg(":warning: " + updMsg) return true } } @@ -60,7 +64,16 @@ func CheckUpdate(version string, githubVersion string, githubURL string) bool { return false } -func inBlacklist(needles ...string) bool { +// map for blacklist +type blacklist struct { + domain string + lastHit time.Time +} + +var blacklistMap = map[int]blacklist{} + +func LoadBlacklist() bool { + // load blacklist file into struct on startup if _, err := os.Stat(os.Getenv("BLACKLIST_FILE")); os.IsNotExist(err) { if os.Getenv("DEBUG") == "true" { Printy("Blacklist file does not exist - ignoring", 3) @@ -77,13 +90,43 @@ func inBlacklist(needles ...string) bool { } scanner := bufio.NewScanner(blklist) - + count := 0 for scanner.Scan() { // foreach blacklist item - for _, needle := range needles { // foreach needle - if strings.Contains(needle, scanner.Text()) && !strings.Contains(needle, "."+scanner.Text()) { + blacklistMap[count] = blacklist{scanner.Text(), time.Now()} // add to struct + count++ + } + + Printy("Monitoring "+strconv.Itoa(count)+" items in blacklist", 1) + logger("Monitoring " + strconv.Itoa(count) + " items in blacklist") + return true +} + +func CheckLastHit() { // this runs once a day + if len(blacklistMap) != 0 { + // iterate through blacklist and look for items >30 days old + for i := range blacklistMap { // foreach blacklist item + expiryDate := blacklistMap[i].lastHit.AddDate(0, 0, 30) + + if time.Now().After(expiryDate) { // let 'em know it's old + go sendMsg(":wrench: Blacklist item `" + blacklistMap[i].domain + "` hasn't had a hit in >30 days. Consider removing it. Configure `BLACKLIST_ALERTING` to supress.") + logger("Blacklist item: " + blacklistMap[i].domain + " hasn't had a hit in >30 days. Consider removing it.") + Printy("Blacklist item: "+blacklistMap[i].domain+" hasn't had a hit in >30 days. Consider removing it.", 1) + } + } + } +} + +func inBlacklist(needles ...string) bool { + for _, needle := range needles { + for i := range blacklistMap { // foreach blacklist item + if strings.Contains(needle, blacklistMap[i].domain) && !strings.Contains(needle, "."+blacklistMap[i].domain) { // matches blacklist.domain or 1.1.1.1 but not x.blacklist.domain + updBL := blacklistMap[i] + updBL.lastHit = time.Now() // update last hit + blacklistMap[i] = updBL + if os.Getenv("DEBUG") == "true" { - Printy(scanner.Text()+" found in blacklist", 3) + Printy(blacklistMap[i].domain+" found in blacklist", 3) } return true } diff --git a/main.go b/main.go index f8b3f7c..bd241d4 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,7 @@ import ( ) const ( - VERSION = "2.0.0" + VERSION = "2.1.0" GITHUB = "https://github.com/sudosammy/knary" GITHUBVERSION = "https://raw.githubusercontent.com/sudosammy/knary/master/VERSION" ) @@ -38,7 +38,10 @@ func main() { for { select { case <-ticker.C: - libknary.CheckUpdate(VERSION, GITHUBVERSION, GITHUB) + libknary.CheckUpdate(VERSION, GITHUBVERSION, GITHUB) // check for updates + if os.Getenv("BLACKLIST_ALERTING") == "" || os.Getenv("BLACKLIST_ALERTING") == "true" { + libknary.CheckLastHit() // flag any old blacklist items + } case <-quit: ticker.Stop() return @@ -81,6 +84,9 @@ func main() { red.Println(`|_____|`) fmt.Println() + // load blacklist file + libknary.LoadBlacklist() + if os.Getenv("HTTP") == "true" { libknary.Printy("Listening for http(s)://*."+os.Getenv("CANARY_DOMAIN")+" requests", 1) } diff --git a/test_knary.sh b/test_knary.sh new file mode 100644 index 0000000..67573cb --- /dev/null +++ b/test_knary.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# https://stackoverflow.com/questions/394230/how-to-detect-the-os-from-a-bash-script/18434831 +domain=$1 +if [[ -z "$domain" ]]; then + echo "usage: $0 mycanary.com" + exit 1 +fi + +echo "----------------------------------" +echo "Tests running..." +echo "----------------------------------" + +if [[ "$OSTYPE" == "linux-gnu" ]]; then + curl "http://test.$domain" + curl "https://test.$domain" + dig "test.dns.$domain" + +elif [[ "$OSTYPE" == "darwin"* ]]; then + curl "http://test.$domain" + curl "https://test.$domain" + dig "test.dns.$domain" + +elif [[ "$OSTYPE" == "cygwin" ]]; then + curl "http://test.$domain" + curl "https://test.$domain" + nslookup "test.dns.$domain" + +elif [[ "$OSTYPE" == "msys" ]]; then + curl "http://test.$domain" + curl "https://test.$domain" + nslookup "test.dns.$domain" + +elif [[ "$OSTYPE" == "win32" ]]; then + # I'm not sure this can happen. + nslookup "test.dns.$domain" + +elif [[ "$OSTYPE" == "freebsd"* ]]; then + curl "http://test.$domain" + curl "https://test.$domain" + dig "test.dns.$domain" +else + echo "Unknown OS. Read script and run commands manually." +fi + +echo "----------------------------------" +echo "Check your webhook(s) for 3 hits!" +echo "----------------------------------" \ No newline at end of file