diff options
Diffstat (limited to 'cmd/silentct-mon')
-rw-r--r-- | cmd/silentct-mon/examples.help2man | 38 | ||||
-rw-r--r-- | cmd/silentct-mon/main.go | 222 | ||||
-rw-r--r-- | cmd/silentct-mon/name.help2man | 2 | ||||
-rw-r--r-- | cmd/silentct-mon/see-also.help2man | 2 |
4 files changed, 264 insertions, 0 deletions
diff --git a/cmd/silentct-mon/examples.help2man b/cmd/silentct-mon/examples.help2man new file mode 100644 index 0000000..7a1e8fc --- /dev/null +++ b/cmd/silentct-mon/examples.help2man @@ -0,0 +1,38 @@ +[EXAMPLES] + +A basic configuration is shown below. + + { + "monitor": [ + { + "bootstrap_at": "2024-05-16T00:00:00Z", + "wildcard": "example.org", + "excludes": [ + "test" + ] + } + ], + "nodes": [ + { + "name": "example.org", + "secret": "sikritpassword", + "url": "https://www.example.org/silent-ct/allowlist", + "issues": [ + "example.org", + "www.example.org" + ] + } + ] + } + +Bootstrap a new monitor in a non-existent directory: + +.B $ silentct-mon -b -d ~/.local/lib/silent-ct -f ~/.config/silent-ct/config.json + +Run the monitor continuously: + +.B $ silentct-mon -d ~/.local/lib/silent-ct -f ~/.config/silent-ct/config.json + +Use +.B -v DEBUG +to see what's happening underneath the hood. diff --git a/cmd/silentct-mon/main.go b/cmd/silentct-mon/main.go new file mode 100644 index 0000000..2d070fb --- /dev/null +++ b/cmd/silentct-mon/main.go @@ -0,0 +1,222 @@ +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "log" + "os" + "os/signal" + "strings" + "sync" + "syscall" + "time" + + "rgdd.se/silent-ct/internal/feedback" + "rgdd.se/silent-ct/internal/flagopt" + "rgdd.se/silent-ct/internal/ioutil" + "rgdd.se/silent-ct/internal/logger" + "rgdd.se/silent-ct/internal/manager" + "rgdd.se/silent-ct/internal/monitor" + "rgdd.se/silent-ct/pkg/policy" +) + +const usage = ` +silentct-mon is a tool that monitors Certificate Transparency logs. The tool +can operate silently, which means there need not be any output unless a +certificate is possibly mis-issued. This requires use of the silentct-mac +utility on the trusted systems that legitimately request certificates. + +The same list of Certificate Transparency logs as Google Chrome is used. This +list can be overridden in the silentct-mon configuration file. + +Usage: silentct-mon [Options] -d DIRECTORY -f POLICY-FILE + +Options: + + -b, --bootstrap Initializes a new state directory (Default: false) + -c, --contact A string that helps log operators know who you are (Default: "") + -d, --directory Path to a directory where all state will be stored + -w, --num-workers Number of parallel workers to fetch each log with (Default: 1) + -o, --output-file File that all output will be written to (Default: stdout) + -e, --please-exit Toggle to only run until up-to-date (Default: false) + -f, --policy-file Path to the monitor's policy file in JSON format + -p, --pull-interval How often nodes are pulled for certificates (Default: 15m) + -v, --verbosity Leveled logging output (default: NOTICE) +` + +type config struct { + // Options + verbosity string + bootstrap bool + contact string + directory string + pleaseExit bool + policyFile string + outputFile string + pullInterval time.Duration + numWorkers uint + + // Extracted + log *logger.Logger + policy policy.Policy +} + +func configure(cmd string, args []string) (cfg config, err error) { + fs := flag.NewFlagSet(cmd, flag.ContinueOnError) + fs.Usage = func() {} + flagopt.BoolOpt(fs, &cfg.bootstrap, "bootstrap", "b", false) + flagopt.StringOpt(fs, &cfg.contact, "contact", "c", "") + flagopt.StringOpt(fs, &cfg.directory, "directory", "d", "") + flagopt.UintOpt(fs, &cfg.numWorkers, "num-workers", "w", 1) + flagopt.StringOpt(fs, &cfg.outputFile, "output-file", "o", "") + flagopt.BoolOpt(fs, &cfg.pleaseExit, "please-exit", "e", false) + flagopt.StringOpt(fs, &cfg.policyFile, "policy-file", "f", "") + flagopt.DurationOpt(fs, &cfg.pullInterval, "pull-interval", "p", 15*time.Minute) + flagopt.StringOpt(fs, &cfg.verbosity, "verbosity", "v", logger.LevelNotice.String()) + if err = fs.Parse(args); err != nil { + return cfg, err + } + + // Options + if cfg.directory == "" { + return cfg, fmt.Errorf("directory is a required option") + } + if cfg.numWorkers == 0 || cfg.numWorkers >= 4 { + return cfg, fmt.Errorf("number of workers must be in [1, 4)") + } + output := os.Stdout + if cfg.outputFile != "" { + if output, err = os.OpenFile(cfg.outputFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644); err != nil { + return cfg, fmt.Errorf("failed to open output file: %v", err) + } + } + if cfg.policyFile == "" { + return cfg, fmt.Errorf("policy file is a required option") + } + if err := ioutil.ReadJSON(cfg.policyFile, &cfg.policy); err != nil { + return cfg, err + } + if len(cfg.policy.Monitor) == 0 { + return cfg, fmt.Errorf("policy: need at least one wildcard to monitor") + } + lv, err := logger.NewLevel(cfg.verbosity) + if err != nil { + return cfg, fmt.Errorf("invalid verbosity: %v", err) + } + cfg.log = logger.New(logger.Config{Level: lv, File: output}) + + // Arguments + if len(fs.Args()) != 0 { + return cfg, fmt.Errorf("trailing arguments are not permitted") + } + + return cfg, nil +} + +func main() { + cfg, err := configure(os.Args[0], os.Args[1:]) + if err != nil { + if errors.Is(err, flag.ErrHelp) { + fmt.Fprintf(os.Stdout, "%s", usage[1:]) + os.Exit(0) + } + if !strings.Contains(err.Error(), "flag provided but not defined") { + fmt.Fprintf(os.Stdout, "%v\n", err) + } + os.Exit(1) + } + + feventCh := make(chan []feedback.Event) + defer close(feventCh) + + mconfigCh := make(chan monitor.MonitoredLog) + defer close(mconfigCh) + + meventCh := make(chan monitor.Event) + defer close(meventCh) + + errorCh := make(chan error) + defer close(errorCh) + + mgr, err := manager.New(manager.Config{ + Policy: cfg.policy, + Bootstrap: cfg.bootstrap, + Directory: cfg.directory, + Logger: cfg.log, + AlertDelay: cfg.pullInterval * 3 / 2, + }, feventCh, meventCh, mconfigCh, errorCh) + if err != nil { + cfg.log.Dief("manager: %v\n", err) + } + mon, err := monitor.New(monitor.Config{ + Matcher: &cfg.policy.Monitor, + Logger: cfg.log, + Contact: cfg.contact, + NumWorkers: cfg.numWorkers, + }, meventCh, mconfigCh, errorCh) + if err != nil { + cfg.log.Dief("monitor: %v\n", err) + } + fb, err := feedback.New(feedback.Config{ + Policy: cfg.policy, + Logger: cfg.log, + PullInterval: cfg.pullInterval, + }, feventCh) + if err != nil { + cfg.log.Dief("feedback: %v\n", err) + } + + if cfg.bootstrap { + os.Exit(0) + } + if cfg.pleaseExit { + cfg.log.Dief("the --please-exit option is not supported yet\n") + } + + var wg sync.WaitGroup + ctx, cancel := context.WithCancel(context.Background()) + + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + await(ctx) + }() + + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + mon.RunForever(ctx) + }() + + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + fb.RunForever(ctx) + }() + + os.Exit(func() int { + defer wg.Wait() + defer cancel() + if err := mgr.Run(ctx); err != nil { + log.Fatalf("manager: %v\n", err) + return 1 + } + return 0 + }()) +} + +func await(ctx context.Context) { + sigs := make(chan os.Signal, 1) + defer close(sigs) + + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + select { + case <-sigs: + case <-ctx.Done(): + } +} diff --git a/cmd/silentct-mon/name.help2man b/cmd/silentct-mon/name.help2man new file mode 100644 index 0000000..a4edcc1 --- /dev/null +++ b/cmd/silentct-mon/name.help2man @@ -0,0 +1,2 @@ +[NAME] +silentct-mon - monitor Certificate Transparency logs diff --git a/cmd/silentct-mon/see-also.help2man b/cmd/silentct-mon/see-also.help2man new file mode 100644 index 0000000..d4b9782 --- /dev/null +++ b/cmd/silentct-mon/see-also.help2man @@ -0,0 +1,2 @@ +[SEE ALSO] +.BR silentct-mac (1) |