Skip to content

Commit 69fdeba

Browse files
committed
feat: initial working prototype
1 parent dad5d82 commit 69fdeba

File tree

6 files changed

+346
-0
lines changed

6 files changed

+346
-0
lines changed

cmd/cmd.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package cmd
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"log"
7+
"os"
8+
9+
"github.com/cemulus/crt/repository"
10+
"github.com/cemulus/crt/result"
11+
)
12+
13+
var (
14+
filename = flag.String("o", "", "")
15+
limit = flag.Int("l", 1000, "")
16+
jsonOut = flag.Bool("json", false, "")
17+
csvOut = flag.Bool("csv", false, "")
18+
)
19+
20+
var usage = `Usage: crt [options...] <domain name>
21+
22+
Options:
23+
-o <path> Output file path. Write to file instead of stdout.
24+
-l <int> Limit the number of results. (default: 1000)
25+
-json Turn results to JSON.
26+
-csv Turn results to CSV.
27+
28+
Examples:
29+
crt example.com
30+
crt -o logs.json -json example.com
31+
crt -csv -o logs.csv -l 15 example.com
32+
`
33+
34+
func Execute() {
35+
flag.Usage = func() { fmt.Fprint(os.Stderr, usage) }
36+
37+
flag.Parse()
38+
39+
if flag.NArg() != 1 {
40+
flag.Usage()
41+
os.Exit(1)
42+
}
43+
44+
domain := flag.Args()[0]
45+
if domain == "" {
46+
flag.Usage()
47+
os.Exit(1)
48+
}
49+
50+
repo, err := repository.New()
51+
if err != nil {
52+
log.Fatal(err)
53+
}
54+
defer repo.Close()
55+
56+
var res result.CertResult
57+
58+
res, err = repo.GetCertLogs(domain, *limit)
59+
if err != nil {
60+
log.Fatal(err)
61+
}
62+
63+
if res.Size() == 0 {
64+
fmt.Println("Found no results.")
65+
os.Exit(0)
66+
}
67+
68+
var out string
69+
70+
if *jsonOut {
71+
out, err = res.JSON()
72+
} else if *csvOut {
73+
out, err = res.CSV()
74+
} else {
75+
out = res.Table()
76+
}
77+
78+
if err != nil {
79+
log.Fatal(err)
80+
}
81+
82+
if *filename == "" {
83+
fmt.Println(out)
84+
os.Exit(0)
85+
}
86+
87+
file, err := os.Create(*filename)
88+
if err != nil {
89+
log.Fatal("failed to create output file:", err)
90+
}
91+
defer file.Close()
92+
93+
if _, err = file.Write([]byte(out)); err != nil {
94+
log.Fatal("failed to write to file:", err)
95+
}
96+
}

go.mod

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module github.com/cemulus/crt
2+
3+
go 1.17
4+
5+
require (
6+
github.com/lib/pq v1.10.4
7+
github.com/olekukonko/tablewriter v0.0.5
8+
)
9+
10+
require github.com/mattn/go-runewidth v0.0.9 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=
2+
github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
3+
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
4+
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
5+
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
6+
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=

main.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package main
2+
3+
import "github.com/cemulus/crt/cmd"
4+
5+
func main() {
6+
cmd.Execute()
7+
}

repository/database.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package repository
2+
3+
import (
4+
"database/sql"
5+
"fmt"
6+
7+
_ "github.com/lib/pq"
8+
9+
"github.com/cemulus/crt/result"
10+
)
11+
12+
var (
13+
driver = "postgres"
14+
host = "crt.sh"
15+
port = 5432
16+
user = "guest"
17+
dbname = "certwatch"
18+
login = fmt.Sprintf("host=%s port=%d user=%s dbname=%s", host, port, user, dbname)
19+
)
20+
21+
const (
22+
statement = `WITH ci AS (
23+
SELECT min(sub.CERTIFICATE_ID) ID,
24+
min(sub.ISSUER_CA_ID) ISSUER_CA_ID,
25+
array_agg(DISTINCT sub.NAME_VALUE) NAME_VALUES,
26+
x509_commonName(sub.CERTIFICATE) COMMON_NAME,
27+
x509_notBefore(sub.CERTIFICATE) NOT_BEFORE,
28+
x509_notAfter(sub.CERTIFICATE) NOT_AFTER,
29+
encode(x509_serialNumber(sub.CERTIFICATE), 'hex') SERIAL_NUMBER
30+
FROM (SELECT *
31+
FROM certificate_and_identities cai
32+
WHERE plainto_tsquery('certwatch', '%s') @@ identities(cai.CERTIFICATE)
33+
AND cai.NAME_VALUE ILIKE ('%%' || '%s' || '%%')
34+
LIMIT 10000
35+
) sub
36+
GROUP BY sub.CERTIFICATE
37+
)
38+
SELECT ci.ISSUER_CA_ID,
39+
ca.NAME ISSUER_NAME,
40+
ci.COMMON_NAME,
41+
array_to_string(ci.NAME_VALUES, chr(10)) NAME_VALUE,
42+
ci.ID ID,
43+
le.ENTRY_TIMESTAMP,
44+
ci.NOT_BEFORE,
45+
ci.NOT_AFTER,
46+
ci.SERIAL_NUMBER
47+
FROM ci
48+
LEFT JOIN LATERAL (
49+
SELECT min(ctle.ENTRY_TIMESTAMP) ENTRY_TIMESTAMP
50+
FROM ct_log_entry ctle
51+
WHERE ctle.CERTIFICATE_ID = ci.ID
52+
) le ON TRUE,
53+
ca
54+
WHERE ci.ISSUER_CA_ID = ca.ID
55+
ORDER BY le.ENTRY_TIMESTAMP DESC NULLS LAST
56+
LIMIT %d`
57+
)
58+
59+
type Repository struct {
60+
db *sql.DB
61+
}
62+
63+
func New() (*Repository, error) {
64+
db, err := sql.Open(driver, login)
65+
if err != nil {
66+
return nil, fmt.Errorf("failed to open db: %s", err)
67+
}
68+
69+
return &Repository{db}, nil
70+
}
71+
72+
func (r *Repository) GetCertLogs(domain string, limit int) (result.CertResult, error) {
73+
stmt := fmt.Sprintf(statement, domain, domain, limit)
74+
75+
rows, err := r.db.Query(stmt)
76+
if err != nil {
77+
return nil, fmt.Errorf("failed to query db: %s", err)
78+
}
79+
defer rows.Close()
80+
81+
var res result.CertResult
82+
83+
var (
84+
issuerCaID sql.NullInt32
85+
id sql.NullInt64
86+
issuerName, commonName, nameValue, serialNumber sql.NullString
87+
entryTimestamp, notBefore, notAfter sql.NullTime
88+
)
89+
90+
for rows.Next() {
91+
err = rows.Scan(
92+
&issuerCaID,
93+
&issuerName,
94+
&commonName,
95+
&nameValue,
96+
&id,
97+
&entryTimestamp,
98+
&notBefore,
99+
&notAfter,
100+
&serialNumber)
101+
if err != nil {
102+
return nil, fmt.Errorf("failed to scan row: %s", err)
103+
}
104+
105+
certificate := result.Certificate{
106+
IssuerCaID: int((issuerCaID).Int32),
107+
IssuerName: issuerName.String,
108+
CommonName: commonName.String,
109+
NameValue: nameValue.String,
110+
ID: int((id).Int64),
111+
EntryTimestamp: entryTimestamp.Time,
112+
NotBefore: notBefore.Time,
113+
NotAfter: notAfter.Time,
114+
SerialNumber: serialNumber.String}
115+
116+
res = append(res, certificate)
117+
}
118+
119+
return res, nil
120+
}
121+
122+
func (r *Repository) Close() error {
123+
return r.db.Close()
124+
}

