package collector import ( "encoding/xml" "log" "os" "os/exec" "strings" ) type CronJob struct { Source string `json:"source"` // "system" oder "opnsense" Enabled bool `json:"enabled"` Minutes string `json:"minutes"` Hours string `json:"hours"` Days string `json:"days"` Months string `json:"months"` Weekdays string `json:"weekdays"` Command string `json:"command"` Parameters string `json:"parameters,omitempty"` Who string `json:"who,omitempty"` Description string `json:"description,omitempty"` UUID string `json:"uuid,omitempty"` Origin string `json:"origin,omitempty"` Schedule string `json:"schedule"` // Menschenlesbar: "*/4 * * * *" } func CollectCron() []CronJob { var jobs []CronJob // 1. System-Crontab (crontab -l) sysJobs := collectSystemCrontab() jobs = append(jobs, sysJobs...) // 2. OPNsense config.xml Cron-Jobs opnJobs := collectOPNsenseCron() jobs = append(jobs, opnJobs...) log.Printf("Cron: %d Jobs gesammelt (%d System, %d OPNsense)", len(jobs), len(sysJobs), len(opnJobs)) return jobs } func collectSystemCrontab() []CronJob { out, err := exec.Command("/bin/sh", "-c", "/usr/bin/crontab -l").Output() output := string(out) if err != nil { return nil } var jobs []CronJob for _, line := range strings.Split(output, "\n") { line = strings.TrimSpace(line) // Kommentare, leere Zeilen, Variablen ueberspringen if line == "" || strings.HasPrefix(line, "#") || strings.Contains(line, "=") { continue } // Crontab-Format: min hour day month weekday command fields := strings.Fields(line) if len(fields) < 6 { continue } command := strings.Join(fields[5:], " ") // Klammern und Redirects bereinigen command = strings.TrimPrefix(command, "(") command = strings.TrimSuffix(command, ") > /dev/null") jobs = append(jobs, CronJob{ Source: "system", Enabled: true, Minutes: fields[0], Hours: fields[1], Days: fields[2], Months: fields[3], Weekdays: fields[4], Command: command, Schedule: strings.Join(fields[:5], " "), }) } return jobs } func collectOPNsenseCron() []CronJob { data, err := os.ReadFile("/conf/config.xml") if err != nil { return nil } type cronJob struct { UUID string `xml:"uuid,attr"` Origin string `xml:"origin"` Enabled string `xml:"enabled"` Minutes string `xml:"minutes"` Hours string `xml:"hours"` Days string `xml:"days"` Months string `xml:"months"` Weekdays string `xml:"weekdays"` Who string `xml:"who"` Command string `xml:"command"` Parameters string `xml:"parameters"` Description string `xml:"description"` } type opnsenseCron struct { Jobs []cronJob `xml:"OPNsense>cron>jobs>job"` } var cfg opnsenseCron if err := xml.Unmarshal(data, &cfg); err != nil { log.Printf("Cron XML parsen fehlgeschlagen: %v", err) return nil } var jobs []CronJob for _, j := range cfg.Jobs { schedule := strings.Join([]string{j.Minutes, j.Hours, j.Days, j.Months, j.Weekdays}, " ") cmd := j.Command if j.Parameters != "" { cmd += " " + j.Parameters } jobs = append(jobs, CronJob{ Source: "opnsense", Enabled: j.Enabled == "1", Minutes: j.Minutes, Hours: j.Hours, Days: j.Days, Months: j.Months, Weekdays: j.Weekdays, Command: cmd, Who: j.Who, Description: strings.TrimSpace(j.Description), UUID: j.UUID, Origin: j.Origin, Schedule: schedule, }) } return jobs }