From 3f3d555658d18f67e4678cc51287c885ad058b76 Mon Sep 17 00:00:00 2001 From: cynfo3000 Date: Sat, 28 Feb 2026 13:19:09 +0100 Subject: [PATCH] Cron-Jobs Collector: System-Crontab + OPNsense config.xml Jobs - Neuer Collector: agent/collector/cron.go - Zwei Quellen: crontab -l (System) + config.xml OPNsense-Jobs - Daten im Heartbeat unter 'cron_jobs' - Pro Job: source, schedule, enabled, command, description, uuid, origin - Backend Model CronJob in models/system.go - Getestet: 18 Jobs auf Produktion (8 System + 10 OPNsense) - README + Dateistruktur aktualisiert - Agent auf allen 3 Firewalls deployed --- README.md | 4 +- agent/collector/cron.go | 145 +++++++++++++++++++++++++++++++++++++++ agent/main.go | 2 + backend/models/system.go | 18 +++++ 4 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 agent/collector/cron.go diff --git a/README.md b/README.md index 1e325d9..0f96a35 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Client ──► Backend:ProxyPort ──► WebSocket (Binary) ──► Agent | Zertifikate | Alle Certs + CAs, Ablaufdatum, Aussteller | `config.xml` + PEM-Parsing | | Plugins | Installierte OPNsense-Plugins | `pkg info` | | Updates | Pending Core + Package Updates | `opnsense-update -c`, `pkg upgrade -n` | +| Cron | System-Crontab + OPNsense config.xml Jobs | `crontab -l`, `config.xml` | ### Remote-Commands (via WebSocket, on-demand) @@ -539,7 +540,8 @@ rmm/ │ │ ├── gateways.go # Gateway-Status (configctl) │ │ ├── certificates.go # Zertifikate + CAs (config.xml + PEM) │ │ ├── plugins.go # Installierte Plugins (pkg info) -│ │ └── updates.go # Pending Updates (core + packages) +│ │ ├── updates.go # Pending Updates (core + packages) +│ │ └── cron.go # Cron-Jobs (System-Crontab + OPNsense config.xml) │ └── ws/ │ ├── client.go # WebSocket Client, Reconnect, Ping/Pong │ ├── handler.go # Command Handler (exec, backup, tunnel_connect/disconnect) diff --git a/agent/collector/cron.go b/agent/collector/cron.go new file mode 100644 index 0000000..8d8034d --- /dev/null +++ b/agent/collector/cron.go @@ -0,0 +1,145 @@ +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 +} diff --git a/agent/main.go b/agent/main.go index a2751ed..5b489e2 100644 --- a/agent/main.go +++ b/agent/main.go @@ -121,6 +121,7 @@ func sendHeartbeat(c *client.Client, agentID string) error { certs := collector.CollectCertificates() plugins := collector.CollectPlugins() updates := collector.CollectUpdates() + cronJobs := collector.CollectCron() req := map[string]interface{}{ "agent_id": agentID, @@ -158,6 +159,7 @@ func sendHeartbeat(c *client.Client, agentID string) error { "certificates": certs, "plugins": plugins, "updates": updates, + "cron_jobs": cronJobs, }, } diff --git a/backend/models/system.go b/backend/models/system.go index 6604f34..d750f33 100644 --- a/backend/models/system.go +++ b/backend/models/system.go @@ -20,6 +20,7 @@ type SystemData struct { Certificates []CertificateInfo `json:"certificates,omitempty"` Plugins []PluginInfo `json:"plugins,omitempty"` Updates *UpdateInfo `json:"updates,omitempty"` + CronJobs []CronJob `json:"cron_jobs,omitempty"` } type WireGuardTunnel struct { @@ -152,6 +153,23 @@ type PendingUpdateInfo struct { NewVer string `json:"new_version"` } +type CronJob struct { + Source string `json:"source"` + 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"` +} + // HeartbeatRequest type HeartbeatRequest struct { AgentID string `json:"agent_id"`