result/certificate.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package result
2+
3+
import (
4+
"bytes"
5+
"encoding/csv"
6+
"encoding/json"
7+
"fmt"
8+
"strconv"
9+
"strings"
10+
"time"
11+
12+
"github.com/olekukonko/tablewriter"
13+
)
14+
15+
type Certificate struct {
16+
IssuerCaID int `json:"issuer_ca_id"`
17+
IssuerName string `json:"issuer_name"`
18+
CommonName string `json:"common_name"`
19+
NameValue string `json:"name_value"`
20+
ID int `json:"id"`
21+
EntryTimestamp time.Time `json:"entry_timestamp"`
22+
NotBefore time.Time `json:"not_before"`
23+
NotAfter time.Time `json:"not_after"`
24+
SerialNumber string `json:"serial_number"`
25+
}
26+
27+
type CertResult []Certificate
28+
29+
func (r CertResult) Table() string {
30+
res := new(bytes.Buffer)
31+
table := tablewriter.NewWriter(res)
32+
33+
info := []string{"Matching", "Logged At", "Not Before", "Not After", "Issuer"}
34+
table.SetHeader(info)
35+
table.SetFooter(info)
36+
37+
blue := tablewriter.Color(tablewriter.FgHiBlueColor)
38+
yellow := tablewriter.Color(tablewriter.FgHiYellowColor)
39+
white := tablewriter.Color(tablewriter.FgWhiteColor)
40+
41+
table.SetHeaderColor(blue, blue, blue, blue, blue)
42+
table.SetFooterColor(blue, blue, blue, blue, blue)
43+
table.SetColumnColor(yellow, white, white, white, white)
44+
45+
for _, cert := range r {
46+
table.Append([]string{
47+
cert.NameValue,
48+
cert.EntryTimestamp.String()[0:10],
49+
cert.NotBefore.String()[0:10],
50+
cert.NotAfter.String()[0:10],
51+
strings.Trim(strings.Split(strings.Split(cert.IssuerName, "O=")[1], ",")[0], "\""),
52+
})
53+
}
54+
55+
table.SetRowLine(true)
56+
table.SetRowSeparator("—")
57+
table.Render()
58+
59+
return res.String()
60+
}
61+
62+
func (r CertResult) JSON() (string, error) {
63+
res, err := json.MarshalIndent(r, "", "\t")
64+
if err != nil {
65+
return "", fmt.Errorf("failed to marshal results: %s", err)
66+
}
67+
68+
return string(res), nil
69+
}
70+
71+
func (r CertResult) CSV() (string, error) {
72+
res := new(bytes.Buffer)
73+
w := csv.NewWriter(res)
74+
75+
err := w.Write([]string{
76+
"issuer_ca_id", "issuer_name", "common_name", "name_value", "id",
77+
"entry_timestamp", "not_before", "not_after", "serial_number",
78+
})
79+
if err != nil {
80+
return "", fmt.Errorf("failed to write CSV headers: %s", err)
81+
}
82+
83+
for _, v := range r {
84+
err = w.Write([]string{
85+
strconv.Itoa(v.IssuerCaID),
86+
v.IssuerName,
87+
v.CommonName,
88+
v.NameValue,
89+
strconv.Itoa(v.ID),
90+
v.EntryTimestamp.String(),
91+
v.NotBefore.String(),
92+
v.NotAfter.String(),
93+
v.SerialNumber})
94+
if err != nil {
95+
return "", fmt.Errorf("failed to write CSV content: %s", err)
96+
}
97+
}
98+
w.Flush()
99+
100+
return res.String(), nil
101+
}
102+
103+
func (r CertResult) Size() int { return len(r) }

0 commit comments

Comments
 (0)