package main import ( "context" "errors" "flag" "fmt" "log" "os" "os/signal" "strings" "sync" "syscall" "time" "rgdd.se/silentct/internal/feedback" "rgdd.se/silentct/internal/flagopt" "rgdd.se/silentct/internal/ioutil" "rgdd.se/silentct/internal/logger" "rgdd.se/silentct/internal/manager" "rgdd.se/silentct/internal/monitor" "rgdd.se/silentct/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] -c CONFIGURATION-FILE -d DIRECTORY Options: -b, --bootstrap Initializes a new state directory (Default: false) -c, --configuration Path to the monitor's configuration file in JSON format -C, --contact A string that helps log operators know who you are, consider seting this to an email address (Default: "") -d, --directory Path to a directory where all state will be stored -e, --please-exit Toggle to only run until up-to-date (Default: false) -f, --force Override santity checks that may not be fatal (Default: false) -o, --output-file File that all output will be written to (Default: stdout) -p, --pull-interval How often nodes are pulled for certificates (Default: 15m) -v, --verbosity Leveled logging output (default: NOTICE) -w, --num-workers Number of parallel workers to fetch each log with (Default: 1) ` type config struct { // Options verbosity string bootstrap bool configFile string contact string directory string pleaseExit bool force bool 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.configFile, "configuration", "c", "") flagopt.StringOpt(fs, &cfg.contact, "contact", "C", "") flagopt.StringOpt(fs, &cfg.directory, "directory", "d", "") flagopt.BoolOpt(fs, &cfg.pleaseExit, "please-exit", "e", false) flagopt.BoolOpt(fs, &cfg.force, "force", "f", false) flagopt.StringOpt(fs, &cfg.outputFile, "output-file", "o", "") flagopt.DurationOpt(fs, &cfg.pullInterval, "pull-interval", "p", 15*time.Minute) flagopt.StringOpt(fs, &cfg.verbosity, "verbosity", "v", logger.LevelNotice.String()) flagopt.UintOpt(fs, &cfg.numWorkers, "num-workers", "w", 1) if err = fs.Parse(args); err != nil { return cfg, err } // Options if cfg.configFile == "" { return cfg, fmt.Errorf("configuration is a required option") } if err := ioutil.ReadJSON(cfg.configFile, &cfg.policy); err != nil { return cfg, err } if len(cfg.policy.Monitor) == 0 { return cfg, fmt.Errorf("configuration: need at least one wildcard to monitor") } if cfg.directory == "" { return cfg, fmt.Errorf("directory is a required option") } 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) } } 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}) if (cfg.numWorkers == 0 || cfg.numWorkers >= 4) && !cfg.force { return cfg, fmt.Errorf("number of workers must be in [1, 4)") } // 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(): } }