955 lines
20 KiB
Go
955 lines
20 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
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
runChannelWatcher(ctx, hostname, chCfg, out)
|
|
}()
|
|
}
|
|
|
|
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
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
runChannelWatcher(ctx, hostname, chCfg, out)
|
|
}()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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] {
|
|
continue
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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()
|
|
|
|
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 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
|
|
}
|