Skip to content

Commit

Permalink
refact lib functions
Browse files Browse the repository at this point in the history
update tests

add cmd/cercat/main.go

add pkg/model

add pkg/homoglyph and config pkg

add pkg/slack

update Dockerfile

use pointer to hymoglyph map

rename NewSlackPayload to NewPayload

fix missing file

use pointer to hymoglyph map
  • Loading branch information
Amine Benseddik committed Jul 13, 2020
1 parent b3bfd96 commit 56c2a73
Show file tree
Hide file tree
Showing 14 changed files with 419 additions and 137 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ WORKDIR /src
ADD . .

RUN go mod download
RUN go build -ldflags="-s -w" -o cercat
RUN go build -ldflags="-s -w" -o cercat ./cmd/cercat

# Final Docker image
FROM alpine AS final-stage
LABEL MAINTAINER "Thomas Labarussias <[email protected]>"

RUN apk add --no-cache ca-certificates

# Create user falcosidekick
# Create user cercat
RUN addgroup -S cercat && adduser -u 1234 -S cercat -G cercat
USER 1234

Expand Down
34 changes: 34 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package main

import (
"cercat/config"
"cercat/lib"
"fmt"
"net/http"
"os"
"path/filepath"

"github.com/pkg/errors"
"gopkg.in/alecthomas/kingpin.v2"
)

func main() {
a := kingpin.New(filepath.Base(os.Args[0]), "")
configFile := a.Flag("configfile", "config file").Short('c').ExistingFile()
a.HelpFlag.Short('h')

_, err := a.Parse(os.Args[1:])
if err != nil {
fmt.Fprintln(os.Stderr, errors.Wrapf(err, "Error parsing commandline arguments"))
a.Usage(os.Args[1:])
os.Exit(2)
}

cfg := config.GetConfig(configFile)
go http.ListenAndServe("localhost:6060", nil)
for i := 0; i < cfg.Workers; i++ {
go lib.CertCheckWorker(cfg.Regexp, &cfg.Homoglyph, cfg.Messages, cfg.Buffer)
}
go lib.Notifier(cfg)
lib.LoopCertStream(cfg.Messages)
}
16 changes: 7 additions & 9 deletions lib/config.go → config/config.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package lib
package config

import (
"cercat/pkg/homoglyph"
"cercat/pkg/model"
"container/ring"
"path"
"path/filepath"
"regexp"

log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)

// Configuration represents a configuration element
Expand All @@ -21,23 +22,20 @@ type Configuration struct {
Regexp string
PreviousCerts *ring.Ring
Messages chan []byte
Buffer chan *Result
Buffer chan *model.Result
Homoglyph map[string]string
}

