From 963c05e8a54208666f84f1bc93e41f2784c9396d Mon Sep 17 00:00:00 2001 From: ayoul3 Date: Mon, 25 May 2020 19:35:43 +0200 Subject: [PATCH 01/11] breakdown for testing and add support of IDN domains --- .gitignore | 3 +- config.go | 73 ----------------- config_example.yaml | 7 -- go.mod | 14 ++-- go.sum | 109 ++++++++++++++++---------- lib/config.go | 92 ++++++++++++++++++++++ lib/lib.go | 165 +++++++++++++++++++++++++++++++++++++++ lib/lib_suite_test.go | 14 ++++ lib/lib_test.go | 68 ++++++++++++++++ slack.go => lib/slack.go | 27 +++---- main.go | 117 ++------------------------- res/cert.json | 1 + res/heartbeat.json | 1 + 13 files changed, 432 insertions(+), 259 deletions(-) delete mode 100644 config.go delete mode 100644 config_example.yaml create mode 100644 lib/config.go create mode 100644 lib/lib.go create mode 100644 lib/lib_suite_test.go create mode 100644 lib/lib_test.go rename slack.go => lib/slack.go (82%) create mode 100644 res/cert.json create mode 100644 res/heartbeat.json diff --git a/.gitignore b/.gitignore index 4e0573b..501c251 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ cercat config.yaml dist -dist/** \ No newline at end of file +dist/** +lib/*.xml \ No newline at end of file diff --git a/config.go b/config.go deleted file mode 100644 index 085e203..0000000 --- a/config.go +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import ( - "log" - "os" - "path" - "path/filepath" - "regexp" - - "github.com/spf13/viper" - kingpin "gopkg.in/alecthomas/kingpin.v2" -) - -type configuration struct { - Workers int - SlackWebHookURL string - SlackIconURL string - SlackUsername string - Regexp string - DisplayErrors string -} - -func getConfig() *configuration { - c := &configuration{} - - configFile := kingpin.Flag("configfile", "config file").Short('c').ExistingFile() - kingpin.Parse() - - v := viper.New() - v.SetDefault("SlackWebhookURL", "") - v.SetDefault("SlackIconURL", "") - v.SetDefault("SlackUsername", "Cercat") - v.SetDefault("Regexp", "") - v.SetDefault("Workers", 20) - v.SetDefault("DisplayErrors", "false") - - if *configFile != "" { - d, f := path.Split(*configFile) - if d == "" { - d = "." - } - v.SetConfigName(f[0 : len(f)-len(filepath.Ext(f))]) - v.AddConfigPath(d) - err := v.ReadInConfig() - if err != nil { - log.Printf("[ERROR] : Error when reading config file : %v\n", err) - os.Exit(1) - } - } - v.AutomaticEnv() - v.Unmarshal(c) - - if c.SlackUsername == "" { - c.SlackUsername = "Cercat" - } - if c.DisplayErrors == "" { - c.DisplayErrors = "false" - } - if c.Regexp == "" { - log.Println("[ERROR] : Regexp can't be empty") - os.Exit(1) - } - if _, err := regexp.Compile(c.Regexp); err != nil { - log.Println("[ERROR] : Bad regexp") - os.Exit(1) - } - if c.Workers < -1 { - log.Println("[ERROR] : Workers must be strictly a positive number") - os.Exit(1) - } - - return c -} diff --git a/config_example.yaml b/config_example.yaml deleted file mode 100644 index 87f2a04..0000000 --- a/config_example.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -SlackWebhookURL: "" #Slack Webhook URL -SlackIconURL: "" #Slack Icon (Avatar) URL -SlackUsername: "" #Slack Username -Regexp: ".*\\.fr$" #Regexp to match. Can't be empty. It uses Golang regexp format -Workers: 20 #Number of workers for consuming stream from CertStream -DisplayErrors: false #Enable/Disable display of errors in logs \ No newline at end of file diff --git a/go.mod b/go.mod index 0a3b691..2dbde8c 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,14 @@ module cercat go 1.14 require ( - github.com/CaliDog/certstream-go v0.0.0-20180219203951-6016c5462366 - github.com/falcosecurity/falcosidekick v0.0.0-20200128210241-efc9dea5ac3a github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect github.com/gobwas/pool v0.2.0 // indirect github.com/gobwas/ws v1.0.3 - github.com/google/pprof v0.0.0-20200413000643-b1a96885c1c6 // indirect - github.com/gorilla/websocket v1.4.2 - github.com/jmoiron/jsonq v0.0.0-20150511023944-e874b168d07e - github.com/likexian/whois-go v1.5.0 - github.com/likexian/whois-parser-go v1.10.4 - github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 - github.com/pkg/errors v0.9.1 + github.com/onsi/ginkgo v1.12.2 + github.com/onsi/gomega v1.10.1 + github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/sirupsen/logrus v1.2.0 github.com/spf13/viper v1.6.3 + github.com/stretchr/testify v1.4.0 // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 ) diff --git a/go.sum b/go.sum index 29813a1..a4482b5 100644 --- a/go.sum +++ b/go.sum @@ -1,40 +1,30 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/CaliDog/certstream-go v0.0.0-20180219203951-6016c5462366 h1:qjPX+NGqyjCTkoQqEctkfhU4C/B4LhFG0ugVLhM7Maw= -github.com/CaliDog/certstream-go v0.0.0-20180219203951-6016c5462366/go.mod h1:JBo69gi8JyPpZoLZgmZeXiq4o7Ib2qf2RiIxiWC0oYQ= -github.com/DataDog/datadog-go v2.3.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/aws/aws-sdk-go v1.23.10/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/emersion/go-sasl v0.0.0-20190704090222-36b50694675c/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= -github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= -github.com/emersion/go-smtp v0.11.2/go.mod h1:byi9Y32SuKwjTJt9DO2tTWYjtF3lEh154tE1AcaJQSY= -github.com/falcosecurity/falcosidekick v0.0.0-20200128210241-efc9dea5ac3a h1:lCsSKSqcpfm1WzZ2u+ZZWz/XvEpE7ekI7kBk1T0QFo8= -github.com/falcosecurity/falcosidekick v0.0.0-20200128210241-efc9dea5ac3a/go.mod h1:pcBs/GBsRah9wSPXvDlEAavbNBghsQAvau+JxZMoniU= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= @@ -53,44 +43,44 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20200413000643-b1a96885c1c6 h1:s/fMbk+RxwM7UzGFVT/QqULw08vuMLJ94qH1KZfNO9E= -github.com/google/pprof v0.0.0-20200413000643-b1a96885c1c6/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmoiron/jsonq v0.0.0-20150511023944-e874b168d07e h1:ZZCvgaRDZg1gC9/1xrsgaJzQUCQgniKtw0xjWywWAOE= -github.com/jmoiron/jsonq v0.0.0-20150511023944-e874b168d07e/go.mod h1:+rHyWac2R9oAZwFe1wGY2HBzFJJy++RHBg1cU23NkD8= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/likexian/gokit v0.21.11/go.mod h1:0WlTw7IPdiMtrwu0t5zrLM7XXik27Ey6MhUJHio2fVo= -github.com/likexian/gokit v0.23.1 h1:1P6J3d4r2D+V1C4xbdxm/pWP+L502CoHe5ttZsniHWM= -github.com/likexian/gokit v0.23.1/go.mod h1:/asXq96N3H5gVxyfyNuQO7HFoSorzcU+ZMEImyBGZB8= -github.com/likexian/whois-go v1.5.0 h1:ZqH1a1vc6Hx/PAJFn+tSnv3bxk0tv0Tch2MSc+5nUM0= -github.com/likexian/whois-go v1.5.0/go.mod h1:vczXVMiHUp5OlIg3mlrEGS92gSBuNg/VsU53MmBtV9c= -github.com/likexian/whois-parser-go v1.10.4 h1:1+fUgMT5jjH39RorfxNJjmBCdcerqB6oMKZDshCj57E= -github.com/likexian/whois-parser-go v1.10.4/go.mod h1:I3zHrhbq4XRmc3nn4xtWNafclqmZXJzW7tSx9KXlCwk= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -100,17 +90,22 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM= -github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.12.2 h1:Ke9m3h2Hu0wsZ45yewCqhYr3Z+emcNTuLY2nMWCkrSI= +github.com/onsi/ginkgo v1.12.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= @@ -122,8 +117,11 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -135,17 +133,18 @@ github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs= github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -153,48 +152,72 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e h1:9vRrk9YW2BTzLP0VCB9ZDjU4cPqkg+IDWL7XgxA1yxQ= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/lib/config.go b/lib/config.go new file mode 100644 index 0000000..5222314 --- /dev/null +++ b/lib/config.go @@ -0,0 +1,92 @@ +package lib + +import ( + "fmt" + "path" + "path/filepath" + "regexp" + "strings" + + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" + kingpin "gopkg.in/alecthomas/kingpin.v2" +) + +type Configuration struct { + Workers int + SlackWebHookURL string + SlackIconURL string + SlackUsername string + DomainName string + RegIP string + Regexp string + RegIDN string + DisplayErrors string +} + +const RegStrIP = `^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$` + +func GetConfig() *Configuration { + c := &Configuration{ + RegIP: RegStrIP, + } + + configFile := kingpin.Flag("configfile", "config file").Short('c').ExistingFile() + kingpin.Parse() + + v := viper.New() + v.SetDefault("SlackWebhookURL", "") + v.SetDefault("SlackIconURL", "") + v.SetDefault("SlackUsername", "Cercat") + v.SetDefault("DomainName", "") + v.SetDefault("Regexp", "") + v.SetDefault("Workers", 20) + v.SetDefault("DisplayErrors", "false") + + if *configFile != "" { + d, f := path.Split(*configFile) + if d == "" { + d = "." + } + v.SetConfigName(f[0 : len(f)-len(filepath.Ext(f))]) + v.AddConfigPath(d) + err := v.ReadInConfig() + if err != nil { + log.Fatalf("[ERROR] : Error when reading config file : %v\n", err) + } + } + v.AutomaticEnv() + v.Unmarshal(c) + + if c.SlackUsername == "" { + c.SlackUsername = "Cercat" + } + if c.DisplayErrors == "" || c.DisplayErrors == "false" { + log.SetLevel(log.DebugLevel) + } + if c.Regexp == "" { + log.Fatal("Regexp can't be empty") + } + if c.DomainName == "" { + log.Fatal("Specify the domain name to monitor for IDN homographs") + } + if _, err := regexp.Compile(c.Regexp); err != nil { + log.Fatal("Bad regexp") + } + if c.Workers < -1 { + log.Fatal("Workers must be strictly a positive number") + } + + c.RegIDN = BuildIDNRegex(c.DomainName) + + return c +} + +func BuildIDNRegex(name string) string { + if len(name) < 2 { + return "" + } + // Can detect up to two unicode characters in the domain name. + // To adjust according to false positive rate & name length + return fmt.Sprintf("[%s]{%d,%d}", strings.ToLower(name), len(name)-2, len(name)-1) +} diff --git a/lib/lib.go b/lib/lib.go new file mode 100644 index 0000000..8401ec8 --- /dev/null +++ b/lib/lib.go @@ -0,0 +1,165 @@ +package lib + +import ( + "context" + "encoding/json" + "fmt" + "net" + "regexp" + "strings" + "time" + + log "github.com/sirupsen/logrus" + + _ "net/http/pprof" + + _ "expvar" + + "github.com/gobwas/ws" + "github.com/gobwas/ws/wsutil" +) + +type Result struct { + Domain string `json:"domain"` + SAN []string `json:"SAN"` + Issuer string `json:"issuer"` + Addresses []string `json:"Addresses"` +} + +type Certificate struct { + MessageType string `json:"message_type"` + Data Data `json:"data"` +} + +type Data struct { + UpdateType string `json:"update_type"` + LeafCert LeafCert `json:"leaf_cert"` + Chain []LeafCert `json:"chain"` + CertIndex float32 `json:"cert_index"` + Seen float32 `json:"seen"` + Source map[string]string `json:"source"` +} + +type LeafCert struct { + Subject map[string]string `json:"subject"` + Extensions map[string]interface{} `json:"extensions"` + NotBefore float32 `json:"not_before"` + NotAfter float32 `json:"not_after"` + SerialNumber string `json:"serial_number"` + FingerPrint string `json:"fingerprint"` + AsDer string `json:"as_der"` + AllDomains []string `json:"all_domains"` +} + +// MsgChan is the communication channel between certCheckWorkers and LoopCheckCerts +var MsgChan chan []byte + +const certInput = "wss://certstream.calidog.io" + +// CertCheckWorker parses certificates and raises alert if matches config +func CertCheckWorker(config *Configuration) { + reg, _ := regexp.Compile(config.Regexp) + regIP, _ := regexp.Compile(config.RegIP) + regIDN, _ := regexp.Compile(config.RegIDN) + + for { + msg := <-MsgChan + + detailedCert, err := ParseResultCertificate(msg, regIP) + if err != nil { + log.Warnf("Error parsing message: %s", err) + continue + } + if detailedCert == nil { + continue + } + if !IsMatchingCert(detailedCert, reg, regIDN) { + continue + } + notify(config, *detailedCert) + } +} + +func ParseResultCertificate(msg []byte, regIP *regexp.Regexp) (*Result, error) { + var c Certificate + var r *Result + + err := json.Unmarshal(msg, &c) + if err != nil || c.MessageType == "heartbeat" { + return nil, err + } + + r = &Result{ + Domain: c.Data.LeafCert.Subject["CN"], + Issuer: c.Data.Chain[0].Subject["O"], + SAN: c.Data.LeafCert.AllDomains, + Addresses: []string{"N/A"}, + } + r.Addresses = FetchIPAddresses(r.Domain, regIP) + return r, nil +} + +func FetchIPAddresses(name string, regIP *regexp.Regexp) []string { + var ipsList []string + + ips, err := net.LookupIP(name) + if err != nil || len(ips) == 0 { + log.Debugf("Could not fetch IP addresses of domain %s", name) + return ipsList + } + for _, j := range ips { + if regIP.MatchString(j.String()) { + ipsList = append(ipsList, j.String()) + } + } + return ipsList +} + +func IsMatchingCert(cert *Result, reg, regIDN *regexp.Regexp) bool { + + domainList := append(cert.SAN, cert.Domain) + for _, domain := range domainList { + if isIDN(domain) && regIDN.MatchString(domain) { + return true + } + if reg.MatchString(domain) { + return true + } + } + return false +} + +func isIDN(domain string) bool { + return strings.HasPrefix(domain, "xn--") +} + +func notify(config *Configuration, detailedCert Result) { + b, _ := json.Marshal(detailedCert) + + if config.SlackWebHookURL != "" { + go newSlackPayload(detailedCert, config).Post(config) + } else { + fmt.Printf("A certificate for '%v' has been issued : %v\n", detailedCert.Domain, string(b)) + } +} + +// LoopCheckCerts Loops on messages from source +func LoopCheckCerts(config *Configuration) { + for { + conn, _, _, err := ws.DefaultDialer.Dial(context.Background(), certInput) + defer conn.Close() + if err != nil { + log.Warn("Error connecting to certstream! Sleeping a few seconds and reconnecting...") + time.Sleep(1 * time.Second) + continue + } + for { + msg, _, err := wsutil.ReadServerData(conn) + if err != nil { + log.Warn("Error reading message from CertStream") + break + } + MsgChan <- msg + } + } +} diff --git a/lib/lib_suite_test.go b/lib/lib_suite_test.go new file mode 100644 index 0000000..a556e7e --- /dev/null +++ b/lib/lib_suite_test.go @@ -0,0 +1,14 @@ +package lib_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/reporters" + . "github.com/onsi/gomega" +) + +func TestLib(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecsWithDefaultAndCustomReporters(t, "CerCat - Lib", []Reporter{reporters.NewJUnitReporter("test_report-lib.xml")}) +} diff --git a/lib/lib_test.go b/lib/lib_test.go new file mode 100644 index 0000000..1b915a3 --- /dev/null +++ b/lib/lib_test.go @@ -0,0 +1,68 @@ +package lib_test + +import ( + "cercat/lib" + "io/ioutil" + "regexp" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Handler", func() { + Describe("isMatchingCert", func() { + reg, _ := regexp.Compile(`.+\.com`) + regIDN, _ := regexp.Compile(lib.BuildIDNRegex("test")) + Describe("If certificate matches", func() { + cert := &lib.Result{Domain: "www.test.com"} + It("should return true", func() { + result := lib.IsMatchingCert(cert, reg, regIDN) + Expect(result).To(BeTrue()) + }) + }) + Describe("If alternative subject matches", func() { + cert := &lib.Result{Domain: "www.test.net", SAN: []string{"www.test.com"}} + It("should return true", func() { + result := lib.IsMatchingCert(cert, reg, regIDN) + Expect(result).To(BeTrue()) + }) + }) + Describe("If domain is IDN", func() { + cert := &lib.Result{Domain: "xn--tst-rdd.com"} + It("should return true", func() { + result := lib.IsMatchingCert(cert, reg, regIDN) + Expect(result).To(BeTrue()) + }) + }) + }) + Describe("parseResultCertificate", func() { + regIP, _ := regexp.Compile(lib.RegStrIP) + Describe("If cannot marshall message", func() { + msg := []byte("") + It("should return nil and error", func() { + result, err := lib.ParseResultCertificate(msg, regIP) + Expect(result).To(BeNil()) + Expect(err).To(HaveOccurred()) + }) + }) + Describe("If message is heartbeat", func() { + msg, _ := ioutil.ReadFile("../res/heartbeat.json") + It("should return nil", func() { + result, err := lib.ParseResultCertificate(msg, regIP) + Expect(result).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) + }) + }) + Describe("If message is regular", func() { + msg, _ := ioutil.ReadFile("../res/cert.json") + It("should return valid infos", func() { + result, err := lib.ParseResultCertificate(msg, regIP) + Expect(result.Domain).Should(Equal("baden-mueller.de")) + Expect(result.SAN).Should(Equal([]string{"baden-mueller.de", "www.baden-mueller.de"})) + Expect(result.Issuer).Should(Equal("Let's Encrypt")) + Expect(result.Addresses).Should(Equal([]string{"23.236.62.147"})) + Expect(err).ToNot(HaveOccurred()) + }) + }) + }) +}) diff --git a/slack.go b/lib/slack.go similarity index 82% rename from slack.go rename to lib/slack.go index b18bb30..065c684 100644 --- a/slack.go +++ b/lib/slack.go @@ -1,10 +1,12 @@ -package main +package lib import ( "bytes" "encoding/json" - "log" "net/http" + "strings" + + log "github.com/sirupsen/logrus" ) type slackAttachmentField struct { @@ -28,7 +30,7 @@ type slackPayload struct { Attachments []slackAttachment `json:"attachments,omitempty"` } -func newSlackPayload(r result) slackPayload { +func newSlackPayload(r Result, config *Configuration) slackPayload { var attachments []slackAttachment var attachment slackAttachment var fields []slackAttachmentField @@ -38,27 +40,20 @@ func newSlackPayload(r result) slackPayload { field.Value = r.Domain field.Short = true fields = append(fields, field) + field.Title = "Issuer" field.Value = r.Issuer field.Short = true fields = append(fields, field) - var s string - for _, i := range r.SAN { - s += i + ", " - } field.Title = "SAN" field.Short = false - field.Value = s[:len(s)-2] + field.Value = strings.Join(r.SAN, ",") fields = append(fields, field) - s = "" - for _, i := range r.Addresses { - s += i + ", " - } field.Title = "Addresses" field.Short = false - field.Value = s[:len(s)-2] + field.Value = strings.Join(r.Addresses, ",") fields = append(fields, field) attachment.Fields = fields @@ -74,13 +69,13 @@ func newSlackPayload(r result) slackPayload { Attachments: attachments} } -func (s slackPayload) Post() { +func (s slackPayload) Post(config *Configuration) { body, _ := json.Marshal(s) req, _ := http.NewRequest(http.MethodPost, config.SlackWebHookURL, bytes.NewBuffer(body)) req.Header.Add("Content-Type", "application/json") client := &http.Client{} _, err := client.Do(req) - if err != nil && config.DisplayErrors == "true" { - log.Println("[ERROR] : Slack Post error") + if err != nil { + log.Warn("Slack Post error") } } diff --git a/main.go b/main.go index d8a441b..bdccdb0 100644 --- a/main.go +++ b/main.go @@ -1,127 +1,24 @@ package main import ( - "context" - "encoding/json" - "log" - "net" - "regexp" - - "time" - + "cercat/lib" + _ "expvar" "net/http" _ "net/http/pprof" - - _ "expvar" - - "github.com/gobwas/ws" - "github.com/gobwas/ws/wsutil" ) -type result struct { - Domain string `json:"domain"` - SAN []string `json:"SAN"` - Issuer string `json:"issuer"` - Addresses []string `json:"Addresses"` -} - -type certificate struct { - MessageType string `json:"message_type"` - Data data `json:"data"` -} - -type data struct { - UpdateType string `json:"update_type"` - LeafCert leafCert `json:"leaf_cert"` - Chain []leafCert `json:"chain"` - CertIndex float32 `json:"cert_index"` - Seen float32 `json:"seen"` - Source map[string]string `json:"source"` -} - -type leafCert struct { - Subject map[string]string `json:"subject"` - Extensions map[string]string `json:"extensions"` - NotBefore float32 `json:"not_before"` - NotAfter float32 `json:"not_after"` - SerialNumber string `json:"serial_number"` - FingerPrint string `json:"fingerprint"` - AsDer string `json:"as_der"` - AllDomains []string `json:"all_domains"` -} - -var config *configuration +var config *lib.Configuration func init() { - config = getConfig() + config = lib.GetConfig() } func main() { go http.ListenAndServe("localhost:6060", nil) - - msgChan := make(chan []byte, 10) + lib.MsgChan = make(chan []byte, 10) for i := 0; i < config.Workers; i++ { - go certCheckWorker(msgChan) - } - - for { - conn, _, _, err := ws.DefaultDialer.Dial(context.Background(), "wss://certstream.calidog.io") - - if err != nil { - if config.DisplayErrors == "true" { - log.Println("[ERROR] : Error connecting to certstream! Sleeping a few seconds and reconnecting...") - } - conn.Close() - time.Sleep(1 * time.Second) - continue - } - for { - msg, _, err := wsutil.ReadServerData(conn) - if err != nil { - if config.DisplayErrors == "true" { - log.Println("[ERROR] : Error reading message from CertStream") - } - break - } - msgChan <- msg - } - conn.Close() + go lib.CertCheckWorker(config) } -} -func certCheckWorker(msgChan <-chan []byte) { - reg, _ := regexp.Compile(config.Regexp) - regIP, _ := regexp.Compile(`^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$`) - - var c certificate - for { - msg := <-msgChan - json.Unmarshal(msg, &c) - if c.MessageType == "heartbeat" { - continue - } - if reg.MatchString(c.Data.LeafCert.Subject["CN"]) { - r := result{ - Domain: c.Data.LeafCert.Subject["CN"], - Issuer: c.Data.Chain[0].Subject["O"], - SAN: c.Data.LeafCert.AllDomains, - Addresses: []string{"N/A"}, - } - ips, _ := net.LookupIP(r.Domain) - if len(ips) != 0 { - ipsList := []string{} - for _, j := range ips { - if regIP.MatchString(j.String()) { - ipsList = append(ipsList, j.String()) - } - } - r.Addresses = ipsList - } - b, _ := json.Marshal(r) - log.Printf("[INFO] : A certificate for '%v' has been issued : %v\n", r.Domain, string(b)) - if config.SlackWebHookURL != "" { - go newSlackPayload(r).Post() - } - } - } + lib.LoopCheckCerts(config) } diff --git a/res/cert.json b/res/cert.json new file mode 100644 index 0000000..7964f08 --- /dev/null +++ b/res/cert.json @@ -0,0 +1 @@ +{"data":{"cert_index":612101919,"cert_link":"http://ct.googleapis.com/logs/argon2020/ct/v1/get-entries?start=612101919&end=612101919","chain":[{"as_der":"MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0NlowSjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMTGkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EFq6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWAa6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIGCCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNvbTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9kc3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAwVAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcCARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwuY3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsFAAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJouM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwuX4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlGPfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==","extensions":{"authorityInfoAccess":"CA Issuers - URI:http://apps.identrust.com/roots/dstrootcax3.p7c\nOCSP - URI:http://isrg.trustid.ocsp.identrust.com\n","authorityKeyIdentifier":"keyid:C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10\n","basicConstraints":"CA:TRUE","certificatePolicies":"Policy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http://cps.root-x1.letsencrypt.org","crlDistributionPoints":"Full Name:\n URI:http://crl.identrust.com/DSTROOTCAX3CRL.crl","keyUsage":"Digital Signature, Key Cert Sign, C R L Sign","subjectKeyIdentifier":"A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1"},"fingerprint":"E6:A3:B4:5B:06:2D:50:9B:33:82:28:2D:19:6E:FE:97:D5:95:6C:CB","not_after":1615999246,"not_before":1458232846,"serial_number":"A0141420000015385736A0B85ECA708","subject":{"C":"US","CN":"Let's Encrypt Authority X3","L":null,"O":"Let's Encrypt","OU":null,"ST":null,"aggregated":"/C=US/CN=Let's Encrypt Authority X3/O=Let's Encrypt"}},{"as_der":"MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVowPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4Orz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEqOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9bxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaDaeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqGSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXrAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZzR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYoOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ","extensions":{"basicConstraints":"CA:TRUE","keyUsage":"Key Cert Sign, C R L Sign","subjectKeyIdentifier":"C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10"},"fingerprint":"DA:C9:02:4F:54:D8:F6:DF:94:93:5F:B1:73:26:38:CA:6A:D7:7C:13","not_after":1633010475,"not_before":970348339,"serial_number":"44AFB080D6A327BA893039862EF8406B","subject":{"C":null,"CN":"DST Root CA X3","L":null,"O":"Digital Signature Trust Co.","OU":null,"ST":null,"aggregated":"/CN=DST Root CA X3/O=Digital Signature Trust Co."}}],"leaf_cert":{"all_domains":["baden-mueller.de","www.baden-mueller.de"],"as_der":"MIIEezCCA2OgAwIBAgISA1Gm0sew3z+8nJrpo5Jj1naBMA0GCSqGSIb3DQEBCwUAMEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQDExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0yMDA1MjQxODE3NDhaFw0yMDA4MjIxODE3NDhaMBsxGTAXBgNVBAMTEGJhZGVuLW11ZWxsZXIuZGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCSDtXT7WQQaNmnaH6+BxHwEz7eqHdTx/02HV7x/q9oozIKfWnfc4A3glkMdnJtZUjLlbV4sgAO4MBDNo65Qsq4L/GesRsVTczmYcAxnrfp8e/eK7wF08oqCvdHddXSHD82aXe/6Y6a3hiLEG+oBMDfG1Skwyt7NGNySlenz3EYEbc35IVoFKIkp2CyMV/nkKQPCgQBL10niEiQd9Q9bHDQJZsBtW59VVCy5K5kIPo6P5v295PCt0WTUppXagY2G/YGpQOmvsjl9MFjMZc4yOOd3RhGhcr2jgd9iF04TvownTxvQAU1EbKcXDHcoPVmhH5zDeiN1JbLNpW2wMf5Vr9VAgMBAAGjggGIMIIBhDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFE8R8swxvB64KS8VqcCaUcMFpEjAMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUFBwEBBGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNyeXB0Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNyeXB0Lm9yZy8wMQYDVR0RBCowKIIQYmFkZW4tbXVlbGxlci5kZYIUd3d3LmJhZGVuLW11ZWxsZXIuZGUwTAYDVR0gBEUwQzAIBgZngQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcwEwYKKwYBBAHWeQIEAwEB/wQCBQAwDQYJKoZIhvcNAQELBQADggEBAIUMEOxJNwvA/pWwFm0BR0HClGdzSC1vQprBaZ6cedUM6b/wfAiCPNJXCvJyrfhJp4T1GcnwU5VnMLPE1/nwJ6LY3My86/M+eQn/3HRuXu3p1GFpp5k2cXsHB7VRlw5X78XVvsnYKc6giwOan7L8fL136EcplQTZRc/5qu9hvazeBOBQQc/lCeWceWz0ZDDVbU2IGvY6aF/SAQREOSq8jLVpEoXB0zwq3dXeEi+PfC2Ea03eOpo1y11nmRYB5Usi/GjMi7oXuBxVQMolJXJj38ziJcp1TT1sv2Ha/00F+Pudo54w1NEo04DbDD9yB2H9wTlMM4YsArmD3K22OGA8wNE=","extensions":{"authorityInfoAccess":"CA Issuers - URI:http://cert.int-x3.letsencrypt.org/\nOCSP - URI:http://ocsp.int-x3.letsencrypt.org\n","authorityKeyIdentifier":"keyid:A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1\n","basicConstraints":"CA:FALSE","certificatePolicies":"Policy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http://cps.letsencrypt.org","ctlPoisonByte":true,"extendedKeyUsage":"TLS Web server authentication, TLS Web client authentication","keyUsage":"Digital Signature, Key Encipherment","subjectAltName":"DNS:www.baden-mueller.de, DNS:baden-mueller.de","subjectKeyIdentifier":"4F:11:F2:CC:31:BC:1E:B8:29:2F:15:A9:C0:9A:51:C3:05:A4:48:C0"},"fingerprint":"64:BF:49:41:3B:7A:FD:5D:C1:04:D9:44:64:9D:1C:25:13:A2:49:86","not_after":1598120268,"not_before":1590344268,"serial_number":"351A6D2C7B0DF3FBC9C9AE9A39263D67681","subject":{"C":null,"CN":"baden-mueller.de","L":null,"O":null,"OU":null,"ST":null,"aggregated":"/CN=baden-mueller.de"}},"seen":1590347943.736608,"source":{"name":"Google 'Argon2020' log","url":"ct.googleapis.com/logs/argon2020/"},"update_type":"PrecertLogEntry"},"message_type":"certificate_update"} \ No newline at end of file diff --git a/res/heartbeat.json b/res/heartbeat.json new file mode 100644 index 0000000..a1bcf74 --- /dev/null +++ b/res/heartbeat.json @@ -0,0 +1 @@ +{"data":{"cert_index":612101919,"cert_link":"http://ct.googleapis.com/logs/argon2020/ct/v1/get-entries?start=612101919&end=612101919","chain":[{"as_der":"MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0NlowSjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMTGkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EFq6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWAa6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIGCCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNvbTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9kc3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAwVAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcCARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwuY3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsFAAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJouM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwuX4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlGPfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==","extensions":{"authorityInfoAccess":"CA Issuers - URI:http://apps.identrust.com/roots/dstrootcax3.p7c\nOCSP - URI:http://isrg.trustid.ocsp.identrust.com\n","authorityKeyIdentifier":"keyid:C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10\n","basicConstraints":"CA:TRUE","certificatePolicies":"Policy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http://cps.root-x1.letsencrypt.org","crlDistributionPoints":"Full Name:\n URI:http://crl.identrust.com/DSTROOTCAX3CRL.crl","keyUsage":"Digital Signature, Key Cert Sign, C R L Sign","subjectKeyIdentifier":"A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1"},"fingerprint":"E6:A3:B4:5B:06:2D:50:9B:33:82:28:2D:19:6E:FE:97:D5:95:6C:CB","not_after":1615999246,"not_before":1458232846,"serial_number":"A0141420000015385736A0B85ECA708","subject":{"C":"US","CN":"Let's Encrypt Authority X3","L":null,"O":"Let's Encrypt","OU":null,"ST":null,"aggregated":"/C=US/CN=Let's Encrypt Authority X3/O=Let's Encrypt"}},{"as_der":"MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVowPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4Orz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEqOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9bxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaDaeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqGSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXrAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZzR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYoOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ","extensions":{"basicConstraints":"CA:TRUE","keyUsage":"Key Cert Sign, C R L Sign","subjectKeyIdentifier":"C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10"},"fingerprint":"DA:C9:02:4F:54:D8:F6:DF:94:93:5F:B1:73:26:38:CA:6A:D7:7C:13","not_after":1633010475,"not_before":970348339,"serial_number":"44AFB080D6A327BA893039862EF8406B","subject":{"C":null,"CN":"DST Root CA X3","L":null,"O":"Digital Signature Trust Co.","OU":null,"ST":null,"aggregated":"/CN=DST Root CA X3/O=Digital Signature Trust Co."}}],"leaf_cert":{"all_domains":["baden-mueller.de","www.baden-mueller.de"],"as_der":"MIIEezCCA2OgAwIBAgISA1Gm0sew3z+8nJrpo5Jj1naBMA0GCSqGSIb3DQEBCwUAMEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQDExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0yMDA1MjQxODE3NDhaFw0yMDA4MjIxODE3NDhaMBsxGTAXBgNVBAMTEGJhZGVuLW11ZWxsZXIuZGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCSDtXT7WQQaNmnaH6+BxHwEz7eqHdTx/02HV7x/q9oozIKfWnfc4A3glkMdnJtZUjLlbV4sgAO4MBDNo65Qsq4L/GesRsVTczmYcAxnrfp8e/eK7wF08oqCvdHddXSHD82aXe/6Y6a3hiLEG+oBMDfG1Skwyt7NGNySlenz3EYEbc35IVoFKIkp2CyMV/nkKQPCgQBL10niEiQd9Q9bHDQJZsBtW59VVCy5K5kIPo6P5v295PCt0WTUppXagY2G/YGpQOmvsjl9MFjMZc4yOOd3RhGhcr2jgd9iF04TvownTxvQAU1EbKcXDHcoPVmhH5zDeiN1JbLNpW2wMf5Vr9VAgMBAAGjggGIMIIBhDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFE8R8swxvB64KS8VqcCaUcMFpEjAMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUFBwEBBGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNyeXB0Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNyeXB0Lm9yZy8wMQYDVR0RBCowKIIQYmFkZW4tbXVlbGxlci5kZYIUd3d3LmJhZGVuLW11ZWxsZXIuZGUwTAYDVR0gBEUwQzAIBgZngQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcwEwYKKwYBBAHWeQIEAwEB/wQCBQAwDQYJKoZIhvcNAQELBQADggEBAIUMEOxJNwvA/pWwFm0BR0HClGdzSC1vQprBaZ6cedUM6b/wfAiCPNJXCvJyrfhJp4T1GcnwU5VnMLPE1/nwJ6LY3My86/M+eQn/3HRuXu3p1GFpp5k2cXsHB7VRlw5X78XVvsnYKc6giwOan7L8fL136EcplQTZRc/5qu9hvazeBOBQQc/lCeWceWz0ZDDVbU2IGvY6aF/SAQREOSq8jLVpEoXB0zwq3dXeEi+PfC2Ea03eOpo1y11nmRYB5Usi/GjMi7oXuBxVQMolJXJj38ziJcp1TT1sv2Ha/00F+Pudo54w1NEo04DbDD9yB2H9wTlMM4YsArmD3K22OGA8wNE=","extensions":{"authorityInfoAccess":"CA Issuers - URI:http://cert.int-x3.letsencrypt.org/\nOCSP - URI:http://ocsp.int-x3.letsencrypt.org\n","authorityKeyIdentifier":"keyid:A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1\n","basicConstraints":"CA:FALSE","certificatePolicies":"Policy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http://cps.letsencrypt.org","ctlPoisonByte":true,"extendedKeyUsage":"TLS Web server authentication, TLS Web client authentication","keyUsage":"Digital Signature, Key Encipherment","subjectAltName":"DNS:www.baden-mueller.de, DNS:baden-mueller.de","subjectKeyIdentifier":"4F:11:F2:CC:31:BC:1E:B8:29:2F:15:A9:C0:9A:51:C3:05:A4:48:C0"},"fingerprint":"64:BF:49:41:3B:7A:FD:5D:C1:04:D9:44:64:9D:1C:25:13:A2:49:86","not_after":1598120268,"not_before":1590344268,"serial_number":"351A6D2C7B0DF3FBC9C9AE9A39263D67681","subject":{"C":null,"CN":"baden-mueller.de","L":null,"O":null,"OU":null,"ST":null,"aggregated":"/CN=baden-mueller.de"}},"seen":1590347943.736608,"source":{"name":"Google 'Argon2020' log","url":"ct.googleapis.com/logs/argon2020/"},"update_type":"PrecertLogEntry"},"message_type":"heartbeat"} \ No newline at end of file From 466fd94d8ebf54ef0842ff796f62a339f4f455a1 Mon Sep 17 00:00:00 2001 From: "thomas.labarussias" Date: Wed, 27 May 2020 01:11:47 +0200 Subject: [PATCH 02/11] add homoglyph map + remove IDN regexp --- example.yaml | 8 ++++++++ go.mod | 1 + go.sum | 4 ++++ lib/config.go | 22 +++++----------------- lib/homoglyph.go | 16 ++++++++++++++++ lib/lib.go | 12 +++++------- trace | Bin 6748 -> 0 bytes trace.out | Bin 77522 -> 0 bytes 8 files changed, 39 insertions(+), 24 deletions(-) create mode 100644 example.yaml create mode 100644 lib/homoglyph.go delete mode 100644 trace delete mode 100644 trace.out diff --git a/example.yaml b/example.yaml new file mode 100644 index 0000000..32499ef --- /dev/null +++ b/example.yaml @@ -0,0 +1,8 @@ +--- +SlackWebhookURL: "" #Slack Webhook URL +SlackIconURL: "" #Slack Icon (Avatar) URL +SlackUsername: "" #Slack Username +Regexp: ".*\\.fr$" #Regexp to match. Can't be empty. It uses Golang regexp format +DomainName: test +Workers: 20 #Number of workers for consuming stream from CertStream +DisplayErrors: false #Enable/Disable display of errors in logs \ No newline at end of file diff --git a/go.mod b/go.mod index 2dbde8c..a487656 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/onsi/ginkgo v1.12.2 github.com/onsi/gomega v1.10.1 github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/picatz/homoglyphr v0.0.0-20180114170158-6e9a0e190785 github.com/sirupsen/logrus v1.2.0 github.com/spf13/viper v1.6.3 github.com/stretchr/testify v1.4.0 // indirect diff --git a/go.sum b/go.sum index a4482b5..e89ab6a 100644 --- a/go.sum +++ b/go.sum @@ -104,6 +104,8 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/picatz/homoglyphr v0.0.0-20180114170158-6e9a0e190785 h1:h1zv5J8K6Hi22jgCuXHJJF+sKG99kSfJO6aJEFJSLGM= +github.com/picatz/homoglyphr v0.0.0-20180114170158-6e9a0e190785/go.mod h1:XC/aunjQY/D2krxYQwCI6ijxR75grw1/keXATRNWX+4= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -183,6 +185,7 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7 golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -212,6 +215,7 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/lib/config.go b/lib/config.go index 5222314..ad5393e 100644 --- a/lib/config.go +++ b/lib/config.go @@ -1,17 +1,16 @@ package lib import ( - "fmt" "path" "path/filepath" "regexp" - "strings" log "github.com/sirupsen/logrus" "github.com/spf13/viper" kingpin "gopkg.in/alecthomas/kingpin.v2" ) +// Configuration represents a configuration element type Configuration struct { Workers int SlackWebHookURL string @@ -20,15 +19,15 @@ type Configuration struct { DomainName string RegIP string Regexp string - RegIDN string DisplayErrors string } -const RegStrIP = `^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$` +const regStrIP = `^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$` +// GetConfig provides a Configuration func GetConfig() *Configuration { c := &Configuration{ - RegIP: RegStrIP, + RegIP: regStrIP, } configFile := kingpin.Flag("configfile", "config file").Short('c').ExistingFile() @@ -73,20 +72,9 @@ func GetConfig() *Configuration { if _, err := regexp.Compile(c.Regexp); err != nil { log.Fatal("Bad regexp") } - if c.Workers < -1 { + if c.Workers < 1 { log.Fatal("Workers must be strictly a positive number") } - c.RegIDN = BuildIDNRegex(c.DomainName) - return c } - -func BuildIDNRegex(name string) string { - if len(name) < 2 { - return "" - } - // Can detect up to two unicode characters in the domain name. - // To adjust according to false positive rate & name length - return fmt.Sprintf("[%s]{%d,%d}", strings.ToLower(name), len(name)-2, len(name)-1) -} diff --git a/lib/homoglyph.go b/lib/homoglyph.go new file mode 100644 index 0000000..9a1a315 --- /dev/null +++ b/lib/homoglyph.go @@ -0,0 +1,16 @@ +package lib + +import ( + "github.com/picatz/homoglyphr" +) + +func getHomoglyphMap() map[string]string { + alphabet := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"} + homoglyph := map[string]string{} + for _, letter := range alphabet { + for i := range homoglyphr.StreamAllRelatedCharacters(letter) { + homoglyph[i] = letter + } + } + return homoglyph +} diff --git a/lib/lib.go b/lib/lib.go index 8401ec8..8c5c241 100644 --- a/lib/lib.go +++ b/lib/lib.go @@ -60,7 +60,6 @@ const certInput = "wss://certstream.calidog.io" func CertCheckWorker(config *Configuration) { reg, _ := regexp.Compile(config.Regexp) regIP, _ := regexp.Compile(config.RegIP) - regIDN, _ := regexp.Compile(config.RegIDN) for { msg := <-MsgChan @@ -73,7 +72,7 @@ func CertCheckWorker(config *Configuration) { if detailedCert == nil { continue } - if !IsMatchingCert(detailedCert, reg, regIDN) { + if !IsMatchingCert(detailedCert, reg) { continue } notify(config, *detailedCert) @@ -115,13 +114,12 @@ func FetchIPAddresses(name string, regIP *regexp.Regexp) []string { return ipsList } -func IsMatchingCert(cert *Result, reg, regIDN *regexp.Regexp) bool { - +func IsMatchingCert(cert *Result, reg *regexp.Regexp) bool { domainList := append(cert.SAN, cert.Domain) for _, domain := range domainList { - if isIDN(domain) && regIDN.MatchString(domain) { - return true - } + // if isIDN(domain) { + // return true + // } if reg.MatchString(domain) { return true } diff --git a/trace b/trace deleted file mode 100644 index c1b161c32b9663d16e82115ed707b59d5c9e059a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6748 zcmb7Id013emaq5fzWZK1HU-oq+KH`}ic6ImlgZXe8|QR-x;r!7c_uUI^u$Iekd}a5 zV3z5bX%qznkwruiMGa^PD1zcHf)pCJa>o@FQE^w?6{Ga;7Gl!#&G)tbL)~|N_ndp~ z*)Nf?J%`$c4(%E53=MObOr{a0OL?wIZkd=m{nuG$>9c^&e4Rw@EYA26M$?&zA7M0m zfx`H1jrKQ48o#d5$|_l_SIQOUYl`?!X6!cy@gj*9ON^gpl!%`hjHWD=O|CbXDWWqo zHc3JJJ5J{`#_Lp?wp%i}`T_Zf86`l9Ic<>{ziFoBZXknzyvK}c7Q|Oc^ev>7DCEvj zOs#d_>`Aa;Dfj08&%&oh%phBhiK zcm$|SKyNT(GOlt|rs( zqN>YW<4Ha=?GWfSI!!NtQD*p1ZoXpTCm9`l;A0Ye%bRid8b;ah8Lv=i4WuS8x-?fd z)w9ppznUV_VeIYj{8Ub<6V00I6=(rB8k0avS7`GJ2>6VC#rR$eooSP_diDo4$`ldK zjID@@k+f_-Gz_DHml$8hXz5Wzz$KO3d9wGsdyR;|GDe&C;rvf2y9l3Ej3zf@TZKla z3-NuFQ9_Z~#H$oqGzZs8Q0UQQB*+zCTCpEuZgMK!huUT3*Xx z4xP?CKnNA8bSe)9o#;dRS|MkO4>j)r-lEawS(=IO*Qlr&e613lodt8WeA0Qg!2_{awQ0d)Pi^h#5&y@A8S;qTmnBv0V)QR06*UKqoq*)5vP_L z@MEQ5dxsf|0bP-4nQG#z6lzS>m}|a(USr0J_Rv`eF1JDlU>bz~gN=~8M(lW(85v+c zDbx30u2ZP#nrd=6fhBO>Qd->CAi`N-GMgu5Uqt4daw*ZUM$hf2O>yx zn%jz7-?Y%<)eyHwrJR$n=E+W!Q2-fve$;ph_%@x==Y#JErzN>40nI9vq=CPPQ)Pm% zQkKk?!b)(ckp>k?WUAYMvy+9!h0tpSh__|RfTfXp)rV17k_0x5F(U_9fl8T)xJM*t z_aMXrTCRm^ab_E%S@$6;MeG;Jnh0!5CrV65DlQVlSD8_V6yGgV8FX(}C>ypbM6x2@ zUGHLp8xpUgX5^~0t$=B+4?zore?ZETkfh$av}vNOxqb~q1o${~HsVS`y_ohZEkJof zx+OCz+Jh_Efdnw)1U46Gv>I1xF;iMH^zIB~OnZJKr8hoAeCA6dq&f52JDQ0PrZp`f z#?ilN{J5E>Toz@Y(_I&Q^Q}a?%M^{bX@uP2iGGy39eZn4`mP4w3mNL6iKk0cv>u;% zj9Qz(y+uGJV5%sZ5iKv8z#~jgJwd zS2bGj5QVWpqieGiR<9H(?B}M4aj0y_h$eJ9R7EsB#4q2#D1D{5-HKh2+pr?)=}gF8 z!02R>+2pN&xc&neCP|^k(6ZS~JL_PW?l27E-ADq{Hiecz>Ip^_u&OJ80m}xq5ZE1s zP9Y9pysQ(z-T~B!8EYZ%0;8+&E-Zb&9u9jC$X(>eML>%fJwQnjhMW&(XD$Pfctl)I3iJ=&3lvn;H zx|dFOYjDX5ovx+7f^R>%eE{DJEp)szNaJ&Sskjiwcr?0DZG!_Os=EOP+*avA0%Bv5 zKYcqBCg14m&G2n1)!xG~(=AlB2HR$%vse0=_&SAVorN~1B-(?(#GT3>;$PM?MH&a6$$T0J4WP=EX3>aybMrU9oUZ&8Ad#F&!W)fW_OQV8| z_?^RO`Sz=*HuKaG=Bh*yn;vGCWNEZ2d6-2uOQU5_bTgAiOJ)!AVG0%ult7L}poj5Z zD)#ik=H40>y}{l`Ec#$!6$>jCeX)@GdYIf#Q2JrfUr_o33K5GCPzDHS02TwqVxWhy zL1Hn;!<4~d&tO0{L9tE=;-XPSK_;w(bWG>7&oFD;R*lK%%Id-e}^ghz#i#yi48ta5-LL}1p9kBW^6 zjSjJzErJpi9_=WcCjTy~SEClt}+!>l87z$vkQv3*>*GY}5m8@0*+sT(tDP_xSZ2)OjK#*A`9bN z(K~7|NNiOoZ6|VPlSGG*5}67WAIIl0KdL_HYvQRoWmY03mitm|4JzGbl}^{-6m-^w z9{eJ1S7snp(0NZ>#V_WP)tTs^ic4j~OcA%xsqUb^Vf?9v6igw9GPveiD%#vp7^DcT z{}`iO>RVXVN`(fb%+@}ED z1U;gjea1dAMT9crES%}$4i-_asJ2t-xeC}GD~_K7KCLi^KWG@VBgaeWDW@EAD^ zo~sNsl8H&SXc_|JQy}^9c`1J)qYCq05znDG8Gdm@b*=ksIO!65c)r zd`e{iDs?KIg(f{1fDbeFfn^<|3(#9UyPBRnp&;xikSG!nl*VYzJg^X0pxQ2+pD)pD zAc)!gIcPJYm;Q+v2f(teGhN3;&zh<2){_K4S6BxmRiSzyNsMNS5EecENoQWms9rbm zHZxt!K_?M2U=L;#AOqU)?nD2{VO04D=M->2=w~ybLbXDxQJXizNL#@q9*dY4Qh;1n zXa(GXCt=5q74Pkc&uckZCP42@lkiYdCVXfG|?Kpyw@_4j`#kD^#@#0X$Ve>yZ-= z!M4_yPUGEkL7~LSn&w(2Fg$s$;mo@-Rl+7SIZfROHcWk{h>K6+{*p|`VHKA~Q(AFa zhQLIcor3-=eW)}8&8J)<@%A{yX?r6+Tm2}z78>60rH03ts;}$RFay1)3bo)6+Uhx- z+>r4ye&WO^`i9p@oHb&h`KV-My6$2R%BC@Q(p43ugD=kW3$H zKslSGP9|baLfa#lUk|}L_>Y74m3kH~zAV!J6U*69~RJC z%qYY(ejTq=K#ws4;0I;0;t`mUh+gJBU~ZIY7m}eyB0PQgS%q58VxmtFJMKfPeK7DT zou*{MGzTR*;m`OLjgBtF=L!o=O!ULs7iB#i2F5s!7s?zok*84lyo2Y_%o>M&qnzLW8&QdH#IWaQ+pNfxf$q@utui&cj~YR8dk4GS%jUF2#=3Q1X>YyT!jyA4h)iu_N8cCJtSq; zr)ZkGLAF|gJ2ra%<@G_V5jNHlZq;9ZzT>^j;$WZ79rBF$*zu#pztOC|9m?3-w+Y1R z_na5>vOW=jbNqv!8g~46XKaM6{iuMSJ)7Qt&uniO0kNH4?m%q642{7Ui^Zqke1c{KJO+1H9x9Cwv|9qYZ&QpGs)=_!BulY;nKeMcXnZSF`A)S`L1F^!ak( z-eTR?Z-!KTzgYJVD3w;G)>%3!4FR{e)>%3$2Lfwv)L8WQ18AOX?fOFi$A>!OM~!v-d#p1$+}f?f={Cbl_eR1CK^=#7L;_gc zBmA_+>gih7;3mE-F<0xRr^#nM#rg|=|6PAqn4{<)n#1~Hu(-M{%;Ah5Io1(24myl; zIIS<)?H|JnarXGJv0sG7*$pIQWK{gv31jSILc>Rj-J$Uac&FXVE{vc_Tv;-X32)Ui^P9V2ePE40Q(v$_+DeweEVFdG)?xy@%gK$@-H%Pots#08vGT zjf{?R#Ke#ERuU)V{w5a5@bcTw=CTf9_OZi8kC`Ci(_2q`r1HEf%THdu{GQw5FSQ_+ zdied-`pQ$!M8rD79Fe$Z^mvEUt(&EUwsh^4;5(AFXV;F zFbC?7R~3s?z9L!c`O~HT)?NV}vQdM+5=E`|o6jxO%jk_xCedj3 zM8(8AoRDH49~&Kw&hd+X4zYt8HAUU^Up8L&h0+&`iX9JE%NRZ?pitSnZnwoV-(6X%F_gvCR={W-nY&L|Wc z@_$=Ioan6XH#@1dZCRSlzEO5J6zf9*Lf3nXZplBaGVY-Pe9LZ8nEv{#q2Shasis)m HZjtggcUTPr diff --git a/trace.out b/trace.out deleted file mode 100644 index 924ef99cd6efdee66f12e734fe154876c5914efe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 77522 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 QI$N-29RqOXAqH^&1#o-S9{>OV From 81549cdad3bbb4cd9408495241b845fce7c2e206 Mon Sep 17 00:00:00 2001 From: Issif Date: Fri, 29 May 2020 17:55:07 +0200 Subject: [PATCH 03/11] new method for detecting IDN + add comments for functions/types + unexport some functions/types --- example.yaml | 5 +-- go.mod | 1 + go.sum | 1 + lib/config.go | 9 ++--- lib/homoglyph.go | 18 +++++++++ lib/lib.go | 94 +++++++++++++++++++++++++------------------ lib/lib_suite_test.go | 2 +- lib/lib_test.go | 28 ++++++------- lib/slack.go | 9 ++++- main.go | 2 +- res/cert.json | 1 - res/heartbeat.json | 1 - 12 files changed, 101 insertions(+), 70 deletions(-) delete mode 100644 res/cert.json delete mode 100644 res/heartbeat.json diff --git a/example.yaml b/example.yaml index 32499ef..39ba9d2 100644 --- a/example.yaml +++ b/example.yaml @@ -2,7 +2,6 @@ SlackWebhookURL: "" #Slack Webhook URL SlackIconURL: "" #Slack Icon (Avatar) URL SlackUsername: "" #Slack Username -Regexp: ".*\\.fr$" #Regexp to match. Can't be empty. It uses Golang regexp format -DomainName: test +Regexp: ".*" #Regexp to match. Can't be empty. It uses Golang regexp format Workers: 20 #Number of workers for consuming stream from CertStream -DisplayErrors: false #Enable/Disable display of errors in logs \ No newline at end of file +DisplayErrors: true #Enable/Disable display of errors in logs \ No newline at end of file diff --git a/go.mod b/go.mod index a487656..54aa741 100644 --- a/go.mod +++ b/go.mod @@ -13,5 +13,6 @@ require ( github.com/sirupsen/logrus v1.2.0 github.com/spf13/viper v1.6.3 github.com/stretchr/testify v1.4.0 // indirect + golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 gopkg.in/alecthomas/kingpin.v2 v2.2.6 ) diff --git a/go.sum b/go.sum index e89ab6a..eb2b2d4 100644 --- a/go.sum +++ b/go.sum @@ -166,6 +166,7 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200528225125-3c3fba18258b h1:IYiJPiJfzktmDAO1HQiwjMjwjlYKHAL7KzeD544RJPs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/lib/config.go b/lib/config.go index ad5393e..0793808 100644 --- a/lib/config.go +++ b/lib/config.go @@ -20,14 +20,13 @@ type Configuration struct { RegIP string Regexp string DisplayErrors string + Homoglyph map[string]string } -const regStrIP = `^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$` - // GetConfig provides a Configuration func GetConfig() *Configuration { c := &Configuration{ - RegIP: regStrIP, + Homoglyph: getHomoglyphMap(), } configFile := kingpin.Flag("configfile", "config file").Short('c').ExistingFile() @@ -66,9 +65,7 @@ func GetConfig() *Configuration { if c.Regexp == "" { log.Fatal("Regexp can't be empty") } - if c.DomainName == "" { - log.Fatal("Specify the domain name to monitor for IDN homographs") - } + if _, err := regexp.Compile(c.Regexp); err != nil { log.Fatal("Bad regexp") } diff --git a/lib/homoglyph.go b/lib/homoglyph.go index 9a1a315..5a61333 100644 --- a/lib/homoglyph.go +++ b/lib/homoglyph.go @@ -4,6 +4,7 @@ import ( "github.com/picatz/homoglyphr" ) +// getHomoglyphMap generates a map of homoglyphs for replacement func getHomoglyphMap() map[string]string { alphabet := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"} homoglyph := map[string]string{} @@ -14,3 +15,20 @@ func getHomoglyphMap() map[string]string { } return homoglyph } + +// replaceHomoglyph replaces homoglyphs in a string by close latin letters +func replaceHomoglyph(idn string, homoglyphMap map[string]string) string { + var s string + for _, i := range idn { + if i > 127 { + if letter, present := homoglyphMap[string(i)]; present { + s += letter + } else { + s += string(i) + } + } else { + s += string(i) + } + } + return s +} diff --git a/lib/lib.go b/lib/lib.go index 8c5c241..cbb2760 100644 --- a/lib/lib.go +++ b/lib/lib.go @@ -3,44 +3,44 @@ package lib import ( "context" "encoding/json" - "fmt" "net" "regexp" "strings" "time" - log "github.com/sirupsen/logrus" - - _ "net/http/pprof" - - _ "expvar" - "github.com/gobwas/ws" "github.com/gobwas/ws/wsutil" + log "github.com/sirupsen/logrus" + "golang.org/x/net/idna" ) -type Result struct { +// result represents a catched certificate +type result struct { Domain string `json:"domain"` + IDN string `json:"IDN,omitempty"` SAN []string `json:"SAN"` Issuer string `json:"issuer"` Addresses []string `json:"Addresses"` } -type Certificate struct { +// certificate represents a certificate from CertStream +type certificate struct { MessageType string `json:"message_type"` - Data Data `json:"data"` + Data data `json:"data"` } -type Data struct { +// data represents data field for a certificate from CertStream +type data struct { UpdateType string `json:"update_type"` - LeafCert LeafCert `json:"leaf_cert"` - Chain []LeafCert `json:"chain"` + LeafCert leafCert `json:"leaf_cert"` + Chain []leafCert `json:"chain"` CertIndex float32 `json:"cert_index"` Seen float32 `json:"seen"` Source map[string]string `json:"source"` } -type LeafCert struct { +// leafCert represents leaf_cert field from CertStream +type leafCert struct { Subject map[string]string `json:"subject"` Extensions map[string]interface{} `json:"extensions"` NotBefore float32 `json:"not_before"` @@ -51,20 +51,20 @@ type LeafCert struct { AllDomains []string `json:"all_domains"` } -// MsgChan is the communication channel between certCheckWorkers and LoopCheckCerts +// MsgChan is the communication channel between certCheckWorkers and LoopCertStream var MsgChan chan []byte +// the websocket stream from calidog const certInput = "wss://certstream.calidog.io" // CertCheckWorker parses certificates and raises alert if matches config func CertCheckWorker(config *Configuration) { reg, _ := regexp.Compile(config.Regexp) - regIP, _ := regexp.Compile(config.RegIP) for { msg := <-MsgChan - detailedCert, err := ParseResultCertificate(msg, regIP) + detailedCert, err := parseResultCertificate(msg) if err != nil { log.Warnf("Error parsing message: %s", err) continue @@ -72,33 +72,40 @@ func CertCheckWorker(config *Configuration) { if detailedCert == nil { continue } - if !IsMatchingCert(detailedCert, reg) { + if !isMatchingCert(config, detailedCert, reg) { continue } - notify(config, *detailedCert) + slackPost(config, *detailedCert) } } -func ParseResultCertificate(msg []byte, regIP *regexp.Regexp) (*Result, error) { - var c Certificate - var r *Result +// parseResultCertificate parses certificate details +func parseResultCertificate(msg []byte) (*result, error) { + var c certificate + var r *result err := json.Unmarshal(msg, &c) if err != nil || c.MessageType == "heartbeat" { return nil, err } - r = &Result{ + r = &result{ Domain: c.Data.LeafCert.Subject["CN"], Issuer: c.Data.Chain[0].Subject["O"], SAN: c.Data.LeafCert.AllDomains, Addresses: []string{"N/A"}, } - r.Addresses = FetchIPAddresses(r.Domain, regIP) + r.Addresses = fetchIPAddresses(r.Domain) return r, nil } -func FetchIPAddresses(name string, regIP *regexp.Regexp) []string { +// isIPv4Net checks if IP is IPv4 +func isIPv4Net(host string) bool { + return net.ParseIP(host) != nil +} + +// fetchIPAddresses resolves domain to get IP +func fetchIPAddresses(name string) []string { var ipsList []string ips, err := net.LookupIP(name) @@ -107,19 +114,29 @@ func FetchIPAddresses(name string, regIP *regexp.Regexp) []string { return ipsList } for _, j := range ips { - if regIP.MatchString(j.String()) { + if isIPv4Net(j.String()) { ipsList = append(ipsList, j.String()) } } return ipsList } -func IsMatchingCert(cert *Result, reg *regexp.Regexp) bool { +// isIDN checks if domain is an IDN +func isIDN(domain string) bool { + return strings.HasPrefix(domain, "xn--") +} + +// isMatchingCert checks if certificate matches the regexp +func isMatchingCert(config *Configuration, cert *result, reg *regexp.Regexp) bool { domainList := append(cert.SAN, cert.Domain) for _, domain := range domainList { - // if isIDN(domain) { - // return true - // } + if isIDN(domain) { + unicodeDomain, _ := idna.ToUnicode(domain) + cert.IDN = unicodeDomain + if reg.MatchString(replaceHomoglyph(unicodeDomain, config.Homoglyph)) { + return true + } + } if reg.MatchString(domain) { return true } @@ -127,27 +144,24 @@ func IsMatchingCert(cert *Result, reg *regexp.Regexp) bool { return false } -func isIDN(domain string) bool { - return strings.HasPrefix(domain, "xn--") -} - -func notify(config *Configuration, detailedCert Result) { +// slackPost posts event to Slack +func slackPost(config *Configuration, detailedCert result) { b, _ := json.Marshal(detailedCert) if config.SlackWebHookURL != "" { - go newSlackPayload(detailedCert, config).Post(config) + go newSlackPayload(&detailedCert, config).post(config) } else { - fmt.Printf("A certificate for '%v' has been issued : %v\n", detailedCert.Domain, string(b)) + log.Infof("A certificate for '%v' has been issued : %v\n", detailedCert.Domain, string(b)) } } -// LoopCheckCerts Loops on messages from source -func LoopCheckCerts(config *Configuration) { +// LoopCertStream gathers messages from CertStream +func LoopCertStream(config *Configuration) { for { conn, _, _, err := ws.DefaultDialer.Dial(context.Background(), certInput) defer conn.Close() if err != nil { - log.Warn("Error connecting to certstream! Sleeping a few seconds and reconnecting...") + log.Warn("Error connecting to CertStream! Sleeping a few seconds and reconnecting...") time.Sleep(1 * time.Second) continue } diff --git a/lib/lib_suite_test.go b/lib/lib_suite_test.go index a556e7e..0c56288 100644 --- a/lib/lib_suite_test.go +++ b/lib/lib_suite_test.go @@ -1,4 +1,4 @@ -package lib_test +package lib import ( "testing" diff --git a/lib/lib_test.go b/lib/lib_test.go index 1b915a3..dac16d4 100644 --- a/lib/lib_test.go +++ b/lib/lib_test.go @@ -1,9 +1,7 @@ -package lib_test +package lib import ( - "cercat/lib" "io/ioutil" - "regexp" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -11,36 +9,36 @@ import ( var _ = Describe("Handler", func() { Describe("isMatchingCert", func() { - reg, _ := regexp.Compile(`.+\.com`) - regIDN, _ := regexp.Compile(lib.BuildIDNRegex("test")) Describe("If certificate matches", func() { - cert := &lib.Result{Domain: "www.test.com"} + cert := &result{Domain: "www.test.com"} + reg := ".*test.*" It("should return true", func() { - result := lib.IsMatchingCert(cert, reg, regIDN) + result := isMatchingCert(cert, reg) Expect(result).To(BeTrue()) }) }) Describe("If alternative subject matches", func() { - cert := &lib.Result{Domain: "www.test.net", SAN: []string{"www.test.com"}} + cert := &Result{Domain: "www.test.net", SAN: []string{"www.test.com"}} + reg := ".*test.*" It("should return true", func() { - result := lib.IsMatchingCert(cert, reg, regIDN) + result := isMatchingCert(cert, reg) Expect(result).To(BeTrue()) }) }) Describe("If domain is IDN", func() { - cert := &lib.Result{Domain: "xn--tst-rdd.com"} + cert := &Result{Domain: "xn--tst-rdd.com"} + reg := ".*test.*" It("should return true", func() { - result := lib.IsMatchingCert(cert, reg, regIDN) + result := isMatchingCert(cert, reg) Expect(result).To(BeTrue()) }) }) }) Describe("parseResultCertificate", func() { - regIP, _ := regexp.Compile(lib.RegStrIP) Describe("If cannot marshall message", func() { msg := []byte("") It("should return nil and error", func() { - result, err := lib.ParseResultCertificate(msg, regIP) + result, err := parseResultCertificate(msg) Expect(result).To(BeNil()) Expect(err).To(HaveOccurred()) }) @@ -48,7 +46,7 @@ var _ = Describe("Handler", func() { Describe("If message is heartbeat", func() { msg, _ := ioutil.ReadFile("../res/heartbeat.json") It("should return nil", func() { - result, err := lib.ParseResultCertificate(msg, regIP) + result, err := parseResultCertificate(msg) Expect(result).To(BeNil()) Expect(err).ToNot(HaveOccurred()) }) @@ -56,7 +54,7 @@ var _ = Describe("Handler", func() { Describe("If message is regular", func() { msg, _ := ioutil.ReadFile("../res/cert.json") It("should return valid infos", func() { - result, err := lib.ParseResultCertificate(msg, regIP) + result, err := parseResultCertificate(msg) Expect(result.Domain).Should(Equal("baden-mueller.de")) Expect(result.SAN).Should(Equal([]string{"baden-mueller.de", "www.baden-mueller.de"})) Expect(result.Issuer).Should(Equal("Let's Encrypt")) diff --git a/lib/slack.go b/lib/slack.go index 065c684..c5d0bcf 100644 --- a/lib/slack.go +++ b/lib/slack.go @@ -9,12 +9,14 @@ import ( log "github.com/sirupsen/logrus" ) +// slackAttachmentField type slackAttachmentField struct { Title string `json:"title"` Value string `json:"value"` Short bool `json:"short"` } +// slackAttachment type slackAttachment struct { Color string `json:"color"` Text string `json:"text,omitempty"` @@ -23,6 +25,7 @@ type slackAttachment struct { // FooterIcon string `json:"footer_icon,omitempty"` } +// slackPayload type slackPayload struct { Text string `json:"text,omitempty"` Username string `json:"username,omitempty"` @@ -30,7 +33,8 @@ type slackPayload struct { Attachments []slackAttachment `json:"attachments,omitempty"` } -func newSlackPayload(r Result, config *Configuration) slackPayload { +// newSlackPayload generates a new Slack Payload +func newSlackPayload(r *result, config *Configuration) slackPayload { var attachments []slackAttachment var attachment slackAttachment var fields []slackAttachmentField @@ -69,7 +73,8 @@ func newSlackPayload(r Result, config *Configuration) slackPayload { Attachments: attachments} } -func (s slackPayload) Post(config *Configuration) { +// post posts to Slack a Payload +func (s slackPayload) post(config *Configuration) { body, _ := json.Marshal(s) req, _ := http.NewRequest(http.MethodPost, config.SlackWebHookURL, bytes.NewBuffer(body)) req.Header.Add("Content-Type", "application/json") diff --git a/main.go b/main.go index bdccdb0..0416821 100644 --- a/main.go +++ b/main.go @@ -20,5 +20,5 @@ func main() { go lib.CertCheckWorker(config) } - lib.LoopCheckCerts(config) + lib.LoopCertStream(config) } diff --git a/res/cert.json b/res/cert.json deleted file mode 100644 index 7964f08..0000000 --- a/res/cert.json +++ /dev/null @@ -1 +0,0 @@ -{"data":{"cert_index":612101919,"cert_link":"http://ct.googleapis.com/logs/argon2020/ct/v1/get-entries?start=612101919&end=612101919","chain":[{"as_der":"MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0NlowSjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMTGkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EFq6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWAa6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIGCCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNvbTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9kc3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAwVAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcCARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwuY3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsFAAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJouM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwuX4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlGPfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==","extensions":{"authorityInfoAccess":"CA Issuers - URI:http://apps.identrust.com/roots/dstrootcax3.p7c\nOCSP - URI:http://isrg.trustid.ocsp.identrust.com\n","authorityKeyIdentifier":"keyid:C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10\n","basicConstraints":"CA:TRUE","certificatePolicies":"Policy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http://cps.root-x1.letsencrypt.org","crlDistributionPoints":"Full Name:\n URI:http://crl.identrust.com/DSTROOTCAX3CRL.crl","keyUsage":"Digital Signature, Key Cert Sign, C R L Sign","subjectKeyIdentifier":"A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1"},"fingerprint":"E6:A3:B4:5B:06:2D:50:9B:33:82:28:2D:19:6E:FE:97:D5:95:6C:CB","not_after":1615999246,"not_before":1458232846,"serial_number":"A0141420000015385736A0B85ECA708","subject":{"C":"US","CN":"Let's Encrypt Authority X3","L":null,"O":"Let's Encrypt","OU":null,"ST":null,"aggregated":"/C=US/CN=Let's Encrypt Authority X3/O=Let's Encrypt"}},{"as_der":"MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVowPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4Orz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEqOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9bxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaDaeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqGSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXrAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZzR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYoOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ","extensions":{"basicConstraints":"CA:TRUE","keyUsage":"Key Cert Sign, C R L Sign","subjectKeyIdentifier":"C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10"},"fingerprint":"DA:C9:02:4F:54:D8:F6:DF:94:93:5F:B1:73:26:38:CA:6A:D7:7C:13","not_after":1633010475,"not_before":970348339,"serial_number":"44AFB080D6A327BA893039862EF8406B","subject":{"C":null,"CN":"DST Root CA X3","L":null,"O":"Digital Signature Trust Co.","OU":null,"ST":null,"aggregated":"/CN=DST Root CA X3/O=Digital Signature Trust Co."}}],"leaf_cert":{"all_domains":["baden-mueller.de","www.baden-mueller.de"],"as_der":"MIIEezCCA2OgAwIBAgISA1Gm0sew3z+8nJrpo5Jj1naBMA0GCSqGSIb3DQEBCwUAMEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQDExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0yMDA1MjQxODE3NDhaFw0yMDA4MjIxODE3NDhaMBsxGTAXBgNVBAMTEGJhZGVuLW11ZWxsZXIuZGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCSDtXT7WQQaNmnaH6+BxHwEz7eqHdTx/02HV7x/q9oozIKfWnfc4A3glkMdnJtZUjLlbV4sgAO4MBDNo65Qsq4L/GesRsVTczmYcAxnrfp8e/eK7wF08oqCvdHddXSHD82aXe/6Y6a3hiLEG+oBMDfG1Skwyt7NGNySlenz3EYEbc35IVoFKIkp2CyMV/nkKQPCgQBL10niEiQd9Q9bHDQJZsBtW59VVCy5K5kIPo6P5v295PCt0WTUppXagY2G/YGpQOmvsjl9MFjMZc4yOOd3RhGhcr2jgd9iF04TvownTxvQAU1EbKcXDHcoPVmhH5zDeiN1JbLNpW2wMf5Vr9VAgMBAAGjggGIMIIBhDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFE8R8swxvB64KS8VqcCaUcMFpEjAMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUFBwEBBGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNyeXB0Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNyeXB0Lm9yZy8wMQYDVR0RBCowKIIQYmFkZW4tbXVlbGxlci5kZYIUd3d3LmJhZGVuLW11ZWxsZXIuZGUwTAYDVR0gBEUwQzAIBgZngQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcwEwYKKwYBBAHWeQIEAwEB/wQCBQAwDQYJKoZIhvcNAQELBQADggEBAIUMEOxJNwvA/pWwFm0BR0HClGdzSC1vQprBaZ6cedUM6b/wfAiCPNJXCvJyrfhJp4T1GcnwU5VnMLPE1/nwJ6LY3My86/M+eQn/3HRuXu3p1GFpp5k2cXsHB7VRlw5X78XVvsnYKc6giwOan7L8fL136EcplQTZRc/5qu9hvazeBOBQQc/lCeWceWz0ZDDVbU2IGvY6aF/SAQREOSq8jLVpEoXB0zwq3dXeEi+PfC2Ea03eOpo1y11nmRYB5Usi/GjMi7oXuBxVQMolJXJj38ziJcp1TT1sv2Ha/00F+Pudo54w1NEo04DbDD9yB2H9wTlMM4YsArmD3K22OGA8wNE=","extensions":{"authorityInfoAccess":"CA Issuers - URI:http://cert.int-x3.letsencrypt.org/\nOCSP - URI:http://ocsp.int-x3.letsencrypt.org\n","authorityKeyIdentifier":"keyid:A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1\n","basicConstraints":"CA:FALSE","certificatePolicies":"Policy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http://cps.letsencrypt.org","ctlPoisonByte":true,"extendedKeyUsage":"TLS Web server authentication, TLS Web client authentication","keyUsage":"Digital Signature, Key Encipherment","subjectAltName":"DNS:www.baden-mueller.de, DNS:baden-mueller.de","subjectKeyIdentifier":"4F:11:F2:CC:31:BC:1E:B8:29:2F:15:A9:C0:9A:51:C3:05:A4:48:C0"},"fingerprint":"64:BF:49:41:3B:7A:FD:5D:C1:04:D9:44:64:9D:1C:25:13:A2:49:86","not_after":1598120268,"not_before":1590344268,"serial_number":"351A6D2C7B0DF3FBC9C9AE9A39263D67681","subject":{"C":null,"CN":"baden-mueller.de","L":null,"O":null,"OU":null,"ST":null,"aggregated":"/CN=baden-mueller.de"}},"seen":1590347943.736608,"source":{"name":"Google 'Argon2020' log","url":"ct.googleapis.com/logs/argon2020/"},"update_type":"PrecertLogEntry"},"message_type":"certificate_update"} \ No newline at end of file diff --git a/res/heartbeat.json b/res/heartbeat.json deleted file mode 100644 index a1bcf74..0000000 --- a/res/heartbeat.json +++ /dev/null @@ -1 +0,0 @@ -{"data":{"cert_index":612101919,"cert_link":"http://ct.googleapis.com/logs/argon2020/ct/v1/get-entries?start=612101919&end=612101919","chain":[{"as_der":"MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0NlowSjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMTGkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EFq6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWAa6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIGCCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNvbTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9kc3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAwVAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcCARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwuY3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsFAAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJouM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwuX4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlGPfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==","extensions":{"authorityInfoAccess":"CA Issuers - URI:http://apps.identrust.com/roots/dstrootcax3.p7c\nOCSP - URI:http://isrg.trustid.ocsp.identrust.com\n","authorityKeyIdentifier":"keyid:C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10\n","basicConstraints":"CA:TRUE","certificatePolicies":"Policy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http://cps.root-x1.letsencrypt.org","crlDistributionPoints":"Full Name:\n URI:http://crl.identrust.com/DSTROOTCAX3CRL.crl","keyUsage":"Digital Signature, Key Cert Sign, C R L Sign","subjectKeyIdentifier":"A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1"},"fingerprint":"E6:A3:B4:5B:06:2D:50:9B:33:82:28:2D:19:6E:FE:97:D5:95:6C:CB","not_after":1615999246,"not_before":1458232846,"serial_number":"A0141420000015385736A0B85ECA708","subject":{"C":"US","CN":"Let's Encrypt Authority X3","L":null,"O":"Let's Encrypt","OU":null,"ST":null,"aggregated":"/C=US/CN=Let's Encrypt Authority X3/O=Let's Encrypt"}},{"as_der":"MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVowPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4Orz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEqOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9bxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaDaeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqGSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXrAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZzR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYoOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ","extensions":{"basicConstraints":"CA:TRUE","keyUsage":"Key Cert Sign, C R L Sign","subjectKeyIdentifier":"C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10"},"fingerprint":"DA:C9:02:4F:54:D8:F6:DF:94:93:5F:B1:73:26:38:CA:6A:D7:7C:13","not_after":1633010475,"not_before":970348339,"serial_number":"44AFB080D6A327BA893039862EF8406B","subject":{"C":null,"CN":"DST Root CA X3","L":null,"O":"Digital Signature Trust Co.","OU":null,"ST":null,"aggregated":"/CN=DST Root CA X3/O=Digital Signature Trust Co."}}],"leaf_cert":{"all_domains":["baden-mueller.de","www.baden-mueller.de"],"as_der":"MIIEezCCA2OgAwIBAgISA1Gm0sew3z+8nJrpo5Jj1naBMA0GCSqGSIb3DQEBCwUAMEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQDExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0yMDA1MjQxODE3NDhaFw0yMDA4MjIxODE3NDhaMBsxGTAXBgNVBAMTEGJhZGVuLW11ZWxsZXIuZGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCSDtXT7WQQaNmnaH6+BxHwEz7eqHdTx/02HV7x/q9oozIKfWnfc4A3glkMdnJtZUjLlbV4sgAO4MBDNo65Qsq4L/GesRsVTczmYcAxnrfp8e/eK7wF08oqCvdHddXSHD82aXe/6Y6a3hiLEG+oBMDfG1Skwyt7NGNySlenz3EYEbc35IVoFKIkp2CyMV/nkKQPCgQBL10niEiQd9Q9bHDQJZsBtW59VVCy5K5kIPo6P5v295PCt0WTUppXagY2G/YGpQOmvsjl9MFjMZc4yOOd3RhGhcr2jgd9iF04TvownTxvQAU1EbKcXDHcoPVmhH5zDeiN1JbLNpW2wMf5Vr9VAgMBAAGjggGIMIIBhDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFE8R8swxvB64KS8VqcCaUcMFpEjAMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUFBwEBBGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNyeXB0Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNyeXB0Lm9yZy8wMQYDVR0RBCowKIIQYmFkZW4tbXVlbGxlci5kZYIUd3d3LmJhZGVuLW11ZWxsZXIuZGUwTAYDVR0gBEUwQzAIBgZngQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcwEwYKKwYBBAHWeQIEAwEB/wQCBQAwDQYJKoZIhvcNAQELBQADggEBAIUMEOxJNwvA/pWwFm0BR0HClGdzSC1vQprBaZ6cedUM6b/wfAiCPNJXCvJyrfhJp4T1GcnwU5VnMLPE1/nwJ6LY3My86/M+eQn/3HRuXu3p1GFpp5k2cXsHB7VRlw5X78XVvsnYKc6giwOan7L8fL136EcplQTZRc/5qu9hvazeBOBQQc/lCeWceWz0ZDDVbU2IGvY6aF/SAQREOSq8jLVpEoXB0zwq3dXeEi+PfC2Ea03eOpo1y11nmRYB5Usi/GjMi7oXuBxVQMolJXJj38ziJcp1TT1sv2Ha/00F+Pudo54w1NEo04DbDD9yB2H9wTlMM4YsArmD3K22OGA8wNE=","extensions":{"authorityInfoAccess":"CA Issuers - URI:http://cert.int-x3.letsencrypt.org/\nOCSP - URI:http://ocsp.int-x3.letsencrypt.org\n","authorityKeyIdentifier":"keyid:A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1\n","basicConstraints":"CA:FALSE","certificatePolicies":"Policy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http://cps.letsencrypt.org","ctlPoisonByte":true,"extendedKeyUsage":"TLS Web server authentication, TLS Web client authentication","keyUsage":"Digital Signature, Key Encipherment","subjectAltName":"DNS:www.baden-mueller.de, DNS:baden-mueller.de","subjectKeyIdentifier":"4F:11:F2:CC:31:BC:1E:B8:29:2F:15:A9:C0:9A:51:C3:05:A4:48:C0"},"fingerprint":"64:BF:49:41:3B:7A:FD:5D:C1:04:D9:44:64:9D:1C:25:13:A2:49:86","not_after":1598120268,"not_before":1590344268,"serial_number":"351A6D2C7B0DF3FBC9C9AE9A39263D67681","subject":{"C":null,"CN":"baden-mueller.de","L":null,"O":null,"OU":null,"ST":null,"aggregated":"/CN=baden-mueller.de"}},"seen":1590347943.736608,"source":{"name":"Google 'Argon2020' log","url":"ct.googleapis.com/logs/argon2020/"},"update_type":"PrecertLogEntry"},"message_type":"heartbeat"} \ No newline at end of file From f14f72dc1752e688e4b951e92858cee791b6bb6f Mon Sep 17 00:00:00 2001 From: Issif Date: Fri, 29 May 2020 22:54:05 +0200 Subject: [PATCH 04/11] update slack event with Unicode for IDN --- example.yaml | 2 +- lib/config.go | 5 +---- lib/lib.go | 22 +++++++++------------- lib/slack.go | 12 +++++++++--- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/example.yaml b/example.yaml index 39ba9d2..d1ecab8 100644 --- a/example.yaml +++ b/example.yaml @@ -2,6 +2,6 @@ SlackWebhookURL: "" #Slack Webhook URL SlackIconURL: "" #Slack Icon (Avatar) URL SlackUsername: "" #Slack Username -Regexp: ".*" #Regexp to match. Can't be empty. It uses Golang regexp format +Regexp: ".*\\.fr" #Regexp to match. Can't be empty. It uses Golang regexp format Workers: 20 #Number of workers for consuming stream from CertStream DisplayErrors: true #Enable/Disable display of errors in logs \ No newline at end of file diff --git a/lib/config.go b/lib/config.go index 0793808..e182aaf 100644 --- a/lib/config.go +++ b/lib/config.go @@ -19,7 +19,6 @@ type Configuration struct { DomainName string RegIP string Regexp string - DisplayErrors string Homoglyph map[string]string } @@ -59,9 +58,7 @@ func GetConfig() *Configuration { if c.SlackUsername == "" { c.SlackUsername = "Cercat" } - if c.DisplayErrors == "" || c.DisplayErrors == "false" { - log.SetLevel(log.DebugLevel) - } + if c.Regexp == "" { log.Fatal("Regexp can't be empty") } diff --git a/lib/lib.go b/lib/lib.go index cbb2760..3eac8f0 100644 --- a/lib/lib.go +++ b/lib/lib.go @@ -75,7 +75,13 @@ func CertCheckWorker(config *Configuration) { if !isMatchingCert(config, detailedCert, reg) { continue } - slackPost(config, *detailedCert) + j, _ := json.Marshal(detailedCert) + log.Infof("A certificate for '%v' has been issued : %v\n", detailedCert.Domain, string(j)) + if config.SlackWebHookURL != "" { + go func(c *Configuration, r *result) { + newSlackPayload(c, detailedCert).post(c) + }(config, detailedCert) + } } } @@ -132,10 +138,11 @@ func isMatchingCert(config *Configuration, cert *result, reg *regexp.Regexp) boo for _, domain := range domainList { if isIDN(domain) { unicodeDomain, _ := idna.ToUnicode(domain) - cert.IDN = unicodeDomain if reg.MatchString(replaceHomoglyph(unicodeDomain, config.Homoglyph)) { + cert.IDN = unicodeDomain return true } + continue } if reg.MatchString(domain) { return true @@ -144,17 +151,6 @@ func isMatchingCert(config *Configuration, cert *result, reg *regexp.Regexp) boo return false } -// slackPost posts event to Slack -func slackPost(config *Configuration, detailedCert result) { - b, _ := json.Marshal(detailedCert) - - if config.SlackWebHookURL != "" { - go newSlackPayload(&detailedCert, config).post(config) - } else { - log.Infof("A certificate for '%v' has been issued : %v\n", detailedCert.Domain, string(b)) - } -} - // LoopCertStream gathers messages from CertStream func LoopCertStream(config *Configuration) { for { diff --git a/lib/slack.go b/lib/slack.go index c5d0bcf..d7d30d3 100644 --- a/lib/slack.go +++ b/lib/slack.go @@ -34,7 +34,7 @@ type slackPayload struct { } // newSlackPayload generates a new Slack Payload -func newSlackPayload(r *result, config *Configuration) slackPayload { +func newSlackPayload(config *Configuration, r *result) slackPayload { var attachments []slackAttachment var attachment slackAttachment var fields []slackAttachmentField @@ -66,11 +66,17 @@ func newSlackPayload(r *result, config *Configuration) slackPayload { attachments = append(attachments, attachment) + domain := r.Domain + if r.IDN != "" { + domain += " (" + r.IDN + ")" + } + return slackPayload{ - Text: "A certificate for *" + r.Domain + "* has been issued", + Text: "A certificate for *" + domain + "* has been issued", Username: config.SlackUsername, IconURL: config.SlackIconURL, - Attachments: attachments} + Attachments: attachments, + } } // post posts to Slack a Payload From 1dbd541e0b8e1583a467ae8c46c0e1728acb60f1 Mon Sep 17 00:00:00 2001 From: Issif Date: Fri, 29 May 2020 22:59:18 +0200 Subject: [PATCH 05/11] add deduplication --- README.md | 2 -- example.yaml | 3 +-- lib/config.go | 1 + lib/lib.go | 4 ++++ 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 857fcb9..865a234 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,6 @@ SlackIconURL: "" #Slack Icon (Avatar) URL SlackUsername: "" #Slack Username Regexp: ".*\\.fr$" #Regexp to match. Can't be empty. It uses Golang regexp format Workers: 20 #Number of workers for consuming feed from CertStream -DisplayErrors: false #Enable/Disable display of errors in logs ``` ### With env vars @@ -38,7 +37,6 @@ DisplayErrors: false #Enable/Disable display of errors in logs - **SLACKUSERNAME**: Slack Username - **REGEXP**: Regexp to match, if empty, '.*' is used. Use Golang regexp format - **WORKERS**: Number of workers for consuming feed from CertStream -- **DISPLAYERRORS**: Enable/Disable display of errors in logs ## Run diff --git a/example.yaml b/example.yaml index d1ecab8..2acbf83 100644 --- a/example.yaml +++ b/example.yaml @@ -3,5 +3,4 @@ SlackWebhookURL: "" #Slack Webhook URL SlackIconURL: "" #Slack Icon (Avatar) URL SlackUsername: "" #Slack Username Regexp: ".*\\.fr" #Regexp to match. Can't be empty. It uses Golang regexp format -Workers: 20 #Number of workers for consuming stream from CertStream -DisplayErrors: true #Enable/Disable display of errors in logs \ No newline at end of file +Workers: 20 #Number of workers for consuming stream from CertStream \ No newline at end of file diff --git a/lib/config.go b/lib/config.go index e182aaf..1fd0c2b 100644 --- a/lib/config.go +++ b/lib/config.go @@ -19,6 +19,7 @@ type Configuration struct { DomainName string RegIP string Regexp string + Deduplication string Homoglyph map[string]string } diff --git a/lib/lib.go b/lib/lib.go index 3eac8f0..37cac7f 100644 --- a/lib/lib.go +++ b/lib/lib.go @@ -75,6 +75,10 @@ func CertCheckWorker(config *Configuration) { if !isMatchingCert(config, detailedCert, reg) { continue } + if detailedCert.Domain == config.Deduplication { + continue + } + config.Deduplication = detailedCert.Domain j, _ := json.Marshal(detailedCert) log.Infof("A certificate for '%v' has been issued : %v\n", detailedCert.Domain, string(j)) if config.SlackWebHookURL != "" { From d2ade2554f093610554f36d2350b4eb5a44b6185 Mon Sep 17 00:00:00 2001 From: Issif Date: Fri, 29 May 2020 23:01:03 +0200 Subject: [PATCH 06/11] fix typo in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 865a234..16f2ac5 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ```bash websocket +----------+ POST -CertSteam <-----------------> certcat +-----------> Slack +CertSteam <-----------------> cercat +-----------> Slack | (regexp) | +----------+ ``` From 1b4d81641119d91669dd279f22c796b76d3c6860 Mon Sep 17 00:00:00 2001 From: ayoul3 Date: Sat, 30 May 2020 12:15:21 +0200 Subject: [PATCH 07/11] separate test lib package, fix race condition and add a couple of tests --- lib/config.go | 3 +-- lib/homoglyph.go | 4 ++-- lib/lib.go | 35 ++++++++++++++--------------------- lib/lib_suite_test.go | 2 +- lib/lib_test.go | 31 ++++++++++++++++++------------- lib/slack.go | 2 +- res/cert.json | 1 + res/cert_idn.json | 1 + res/heartbeat.json | 1 + 9 files changed, 40 insertions(+), 40 deletions(-) create mode 100644 res/cert.json create mode 100644 res/cert_idn.json create mode 100644 res/heartbeat.json diff --git a/lib/config.go b/lib/config.go index 1fd0c2b..b9fb4cc 100644 --- a/lib/config.go +++ b/lib/config.go @@ -26,7 +26,7 @@ type Configuration struct { // GetConfig provides a Configuration func GetConfig() *Configuration { c := &Configuration{ - Homoglyph: getHomoglyphMap(), + Homoglyph: GetHomoglyphMap(), } configFile := kingpin.Flag("configfile", "config file").Short('c').ExistingFile() @@ -39,7 +39,6 @@ func GetConfig() *Configuration { v.SetDefault("DomainName", "") v.SetDefault("Regexp", "") v.SetDefault("Workers", 20) - v.SetDefault("DisplayErrors", "false") if *configFile != "" { d, f := path.Split(*configFile) diff --git a/lib/homoglyph.go b/lib/homoglyph.go index 5a61333..23c42c4 100644 --- a/lib/homoglyph.go +++ b/lib/homoglyph.go @@ -4,8 +4,8 @@ import ( "github.com/picatz/homoglyphr" ) -// getHomoglyphMap generates a map of homoglyphs for replacement -func getHomoglyphMap() map[string]string { +// GetHomoglyphMap generates a map of homoglyphs for replacement +func GetHomoglyphMap() map[string]string { alphabet := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"} homoglyph := map[string]string{} for _, letter := range alphabet { diff --git a/lib/lib.go b/lib/lib.go index 37cac7f..1e5a932 100644 --- a/lib/lib.go +++ b/lib/lib.go @@ -14,8 +14,8 @@ import ( "golang.org/x/net/idna" ) -// result represents a catched certificate -type result struct { +// Result represents a catched certificate +type Result struct { Domain string `json:"domain"` IDN string `json:"IDN,omitempty"` SAN []string `json:"SAN"` @@ -64,7 +64,7 @@ func CertCheckWorker(config *Configuration) { for { msg := <-MsgChan - detailedCert, err := parseResultCertificate(msg) + detailedCert, err := ParseResultCertificate(msg) if err != nil { log.Warnf("Error parsing message: %s", err) continue @@ -72,34 +72,31 @@ func CertCheckWorker(config *Configuration) { if detailedCert == nil { continue } - if !isMatchingCert(config, detailedCert, reg) { + if !IsMatchingCert(config, detailedCert, reg) { continue } - if detailedCert.Domain == config.Deduplication { - continue - } - config.Deduplication = detailedCert.Domain + j, _ := json.Marshal(detailedCert) log.Infof("A certificate for '%v' has been issued : %v\n", detailedCert.Domain, string(j)) if config.SlackWebHookURL != "" { - go func(c *Configuration, r *result) { + go func(c *Configuration, r *Result) { newSlackPayload(c, detailedCert).post(c) }(config, detailedCert) } } } -// parseResultCertificate parses certificate details -func parseResultCertificate(msg []byte) (*result, error) { +// ParseResultCertificate parses certificate details +func ParseResultCertificate(msg []byte) (*Result, error) { var c certificate - var r *result + var r *Result err := json.Unmarshal(msg, &c) if err != nil || c.MessageType == "heartbeat" { return nil, err } - r = &result{ + r = &Result{ Domain: c.Data.LeafCert.Subject["CN"], Issuer: c.Data.Chain[0].Subject["O"], SAN: c.Data.LeafCert.AllDomains, @@ -136,17 +133,13 @@ func isIDN(domain string) bool { return strings.HasPrefix(domain, "xn--") } -// isMatchingCert checks if certificate matches the regexp -func isMatchingCert(config *Configuration, cert *result, reg *regexp.Regexp) bool { +// IsMatchingCert checks if certificate matches the regexp +func IsMatchingCert(config *Configuration, cert *Result, reg *regexp.Regexp) bool { domainList := append(cert.SAN, cert.Domain) for _, domain := range domainList { if isIDN(domain) { - unicodeDomain, _ := idna.ToUnicode(domain) - if reg.MatchString(replaceHomoglyph(unicodeDomain, config.Homoglyph)) { - cert.IDN = unicodeDomain - return true - } - continue + cert.IDN, _ = idna.ToUnicode(domain) + domain = replaceHomoglyph(cert.IDN, config.Homoglyph) } if reg.MatchString(domain) { return true diff --git a/lib/lib_suite_test.go b/lib/lib_suite_test.go index 0c56288..a556e7e 100644 --- a/lib/lib_suite_test.go +++ b/lib/lib_suite_test.go @@ -1,4 +1,4 @@ -package lib +package lib_test import ( "testing" diff --git a/lib/lib_test.go b/lib/lib_test.go index dac16d4..e7981c1 100644 --- a/lib/lib_test.go +++ b/lib/lib_test.go @@ -1,36 +1,40 @@ -package lib +package lib_test import ( + "cercat/lib" "io/ioutil" + "regexp" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) var _ = Describe("Handler", func() { + config := &lib.Configuration{ + Homoglyph: lib.GetHomoglyphMap(), + } + reg, _ := regexp.Compile(".*test.*") Describe("isMatchingCert", func() { Describe("If certificate matches", func() { - cert := &result{Domain: "www.test.com"} - reg := ".*test.*" + cert := &lib.Result{Domain: "www.test.com"} It("should return true", func() { - result := isMatchingCert(cert, reg) + result := lib.IsMatchingCert(config, cert, reg) Expect(result).To(BeTrue()) }) }) Describe("If alternative subject matches", func() { - cert := &Result{Domain: "www.test.net", SAN: []string{"www.test.com"}} - reg := ".*test.*" + cert := &lib.Result{Domain: "www.test.net", SAN: []string{"www.test.com"}} It("should return true", func() { - result := isMatchingCert(cert, reg) + result := lib.IsMatchingCert(config, cert, reg) Expect(result).To(BeTrue()) }) }) Describe("If domain is IDN", func() { - cert := &Result{Domain: "xn--tst-rdd.com"} - reg := ".*test.*" + cert := &lib.Result{Domain: "xn--tst-rdd.com"} It("should return true", func() { - result := isMatchingCert(cert, reg) + result := lib.IsMatchingCert(config, cert, reg) Expect(result).To(BeTrue()) + Expect(cert.IDN).To(Equal("tеst.com")) // e is cyrillic }) }) }) @@ -38,7 +42,7 @@ var _ = Describe("Handler", func() { Describe("If cannot marshall message", func() { msg := []byte("") It("should return nil and error", func() { - result, err := parseResultCertificate(msg) + result, err := lib.ParseResultCertificate(msg) Expect(result).To(BeNil()) Expect(err).To(HaveOccurred()) }) @@ -46,7 +50,7 @@ var _ = Describe("Handler", func() { Describe("If message is heartbeat", func() { msg, _ := ioutil.ReadFile("../res/heartbeat.json") It("should return nil", func() { - result, err := parseResultCertificate(msg) + result, err := lib.ParseResultCertificate(msg) Expect(result).To(BeNil()) Expect(err).ToNot(HaveOccurred()) }) @@ -54,8 +58,9 @@ var _ = Describe("Handler", func() { Describe("If message is regular", func() { msg, _ := ioutil.ReadFile("../res/cert.json") It("should return valid infos", func() { - result, err := parseResultCertificate(msg) + result, err := lib.ParseResultCertificate(msg) Expect(result.Domain).Should(Equal("baden-mueller.de")) + Expect(result.IDN).Should(Equal("")) Expect(result.SAN).Should(Equal([]string{"baden-mueller.de", "www.baden-mueller.de"})) Expect(result.Issuer).Should(Equal("Let's Encrypt")) Expect(result.Addresses).Should(Equal([]string{"23.236.62.147"})) diff --git a/lib/slack.go b/lib/slack.go index d7d30d3..719881c 100644 --- a/lib/slack.go +++ b/lib/slack.go @@ -34,7 +34,7 @@ type slackPayload struct { } // newSlackPayload generates a new Slack Payload -func newSlackPayload(config *Configuration, r *result) slackPayload { +func newSlackPayload(config *Configuration, r *Result) slackPayload { var attachments []slackAttachment var attachment slackAttachment var fields []slackAttachmentField diff --git a/res/cert.json b/res/cert.json new file mode 100644 index 0000000..7964f08 --- /dev/null +++ b/res/cert.json @@ -0,0 +1 @@ +{"data":{"cert_index":612101919,"cert_link":"http://ct.googleapis.com/logs/argon2020/ct/v1/get-entries?start=612101919&end=612101919","chain":[{"as_der":"MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0NlowSjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMTGkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EFq6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWAa6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIGCCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNvbTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9kc3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAwVAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcCARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwuY3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsFAAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJouM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwuX4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlGPfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==","extensions":{"authorityInfoAccess":"CA Issuers - URI:http://apps.identrust.com/roots/dstrootcax3.p7c\nOCSP - URI:http://isrg.trustid.ocsp.identrust.com\n","authorityKeyIdentifier":"keyid:C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10\n","basicConstraints":"CA:TRUE","certificatePolicies":"Policy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http://cps.root-x1.letsencrypt.org","crlDistributionPoints":"Full Name:\n URI:http://crl.identrust.com/DSTROOTCAX3CRL.crl","keyUsage":"Digital Signature, Key Cert Sign, C R L Sign","subjectKeyIdentifier":"A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1"},"fingerprint":"E6:A3:B4:5B:06:2D:50:9B:33:82:28:2D:19:6E:FE:97:D5:95:6C:CB","not_after":1615999246,"not_before":1458232846,"serial_number":"A0141420000015385736A0B85ECA708","subject":{"C":"US","CN":"Let's Encrypt Authority X3","L":null,"O":"Let's Encrypt","OU":null,"ST":null,"aggregated":"/C=US/CN=Let's Encrypt Authority X3/O=Let's Encrypt"}},{"as_der":"MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVowPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4Orz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEqOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9bxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaDaeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqGSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXrAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZzR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYoOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ","extensions":{"basicConstraints":"CA:TRUE","keyUsage":"Key Cert Sign, C R L Sign","subjectKeyIdentifier":"C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10"},"fingerprint":"DA:C9:02:4F:54:D8:F6:DF:94:93:5F:B1:73:26:38:CA:6A:D7:7C:13","not_after":1633010475,"not_before":970348339,"serial_number":"44AFB080D6A327BA893039862EF8406B","subject":{"C":null,"CN":"DST Root CA X3","L":null,"O":"Digital Signature Trust Co.","OU":null,"ST":null,"aggregated":"/CN=DST Root CA X3/O=Digital Signature Trust Co."}}],"leaf_cert":{"all_domains":["baden-mueller.de","www.baden-mueller.de"],"as_der":"MIIEezCCA2OgAwIBAgISA1Gm0sew3z+8nJrpo5Jj1naBMA0GCSqGSIb3DQEBCwUAMEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQDExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0yMDA1MjQxODE3NDhaFw0yMDA4MjIxODE3NDhaMBsxGTAXBgNVBAMTEGJhZGVuLW11ZWxsZXIuZGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCSDtXT7WQQaNmnaH6+BxHwEz7eqHdTx/02HV7x/q9oozIKfWnfc4A3glkMdnJtZUjLlbV4sgAO4MBDNo65Qsq4L/GesRsVTczmYcAxnrfp8e/eK7wF08oqCvdHddXSHD82aXe/6Y6a3hiLEG+oBMDfG1Skwyt7NGNySlenz3EYEbc35IVoFKIkp2CyMV/nkKQPCgQBL10niEiQd9Q9bHDQJZsBtW59VVCy5K5kIPo6P5v295PCt0WTUppXagY2G/YGpQOmvsjl9MFjMZc4yOOd3RhGhcr2jgd9iF04TvownTxvQAU1EbKcXDHcoPVmhH5zDeiN1JbLNpW2wMf5Vr9VAgMBAAGjggGIMIIBhDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFE8R8swxvB64KS8VqcCaUcMFpEjAMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUFBwEBBGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNyeXB0Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNyeXB0Lm9yZy8wMQYDVR0RBCowKIIQYmFkZW4tbXVlbGxlci5kZYIUd3d3LmJhZGVuLW11ZWxsZXIuZGUwTAYDVR0gBEUwQzAIBgZngQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcwEwYKKwYBBAHWeQIEAwEB/wQCBQAwDQYJKoZIhvcNAQELBQADggEBAIUMEOxJNwvA/pWwFm0BR0HClGdzSC1vQprBaZ6cedUM6b/wfAiCPNJXCvJyrfhJp4T1GcnwU5VnMLPE1/nwJ6LY3My86/M+eQn/3HRuXu3p1GFpp5k2cXsHB7VRlw5X78XVvsnYKc6giwOan7L8fL136EcplQTZRc/5qu9hvazeBOBQQc/lCeWceWz0ZDDVbU2IGvY6aF/SAQREOSq8jLVpEoXB0zwq3dXeEi+PfC2Ea03eOpo1y11nmRYB5Usi/GjMi7oXuBxVQMolJXJj38ziJcp1TT1sv2Ha/00F+Pudo54w1NEo04DbDD9yB2H9wTlMM4YsArmD3K22OGA8wNE=","extensions":{"authorityInfoAccess":"CA Issuers - URI:http://cert.int-x3.letsencrypt.org/\nOCSP - URI:http://ocsp.int-x3.letsencrypt.org\n","authorityKeyIdentifier":"keyid:A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1\n","basicConstraints":"CA:FALSE","certificatePolicies":"Policy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http://cps.letsencrypt.org","ctlPoisonByte":true,"extendedKeyUsage":"TLS Web server authentication, TLS Web client authentication","keyUsage":"Digital Signature, Key Encipherment","subjectAltName":"DNS:www.baden-mueller.de, DNS:baden-mueller.de","subjectKeyIdentifier":"4F:11:F2:CC:31:BC:1E:B8:29:2F:15:A9:C0:9A:51:C3:05:A4:48:C0"},"fingerprint":"64:BF:49:41:3B:7A:FD:5D:C1:04:D9:44:64:9D:1C:25:13:A2:49:86","not_after":1598120268,"not_before":1590344268,"serial_number":"351A6D2C7B0DF3FBC9C9AE9A39263D67681","subject":{"C":null,"CN":"baden-mueller.de","L":null,"O":null,"OU":null,"ST":null,"aggregated":"/CN=baden-mueller.de"}},"seen":1590347943.736608,"source":{"name":"Google 'Argon2020' log","url":"ct.googleapis.com/logs/argon2020/"},"update_type":"PrecertLogEntry"},"message_type":"certificate_update"} \ No newline at end of file diff --git a/res/cert_idn.json b/res/cert_idn.json new file mode 100644 index 0000000..7964f08 --- /dev/null +++ b/res/cert_idn.json @@ -0,0 +1 @@ +{"data":{"cert_index":612101919,"cert_link":"http://ct.googleapis.com/logs/argon2020/ct/v1/get-entries?start=612101919&end=612101919","chain":[{"as_der":"MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0NlowSjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMTGkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EFq6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWAa6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIGCCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNvbTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9kc3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAwVAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcCARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwuY3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsFAAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJouM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwuX4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlGPfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==","extensions":{"authorityInfoAccess":"CA Issuers - URI:http://apps.identrust.com/roots/dstrootcax3.p7c\nOCSP - URI:http://isrg.trustid.ocsp.identrust.com\n","authorityKeyIdentifier":"keyid:C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10\n","basicConstraints":"CA:TRUE","certificatePolicies":"Policy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http://cps.root-x1.letsencrypt.org","crlDistributionPoints":"Full Name:\n URI:http://crl.identrust.com/DSTROOTCAX3CRL.crl","keyUsage":"Digital Signature, Key Cert Sign, C R L Sign","subjectKeyIdentifier":"A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1"},"fingerprint":"E6:A3:B4:5B:06:2D:50:9B:33:82:28:2D:19:6E:FE:97:D5:95:6C:CB","not_after":1615999246,"not_before":1458232846,"serial_number":"A0141420000015385736A0B85ECA708","subject":{"C":"US","CN":"Let's Encrypt Authority X3","L":null,"O":"Let's Encrypt","OU":null,"ST":null,"aggregated":"/C=US/CN=Let's Encrypt Authority X3/O=Let's Encrypt"}},{"as_der":"MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVowPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4Orz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEqOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9bxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaDaeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqGSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXrAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZzR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYoOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ","extensions":{"basicConstraints":"CA:TRUE","keyUsage":"Key Cert Sign, C R L Sign","subjectKeyIdentifier":"C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10"},"fingerprint":"DA:C9:02:4F:54:D8:F6:DF:94:93:5F:B1:73:26:38:CA:6A:D7:7C:13","not_after":1633010475,"not_before":970348339,"serial_number":"44AFB080D6A327BA893039862EF8406B","subject":{"C":null,"CN":"DST Root CA X3","L":null,"O":"Digital Signature Trust Co.","OU":null,"ST":null,"aggregated":"/CN=DST Root CA X3/O=Digital Signature Trust Co."}}],"leaf_cert":{"all_domains":["baden-mueller.de","www.baden-mueller.de"],"as_der":"MIIEezCCA2OgAwIBAgISA1Gm0sew3z+8nJrpo5Jj1naBMA0GCSqGSIb3DQEBCwUAMEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQDExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0yMDA1MjQxODE3NDhaFw0yMDA4MjIxODE3NDhaMBsxGTAXBgNVBAMTEGJhZGVuLW11ZWxsZXIuZGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCSDtXT7WQQaNmnaH6+BxHwEz7eqHdTx/02HV7x/q9oozIKfWnfc4A3glkMdnJtZUjLlbV4sgAO4MBDNo65Qsq4L/GesRsVTczmYcAxnrfp8e/eK7wF08oqCvdHddXSHD82aXe/6Y6a3hiLEG+oBMDfG1Skwyt7NGNySlenz3EYEbc35IVoFKIkp2CyMV/nkKQPCgQBL10niEiQd9Q9bHDQJZsBtW59VVCy5K5kIPo6P5v295PCt0WTUppXagY2G/YGpQOmvsjl9MFjMZc4yOOd3RhGhcr2jgd9iF04TvownTxvQAU1EbKcXDHcoPVmhH5zDeiN1JbLNpW2wMf5Vr9VAgMBAAGjggGIMIIBhDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFE8R8swxvB64KS8VqcCaUcMFpEjAMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUFBwEBBGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNyeXB0Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNyeXB0Lm9yZy8wMQYDVR0RBCowKIIQYmFkZW4tbXVlbGxlci5kZYIUd3d3LmJhZGVuLW11ZWxsZXIuZGUwTAYDVR0gBEUwQzAIBgZngQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcwEwYKKwYBBAHWeQIEAwEB/wQCBQAwDQYJKoZIhvcNAQELBQADggEBAIUMEOxJNwvA/pWwFm0BR0HClGdzSC1vQprBaZ6cedUM6b/wfAiCPNJXCvJyrfhJp4T1GcnwU5VnMLPE1/nwJ6LY3My86/M+eQn/3HRuXu3p1GFpp5k2cXsHB7VRlw5X78XVvsnYKc6giwOan7L8fL136EcplQTZRc/5qu9hvazeBOBQQc/lCeWceWz0ZDDVbU2IGvY6aF/SAQREOSq8jLVpEoXB0zwq3dXeEi+PfC2Ea03eOpo1y11nmRYB5Usi/GjMi7oXuBxVQMolJXJj38ziJcp1TT1sv2Ha/00F+Pudo54w1NEo04DbDD9yB2H9wTlMM4YsArmD3K22OGA8wNE=","extensions":{"authorityInfoAccess":"CA Issuers - URI:http://cert.int-x3.letsencrypt.org/\nOCSP - URI:http://ocsp.int-x3.letsencrypt.org\n","authorityKeyIdentifier":"keyid:A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1\n","basicConstraints":"CA:FALSE","certificatePolicies":"Policy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http://cps.letsencrypt.org","ctlPoisonByte":true,"extendedKeyUsage":"TLS Web server authentication, TLS Web client authentication","keyUsage":"Digital Signature, Key Encipherment","subjectAltName":"DNS:www.baden-mueller.de, DNS:baden-mueller.de","subjectKeyIdentifier":"4F:11:F2:CC:31:BC:1E:B8:29:2F:15:A9:C0:9A:51:C3:05:A4:48:C0"},"fingerprint":"64:BF:49:41:3B:7A:FD:5D:C1:04:D9:44:64:9D:1C:25:13:A2:49:86","not_after":1598120268,"not_before":1590344268,"serial_number":"351A6D2C7B0DF3FBC9C9AE9A39263D67681","subject":{"C":null,"CN":"baden-mueller.de","L":null,"O":null,"OU":null,"ST":null,"aggregated":"/CN=baden-mueller.de"}},"seen":1590347943.736608,"source":{"name":"Google 'Argon2020' log","url":"ct.googleapis.com/logs/argon2020/"},"update_type":"PrecertLogEntry"},"message_type":"certificate_update"} \ No newline at end of file diff --git a/res/heartbeat.json b/res/heartbeat.json new file mode 100644 index 0000000..a1bcf74 --- /dev/null +++ b/res/heartbeat.json @@ -0,0 +1 @@ +{"data":{"cert_index":612101919,"cert_link":"http://ct.googleapis.com/logs/argon2020/ct/v1/get-entries?start=612101919&end=612101919","chain":[{"as_der":"MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0NlowSjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMTGkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EFq6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWAa6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIGCCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNvbTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9kc3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAwVAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcCARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwuY3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsFAAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJouM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwuX4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlGPfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==","extensions":{"authorityInfoAccess":"CA Issuers - URI:http://apps.identrust.com/roots/dstrootcax3.p7c\nOCSP - URI:http://isrg.trustid.ocsp.identrust.com\n","authorityKeyIdentifier":"keyid:C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10\n","basicConstraints":"CA:TRUE","certificatePolicies":"Policy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http://cps.root-x1.letsencrypt.org","crlDistributionPoints":"Full Name:\n URI:http://crl.identrust.com/DSTROOTCAX3CRL.crl","keyUsage":"Digital Signature, Key Cert Sign, C R L Sign","subjectKeyIdentifier":"A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1"},"fingerprint":"E6:A3:B4:5B:06:2D:50:9B:33:82:28:2D:19:6E:FE:97:D5:95:6C:CB","not_after":1615999246,"not_before":1458232846,"serial_number":"A0141420000015385736A0B85ECA708","subject":{"C":"US","CN":"Let's Encrypt Authority X3","L":null,"O":"Let's Encrypt","OU":null,"ST":null,"aggregated":"/C=US/CN=Let's Encrypt Authority X3/O=Let's Encrypt"}},{"as_der":"MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVowPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4Orz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEqOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9bxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaDaeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqGSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXrAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZzR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYoOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ","extensions":{"basicConstraints":"CA:TRUE","keyUsage":"Key Cert Sign, C R L Sign","subjectKeyIdentifier":"C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10"},"fingerprint":"DA:C9:02:4F:54:D8:F6:DF:94:93:5F:B1:73:26:38:CA:6A:D7:7C:13","not_after":1633010475,"not_before":970348339,"serial_number":"44AFB080D6A327BA893039862EF8406B","subject":{"C":null,"CN":"DST Root CA X3","L":null,"O":"Digital Signature Trust Co.","OU":null,"ST":null,"aggregated":"/CN=DST Root CA X3/O=Digital Signature Trust Co."}}],"leaf_cert":{"all_domains":["baden-mueller.de","www.baden-mueller.de"],"as_der":"MIIEezCCA2OgAwIBAgISA1Gm0sew3z+8nJrpo5Jj1naBMA0GCSqGSIb3DQEBCwUAMEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQDExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0yMDA1MjQxODE3NDhaFw0yMDA4MjIxODE3NDhaMBsxGTAXBgNVBAMTEGJhZGVuLW11ZWxsZXIuZGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCSDtXT7WQQaNmnaH6+BxHwEz7eqHdTx/02HV7x/q9oozIKfWnfc4A3glkMdnJtZUjLlbV4sgAO4MBDNo65Qsq4L/GesRsVTczmYcAxnrfp8e/eK7wF08oqCvdHddXSHD82aXe/6Y6a3hiLEG+oBMDfG1Skwyt7NGNySlenz3EYEbc35IVoFKIkp2CyMV/nkKQPCgQBL10niEiQd9Q9bHDQJZsBtW59VVCy5K5kIPo6P5v295PCt0WTUppXagY2G/YGpQOmvsjl9MFjMZc4yOOd3RhGhcr2jgd9iF04TvownTxvQAU1EbKcXDHcoPVmhH5zDeiN1JbLNpW2wMf5Vr9VAgMBAAGjggGIMIIBhDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFE8R8swxvB64KS8VqcCaUcMFpEjAMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUFBwEBBGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNyeXB0Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNyeXB0Lm9yZy8wMQYDVR0RBCowKIIQYmFkZW4tbXVlbGxlci5kZYIUd3d3LmJhZGVuLW11ZWxsZXIuZGUwTAYDVR0gBEUwQzAIBgZngQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcwEwYKKwYBBAHWeQIEAwEB/wQCBQAwDQYJKoZIhvcNAQELBQADggEBAIUMEOxJNwvA/pWwFm0BR0HClGdzSC1vQprBaZ6cedUM6b/wfAiCPNJXCvJyrfhJp4T1GcnwU5VnMLPE1/nwJ6LY3My86/M+eQn/3HRuXu3p1GFpp5k2cXsHB7VRlw5X78XVvsnYKc6giwOan7L8fL136EcplQTZRc/5qu9hvazeBOBQQc/lCeWceWz0ZDDVbU2IGvY6aF/SAQREOSq8jLVpEoXB0zwq3dXeEi+PfC2Ea03eOpo1y11nmRYB5Usi/GjMi7oXuBxVQMolJXJj38ziJcp1TT1sv2Ha/00F+Pudo54w1NEo04DbDD9yB2H9wTlMM4YsArmD3K22OGA8wNE=","extensions":{"authorityInfoAccess":"CA Issuers - URI:http://cert.int-x3.letsencrypt.org/\nOCSP - URI:http://ocsp.int-x3.letsencrypt.org\n","authorityKeyIdentifier":"keyid:A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1\n","basicConstraints":"CA:FALSE","certificatePolicies":"Policy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http://cps.letsencrypt.org","ctlPoisonByte":true,"extendedKeyUsage":"TLS Web server authentication, TLS Web client authentication","keyUsage":"Digital Signature, Key Encipherment","subjectAltName":"DNS:www.baden-mueller.de, DNS:baden-mueller.de","subjectKeyIdentifier":"4F:11:F2:CC:31:BC:1E:B8:29:2F:15:A9:C0:9A:51:C3:05:A4:48:C0"},"fingerprint":"64:BF:49:41:3B:7A:FD:5D:C1:04:D9:44:64:9D:1C:25:13:A2:49:86","not_after":1598120268,"not_before":1590344268,"serial_number":"351A6D2C7B0DF3FBC9C9AE9A39263D67681","subject":{"C":null,"CN":"baden-mueller.de","L":null,"O":null,"OU":null,"ST":null,"aggregated":"/CN=baden-mueller.de"}},"seen":1590347943.736608,"source":{"name":"Google 'Argon2020' log","url":"ct.googleapis.com/logs/argon2020/"},"update_type":"PrecertLogEntry"},"message_type":"heartbeat"} \ No newline at end of file From 4e82160aaece3baa72f322936b7b495bc149cfca Mon Sep 17 00:00:00 2001 From: ayoul3 Date: Sat, 30 May 2020 12:16:54 +0200 Subject: [PATCH 08/11] clean up domainName variable --- lib/config.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/config.go b/lib/config.go index b9fb4cc..08c311a 100644 --- a/lib/config.go +++ b/lib/config.go @@ -16,7 +16,6 @@ type Configuration struct { SlackWebHookURL string SlackIconURL string SlackUsername string - DomainName string RegIP string Regexp string Deduplication string @@ -36,7 +35,6 @@ func GetConfig() *Configuration { v.SetDefault("SlackWebhookURL", "") v.SetDefault("SlackIconURL", "") v.SetDefault("SlackUsername", "Cercat") - v.SetDefault("DomainName", "") v.SetDefault("Regexp", "") v.SetDefault("Workers", 20) From 660ea86607f79e6e9450c8e2555f2e715c2e3924 Mon Sep 17 00:00:00 2001 From: Issif Date: Tue, 2 Jun 2020 23:06:05 +0200 Subject: [PATCH 09/11] notifier worker for deduplication --- lib/config.go | 8 +++-- lib/lib.go | 48 +++++++++++++++++++---------- {lib => lib_test}/lib_suite_test.go | 0 {lib => lib_test}/lib_test.go | 0 main.go | 2 +- 5 files changed, 39 insertions(+), 19 deletions(-) rename {lib => lib_test}/lib_suite_test.go (100%) rename {lib => lib_test}/lib_test.go (100%) diff --git a/lib/config.go b/lib/config.go index 08c311a..8eb301a 100644 --- a/lib/config.go +++ b/lib/config.go @@ -1,6 +1,7 @@ package lib import ( + "container/ring" "path" "path/filepath" "regexp" @@ -18,14 +19,17 @@ type Configuration struct { SlackUsername string RegIP string Regexp string - Deduplication string + PreviousCerts *ring.Ring + Buffer chan *Result Homoglyph map[string]string } // GetConfig provides a Configuration func GetConfig() *Configuration { c := &Configuration{ - Homoglyph: GetHomoglyphMap(), + Homoglyph: GetHomoglyphMap(), + PreviousCerts: ring.New(20), + Buffer: make(chan *Result, 50), } configFile := kingpin.Flag("configfile", "config file").Short('c').ExistingFile() diff --git a/lib/lib.go b/lib/lib.go index 1e5a932..dda8826 100644 --- a/lib/lib.go +++ b/lib/lib.go @@ -63,26 +63,18 @@ func CertCheckWorker(config *Configuration) { for { msg := <-MsgChan - - detailedCert, err := ParseResultCertificate(msg) + result, err := ParseResultCertificate(msg) if err != nil { log.Warnf("Error parsing message: %s", err) continue } - if detailedCert == nil { + if result == nil { continue } - if !IsMatchingCert(config, detailedCert, reg) { + if !IsMatchingCert(config, result, reg) { continue } - - j, _ := json.Marshal(detailedCert) - log.Infof("A certificate for '%v' has been issued : %v\n", detailedCert.Domain, string(j)) - if config.SlackWebHookURL != "" { - go func(c *Configuration, r *Result) { - newSlackPayload(c, detailedCert).post(c) - }(config, detailedCert) - } + config.Buffer <- result } } @@ -134,12 +126,12 @@ func isIDN(domain string) bool { } // IsMatchingCert checks if certificate matches the regexp -func IsMatchingCert(config *Configuration, cert *Result, reg *regexp.Regexp) bool { - domainList := append(cert.SAN, cert.Domain) +func IsMatchingCert(config *Configuration, result *Result, reg *regexp.Regexp) bool { + domainList := append(result.SAN, result.Domain) for _, domain := range domainList { if isIDN(domain) { - cert.IDN, _ = idna.ToUnicode(domain) - domain = replaceHomoglyph(cert.IDN, config.Homoglyph) + result.IDN, _ = idna.ToUnicode(domain) + domain = replaceHomoglyph(result.IDN, config.Homoglyph) } if reg.MatchString(domain) { return true @@ -168,3 +160,27 @@ func LoopCertStream(config *Configuration) { } } } + +// Notifier is a worker that receives cert, depduplicates and sends to Slack the event +func Notifier(config *Configuration) { + for { + result := <-config.Buffer + duplicate := false + config.PreviousCerts.Do(func(d interface{}) { + if result.Domain == d { + duplicate = true + } + }) + if !duplicate { + config.PreviousCerts = config.PreviousCerts.Prev() + config.PreviousCerts.Value = result.Domain + j, _ := json.Marshal(result) + log.Infof("A certificate for '%v' has been issued : %v\n", result.Domain, string(j)) + if config.SlackWebHookURL != "" { + go func(c *Configuration, r *Result) { + newSlackPayload(c, result).post(c) + }(config, result) + } + } + } +} diff --git a/lib/lib_suite_test.go b/lib_test/lib_suite_test.go similarity index 100% rename from lib/lib_suite_test.go rename to lib_test/lib_suite_test.go diff --git a/lib/lib_test.go b/lib_test/lib_test.go similarity index 100% rename from lib/lib_test.go rename to lib_test/lib_test.go diff --git a/main.go b/main.go index 0416821..b67fc86 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,6 @@ func main() { for i := 0; i < config.Workers; i++ { go lib.CertCheckWorker(config) } - + go lib.Notifier(config) lib.LoopCertStream(config) } From 3bfc5fb56324490f17b5902acabbec11d1d5b231 Mon Sep 17 00:00:00 2001 From: Issif Date: Tue, 2 Jun 2020 23:31:56 +0200 Subject: [PATCH 10/11] remove config for nb of workers + add messages chan in config object --- README.md | 2 -- lib/config.go | 6 +++--- lib/lib.go | 7 ++----- main.go | 1 - 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 16f2ac5..54ca37d 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,6 @@ SlackWebhookURL: "" #Slack Webhook URL SlackIconURL: "" #Slack Icon (Avatar) URL SlackUsername: "" #Slack Username Regexp: ".*\\.fr$" #Regexp to match. Can't be empty. It uses Golang regexp format -Workers: 20 #Number of workers for consuming feed from CertStream ``` ### With env vars @@ -36,7 +35,6 @@ Workers: 20 #Number of workers for consuming feed from CertStream - **SLACKICONURL**: Slack Icon (Avatar) URL - **SLACKUSERNAME**: Slack Username - **REGEXP**: Regexp to match, if empty, '.*' is used. Use Golang regexp format -- **WORKERS**: Number of workers for consuming feed from CertStream ## Run diff --git a/lib/config.go b/lib/config.go index 8eb301a..ed74615 100644 --- a/lib/config.go +++ b/lib/config.go @@ -20,6 +20,7 @@ type Configuration struct { RegIP string Regexp string PreviousCerts *ring.Ring + Messages chan []byte Buffer chan *Result Homoglyph map[string]string } @@ -27,8 +28,10 @@ type Configuration struct { // GetConfig provides a Configuration func GetConfig() *Configuration { c := &Configuration{ + Workers: 50, Homoglyph: GetHomoglyphMap(), PreviousCerts: ring.New(20), + Messages: make(chan []byte, 50), Buffer: make(chan *Result, 50), } @@ -68,9 +71,6 @@ func GetConfig() *Configuration { if _, err := regexp.Compile(c.Regexp); err != nil { log.Fatal("Bad regexp") } - if c.Workers < 1 { - log.Fatal("Workers must be strictly a positive number") - } return c } diff --git a/lib/lib.go b/lib/lib.go index dda8826..224cb29 100644 --- a/lib/lib.go +++ b/lib/lib.go @@ -51,9 +51,6 @@ type leafCert struct { AllDomains []string `json:"all_domains"` } -// MsgChan is the communication channel between certCheckWorkers and LoopCertStream -var MsgChan chan []byte - // the websocket stream from calidog const certInput = "wss://certstream.calidog.io" @@ -62,7 +59,7 @@ func CertCheckWorker(config *Configuration) { reg, _ := regexp.Compile(config.Regexp) for { - msg := <-MsgChan + msg := <-config.Messages result, err := ParseResultCertificate(msg) if err != nil { log.Warnf("Error parsing message: %s", err) @@ -156,7 +153,7 @@ func LoopCertStream(config *Configuration) { log.Warn("Error reading message from CertStream") break } - MsgChan <- msg + config.Messages <- msg } } } diff --git a/main.go b/main.go index b67fc86..e76a38c 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,6 @@ func init() { func main() { go http.ListenAndServe("localhost:6060", nil) - lib.MsgChan = make(chan []byte, 10) for i := 0; i < config.Workers; i++ { go lib.CertCheckWorker(config) } From 1a78371dfec11e309cdf552dc7c587a1323b9a12 Mon Sep 17 00:00:00 2001 From: Issif Date: Sun, 7 Jun 2020 21:48:08 +0200 Subject: [PATCH 11/11] update readme + back of example --- .gitignore | 3 ++- README.md | 14 +++++++++----- example.yaml | 3 +-- {lib_test => lib}/lib_suite_test.go | 0 {lib_test => lib}/lib_test.go | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) rename {lib_test => lib}/lib_suite_test.go (100%) rename {lib_test => lib}/lib_test.go (97%) diff --git a/.gitignore b/.gitignore index 501c251..3b9a9c9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ cercat config.yaml dist dist/** -lib/*.xml \ No newline at end of file +lib/*.xml +.vscode \ No newline at end of file diff --git a/README.md b/README.md index 54ca37d..d294ccb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # cercat -`certcat` is for **Certificate Catcher**. It's monitors issued certificates from [CertStream](https://certstream.calidog.io/) stream and send an alert to **Slack** if a domain matchs a specified **regexp**. +`certcat` is for **Certificate Catcher**. It monitors issued certificates from [CertStream](https://certstream.calidog.io/) stream and sends an alert to **Slack** if a domain matches a specified **regexp**. ```bash websocket +----------+ POST @@ -13,6 +13,8 @@ CertSteam <-----------------> cercat +-----------> Slack It's highly inspired by [CertStreamMonitor](https://github.com/AssuranceMaladieSec/CertStreamMonitor/blob/master/README.md), the first idea was to improve performances for catching with a **Golang** version. +The regexp is applied on principal an SAN domains. If one of these domains is an [IDN](https://en.wikipedia.org/wiki/Internationalized_domain_name), it's converted in an equivalend in ASCII before applying the regexp. + ## Configuration Two methods are available for configuration and can be mixed : @@ -51,14 +53,14 @@ Flags: You can run with Docker : ``` -docker run -d -e SLACKWEBHOOKURL=https://hooks.slack.com/services/XXXXX -e REGEXP=".*fr$" issif/cercat:latest +docker run -d -e SLACKWEBHOOKURL=https://hooks.slack.com/services/XXXXX -e REGEXP=".*\\.fr$" issif/cercat:latest ``` ## Logs ```bash -2020/04/14 17:29:40 [INFO] : A certificate for 'www.XXXX.fr' has been issued : {"domain":"www.XXXX.fr","SAN":["www.XXXX.fr"],"issuer":"Let's Encrypt","Addresses":["XX.XX.XX.183","XX.XX.XX.182"]} -2020/04/14 17:29:41 [INFO] : A certificate for 'XXXX.fr' has been issued : {"domain":"XXXX.fr","SAN":["mail.XXXX.fr","XXXX.fr","www.XXXX.fr"],"issuer":"Let's Encrypt","Addresses":["XX.XX.XX.108"]} +INFO[0005] A certificate for 'xxxx.fr' has been issued : {"domain":"xxxx.fr","SAN":["xxxx.fr","www.xxxx.fr"],"issuer":"Let's Encrypt","Addresses":["X.X.X.129"]} +INFO[0008] A certificate for 'xxxx.fr' has been issued : {"domain":"xxxx.fr","SAN":["xxxx.fr","www.xxxx.fr"],"issuer":"Let's Encrypt","Addresses":["X.X.X.116"]} ``` ## Profiles, Traces and Metrics @@ -69,6 +71,8 @@ The service opens port `6060` for `profiles`, `traces` and `expvar`. Go to [http MIT -## Author +## Authors Thomas Labarussias - [@Issif](https://www.github.com/issif) +Ayoul Elaassal - [@Ayoul3](https://github.com/ayoul3) + diff --git a/example.yaml b/example.yaml index 2acbf83..500483b 100644 --- a/example.yaml +++ b/example.yaml @@ -2,5 +2,4 @@ SlackWebhookURL: "" #Slack Webhook URL SlackIconURL: "" #Slack Icon (Avatar) URL SlackUsername: "" #Slack Username -Regexp: ".*\\.fr" #Regexp to match. Can't be empty. It uses Golang regexp format -Workers: 20 #Number of workers for consuming stream from CertStream \ No newline at end of file +Regexp: ".*\\.fr$" #Regexp to match. Can't be empty. It uses Golang regexp format diff --git a/lib_test/lib_suite_test.go b/lib/lib_suite_test.go similarity index 100% rename from lib_test/lib_suite_test.go rename to lib/lib_suite_test.go diff --git a/lib_test/lib_test.go b/lib/lib_test.go similarity index 97% rename from lib_test/lib_test.go rename to lib/lib_test.go index e7981c1..f2e5224 100644 --- a/lib_test/lib_test.go +++ b/lib/lib_test.go @@ -23,7 +23,7 @@ var _ = Describe("Handler", func() { }) }) Describe("If alternative subject matches", func() { - cert := &lib.Result{Domain: "www.test.net", SAN: []string{"www.test.com"}} + cert := &lib.Result{Domain: "www.tset.net", SAN: []string{"www.test.com"}} It("should return true", func() { result := lib.IsMatchingCert(config, cert, reg) Expect(result).To(BeTrue())