1171 lines
25 KiB
Go
1171 lines
25 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 = "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
|
|
}
|