3 Commits
v1.0.1 ... main

Author SHA1 Message Date
45d3ed4db0 RC-2
All checks were successful
build-binaries / build (.exe, amd64, windows) (push) Successful in 10m21s
build-binaries / release (push) Successful in 14s
build-binaries / publish-agent (push) Successful in 15s
2026-04-25 08:26:09 +02:00
670e89efa9 RC-1
All checks were successful
build-binaries / build (.exe, amd64, windows) (push) Has been skipped
build-binaries / release (push) Has been skipped
build-binaries / publish-agent (push) Has been skipped
2026-04-24 21:57:24 +02:00
7515bda711 Bugfix - First Working
All checks were successful
build-binaries / build (.exe, amd64, windows) (push) Has been skipped
build-binaries / release (push) Has been skipped
build-binaries / publish-agent (push) Has been skipped
2026-04-24 18:06:49 +02:00
2 changed files with 239 additions and 86 deletions

325
main.go
View File

@@ -32,7 +32,7 @@ import (
)
const (
ServiceName = "WinEventForwarder"
ServiceName = "SENDNRW-SIEM-AGENT"
BatchSize = 8
PollWaitMS = 2000
@@ -41,6 +41,10 @@ const (
ServiceLogInfo = 1
)
const (
ERROR_EVT_INVALID_OPERATION syscall.Errno = 15010
)
const AgentConfigPath = `C:\ProgramData\WinEventForwarder\agent.json`
type ChannelConfig struct {
@@ -48,31 +52,6 @@ type ChannelConfig struct {
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"`
@@ -181,6 +160,7 @@ func (m *myservice) Execute(args []string, r <-chan svc.ChangeRequest, status ch
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)
}
}
@@ -325,16 +305,16 @@ func runDebug(cfg *AgentConfig, state *AgentState) {
}
func runChannelWatcher(ctx context.Context, hostname string, cfg ChannelConfig, out chan<- LogPayload) {
query := buildXPathQuery(cfg.IDs)
query := buildXPathQuery2(cfg.IDs)
signal, err := windows.CreateEvent(nil, 1, 0, nil)
signalEvent, err := windows.CreateEvent(nil, 1, 0, nil)
if err != nil {
log.Printf("[%s] CreateEvent-Fehler: %v", cfg.Name, err)
return
}
defer windows.CloseHandle(signal)
defer windows.CloseHandle(signalEvent)
sub, err := evtSubscribe(cfg.Name, query, signal, evtSubscribeToFutureEvents)
sub, err := evtSubscribe(cfg.Name, query, signalEvent, evtSubscribeToFutureEvents)
if err != nil {
log.Printf("[%s] Subscribe-Fehler: %v | Query=%s", cfg.Name, err, query)
return
@@ -343,35 +323,28 @@ func runChannelWatcher(ctx context.Context, hostname string, cfg ChannelConfig,
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):
drain := func() {
for {
events, err := evtNext(sub, BatchSize, 0)
if err != nil {
if isIgnorableEvtNextError(err) {
_ = windows.ResetEvent(signal)
continue
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", cfg.Name, err)
_ = windows.ResetEvent(signal)
time.Sleep(2 * time.Second)
continue
log.Printf("[%s] EvtNext-Fehler: %v | Code=%d", cfg.Name, err, uint32(code))
return
}
log.Printf("[%s] EvtNext lieferte %d Events", cfg.Name, len(events))
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)
@@ -382,37 +355,64 @@ func runChannelWatcher(ctx context.Context, hostname string, cfg ChannelConfig,
continue
}
log.Printf("[%s] Event empfangen: EventID=%d Source=%s Time=%s",
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),
)
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)
}
}
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(
@@ -557,6 +557,28 @@ func buildXPathQuery(ids map[uint32]bool) string {
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 {
@@ -660,11 +682,14 @@ func evtNext(resultSet windows.Handle, maxHandles uint32, timeout uint32) ([]win
0,
uintptr(unsafe.Pointer(&returned)),
)
// r1 == 0 bedeutet, die Funktion war nicht erfolgreich (False)
if r1 == 0 {
if e1 != syscall.Errno(0) {
return nil, e1
// 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, errors.New("EvtNext fehlgeschlagen")
return nil, e1 // Ein echter Windows-Fehler
}
if returned == 0 {
@@ -734,16 +759,6 @@ func evtClose(h windows.Handle) error {
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 {
@@ -901,8 +916,146 @@ func defaultAgentConfig() *AgentConfig {
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}},
// =========================
// 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!)
},
},
*/
},
}
}

BIN
siem-agent.exe Normal file

Binary file not shown.