//go:build windows package main import ( "bytes" "context" "crypto/rand" "encoding/base64" "encoding/json" "encoding/xml" "errors" "fmt" "io" "log" "net/http" "os" "os/signal" "path/filepath" "sort" "strings" "sync" "syscall" "time" "unsafe" "golang.org/x/sys/windows" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/debug" "golang.org/x/sys/windows/svc/eventlog" "golang.org/x/sys/windows/svc/mgr" ) const ( ServiceName = "SENDNRW-SIEM-AGENT" BatchSize = 8 PollWaitMS = 2000 FlushInterval = 5 * time.Second HTTPTimeout = 10 * time.Second ServiceLogInfo = 1 ) const ( ERROR_EVT_INVALID_OPERATION syscall.Errno = 15010 ) const AgentConfigPath = `C:\ProgramData\WinEventForwarder\agent.json` type ChannelConfig struct { Name string IDs map[uint32]bool } type AgentConfig struct { BackendURL string `json:"backend_url"` EnrollmentKey string `json:"enrollment_key"` ChannelRules []ChannelRule `json:"channel_rules"` StateFile string `json:"state_file"` } type ChannelRule struct { Name string `json:"name"` IDs []uint32 `json:"ids"` } type AgentState struct { Hostname string `json:"hostname"` APIKey string `json:"api_key"` Enrolled bool `json:"enrolled"` EnrolledAt time.Time `json:"enrolled_at,omitempty"` } type LogPayload struct { Hostname string `json:"host"` Channel string `json:"channel"` EventID uint32 `json:"id"` Source string `json:"source"` Time time.Time `json:"ts"` Message string `json:"msg"` } type eventXML struct { System struct { Provider struct { Name string `xml:"Name,attr"` } `xml:"Provider"` EventID uint32 `xml:"EventID"` Computer string `xml:"Computer"` TimeCreated struct { SystemTime string `xml:"SystemTime,attr"` } `xml:"TimeCreated"` } `xml:"System"` } var ( modWevtapi = windows.NewLazySystemDLL("wevtapi.dll") procEvtSubscribe = modWevtapi.NewProc("EvtSubscribe") procEvtNext = modWevtapi.NewProc("EvtNext") procEvtRender = modWevtapi.NewProc("EvtRender") procEvtClose = modWevtapi.NewProc("EvtClose") ) const ( evtSubscribeToFutureEvents = 1 evtRenderEventXML = 1 ) type myservice struct { cfg *AgentConfig state *AgentState } func (m *myservice) Execute(args []string, r <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) { const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown status <- svc.Status{State: svc.StartPending} ctx, cancel := context.WithCancel(context.Background()) defer cancel() hostname, _ := os.Hostname() client := &http.Client{Timeout: HTTPTimeout} out := make(chan LogPayload, 256) sendInitialHello(client, m.cfg, m.state, hostname) var wg sync.WaitGroup // Sender wg.Add(1) go func() { defer wg.Done() runSender( ctx, client, m.cfg.BackendURL, m.cfg.EnrollmentKey, m.cfg.StateFile, m.state, out, ) }() // Channel-Worker runtimeChannels := buildChannelConfigs(m.cfg.ChannelRules) for _, chCfg := range runtimeChannels { chCfg := chCfg chunks := splitIDMap(chCfg.IDs, 20) for i, idsChunk := range chunks { workerCfg := ChannelConfig{ Name: chCfg.Name, IDs: idsChunk, } wg.Add(1) go func(chunkNo int, cfg ChannelConfig) { defer wg.Done() log.Printf("[%s] Starte Watcher-Chunk %d/%d mit %d IDs", cfg.Name, chunkNo+1, len(chunks), len(cfg.IDs)) runChannelWatcher(ctx, hostname, cfg, out) log.Println("Prozess fertig oder abgebrochen", cfg) }(i, workerCfg) } } status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} loop: for c := range r { // "for range" is what Staticcheck prefers switch c.Cmd { case svc.Interrogate: status <- c.CurrentStatus case svc.Stop, svc.Shutdown: break loop } } cancel() wg.Wait() status <- svc.Status{State: svc.Stopped} return false, 0 } func main() { if err := setupFileLogging(); err != nil { fmt.Printf("Logging-Setup fehlgeschlagen: %v\n", err) } cfg, state, hostname, err := initAgent(AgentConfigPath) if err != nil { log.Fatalf("Agent-Initialisierung fehlgeschlagen: %v", err) } fmt.Printf("Agent gestartet auf %s\n", hostname) fmt.Printf("Backend: %s\n", cfg.BackendURL) fmt.Printf("Enrolled: %v\n", state.Enrolled) isService, err := svc.IsWindowsService() if err != nil { log.Fatalf("svc.IsWindowsService: %v", err) } if len(os.Args) > 1 { switch strings.ToLower(os.Args[1]) { case "install": if err := installService(); err != nil { log.Fatalf("Install fehlgeschlagen: %v", err) } fmt.Println("Dienst installiert.") return case "remove", "uninstall": if err := removeService(); err != nil { log.Fatalf("Entfernen fehlgeschlagen: %v", err) } fmt.Println("Dienst entfernt.") return case "debug": runDebug(cfg, state) return } } if !isService { fmt.Println("Interaktive Sitzung erkannt. Starte im Debug-Modus.") runDebug(cfg, state) return } if err := svc.Run(ServiceName, &myservice{ cfg: cfg, state: state, }); err != nil { log.Fatalf("svc.Run: %v", err) } } func runDebug(cfg *AgentConfig, state *AgentState) { elog := debug.New(ServiceName) defer elog.Close() hostname, _ := os.Hostname() client := &http.Client{Timeout: HTTPTimeout} out := make(chan LogPayload, 256) sendInitialHello(client, cfg, state, hostname) ctx, cancel := context.WithCancel(context.Background()) defer cancel() var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() runSender( ctx, client, cfg.BackendURL, cfg.EnrollmentKey, cfg.StateFile, state, out, ) }() runtimeChannels := buildChannelConfigs(cfg.ChannelRules) for _, chCfg := range runtimeChannels { chCfg := chCfg chunks := splitIDMap(chCfg.IDs, 20) for i, idsChunk := range chunks { workerCfg := ChannelConfig{ Name: chCfg.Name, IDs: idsChunk, } wg.Add(1) go func(chunkNo int, cfg ChannelConfig) { defer wg.Done() log.Printf("[%s] Starte Watcher-Chunk %d/%d mit %d IDs", cfg.Name, chunkNo+1, len(chunks), len(cfg.IDs)) runChannelWatcher(ctx, hostname, cfg, out) }(i, workerCfg) } } elog.Info(ServiceLogInfo, "Debug-Modus läuft. Mit Strg+C beenden.") // Create a channel to listen for Windows signals (like Ctrl+C) sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) select { case <-sigChan: elog.Info(ServiceLogInfo, "Beenden-Signal empfangen...") case <-ctx.Done(): } cancel() // Trigger shutdown for workers wg.Wait() // Wait for workers to clean up } func runChannelWatcher(ctx context.Context, hostname string, cfg ChannelConfig, out chan<- LogPayload) { query := buildXPathQuery2(cfg.IDs) signalEvent, err := windows.CreateEvent(nil, 1, 0, nil) if err != nil { log.Printf("[%s] CreateEvent-Fehler: %v", cfg.Name, err) return } defer windows.CloseHandle(signalEvent) sub, err := evtSubscribe(cfg.Name, query, signalEvent, evtSubscribeToFutureEvents) if err != nil { log.Printf("[%s] Subscribe-Fehler: %v | Query=%s", cfg.Name, err, query) return } defer evtClose(sub) log.Printf("[%s] Überwachung gestartet mit Filter %s", cfg.Name, query) drain := func() { for { events, err := evtNext(sub, BatchSize, 0) if err != nil { code := winErrCode(err) // Diese Fehler sind bei deinem Polling nicht fatal. if code == windows.ERROR_TIMEOUT || code == windows.ERROR_NO_MORE_ITEMS || strings.Contains(strings.ToLower(err.Error()), "operation identifier is not valid") { return } log.Printf("[%s] EvtNext-Fehler: %v | Code=%d", cfg.Name, err, uint32(code)) return } if len(events) == 0 { return } log.Printf("[%s] %d neue Events gefunden", cfg.Name, len(events)) for _, h := range events { payload, err := buildPayloadFromEventHandle(hostname, cfg.Name, h) _ = evtClose(h) if err != nil { log.Printf("[%s] Event-Verarbeitung fehlgeschlagen: %v", cfg.Name, err) continue } if !cfg.IDs[payload.EventID] { log.Printf("[%s] EventID %d ignoriert, nicht in Filter", cfg.Name, payload.EventID) continue } log.Printf("[%s] Event erkannt: ID=%d Source=%s Time=%s", cfg.Name, payload.EventID, payload.Source, payload.Time.Format(time.RFC3339), ) select { case out <- payload: case <-ctx.Done(): return } } } } for { select { case <-ctx.Done(): return default: } waitStatus, err := windows.WaitForSingleObject(signalEvent, PollWaitMS) if err != nil { log.Printf("[%s] WaitForSingleObject-Fehler: %v", cfg.Name, err) time.Sleep(2 * time.Second) continue } if waitStatus == uint32(windows.WAIT_OBJECT_0) { _ = windows.ResetEvent(signalEvent) drain() continue } if waitStatus == uint32(windows.WAIT_TIMEOUT) { // Wichtig: bei dir nötig, weil das Signal offenbar nicht zuverlässig kommt. drain() continue } _ = windows.ResetEvent(signalEvent) log.Printf("[%s] Unerwarteter Wait-Status: %d", cfg.Name, waitStatus) time.Sleep(2 * time.Second) } } func winErrCode(err error) syscall.Errno { var errno syscall.Errno if errors.As(err, &errno) { return errno } return 0 } func runSender( ctx context.Context, client *http.Client, backendURL string, enrollmentKey string, stateFile string, state *AgentState, in <-chan LogPayload, ) { ticker := time.NewTicker(FlushInterval) defer ticker.Stop() var batch []LogPayload flush := func() { if len(batch) == 0 { return } log.Printf("Flush: sende %d Events an %s", len(batch), backendURL) ok, err := sendBatch(client, backendURL, state, enrollmentKey, batch) if err != nil { log.Printf("sendBatch Fehler: %v", err) return } if ok { if !state.Enrolled { state.Enrolled = true state.EnrolledAt = time.Now().UTC() if err := saveAgentState(stateFile, state); err != nil { log.Printf("state speichern fehlgeschlagen: %v", err) } else { log.Printf("Agent erfolgreich enrolled") } } log.Printf("%d Events erfolgreich gesendet.", len(batch)) batch = nil } } for { select { case <-ctx.Done(): flush() return case item, ok := <-in: if !ok { flush() return } batch = append(batch, item) if len(batch) >= 25 { flush() } case <-ticker.C: flush() } } } func buildPayloadFromEventHandle(hostname, channel string, h windows.Handle) (LogPayload, error) { xmlStr, err := evtRenderXML(h) if err != nil { return LogPayload{}, fmt.Errorf("render xml: %w", err) } id, source, ts, err := extractEventMeta(xmlStr) if err != nil { return LogPayload{}, fmt.Errorf("parse xml: %w", err) } return LogPayload{ Hostname: hostname, Channel: channel, EventID: id, Source: source, Time: ts, Message: xmlStr, }, nil } func extractEventMeta(xmlStr string) (uint32, string, time.Time, error) { var evt eventXML if err := xml.Unmarshal([]byte(xmlStr), &evt); err != nil { return 0, "", time.Time{}, err } source := evt.System.Provider.Name if source == "" { source = "unknown" } ts := time.Now() if evt.System.TimeCreated.SystemTime != "" { if parsed, err := parseWindowsSystemTime(evt.System.TimeCreated.SystemTime); err == nil { ts = parsed } } return evt.System.EventID, source, ts, nil } func parseWindowsSystemTime(v string) (time.Time, error) { layouts := []string{ time.RFC3339Nano, "2006-01-02T15:04:05.9999999Z07:00", "2006-01-02T15:04:05.999999Z07:00", "2006-01-02T15:04:05Z07:00", } for _, layout := range layouts { if t, err := time.Parse(layout, v); err == nil { return t, nil } } return time.Time{}, fmt.Errorf("unbekanntes Zeitformat: %s", v) } func buildXPathQuery(ids map[uint32]bool) string { if len(ids) == 0 { return "*" } list := make([]int, 0, len(ids)) for id := range ids { list = append(list, int(id)) } sort.Ints(list) parts := make([]string, 0, len(list)) for _, id := range list { // Correct syntax: EventID=1074 parts = append(parts, fmt.Sprintf("EventID=%d", id)) } // Ensure the structure is exactly: *[System[(EventID=1074 or EventID=6005)]] return fmt.Sprintf("*[System[(%s)]]", strings.Join(parts, " or ")) } func buildXPathQuery2(ids map[uint32]bool) string { if len(ids) == 0 { return "*" } list := make([]int, 0, len(ids)) for id := range ids { list = append(list, int(id)) } sort.Ints(list) parts := make([]string, 0, len(list)) for _, id := range list { // Manche Windows-Versionen bevorzugen EventID ohne System/ davor // innerhalb des System-Knotens. parts = append(parts, fmt.Sprintf("EventID=%d", id)) } // WICHTIG: Keine unnötigen Leerzeichen innerhalb der XPath-Klammern return fmt.Sprintf("*[System[(%s)]]", strings.Join(parts, " or ")) } func sendBatch(client *http.Client, backendURL string, state *AgentState, enrollmentKey string, batch []LogPayload) (bool, error) { data, err := json.Marshal(batch) if err != nil { return false, err } req, err := http.NewRequest(http.MethodPost, backendURL, bytes.NewBuffer(data)) if err != nil { return false, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-API-Key", state.APIKey) if !state.Enrolled { req.Header.Set("X-Enrollment-Key", enrollmentKey) } resp, err := client.Do(req) if err != nil { return false, err } defer resp.Body.Close() log.Printf("Backend Response: HTTP %d", resp.StatusCode) if resp.StatusCode == http.StatusAccepted { return true, nil } body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) return false, fmt.Errorf("backend antwortete mit HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) } func splitIDMap(ids map[uint32]bool, maxPerChunk int) []map[uint32]bool { if maxPerChunk <= 0 { maxPerChunk = 20 } list := make([]int, 0, len(ids)) for id := range ids { list = append(list, int(id)) } sort.Ints(list) var chunks []map[uint32]bool for start := 0; start < len(list); start += maxPerChunk { end := start + maxPerChunk if end > len(list) { end = len(list) } chunk := make(map[uint32]bool, end-start) for _, id := range list[start:end] { chunk[uint32(id)] = true } chunks = append(chunks, chunk) } return chunks } func evtSubscribe(channelPath, query string, signal windows.Handle, flags uint32) (windows.Handle, error) { chPtr, err := windows.UTF16PtrFromString(channelPath) if err != nil { return 0, err } qPtr, err := windows.UTF16PtrFromString(query) if err != nil { return 0, err } r1, _, e1 := procEvtSubscribe.Call( 0, // Session = local uintptr(signal), uintptr(unsafe.Pointer(chPtr)), uintptr(unsafe.Pointer(qPtr)), 0, // Bookmark 0, // Context 0, // Callback = NULL, weil Pull-Modell uintptr(flags), ) if r1 == 0 { if e1 != syscall.Errno(0) { return 0, e1 } return 0, errors.New("EvtSubscribe fehlgeschlagen") } return windows.Handle(r1), nil } func evtNext(resultSet windows.Handle, maxHandles uint32, timeout uint32) ([]windows.Handle, error) { handles := make([]windows.Handle, maxHandles) var returned uint32 r1, _, e1 := procEvtNext.Call( uintptr(resultSet), uintptr(maxHandles), uintptr(unsafe.Pointer(&handles[0])), uintptr(timeout), 0, uintptr(unsafe.Pointer(&returned)), ) // r1 == 0 bedeutet, die Funktion war nicht erfolgreich (False) if r1 == 0 { // Wir prüfen, welcher Fehlercode vorliegt if e1 == windows.ERROR_NO_MORE_ITEMS || e1 == windows.ERROR_TIMEOUT { return nil, nil // Das ist kein Fehler, nur das Ende der Schlange } return nil, e1 // Ein echter Windows-Fehler } if returned == 0 { return nil, nil } return handles[:returned], nil } func evtRenderXML(eventHandle windows.Handle) (string, error) { var bufferUsed uint32 var propertyCount uint32 r1, _, e1 := procEvtRender.Call( 0, uintptr(eventHandle), uintptr(evtRenderEventXML), 0, 0, uintptr(unsafe.Pointer(&bufferUsed)), uintptr(unsafe.Pointer(&propertyCount)), ) if r1 == 0 { if errno, ok := e1.(syscall.Errno); ok { if errno != windows.ERROR_INSUFFICIENT_BUFFER { return "", errno } } else if e1 != syscall.Errno(0) { return "", e1 } } if bufferUsed == 0 { return "", errors.New("EvtRender lieferte keine Buffergröße") } buf := make([]uint16, bufferUsed/2) r1, _, e1 = procEvtRender.Call( 0, uintptr(eventHandle), uintptr(evtRenderEventXML), uintptr(bufferUsed), uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&bufferUsed)), uintptr(unsafe.Pointer(&propertyCount)), ) if r1 == 0 { if e1 != syscall.Errno(0) { return "", e1 } return "", errors.New("EvtRender fehlgeschlagen") } return windows.UTF16ToString(buf), nil } func evtClose(h windows.Handle) error { r1, _, e1 := procEvtClose.Call(uintptr(h)) if r1 == 0 { if e1 != syscall.Errno(0) { return e1 } return errors.New("EvtClose fehlgeschlagen") } return nil } func installService() error { exepath, err := os.Executable() if err != nil { return err } m, err := mgr.Connect() if err != nil { return err } defer m.Disconnect() s, err := m.OpenService(ServiceName) if err == nil { s.Close() return fmt.Errorf("Dienst %q existiert bereits", ServiceName) } s, err = m.CreateService(ServiceName, exepath, mgr.Config{ DisplayName: ServiceName, StartType: mgr.StartAutomatic, Description: "Forwardet Windows Eventlogs an ein HTTP-Backend", }) if err != nil { return err } defer s.Close() err = eventlog.InstallAsEventCreate(ServiceName, eventlog.Info|eventlog.Warning|eventlog.Error) if err != nil { _ = s.Delete() return err } return nil } func removeService() error { m, err := mgr.Connect() if err != nil { return err } defer m.Disconnect() s, err := m.OpenService(ServiceName) if err != nil { return err } defer s.Close() if err := s.Delete(); err != nil { return err } _ = eventlog.Remove(ServiceName) return nil } func loadAgentState(path string) (*AgentState, error) { data, err := os.ReadFile(path) if err != nil { if errors.Is(err, os.ErrNotExist) { return &AgentState{}, nil } return nil, err } var st AgentState if err := json.Unmarshal(data, &st); err != nil { return nil, err } return &st, nil } func saveAgentState(path string, st *AgentState) error { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } data, err := json.MarshalIndent(st, "", " ") if err != nil { return err } tmp := path + ".tmp" if err := os.WriteFile(tmp, data, 0o600); err != nil { return err } return os.Rename(tmp, path) } func generateAgentKey() (string, error) { buf := make([]byte, 32) if _, err := rand.Read(buf); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(buf), nil } func loadAgentConfig(path string) (*AgentConfig, error) { cfg, err := ensureAgentConfig(path) if err != nil { return nil, err } if strings.TrimSpace(cfg.BackendURL) == "" { return nil, errors.New("backend_url fehlt") } if strings.TrimSpace(cfg.StateFile) == "" { return nil, errors.New("state_file fehlt") } return cfg, nil } func initAgent(configPath string) (*AgentConfig, *AgentState, string, error) { cfg, err := loadAgentConfig(configPath) if err != nil { return nil, nil, "", err } hostname, err := os.Hostname() if err != nil { return nil, nil, "", err } state, err := loadAgentState(cfg.StateFile) if err != nil { return nil, nil, "", err } if strings.TrimSpace(state.Hostname) == "" { state.Hostname = hostname } if strings.TrimSpace(state.APIKey) == "" { key, err := generateAgentKey() if err != nil { return nil, nil, "", err } state.APIKey = key } if err := saveAgentState(cfg.StateFile, state); err != nil { return nil, nil, "", err } return cfg, state, hostname, nil } func defaultAgentConfig() *AgentConfig { return &AgentConfig{ BackendURL: getenvDefault("SIEM_BACKEND_URL", "http://10.10.5.220:8090/ingest"), EnrollmentKey: getenvDefault("SIEM_ENROLLMENT_KEY", "BITTE_SEHR_LANG_UND_ZUFAELLIG"), StateFile: `C:\ProgramData\WinEventForwarder\state.json`, ChannelRules: []ChannelRule{ // ========================= // SYSTEM // ========================= { Name: "System", IDs: []uint32{ 1074, // planned shutdown 6005, // startup 6006, // shutdown 6008, // unexpected shutdown 7045, // service installed }, }, // ========================= // SECURITY // ========================= { Name: "Security", IDs: []uint32{ // --- Logon / Auth --- 4624, // logon success 4625, // logon failed 4648, // explicit credentials 4672, // special privileges 4673, 4674, // --- Security / Audit --- 1102, // log cleared 4719, // audit policy 4902, 4904, 4905, 4906, 4907, 4908, 4912, // --- Time --- 4616, // system time changed // --- User --- 4720, 4722, 4723, 4724, 4725, 4726, 4738, 4740, // --- Groups --- 4727, 4728, 4729, 4730, 4731, 4732, 4733, 4734, 4735, 4737, 4754, 4755, 4756, 4757, 4758, // --- Computer --- 4741, 4742, 4743, // --- Kerberos / NTLM --- 4768, 4769, 4771, 4776, // --- Services / Tasks --- 4697, 4698, 4699, 4700, 4701, 4702, // --- AD / Directory --- 4662, // object access 4670, // permission change 5136, 5137, 5141, // --- Shares --- 5140, 5145, // --- AD CS --- 4882, 4885, 4886, 4887, 4890, 4891, 4892, 4898, 4899, 4900, // --- Defender --- 5001, // real-time protection disabled }, }, // ========================= // POWERSHELL // ========================= { Name: "Microsoft-Windows-PowerShell/Operational", IDs: []uint32{ 4104, // script block }, }, { Name: "Windows PowerShell", IDs: []uint32{ 4104, }, }, // ========================= // DEFENDER // ========================= { Name: "Microsoft-Windows-Windows Defender/Operational", IDs: []uint32{ 1116, // malware detected 1117, // remediation 1118, 1119, 5007, // config change 5013, }, }, // ========================= // WMI (Lateral Movement!) // ========================= { Name: "Microsoft-Windows-WMI-Activity/Operational", IDs: []uint32{ 5857, 5858, 5859, 5860, 5861, }, }, // ========================= // RDP // ========================= { Name: "Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational", IDs: []uint32{ 1149, // RDP login }, }, // ========================= // OPTIONAL (laut!) // ========================= /* { Name: "Security", IDs: []uint32{ 4688, // process creation (SEHR LAUT!) }, }, */ }, } } func ensureAgentConfig(path string) (*AgentConfig, error) { data, err := os.ReadFile(path) if err == nil { var cfg AgentConfig if err := json.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("config ungültig: %w", err) } return &cfg, nil } if !errors.Is(err, os.ErrNotExist) { return nil, err } cfg := defaultAgentConfig() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return nil, err } raw, err := json.MarshalIndent(cfg, "", " ") if err != nil { return nil, err } if err := os.WriteFile(path, raw, 0o600); err != nil { return nil, err } return cfg, nil } func getenvDefault(key, def string) string { v := strings.TrimSpace(os.Getenv(key)) if v == "" { return def } return v } func buildChannelConfigs(rules []ChannelRule) []ChannelConfig { out := make([]ChannelConfig, 0, len(rules)) for _, r := range rules { ids := make(map[uint32]bool, len(r.IDs)) for _, id := range r.IDs { ids[id] = true } out = append(out, ChannelConfig{ Name: r.Name, IDs: ids, }) } return out } func sendInitialHello(client *http.Client, cfg *AgentConfig, state *AgentState, hostname string) { batch := []LogPayload{ { Hostname: hostname, Channel: "Agent", EventID: 1, Source: "WinEventForwarder", Time: time.Now().UTC(), Message: "Agent startup / enrollment hello", }, } ok, err := sendBatch(client, cfg.BackendURL, state, cfg.EnrollmentKey, batch) if err != nil { log.Printf("Initial hello fehlgeschlagen: %v", err) return } if ok { if !state.Enrolled { state.Enrolled = true state.EnrolledAt = time.Now().UTC() if err := saveAgentState(cfg.StateFile, state); err != nil { log.Printf("state speichern nach Initial-Hello fehlgeschlagen: %v", err) } else { log.Printf("Agent erfolgreich per Initial-Hello enrolled") } } log.Printf("Initial hello erfolgreich gesendet") } } func setupFileLogging() error { logDir := `C:\ProgramData\WinEventForwarder` if err := os.MkdirAll(logDir, 0o755); err != nil { return err } f, err := os.OpenFile( filepath.Join(logDir, "agent.log"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644, ) if err != nil { return err } log.SetOutput(f) log.SetFlags(log.LstdFlags | log.Lmicroseconds | log.LUTC) return nil }