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 = ` A utility that follows relevant Certificate Transparency logs to discover certificates that may be mis-issued. To be silent, any legitimately issued certificates are pulled from trusted nodes. Usage: silent-ctmoon --help silent-ctmoon [Opts] -d DIRECTORY -f POLICY_FILE Options: -h, --help: Output usage message and exit -v, --verbosity: Leveled logging output (default: NOTICE) -b, --bootstrap: Initializate 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 -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 -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) -w, --num-workers: Number of parallel workers to fetch each log with (Default: 2) ` 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.StringOpt(fs, &cfg.verbosity, "verbosity", "v", logger.LevelNotice.String()) flagopt.BoolOpt(fs, &cfg.bootstrap, "bootstrap", "b", false) flagopt.StringOpt(fs, &cfg.contact, "contact", "c", "") flagopt.StringOpt(fs, &cfg.directory, "directory", "d", "") flagopt.BoolOpt(fs, &cfg.pleaseExit, "please-exit", "e", false) flagopt.StringOpt(fs, &cfg.policyFile, "policy-file", "f", "") flagopt.StringOpt(fs, &cfg.outputFile, "output-file", "o", "") flagopt.DurationOpt(fs, &cfg.pullInterval, "pull-interval", "p", 15*time.Minute) flagopt.UintOpt(fs, &cfg.numWorkers, "num-workers", "w", 2) if err = fs.Parse(args); err != nil { return cfg, err } // Options lv, err := logger.NewLevel(cfg.verbosity) if err != nil { return cfg, fmt.Errorf("invalid verbosity: %v", err) } if cfg.directory == "" { return cfg, fmt.Errorf("directory is a required option") } if cfg.policyFile == "" { return cfg, fmt.Errorf("policy file is a required option") } if cfg.numWorkers == 0 || cfg.numWorkers > 4 { return cfg, fmt.Errorf("number of workers must be in [1, 4]") } cfg.log = logger.New(logger.Config{Level: lv, File: os.Stdout}) 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") } // 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.Stderr, "%s", usage[1:]) os.Exit(0) } if !strings.Contains(err.Error(), "flag provided but not defined") { fmt.Fprintf(os.Stderr, "%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(): } }