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
This commit is contained in:
cynfo3000 2026-02-28 13:19:09 +01:00
parent c72b791796
commit 3f3d555658
4 changed files with 168 additions and 1 deletions

View File

@ -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)

145
agent/collector/cron.go Normal file
View File

@ -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
}

View File

@ -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,
},
}

View File

@ -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"`