From aaa709f6289e73c845ed0ff79d9e5feb5068278c Mon Sep 17 00:00:00 2001 From: Florian Ritterhoff Date: Tue, 2 Jan 2024 10:40:26 +0100 Subject: [PATCH] feat: add json loggings --- cmd/certspotter/main.go | 5 ++++- monitor/config.go | 1 + monitor/discoveredcert.go | 37 +++++++++++++++++++++++++++++++++++++ monitor/healthcheck.go | 10 ++++++++++ monitor/malformed.go | 17 +++++++++++++++++ monitor/notify.go | 10 +++++++++- 6 files changed, 78 insertions(+), 2 deletions(-) diff --git a/cmd/certspotter/main.go b/cmd/certspotter/main.go index 9dea831..01698f6 100644 --- a/cmd/certspotter/main.go +++ b/cmd/certspotter/main.go @@ -163,6 +163,7 @@ func main() { startAtEnd bool stateDir string stdout bool + jsonLog bool verbose bool version bool watchlist string @@ -175,6 +176,7 @@ func main() { flag.StringVar(&flags.script, "script", "", "Program to execute when a matching certificate is discovered") flag.BoolVar(&flags.startAtEnd, "start_at_end", false, "Start monitoring logs from the end rather than the beginning (saves considerable bandwidth)") flag.StringVar(&flags.stateDir, "state_dir", defaultStateDir(), "Directory for storing log position and discovered certificates") + flag.BoolVar(&flags.jsonLog, "jsonLog", false, "Write matching certificates to stdout in JSON format") flag.BoolVar(&flags.stdout, "stdout", false, "Write matching certificates to stdout") flag.BoolVar(&flags.verbose, "verbose", false, "Be verbose") flag.BoolVar(&flags.version, "version", false, "Print version and exit") @@ -200,6 +202,7 @@ func main() { ScriptDir: defaultScriptDir(), Email: flags.email, Stdout: flags.stdout, + JsonLog: flags.jsonLog, HealthCheckInterval: flags.healthcheck, } @@ -212,7 +215,7 @@ func main() { os.Exit(1) } - if len(config.Email) == 0 && !emailFileExists && config.Script == "" && !fileExists(config.ScriptDir) && config.Stdout == false { + if len(config.Email) == 0 && !emailFileExists && config.Script == "" && !fileExists(config.ScriptDir) && !config.Stdout { fmt.Fprintf(os.Stderr, "%s: no notification methods were specified\n", programName) fmt.Fprintf(os.Stderr, "Please specify at least one of the following notification methods:\n") fmt.Fprintf(os.Stderr, " - Place one or more email addresses in %s (one address per line)\n", defaultEmailFile()) diff --git a/monitor/config.go b/monitor/config.go index 1e0d60c..cc96a9c 100644 --- a/monitor/config.go +++ b/monitor/config.go @@ -24,5 +24,6 @@ type Config struct { ScriptDir string Email []string Stdout bool + JsonLog bool HealthCheckInterval time.Duration } diff --git a/monitor/discoveredcert.go b/monitor/discoveredcert.go index f53b399..6821ef6 100644 --- a/monitor/discoveredcert.go +++ b/monitor/discoveredcert.go @@ -12,8 +12,10 @@ package monitor import ( "bytes" "encoding/hex" + "encoding/json" "encoding/pem" "fmt" + "net" "strings" "time" @@ -130,6 +132,41 @@ func (cert *discoveredCert) Environ() []string { return env } +func (cert *discoveredCert) Json() string { + log := struct { + Sha256 string + DNSNames []string + IPs []net.IP + Issuer string + Pubkey string + NotBefore string + NotAfter string + }{ + Sha256: hex.EncodeToString(cert.SHA256[:]), + DNSNames: cert.Identifiers.DNSNames, + IPs: cert.Identifiers.IPAddrs, + Issuer: cert.Info.Issuer.String(), + Pubkey: hex.EncodeToString(cert.PubkeySHA256[:]), + } + if cert.Info.IssuerParseError == nil { + log.Issuer = cert.Info.Issuer.String() + } else { + log.Issuer = fmt.Sprintf("[unable to parse: %s]", cert.Info.IssuerParseError) + } + if cert.Info.ValidityParseError == nil { + log.NotBefore = cert.Info.Validity.NotBefore.String() + log.NotAfter = cert.Info.Validity.NotAfter.String() + } else { + log.NotBefore = fmt.Sprintf("[unable to parse: %s]", cert.Info.ValidityParseError) + log.NotAfter = fmt.Sprintf("[unable to parse: %s]", cert.Info.ValidityParseError) + } + json, err := json.Marshal(log) + if err != nil { + fmt.Printf("error marshaling log entry: %s\n", err) + } + return string(json) +} + func (cert *discoveredCert) Text() string { // TODO-4: improve the output: include WatchItem, indicate hash algorithm used for fingerprints, ... (look at SSLMate email for inspiration) diff --git a/monitor/healthcheck.go b/monitor/healthcheck.go index 51d12ee..3456c41 100644 --- a/monitor/healthcheck.go +++ b/monitor/healthcheck.go @@ -136,6 +136,16 @@ func (e *staleLogListEvent) Summary() string { return fmt.Sprintf("Unable to retrieve log list since %s", e.LastSuccess) } +func (cert *staleLogListEvent) Json() string { + return "" +} +func (cert *backlogEvent) Json() string { + return "" +} +func (cert *staleSTHEvent) Json() string { + return "" +} + func (e *staleSTHEvent) Text() string { text := new(strings.Builder) fmt.Fprintf(text, "certspotter has been unable to contact %s since %s. Consequentially, certspotter may fail to notify you about certificates in this log.\n", e.Log.URL, e.LastSuccess) diff --git a/monitor/malformed.go b/monitor/malformed.go index 35cac00..5d3af7c 100644 --- a/monitor/malformed.go +++ b/monitor/malformed.go @@ -10,6 +10,7 @@ package monitor import ( + "encoding/json" "fmt" "strings" ) @@ -55,6 +56,22 @@ func (malformed *malformedLogEntry) Environ() []string { } } +func (cert *malformedLogEntry) Json() string { + json, err := json.Marshal(struct { + LogEntry string + LeafHash string + Error string + }{ + LogEntry: fmt.Sprintf("%d @ %s", cert.Entry.Index, cert.Entry.Log.URL), + LeafHash: cert.Entry.LeafHash.Base64String(), + Error: cert.Error, + }) + if err != nil { + fmt.Printf("error marshaling malformed log entry: %s\n", err) + } + return string(json) +} + func (malformed *malformedLogEntry) Text() string { text := new(strings.Builder) writeField := func(name string, value any) { fmt.Fprintf(text, "\t%13s = %s\n", name, value) } diff --git a/monitor/notify.go b/monitor/notify.go index d59fbbc..55aecf1 100644 --- a/monitor/notify.go +++ b/monitor/notify.go @@ -29,11 +29,14 @@ type notification interface { Environ() []string Summary() string Text() string + Json() string } func notify(ctx context.Context, config *Config, notif notification) error { - if config.Stdout { + if config.Stdout && !config.JsonLog { writeToStdout(notif) + } else if config.JsonLog { + writeJsonToStdout(notif) } if len(config.Email) > 0 { @@ -56,6 +59,11 @@ func notify(ctx context.Context, config *Config, notif notification) error { return nil } +func writeJsonToStdout(notif notification) { + stdoutMu.Lock() + defer stdoutMu.Unlock() + os.Stdout.WriteString(notif.Json() + "\n") +} func writeToStdout(notif notification) { stdoutMu.Lock()