// GetConfig provides a Configuration
func GetConfig() *Configuration {
func GetConfig(configFile *string) *Configuration {
c := &Configuration{
Workers: 50,
Homoglyph: GetHomoglyphMap(),
Homoglyph: homoglyph.GetHomoglyphMap(),
PreviousCerts: ring.New(20),
Messages: make(chan []byte, 50),
Buffer: make(chan *Result, 50),
Buffer: make(chan *model.Result, 50),
}

configFile := kingpin.Flag("configfile", "config file").Short('c').ExistingFile()
kingpin.Parse()

v := viper.New()
v.SetDefault("SlackWebhookURL", "")
v.SetDefault("SlackIconURL", "")
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ require (
github.com/gobwas/ws v1.0.3
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/pkg/errors v0.8.0
github.com/sirupsen/logrus v1.2.0
github.com/spf13/viper v1.6.3
github.com/stretchr/testify v1.4.0 // indirect
Expand Down
4 changes: 1 addition & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,11 @@ github.com/onsi/ginkgo v1.12.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9k
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/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 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
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=
Expand Down Expand Up @@ -166,7 +165,6 @@ 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=
Expand Down
85 changes: 26 additions & 59 deletions lib/lib.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package lib

import (
"cercat/config"
"cercat/pkg/homoglyph"
"cercat/pkg/model"
"cercat/pkg/slack"
"context"
"encoding/json"
"net"
Expand All @@ -14,52 +18,15 @@ import (
"golang.org/x/net/idna"
)

// 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"`
}

// certificate represents a certificate from CertStream
type certificate struct {
MessageType string `json:"message_type"`
Data data `json:"data"`
}

// 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"`
CertIndex float32 `json:"cert_index"`
Seen float32 `json:"seen"`
Source map[string]string `json:"source"`
}

// 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"`
NotAfter float32 `json:"not_after"`
SerialNumber string `json:"serial_number"`
FingerPrint string `json:"fingerprint"`
AsDer string `json:"as_der"`
AllDomains []string `json:"all_domains"`
}

// 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)
func CertCheckWorker(r string, homoglyph *map[string]string, msgChan chan []byte, bufferChan chan *model.Result) {
reg, _ := regexp.Compile(r)

for {
msg := <-config.Messages
msg := <-msgChan
result, err := ParseResultCertificate(msg)
if err != nil {
log.Warnf("Error parsing message: %s", err)
Expand All @@ -68,24 +35,24 @@ func CertCheckWorker(config *Configuration) {
if result == nil {
continue
}
if !IsMatchingCert(config, result, reg) {
if !IsMatchingCert(homoglyph, result, reg) {
continue
}
config.Buffer <- result
bufferChan <- result
}
}

// ParseResultCertificate parses certificate details
func ParseResultCertificate(msg []byte) (*Result, error) {
var c certificate
var r *Result
func ParseResultCertificate(msg []byte) (*model.Result, error) {
var c model.Certificate
var r *model.Result

err := json.Unmarshal(msg, &c)
if err != nil || c.MessageType == "heartbeat" {
return nil, err
}

r = &Result{
r = &model.Result{
Domain: c.Data.LeafCert.Subject["CN"],
Issuer: c.Data.Chain[0].Subject["O"],
SAN: c.Data.LeafCert.AllDomains,
Expand Down Expand Up @@ -129,12 +96,12 @@ func isIDN(domain string) bool {
}

// IsMatchingCert checks if certificate matches the regexp
func IsMatchingCert(config *Configuration, result *Result, reg *regexp.Regexp) bool {
func IsMatchingCert(homoglyphs *map[string]string, result *model.Result, reg *regexp.Regexp) bool {
domainList := append(result.SAN, result.Domain)
for _, domain := range domainList {
if isIDN(domain) {
result.IDN, _ = idna.ToUnicode(domain)
domain = replaceHomoglyph(result.IDN, config.Homoglyph)
domain = homoglyph.ReplaceHomoglyph(result.IDN, *homoglyphs)
}
if reg.MatchString(domain) {
return true
Expand All @@ -144,7 +111,7 @@ func IsMatchingCert(config *Configuration, result *Result, reg *regexp.Regexp) b
}

// LoopCertStream gathers messages from CertStream
func LoopCertStream(config *Configuration) {
func LoopCertStream(msgBuf chan []byte) {
dial := ws.Dialer{
ReadBufferSize: 8192,
WriteBufferSize: 512,
Expand All @@ -165,31 +132,31 @@ func LoopCertStream(config *Configuration) {
log.Warn("Error reading message from CertStream")
break
}
config.Messages <- msg
msgBuf <- msg
}
conn.Close()
}
}

// Notifier is a worker that receives cert, depduplicates and sends to Slack the event
func Notifier(config *Configuration) {
func Notifier(cfg *config.Configuration) {
for {
result := <-config.Buffer
result := <-cfg.Buffer
duplicate := false
config.PreviousCerts.Do(func(d interface{}) {
cfg.PreviousCerts.Do(func(d interface{}) {
if result.Domain == d {
duplicate = true
}
})
if !duplicate {
config.PreviousCerts = config.PreviousCerts.Prev()
config.PreviousCerts.Value = result.Domain
cfg.PreviousCerts = cfg.PreviousCerts.Prev()
cfg.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)
if cfg.SlackWebHookURL != "" {
go func(c *config.Configuration, r *model.Result) {
slack.NewPayload(c, result).Post(c)
}(cfg, result)
}
}
}
Expand Down
26 changes: 15 additions & 11 deletions lib/lib_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package lib_test

import (
"cercat/config"
"cercat/lib"
"cercat/pkg/homoglyph"
"cercat/pkg/model"
"cercat/pkg/slack"
"io/ioutil"
"regexp"

Expand All @@ -10,31 +14,31 @@ import (
)

var _ = Describe("Handler", func() {
config := &lib.Configuration{
Homoglyph: lib.GetHomoglyphMap(),
config := &config.Configuration{
Homoglyph: homoglyph.GetHomoglyphMap(),
SlackUsername: "test",
SlackIconURL: "http://test",
}
reg, _ := regexp.Compile(".*test.*")
Describe("isMatchingCert", func() {
Describe("If certificate matches", func() {
cert := &lib.Result{Domain: "www.test.com"}
cert := &model.Result{Domain: "www.test.com"}
It("should return true", func() {
result := lib.IsMatchingCert(config, cert, reg)
result := lib.IsMatchingCert(&config.Homoglyph, cert, reg)
Expect(result).To(BeTrue())
})
})
Describe("If alternative subject matches", func() {
cert := &lib.Result{Domain: "www.tset.net", SAN: []string{"www.test.com"}}
cert := &model.Result{Domain: "www.tset.net", SAN: []string{"www.test.com"}}
It("should return true", func() {
result := lib.IsMatchingCert(config, cert, reg)
result := lib.IsMatchingCert(&config.Homoglyph, cert, reg)
Expect(result).To(BeTrue())
})
})
Describe("If domain is IDN", func() {
cert := &lib.Result{Domain: "www.xn--tst-rdd.com"}
cert := &model.Result{Domain: "www.xn--tst-rdd.com"}
It("should return true", func() {
result := lib.IsMatchingCert(config, cert, reg)
result := lib.IsMatchingCert(&config.Homoglyph, cert, reg)
Expect(result).To(BeTrue())
Expect(cert.IDN).To(Equal("www.tеst.com")) // e is cyrillic
})
Expand All @@ -44,8 +48,8 @@ var _ = Describe("Handler", func() {
msg, _ := ioutil.ReadFile("../res/cert.json")
It("should return a valid payload", func() {
result, err := lib.ParseResultCertificate(msg)
slackPayload := lib.NewSlackPayload(config, result)
Expect(slackPayload.Text).Should(Equal("A certificate for *baden-mueller.de* has been issued"))
slackPayload := slack.NewPayload(config, result)
Expect(slackPayload.Text).Should(Equal("A certificate for baden-mueller.de has been issued"))
Expect(slackPayload.Username).Should(Equal("test"))
Expect(slackPayload.IconURL).Should(Equal("http://test"))
Expect(err).ToNot(HaveOccurred())
Expand Down Expand Up @@ -84,7 +88,7 @@ var _ = Describe("Handler", func() {
msg, _ := ioutil.ReadFile("../res/cert_idn.json")
It("should return valid infos", func() {
result, err := lib.ParseResultCertificate(msg)
lib.IsMatchingCert(config, result, reg)
lib.IsMatchingCert(&config.Homoglyph, result, reg)
Expect(result.Domain).Should(Equal("xn--badn-mullr-msiec.de"))
Expect(result.IDN).Should(Equal("badеn-muеllеr.de")) // e is cyrillic
Expect(result.SAN).Should(Equal([]string{"xn--badn-mullr-msiec.de", "www.baden-mueller.de"}))
Expand Down
23 changes: 0 additions & 23 deletions main.go

This file was deleted.

Loading

0 comments on commit 56c2a73

Please sign in to comment.