From fd0a2a4d44412049091e11e8e9800f7876bc2ec7 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Mon, 20 Feb 2023 10:02:48 -0500 Subject: [PATCH] Execute scripts under $CERTSPOTTER_CONFIG_DIR/hooks.d, if it exists --- cmd/certspotter/main.go | 19 ++++++++++++++----- monitor/config.go | 1 + monitor/notify.go | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/cmd/certspotter/main.go b/cmd/certspotter/main.go index e9dd557..5266ba4 100644 --- a/cmd/certspotter/main.go +++ b/cmd/certspotter/main.go @@ -99,6 +99,9 @@ func defaultWatchListPathIfExists() string { return "" } } +func defaultScriptDir() string { + return filepath.Join(defaultConfigDir(), "hooks.d") +} func readWatchListFile(filename string) (monitor.WatchList, error) { file, err := os.Open(filename) @@ -162,11 +165,6 @@ func main() { os.Exit(2) } - if len(flags.email) == 0 && len(flags.script) == 0 && flags.stdout == false { - fmt.Fprintf(os.Stderr, "%s: at least one of -email, -script, or -stdout must be specified (see -help for details)\n", programName) - os.Exit(2) - } - config := &monitor.Config{ LogListSource: flags.logs, StateDir: flags.stateDir, @@ -174,11 +172,22 @@ func main() { StartAtEnd: flags.startAtEnd, Verbose: flags.verbose, Script: flags.script, + ScriptDir: defaultScriptDir(), Email: flags.email, Stdout: flags.stdout, HealthCheckInterval: flags.healthcheck, } + if len(config.Email) == 0 && config.Script == "" && !fileExists(config.ScriptDir) && config.Stdout == false { + 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 executable scripts in the %s directory\n", config.ScriptDir) + fmt.Fprintf(os.Stderr, " - Specify an email address using the -email flag\n") + fmt.Fprintf(os.Stderr, " - Specify the path to an executable script using the -script flag\n") + fmt.Fprintf(os.Stderr, " - Specify the -stdout flag\n") + os.Exit(2) + } + if flags.watchlist == "-" { watchlist, err := monitor.ReadWatchList(os.Stdin) if err != nil { diff --git a/monitor/config.go b/monitor/config.go index 552335f..1e0d60c 100644 --- a/monitor/config.go +++ b/monitor/config.go @@ -21,6 +21,7 @@ type Config struct { Verbose bool SaveCerts bool Script string + ScriptDir string Email []string Stdout bool HealthCheckInterval time.Duration diff --git a/monitor/notify.go b/monitor/notify.go index d2194e5..8fc6d09 100644 --- a/monitor/notify.go +++ b/monitor/notify.go @@ -12,9 +12,12 @@ package monitor import ( "bytes" "context" + "errors" "fmt" + "io/fs" "os" "os/exec" + "path/filepath" "strings" "sync" ) @@ -44,6 +47,12 @@ func notify(ctx context.Context, config *Config, notif notification) error { } } + if config.ScriptDir != "" { + if err := execScriptDir(ctx, config.ScriptDir, notif); err != nil { + return err + } + } + return nil } @@ -104,6 +113,32 @@ func execScript(ctx context.Context, scriptName string, notif notification) erro } } +func execScriptDir(ctx context.Context, dirPath string, notif notification) error { + dirents, err := os.ReadDir(dirPath) + if errors.Is(err, fs.ErrNotExist) { + return nil + } else if err != nil { + return fmt.Errorf("error executing scripts in directory %q: %w", dirPath, err) + } + for _, dirent := range dirents { + if strings.HasPrefix(dirent.Name(), ".") { + continue + } + scriptPath := filepath.Join(dirPath, dirent.Name()) + info, err := os.Stat(scriptPath) + if errors.Is(err, fs.ErrNotExist) { + continue + } else if err != nil { + return fmt.Errorf("error executing %q in directory %q: %w", dirent.Name(), dirPath, err) + } else if info.Mode().IsRegular() && isExecutable(info.Mode()) { + if err := execScript(ctx, scriptPath, notif); err != nil { + return err + } + } + } + return nil +} + func isExecutable(mode os.FileMode) bool { return mode&0111 != 0 }