Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions agent/collectors/collectors.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ func getCollectorsInstances() []Collector {
var collectors []Collector
switch runtime.GOOS {
case "windows":
collectors = append(collectors, Winlogbeat{})
collectors = append(collectors, Filebeat{})
collectors = append(collectors, Windows{})
if runtime.GOARCH == "amd64" {
collectors = append(collectors, Filebeat{})
}
case "linux":
collectors = append(collectors, Filebeat{})
}
Expand Down
47 changes: 20 additions & 27 deletions agent/collectors/filebeat.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,36 +74,29 @@ func (f Filebeat) Install() error {
return fmt.Errorf("error reloading daemon: %v", err)
}

family, err := utils.DetectLinuxFamily()
err := utils.Execute("systemctl", filebLogPath, "enable", config.ModulesServName)
if err != nil {
return err
return fmt.Errorf("%s", err)
}

if family == "debian" || family == "rhel" {
err := utils.Execute("systemctl", filebLogPath, "enable", config.ModulesServName)
if err != nil {
return fmt.Errorf("%s", err)
}

err = utils.Execute("systemctl", filebLogPath, "start", config.ModulesServName)
if err != nil {
return fmt.Errorf("%s", err)
}

err = utils.Execute("./filebeat", filebLogPath, "modules", "enable", "system")
if err != nil {
return fmt.Errorf("%s", err)
}

err = utils.Execute("sed", filepath.Join(filebLogPath, "modules.d"), "-i", "s/enabled: false/enabled: true/g", "system.yml")
if err != nil {
return fmt.Errorf("%s", err)
}

err = utils.Execute("systemctl", filebLogPath, "restart", config.ModulesServName)
if err != nil {
return fmt.Errorf("%s", err)
}
err = utils.Execute("systemctl", filebLogPath, "start", config.ModulesServName)
if err != nil {
return fmt.Errorf("%s", err)
}

err = utils.Execute("./filebeat", filebLogPath, "modules", "enable", "system")
if err != nil {
return fmt.Errorf("%s", err)
}

err = utils.Execute("sed", filepath.Join(filebLogPath, "modules.d"), "-i", "s/enabled: false/enabled: true/g", "system.yml")
if err != nil {
return fmt.Errorf("%s", err)
}

err = utils.Execute("systemctl", filebLogPath, "restart", config.ModulesServName)
if err != nil {
return fmt.Errorf("%s", err)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//go:build windows && amd64
// +build windows,amd64

package collectors

import (
Expand All @@ -10,9 +13,9 @@ import (
"github.com/utmstack/UTMStack/agent/utils"
)

type Winlogbeat struct{}
type Windows struct{}

func (w Winlogbeat) Install() error {
func (w Windows) Install() error {
path := utils.GetMyPath()

winlogPath := filepath.Join(path, "beats", "winlogbeat")
Expand Down Expand Up @@ -59,7 +62,7 @@ func (w Winlogbeat) Install() error {
return nil
}

func (w Winlogbeat) SendSystemLogs() {
func (w Windows) SendSystemLogs() {
logLinesChan := make(chan []string)
path := utils.GetMyPath()
winbLogPath := filepath.Join(path, "beats", "winlogbeat", "logs")
Expand All @@ -82,7 +85,7 @@ func (w Winlogbeat) SendSystemLogs() {
}
}

func (w Winlogbeat) Uninstall() error {
func (w Windows) Uninstall() error {
if isInstalled, err := utils.CheckIfServiceIsInstalled(config.WinServName); err != nil {
return fmt.Errorf("error checking if %s is running: %v", config.WinServName, err)
} else if isInstalled {
Expand Down
199 changes: 199 additions & 0 deletions agent/collectors/windows_arm64.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
//go:build windows && arm64
// +build windows,arm64

package collectors

import (
"github.com/utmstack/UTMStack/agent/config"
"github.com/utmstack/UTMStack/agent/logservice"
"os"
"os/exec"
"path/filepath"
"strings"
"time"

"github.com/threatwinds/validations"
"github.com/utmstack/UTMStack/agent/utils"
)

type Windows struct{}

const PowerShellScript = `
<#
.SYNOPSIS
Collects Windows Application, System, and Security logs from the last 5 minutes, then prints them to the console in a compact, single-line JSON format,
emulating the field structure that Winlogbeat typically produces.

.DESCRIPTION
1. Retrieves the last 5 minutes of Windows logs (Application, System, Security) using FilterHashtable (no post-fetch filtering).
2. Maps event properties to a schema similar to Winlogbeat's, including:
- @timestamp
- message
- event.code
- event.provider
- event.kind
- winlog fields (e.g. record_id, channel, activity_id, etc.)
3. Prints each log record as a single-line JSON object with no indentation/extra spacing.
4. If no logs are found, the script produces no output at all.
#>

# Suppress any runtime errors that would clutter the console.
$ErrorActionPreference = 'SilentlyContinue'

# Calculate the start time for filtering
$startTime = (Get-Date).AddSeconds(-30)

# Retrieve logs with filter hashtable
$applicationLogs = Get-WinEvent -FilterHashtable @{ LogName='Application'; StartTime=$startTime }
$systemLogs = Get-WinEvent -FilterHashtable @{ LogName='System'; StartTime=$startTime }
$securityLogs = Get-WinEvent -FilterHashtable @{ LogName='Security'; StartTime=$startTime }

# Safeguard against null results
if (-not $applicationLogs) { $applicationLogs = @() }
if (-not $systemLogs) { $systemLogs = @() }
if (-not $securityLogs) { $securityLogs = @() }

# Combine them
$recentLogs = $applicationLogs + $systemLogs + $securityLogs

# If no logs are found, produce no output at all
if (-not $recentLogs) {
return
}

# Function to convert the raw Properties array to a dictionary-like object under winlog.event_data
function Convert-PropertiesToEventData {
param([Object[]] $Properties)

# If nothing is there, return an empty hashtable
if (-not $Properties) { return @{} }

# Winlogbeat places custom fields under winlog.event_data.
# Typically, it tries to parse known keys, but we'll do a simple best-effort approach:
# We'll create paramN = <value> pairs for each array index.
$eventData = [ordered]@{}

for ($i = 0; $i -lt $Properties.Count; $i++) {
$value = $Properties[$i].Value

# If the property is itself an object with nested fields, we can flatten or store as-is.
# We'll store as-is for clarity.
# We'll name them param1, param2, param3,... unless you'd like more specific field logic.
$paramName = "param$($i+1)"

$eventData[$paramName] = $value
}

return $eventData
}

# Transform each event into a structure emulating Winlogbeat
foreach ($rawEvent in $recentLogs) {
# Convert TimeCreated to a universal ISO8601 string
$timestamp = $rawEvent.TimeCreated.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')

# Build the top-level document
$doc = [ordered]@{
# Matches Winlogbeat's typical top-level timestamp field
'@timestamp' = $timestamp

# The main message content from the event
'message' = $rawEvent.Message

# "event" block: minimal example
'event' = [ordered]@{
'code' = $rawEvent.Id # event_id in Winlogbeat is typically a string or numeric
'provider' = $rawEvent.ProviderName
'kind' = 'event'
'created' = $timestamp # or you could omit if desired
}

# "winlog" block: tries to mirror Winlogbeat's structure for Windows
'winlog' = [ordered]@{
'record_id' = $rawEvent.RecordId
'computer_name' = $rawEvent.MachineName
'channel' = $rawEvent.LogName
'provider_name' = $rawEvent.ProviderName
'provider_guid' = $rawEvent.ProviderId
'process' = [ordered]@{
'pid' = $rawEvent.ProcessId
'thread' = @{
'id' = $rawEvent.ThreadId
}
}
'event_id' = $rawEvent.Id
'version' = $rawEvent.Version
'activity_id' = $rawEvent.ActivityId
'related_activity_id'= $rawEvent.RelatedActivityId
'task' = $rawEvent.TaskDisplayName
'opcode' = $rawEvent.OpcodeDisplayName
'keywords' = $rawEvent.KeywordsDisplayNames
'time_created' = $timestamp
# Convert "Properties" into a dictionary for event_data
'event_data' = Convert-PropertiesToEventData $rawEvent.Properties
}
}

# Convert our object to JSON (with no extra formatting).
$json = $doc | ConvertTo-Json -Depth 20

# Remove all newlines and indentation for a single-line representation
$compactJson = $json -replace '(\r?\n\s*)+', ''

# Output the line
Write-Output $compactJson
}
`

func (w Windows) Install() error {
path := utils.GetMyPath()
collectorPath := filepath.Join(path, "collector.ps1")
err := os.WriteFile(collectorPath, []byte(PowerShellScript), 0644)
return err
}

func (w Windows) SendSystemLogs() {
path := utils.GetMyPath()
collectorPath := filepath.Join(path, "collector.ps1")

for {
select {
case <-time.After(30 * time.Second):
go func() {
cmd := exec.Command("Powershell.exe", "-File", collectorPath)

output, err := cmd.Output()
if err != nil {
_ = utils.Logger.ErrorF("error executing powershell script: %v", err)
return
}

logLines := strings.Split(string(output), "\n")

validatedLogs := make([]string, 0, len(logLines))

for logLine := range logLines {
validatedLog, _, err := validations.ValidateString(logLine, false)
if err != nil {
_ = utils.Logger.ErrorF("error validating log: %s: %v", logLine, err)
continue
}

validatedLogs = append(validatedLogs, validatedLog)
}

logservice.LogQueue <- logservice.LogPipe{
Src: string(config.DataTypeWindowsAgent),
Logs: validatedLogs,
}
}()
}
}
}

func (w Windows) Uninstall() error {
path := utils.GetMyPath()
collectorPath := filepath.Join(path, "collector.ps1")
err := os.Remove(collectorPath)
return err
}
9 changes: 0 additions & 9 deletions agent/utils/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,3 @@ func GetIPAddress() (string, error) {

return "", errors.New("failed to get IP address")
}

func IsPortUsed(proto string, port string) bool {
conn, err := net.Listen(proto, ":"+port)
if err != nil {
return true
}
conn.Close()
return false
}
8 changes: 0 additions & 8 deletions agent/utils/delay.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,3 @@ func IncrementReconnectDelay(delay time.Duration, maxReconnectDelay time.Duratio
}
return delay
}

func IncrementReconnectTime(currentTime time.Duration, delay time.Duration, maxReconnectTime time.Duration) time.Duration {
currentTime = currentTime + delay
if currentTime >= maxReconnectTime {
currentTime = maxReconnectTime
}
return currentTime
}
24 changes: 0 additions & 24 deletions agent/utils/os.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,13 @@ package utils
import (
"fmt"
"os"
"os/exec"
"os/user"
"strconv"
"strings"

"github.com/elastic/go-sysinfo"
)

func DetectLinuxFamily() (string, error) {
var pmCommands map[string]string = map[string]string{
"debian": "apt list",
"rhel": "yum list",
}

for dist, command := range pmCommands {
cmd := strings.Split(command, " ")
var err error

if len(cmd) > 1 {
_, err = exec.Command(cmd[0], cmd[1:]...).Output()
} else {
_, err = exec.Command(cmd[0]).Output()
}

if err == nil {
return dist, nil
}
}
return "", fmt.Errorf("unknown distribution")
}

type OSInfo struct {
Hostname string
OsType string
Expand Down
Loading
Loading