init
All checks were successful
ci/woodpecker/push/build Pipeline was successful

This commit is contained in:
Simon Vieille 2024-07-22 10:36:51 +02:00
commit 91e551f6d5
Signed by: deblan
GPG key ID: 579388D585F70417
12 changed files with 388 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/expiration-check
/test.sh

32
.woodpecker/build.yml Normal file
View file

@ -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]

2
Makefile Normal file
View file

@ -0,0 +1,2 @@
build:
go build

26
README.md Normal file
View file

@ -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 |
+-------------------+------+---------------------+
```

44
app.go Normal file
View file

@ -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
},
},
},
}
}

46
checker/certificates.go Normal file
View file

@ -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
}

91
checker/domains.go Normal file
View file

@ -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
}

8
checker/struct.go Normal file
View file

@ -0,0 +1,8 @@
package checker
type Domain struct {
Name string
DaysLeft float64
Date string
Failed bool
}

22
go.mod Normal file
View file

@ -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
)

32
go.sum Normal file
View file

@ -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=

12
main.go Normal file
View file

@ -0,0 +1,12 @@
package main
import (
"log"
"os"
)
func main() {
if err := App().Run(os.Args); err != nil {
log.Fatal(err)
}
}

71
render/render.go Normal file
View file

@ -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()
}