Files
siem-agent/main.go
groot 0059cb1978
All checks were successful
build-binaries / build (.exe, amd64, windows) (push) Successful in 10m18s
build-binaries / release (push) Successful in 23s
build-binaries / publish-agent (push) Successful in 12s
Update
2026-04-24 08:57:09 +02:00

1018 lines
22 KiB
Go

//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 = "WinEventForwarder"
BatchSize = 8
PollWaitMS = 2000
FlushInterval = 5 * time.Second
HTTPTimeout = 10 * time.Second
ServiceLogInfo = 1
)
const AgentConfigPath = `C:\ProgramData\WinEventForwarder\agent.json`
type ChannelConfig struct {
Name string
IDs map[uint32]bool
}
var channelConfigs = []ChannelConfig{
{
Name: "System",
IDs: map[uint32]bool{
1074: true, // Shutdown/Reboot
6005: true, // Eventlog gestartet
6006: true, // Eventlog gestoppt
},
},
{
Name: "Application",
IDs: map[uint32]bool{
1000: true, // Beispiel: Application Error
},
},
// Beispiel:
// {
// Name: "Security",
// IDs: map[uint32]bool{
// 4624: true,
// 4625: true,
// },
// },
}
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)
}(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 := buildXPathQuery(cfg.IDs)
signal, err := windows.CreateEvent(nil, 1, 0, nil)
if err != nil {
log.Printf("[%s] CreateEvent-Fehler: %v", cfg.Name, err)
return
}
defer windows.CloseHandle(signal)
sub, err := evtSubscribe(cfg.Name, query, signal, 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)
for {
select {
case <-ctx.Done():
return
default:
}
waitStatus, err := windows.WaitForSingleObject(signal, PollWaitMS)
if err != nil {
log.Printf("[%s] WaitForSingleObject-Fehler: %v", cfg.Name, err)
time.Sleep(2 * time.Second)
continue
}
switch waitStatus {
case uint32(windows.WAIT_OBJECT_0):
events, err := evtNext(sub, BatchSize, 0)
if err != nil {
if isIgnorableEvtNextError(err) {
_ = windows.ResetEvent(signal)
continue
}
log.Printf("[%s] EvtNext-Fehler: %v", cfg.Name, err)
_ = windows.ResetEvent(signal)
time.Sleep(2 * time.Second)
continue
}
log.Printf("[%s] EvtNext lieferte %d Events", 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
}
log.Printf("[%s] Event empfangen: EventID=%d Source=%s Time=%s",
cfg.Name,
payload.EventID,
payload.Source,
payload.Time.Format(time.RFC3339),
)
if !cfg.IDs[payload.EventID] {
log.Printf("[%s] Event weggefiltert: EventID=%d", cfg.Name, payload.EventID)
continue
}
log.Printf("[%s] Event wird gesendet: EventID=%d", cfg.Name, payload.EventID)
select {
case out <- payload:
case <-ctx.Done():
return
}
}
_ = windows.ResetEvent(signal)
case uint32(windows.WAIT_TIMEOUT):
continue
default:
log.Printf("[%s] Unerwarteter Wait-Status: %d", cfg.Name, waitStatus)
time.Sleep(2 * time.Second)
}
}
}
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 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)),
)
if r1 == 0 {
if e1 != syscall.Errno(0) {
return nil, e1
}
return nil, errors.New("EvtNext fehlgeschlagen")
}
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 isIgnorableEvtNextError(err error) bool {
var errno syscall.Errno
if errors.As(err, &errno) {
if errno == windows.ERROR_TIMEOUT || errno == windows.ERROR_NO_MORE_ITEMS {
return true
}
}
return false
}
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{
{Name: "System", IDs: []uint32{1074, 6005, 6006}},
{Name: "Security", IDs: []uint32{4624, 4625}},
},
}
}
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
}