package main import ( "flag" "fmt" "log" "os" "strings" "time" "github.com/hexops/gotextdiff" "github.com/hexops/gotextdiff/myers" "github.com/hexops/gotextdiff/span" "gopkg.in/yaml.v3" ) type Config struct { Listen []string `yaml:"listen"` Checkpoints string `yaml:"checkpoints"` Roots string `yaml:"roots"` Logs []Log `yaml:"logs"` } type Log struct { ShortName string `yaml:"shortname"` Inception string `yaml:"inception"` Period int `yaml:"period"` PoolSize int `yaml:"poolsize"` SubmissionPrefix string `yaml:"submissionprefix"` MonitoringPrefix string `yaml:"monitoringprefix"` CCadbRoots string `yaml:"ccadbroots"` ExtraRoots string `yaml:"extraroots"` Secret string `yaml:"secret"` Cache string `yaml:"cache"` LocalDirectory string `yaml:"localdirectory"` Listen string `yaml:"listen"` NotAfterStart time.Time `yaml:"notafterstart"` NotAfterLimit time.Time `yaml:"notafterlimit"` // Computed fields LogID string PublicKeyPEM string PublicKeyDERB64 string PublicKeyBase64 string } func main() { configFile := flag.String("c", "./tesseract-staging.yaml", "Path to the YAML configuration file") wantDiff := flag.Bool("diff", false, "Show unified diff of changes") allowWrite := flag.Bool("write", false, "Allow writing files (required for actual file modifications)") noColor := flag.Bool("no-color", false, "Disable colored diff output") flag.Parse() args := flag.Args() if len(args) == 0 { showHelp() return } switch args[0] { case "gen-html": generateHTML(*configFile, *wantDiff, *allowWrite, !*noColor) case "gen-env": generateEnv(*configFile, *wantDiff, *allowWrite, !*noColor) case "gen-key": generateKeys(*configFile, *wantDiff, *allowWrite, !*noColor) case "gen-nginx": generateNginx(*configFile, *wantDiff, *allowWrite, !*noColor) case "gen-roots": generateRoots(args[1:], *wantDiff, *allowWrite, !*noColor) default: fmt.Fprintf(os.Stderr, "Unknown command: %s\n", args[0]) showHelp() os.Exit(1) } } func loadConfig(yamlFile string) Config { data, err := os.ReadFile(yamlFile) if err != nil { log.Fatalf("Failed to read YAML file: %v", err) } var config Config err = yaml.Unmarshal(data, &config) if err != nil { log.Fatalf("Failed to parse YAML: %v", err) } // Set default listen port if not configured if len(config.Listen) == 0 { config.Listen = []string{":8080"} } // Set defaults for log entries for i := range config.Logs { if config.Logs[i].PoolSize == 0 { config.Logs[i].PoolSize = 750 } if config.Logs[i].Period == 0 { config.Logs[i].Period = 200 } } return config } // ANSI color codes const ( colorReset = "\033[0m" colorRed = "\033[31m" colorGreen = "\033[32m" colorYellow = "\033[33m" colorCyan = "\033[36m" ) // colorizeUnifiedDiff adds ANSI color codes to unified diff output func colorizeUnifiedDiff(diff string) string { lines := strings.Split(diff, "\n") var colorizedLines []string for _, line := range lines { switch { case strings.HasPrefix(line, "---"): // File deletion header in cyan colorizedLines = append(colorizedLines, colorCyan+line+colorReset) case strings.HasPrefix(line, "+++"): // File addition header in cyan colorizedLines = append(colorizedLines, colorCyan+line+colorReset) case strings.HasPrefix(line, "@@"): // Hunk header in yellow colorizedLines = append(colorizedLines, colorYellow+line+colorReset) case strings.HasPrefix(line, "-"): // Deleted lines in red colorizedLines = append(colorizedLines, colorRed+line+colorReset) case strings.HasPrefix(line, "+"): // Added lines in green colorizedLines = append(colorizedLines, colorGreen+line+colorReset) default: // Context lines unchanged colorizedLines = append(colorizedLines, line) } } return strings.Join(colorizedLines, "\n") } func writeFileWithStatus(filename string, content []byte, wantDiff bool, allowWrite bool, useColor bool) error { existingContent, err := os.ReadFile(filename) isNew := os.IsNotExist(err) isUnchanged := false if isNew { if allowWrite { fmt.Printf("Creating %s\n", filename) } else { fmt.Printf("Would create %s\n", filename) } } else if err != nil { return fmt.Errorf("failed to read existing file %s: %v", filename, err) } else if string(existingContent) == string(content) { fmt.Printf("Unchanged %s\n", filename) isUnchanged = true } else { if allowWrite { fmt.Printf("Updating %s\n", filename) } else { fmt.Printf("Would update %s\n", filename) } } if wantDiff && !isUnchanged { if isNew { // For new files, show the entire content as added edits := myers.ComputeEdits(span.URIFromPath(filename), "", string(content)) diff := fmt.Sprint(gotextdiff.ToUnified("/dev/null", filename, "", edits)) if useColor { fmt.Print(colorizeUnifiedDiff(diff)) } else { fmt.Print(diff) } } else { // For existing files, show the diff edits := myers.ComputeEdits(span.URIFromPath(filename), string(existingContent), string(content)) diff := fmt.Sprint(gotextdiff.ToUnified(filename, filename+".new", string(existingContent), edits)) if useColor { fmt.Print(colorizeUnifiedDiff(diff)) } else { fmt.Print(diff) } } } if isUnchanged || !allowWrite { return nil } err = os.WriteFile(filename, content, 0644) if err != nil { return fmt.Errorf("failed to write file %s: %v", filename, err) } return nil } func showHelp() { fmt.Printf("Usage: %s [options] \n\n", os.Args[0]) fmt.Printf("Note: Flags must come before the command name.\n\n") fmt.Printf("Options:\n") fmt.Printf(" -c Path to YAML configuration file (default: ./tesseract-staging.yaml)\n") fmt.Printf(" --diff Show unified diff of changes without writing files\n") fmt.Printf(" --write Allow writing files (required for actual file modifications)\n") fmt.Printf(" --no-color Disable colored diff output\n\n") fmt.Printf("Examples:\n") fmt.Printf(" %s --diff gen-html # Show colored diffs without writing\n", os.Args[0]) fmt.Printf(" %s --diff --no-color gen-html # Show plain diffs without writing\n", os.Args[0]) fmt.Printf(" %s --write gen-html # Write files\n", os.Args[0]) fmt.Printf(" %s --diff --write gen-html # Show colored diffs and write files\n\n", os.Args[0]) fmt.Printf("Commands:\n") fmt.Printf(" gen-html Generate index.html and log.v3.json files in each log's localdirectory.\n") fmt.Printf(" Creates HTML pages with log information and CT log metadata JSON.\n") fmt.Printf(" Computes LOG_ID and public keys from private keys.\n\n") fmt.Printf(" gen-env Generate .env files and combined roots.pem in each log's localdirectory.\n") fmt.Printf(" Creates TESSERACT_ARGS environment variable with command line flags.\n") fmt.Printf(" Combines global roots and log-specific extraroots into roots.pem.\n\n") fmt.Printf(" gen-key Generate prime256v1 private keys for each log (only if they don't exist).\n") fmt.Printf(" Creates EC private key files at the path specified in log.secret.\n\n") fmt.Printf(" gen-nginx Generate nginx configuration files for each log's monitoring endpoint.\n") fmt.Printf(" Creates nginx-.conf files in each log's localdirectory.\n\n") fmt.Printf(" gen-roots Download root certificates from a Certificate Transparency log.\n") fmt.Printf(" Options: --source (default: https://rennet2027h2.log.ct.ipng.ch/)\n") fmt.Printf(" --output (default: roots.pem)\n\n") }