From 91e551f6d5d83ea9358f6b5f1ccab732e19b9d9c Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 22 Jul 2024 10:36:51 +0200 Subject: [PATCH] init --- .gitignore | 2 + .woodpecker/build.yml | 32 +++++++++++++++ Makefile | 2 + README.md | 26 ++++++++++++ app.go | 44 ++++++++++++++++++++ checker/certificates.go | 46 +++++++++++++++++++++ checker/domains.go | 91 +++++++++++++++++++++++++++++++++++++++++ checker/struct.go | 8 ++++ go.mod | 22 ++++++++++ go.sum | 32 +++++++++++++++ main.go | 12 ++++++ render/render.go | 71 ++++++++++++++++++++++++++++++++ 12 files changed, 388 insertions(+) create mode 100644 .gitignore create mode 100644 .woodpecker/build.yml create mode 100644 Makefile create mode 100644 README.md create mode 100644 app.go create mode 100644 checker/certificates.go create mode 100644 checker/domains.go create mode 100644 checker/struct.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 render/render.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a67dda3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/expiration-check +/test.sh diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml new file mode 100644 index 0000000..5a800b6 --- /dev/null +++ b/.woodpecker/build.yml @@ -0,0 +1,32 @@ +when: + - event: [pull_request, tag] + - event: push + branch: + - ${CI_REPO_DEFAULT_BRANCH} + - feature/* + - release/* + - renovate/* + +variables: + - &golang_image 'golang:1.22' + +steps: + "Add vendor": + image: *golang_image + commands: + - go mod vendor + + "Run build": + image: *golang_image + commands: + - go build + + "Publish": + image: plugins/gitea-release + settings: + api_key: + from_secret: gitnet_api_key + base_url: https://gitnet.fr + files: ./expiration-check + when: + event: [tag] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..79eee22 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +build: + go build diff --git a/README.md b/README.md new file mode 100644 index 0000000..41b3352 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +Expiration checker +================== + +Checks the expiration dates of domains et certificates. + +## Usage + +Go to [releases](https://gitnet.fr/deblan/expiration-check/releases) and download the latest version. + +```text +$ expiration-check domains -d example.com -d other-example.com ++-------------------+------+---------------------+ +| DOMAIN | DAYS | DATE | ++-------------------+------+---------------------+ +| example.com | XX | YYYY-MM-DD HH:MM:SS | +| other-example.com | XXX | YYYY-MM-DD HH:MM:SS | ++-------------------+------+---------------------+ + +$ expiration-check certificates -d example.com -d other-example.com -d mail.example.com:993 ++-------------------+------+---------------------+ +| DOMAIN | DAYS | DATE | ++-------------------+------+---------------------+ +| example.com | XX | YYYY-MM-DD HH:MM:SS | +| other-example.com | XXX | YYYY-MM-DD HH:MM:SS | ++-------------------+------+---------------------+ +``` diff --git a/app.go b/app.go new file mode 100644 index 0000000..bfa9e7f --- /dev/null +++ b/app.go @@ -0,0 +1,44 @@ +package main + +import ( + "github.com/urfave/cli/v2" + "gitnet.fr/deblan/expiration-check/checker" + "gitnet.fr/deblan/expiration-check/render" +) + +func App() *cli.App { + flags := []cli.Flag{ + &cli.StringSliceFlag{ + Name: "domain", + Aliases: []string{"d"}, + Required: true, + }, + } + + return &cli.App{ + Commands: []*cli.Command{ + { + Name: "certificate", + Aliases: []string{"c", "cert", "certs", "certificates"}, + Usage: "Checks certificate", + Flags: flags, + Action: func(c *cli.Context) error { + render.Render(checker.CheckCertificates(c.StringSlice("domain")), 30, 14) + + return nil + }, + }, + { + Name: "domain", + Usage: "Checks domain expirations", + Aliases: []string{"d", "domains"}, + Flags: flags, + Action: func(c *cli.Context) error { + render.Render(checker.CheckDomains(c.StringSlice("domain")), 30, 14) + + return nil + }, + }, + }, + } +} diff --git a/checker/certificates.go b/checker/certificates.go new file mode 100644 index 0000000..7f4db3e --- /dev/null +++ b/checker/certificates.go @@ -0,0 +1,46 @@ +package checker + +import ( + "crypto/tls" + "fmt" + "math" + "strings" + "time" +) + +func FormatDomain(domain string) string { + elements := strings.Split(domain, ":") + + if len(elements) == 1 { + return fmt.Sprintf("%s:443", elements[0]) + } + + return domain +} + +func CheckCertificate(domain string) Domain { + now := time.Now() + conn, err := tls.Dial("tcp", FormatDomain(domain), nil) + + if err == nil { + date := conn.ConnectionState().PeerCertificates[0].NotAfter + daysLeft := date.Sub(now).Hours() / 24 + + return Domain{ + Name: domain, + DaysLeft: math.Floor(daysLeft), + Date: date.Format(time.DateTime), + } + } + return Domain{Name: domain, Failed: true} +} + +func CheckCertificates(domains []string) []Domain { + values := []Domain{} + + for _, domain := range domains { + values = append(values, CheckCertificate(domain)) + } + + return values +} diff --git a/checker/domains.go b/checker/domains.go new file mode 100644 index 0000000..27704c0 --- /dev/null +++ b/checker/domains.go @@ -0,0 +1,91 @@ +package checker + +import ( + "encoding/json" + "fmt" + "math" + "net/http" + "strings" + "time" +) + +type RdapResponseData struct { + Events []struct { + EventAction string `json:"eventAction"` + EventDate string `json:"eventDate"` + } `json:"events"` +} + +type RdapServices struct { + Services [][][]string `json:"services"` +} + +func GetRdapServices() map[string]string { + response, _ := http.Get("https://data.iana.org/rdap/dns.json") + values := make(map[string]string) + + defer response.Body.Close() + var data RdapServices + json.NewDecoder(response.Body).Decode(&data) + + for _, value := range data.Services { + for _, tld := range value[0] { + values[tld] = value[1][0] + } + } + + return values +} + +func ExtractTld(domain string) string { + elements := strings.Split(domain, ".") + + if len(elements) == 1 { + return elements[0] + } + + return strings.Join(elements[1:], ".") +} + +func RdapCheck(domain, service string) Domain { + url := fmt.Sprintf("%sdomain/%s?jscard=1", service, domain) + response, _ := http.Get(url) + now := time.Now() + + defer response.Body.Close() + var data RdapResponseData + json.NewDecoder(response.Body).Decode(&data) + + for _, event := range data.Events { + if event.EventAction == "expiration" { + date, _ := time.Parse(time.RFC3339, event.EventDate) + daysLeft := date.Sub(now).Hours() / 24 + + return Domain{ + Name: domain, + DaysLeft: math.Floor(daysLeft), + Date: date.Format(time.DateTime), + } + } + } + + return Domain{Name: domain, Failed: true} +} + +func CheckDomains(domains []string) []Domain { + values := []Domain{} + services := GetRdapServices() + + for _, domain := range domains { + tld := ExtractTld(domain) + service := services[tld] + + if service != "" { + values = append(values, RdapCheck(domain, service)) + } else { + values = append(values, Domain{Name: domain, Failed: true}) + } + } + + return values +} diff --git a/checker/struct.go b/checker/struct.go new file mode 100644 index 0000000..645dc88 --- /dev/null +++ b/checker/struct.go @@ -0,0 +1,8 @@ +package checker + +type Domain struct { + Name string + DaysLeft float64 + Date string + Failed bool +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f57937e --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +module gitnet.fr/deblan/expiration-check + +go 1.22.2 + +require ( + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/go-openapi/errors v0.22.0 // indirect + github.com/go-openapi/strfmt v0.23.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jedib0t/go-pretty v4.3.0+incompatible // indirect + github.com/jedib0t/go-pretty/v6 v6.5.9 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/urfave/cli/v2 v2.27.2 // indirect + github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect + go.mongodb.org/mongo-driver v1.14.0 // indirect + golang.org/x/sys v0.17.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7f8bb64 --- /dev/null +++ b/go.sum @@ -0,0 +1,32 @@ +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= +github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= +github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= +github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= +github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= +github.com/jedib0t/go-pretty/v6 v6.5.9 h1:ACteMBRrrmm1gMsXe9PSTOClQ63IXDUt03H5U+UV8OU= +github.com/jedib0t/go-pretty/v6 v6.5.9/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= +github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= +go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= +go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/main.go b/main.go new file mode 100644 index 0000000..b2bb647 --- /dev/null +++ b/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "log" + "os" +) + +func main() { + if err := App().Run(os.Args); err != nil { + log.Fatal(err) + } +} diff --git a/render/render.go b/render/render.go new file mode 100644 index 0000000..2fd4b97 --- /dev/null +++ b/render/render.go @@ -0,0 +1,71 @@ +package render + +import ( + "github.com/jedib0t/go-pretty/v6/table" + "github.com/jedib0t/go-pretty/v6/text" + "gitnet.fr/deblan/expiration-check/checker" + "os" + "sort" +) + +func Render(values []checker.Domain, warning, danger float64) { + sort.SliceStable(values, func(i, j int) bool { + if values[i].Failed && values[j].Failed { + return values[i].Name < values[j].Name + } + + if values[i].Failed { + return false + } + + if values[j].Failed { + return true + } + + return values[i].DaysLeft < values[j].DaysLeft + }) + + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{ + text.Colors{0, text.FgCyan}.Sprint("Domain"), + text.Colors{0, text.FgCyan}.Sprint("Days"), + text.Colors{0, text.FgCyan}.Sprint("Date"), + }) + t.SetColumnConfigs([]table.ColumnConfig{ + {Number: 2, Align: text.AlignRight}, + {Number: 3, Align: text.AlignRight}, + }) + + for _, value := range values { + if value.Failed { + t.AppendRow(table.Row{ + value.Name, + text.Colors{0, text.FgRed}.Sprint("FAIL"), + text.Colors{0, text.FgRed}.Sprint("FAIL"), + }, table.RowConfig{}) + } else { + var days string + var date string + + if value.DaysLeft <= danger { + days = text.Colors{0, text.FgRed}.Sprint("FAIL") + date = text.Colors{0, text.FgRed}.Sprint("FAIL") + } else if value.DaysLeft <= warning { + days = text.Colors{0, text.FgYellow}.Sprint(value.DaysLeft) + date = text.Colors{0, text.FgYellow}.Sprint(value.Date) + } else { + days = text.Colors{0, text.FgGreen}.Sprint(value.DaysLeft) + date = text.Colors{0, text.FgGreen}.Sprint(value.Date) + } + + t.AppendRow(table.Row{ + value.Name, + days, + date, + }, table.RowConfig{}) + } + } + + t.Render() +}