<#

Deploy_SOD_Winlog.ps1
Version: 1.0
Author: Randy Baca
Company: Security On-Demand

PowerShell script to deploy the SOD_Winlog service

Description:
    The SOD_Winlog service is based on open source Winlogbeat from Elastic.  The config is generic to pull events from
    the base three Windows event log databases, Security, Application, and System, plus Forwarded Events.  There is one other
    database listed, Sysmon, which is for future services.  All of these databases are enabled by default.  There is no
    resource hit if any of the databases are empty or if they don't exist.
    
    The SOD collectors have a Redis service running to receive the output from SOD_Winlog.  Once the service is installed, events
    will begin to flow to the SOD collector.  If the -uninstall switch is used, all other parameters will be ignored, and the
    service will be stopped and removed.  Finally, once the SOD_Winlog service is successfully installed the legacy Snare service will be
    stopped and disabled, unless the -keepsnare switch is set, in which case the Snare service will be left alone.

    > Deployment can be run manually from the server.  The script will look for the source files in the destination folder
      first (%ProgramFiles%\SOD_Winlog by default).  The location for the source files can be changed in the command line.  Use 
      the -source switch to set a different source path, for example, if the source files are located on the network.
    > Deployment can be run from a GPO *Computer Startup Script* (requires reboot by others) if the SOD_Winlog source files are on a network share.
    > This script is not designed to deploy the service to remote computers, it must be run locally either manually or via GPO.
    > If the SOD_Winlog service is already installed and -reinstall is not set, the script will simply exit.
    > If the SOD_Winlog service does not exist on the host, it will be installed.
    > If the SOD_Winlog service is already installed and -reinstall is set, the script will force a remove/reinstall of the service (for upgrades).
    > The install log is in the %ProgramData%\SOD_Winlog folder (typically, C:\ProgramData\SOD_Filelog).

Arguments:
    -source "<path to SOD_Winlog>"   If this argument is missing, the script will look in "%ProgramFiles%\SOD_Winlog" by default.  If running manually,
                                     the source can be local or a network share UNC path.  When deploying with GPO, -source must be a UNC path.
    -redispass <password>            Mandatory. This is the password for connecting to the SOD collector Redis service.
    -ip <SOD collector IP>           Mandatory. This is the IP address of the nearest SOD collector
    -32                              When present will install the 32-bit service instead of the default 64-bit.
    -reinstall                       When present and the service is already installed, will uninstall and reinstall it.
    -console                         Will write some logs to the console.
    -uninstall                       Will stop the service and delete it, ignoring all other parameters.
    -keepsnare                       If set, will ignore the existing Snare service. Otherwise, if the Snare service is found, it will be stopped and disabled.

Example:
To deploy the SOD_Winlog service manually from a network share, where the IP of the nearest SOD collector is 10.10.0.20:
  1. Copy Deploy_SOD_Winlog.ps1 to a temporary location on the computer where it will be deployed.
  2. Open an elevated PowerShell command prompt.
  3. Run Deploy_SOD_Winlog.ps1 from the host where the service is being installed (e.g., from c:\temp):
         powershell.exe -ExecutionPolicy UnRestricted -File "c:\temp\Deploy_SOD_Winlog.ps1" -source "\\file01.domain.local\ShareName\AppDeploy\SOD_Winlog" -redispass xxxxxxxxxxxxxxxx -ip 10.200.6.20 -console
#>

param (
    [string] $source     = "",
    [string] $redispass  = "",
    [string] $ip         = "",
    [switch] $32         = $false,
    [switch] $reinstall  = $false,
    [switch] $console    = $false,
    [switch] $uninstall  = $false,
    [switch] $keepsnare  = $false
)

# if no params, print usage
$argsPresent = $false
foreach ( $key in $PSBoundParameters.keys ) {
    $argsPresent = $true
}
if  (!($argsPresent)) {
    write-host "`n  Usage:"
    write-host "    .\Deploy_SOD_Winlog.ps1 [-source <folderpath>] [-redispass <password>] [-ip <collectorIP>] [<options>]"
    write-host "`n  Options:"
    write-host "    -source <path to SOD_Winlog>   If argument is missing, script will look in <%ProgramFiles%\SOD_Winlog>."
    write-host "    -redispass <password>          Mandatory. The password for connecting to the SOD collector Redis service."
    write-host "    -ip <SOD collector IP>         Mandatory. IP address of the nearest SOD collector."
    write-host "    -32                            When present will install the 32-bit service instead of the default 64-bit."
    write-host "    -reinstall                     When present will uninstall and reinstall the service (if it exists)."
    write-host "    -console                       Will log to the console."
    write-host "    -uninstall                     Will stop the service and delete it, ignoring all other parameters."
    write-host "    -keepsnare                     Will ignore the Snare service. The default is to stop the service and disable it."
    write-host "`n  Example for installing SOD_Winlog, where the source zip file has been extracted to temp folder:"
    write-host "    powershell.exe -ExecutionPolicy UnRestricted -File c:\temp\Deploy_SOD_Winlog.ps1 -source c:\temp\SOD_Winlog -redispass xxxxxxxxxxxx -ip 10.10.0.20 -console"
    write-host "`n"
    exit
}

$serviceName        = "SOD_Winlog"
$exeName            = "winlogbeat"
$hostname           = "$Env:COMPUTERNAME"
$destination        = "$Env:ProgramFiles\$serviceName"
$dataDir            = "$Env:ProgramData\$serviceName"
$configFile         = "$exeName.yml"
$logFile            = "$dataDir\$($serviceName)_$($hostname)_install.log"
$port               = 6379 #default redis port

# create the $dataDir for logging
if (-not (Test-Path $dataDir)) {
    New-Item -Path $dataDir -ItemType Directory
}

# if source is not set, use %ProgramFiles% by default
if (-not $source) {
    $source = $destination
}

########################## Functions
function Logger {
    param (
        [string] $sev,
        [string] $msg
    )

    $timestamp = Get-Date -format 'yyyy-MM-dd HH:mm:ss'
    $response = "$timestamp $sev $msg"
    Add-content $logFile -value $response
    if ($console) {
        if ($sev -eq "ERROR") {
            write-host -f red $response
        } elseif ($sev -eq "WARNING") {
            write-host -f yellow $response
        } else {
            write-host $response
        }
    }
}

Logger("Info", "--------starting up...")

########################## Validation
# Validate admin permissions
$identity = (New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) | Select-Object -ExpandProperty Identities).Name
Logger("Info", "Current identity: $identity")
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
if (-Not ($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))) {
    Logger("ERROR", "Administrator permissions required, quitting.")
    exit
}

# Validate service
$serviceInstalled = $false
if (Get-Service $serviceName -ErrorAction SilentlyContinue) {
    $serviceInstalled = $true
}

# uninstall overrides everything else
if ($uninstall) {
    if ($reinstall) {
        Logger("ERROR", "-uninstall and -reinstall are both set, quitting.")
        exit
    }
    if ($serviceInstalled)  {
        Logger("Info", "Uninstalling the $serviceName service.")
        try {
            $service = Get-WmiObject -Class Win32_Service -Filter "name='$serviceName'"
            $service.StopService()
            Start-Sleep -s 1
            $service.delete()
            Start-Sleep -s 1

            Remove-Item -Recurse -Force $destination -ErrorAction SilentlyContinue -ErrorVariable $msg
            if ($msg) {
                Logger("WARNING", "$msg. Some files may have to be manually removed.")
            }

            Logger("Info", "$serviceName has been successfully uninstalled. Log files are in $dataDir.")
            exit
        }
        catch {
            Logger("ERROR", "There was a problem uninstalling $serviceName, quitting.")
            exit
        }
    } else {
        Logger("ERROR", "Cannot uninstall $serviceName because it is not installed, quitting.")
        exit
    }
}

# validate redis password is set
if (-not ($redispass)) { 
    Logger("ERROR", "-redispass is not set, quitting.")
    exit
}

# Validate reinstall
if ($serviceInstalled) {
    if ($reinstall) {
        Logger("Info", "$serviceName will be reinstalled.")
    } else {
        Logger("ERROR", "$serviceName is installed but -reinstall is not set, quitting.")
        exit
    }
} else {
    # service is not installed
    if ($reinstall)  {
        Logger("WARNING", "-reinstall is set but $serviceName is not installed, will attempt a fresh install.")
        $reinstall = $false
    }
}

# Validate source directory
$source = $source.TrimEnd('\')
if (-not (Test-Path $source)) {
    $sourceFound = $false
    $loops = 0
    Logger("WARNING", "Source directory ($source) is not available (yet?).  Will keep checking for 5 minutes.")
    while (-not $sourceFound) {
        Start-Sleep -s 15
        if (Test-Path $source) {
            $sourceFound = $true
        } else {
            $loops += 1
            Logger("Info", "...Source directory not yet available.")
            if ($loops -ge 20) {
                # 5 min timeout, cannot continue without source files
                Logger("ERROR", "Timed out (5 min) checking for source directory, quitting.")
                exit
            }
        }
    }
}

# Validate source files (if the config file is present we will assume all the needed files are also present)
if (-not (Test-Path $source\$configFile)) {
    Logger("ERROR", "$configFile is missing from the source, validation failed (check the source path?), quitting.")
    exit
}

# Validate the IP
if (-not ($ip)) { 
    Logger("ERROR", "SOD Collector IP address is not set, quitting.")
    exit
} else {
    if (-not ([ipaddress]::TryParse($ip,[ref][ipaddress]::Loopback))) {
        Logger("ERROR", "Invalid SOD Collector IP address ($ip), quitting.")
        exit
    }
    # see if we have a connection to the collector
    if (-not (Test-NetConnection -RemoteAddress $ip -Port $port -InformationLevel Quiet)) {
        Logger("WARNING", "Cannot open a network connection to $ip on port $port. Check firewalls? Is the collector up?")
    }
    Logger("Info", "$env:computername will send events to this SOD Collector: $ip")
}

# log the script startup params
Logger("Info", "Starting $serviceName deployment.")
if ($32) {Logger("Info", "Using 32-bit executable")} else {Logger("Info", "Using 64-bit executable")}
if ($serviceInstalled) {Logger("Info", "Existing service will be removed and reinstalled")} else {Logger("Info", "New service will be installed")}

# Validate the destination folder
$createDest = $true
if (Test-Path $destination) {
    $createDest = $false
}

# If source and destination are the same, no need to copy files
$copyFiles = $true
if ((Join-Path $source '') -eq (Join-Path $destination '')) {
    $copyFiles = $false
}

########################## Main
# Remove service if already installed
if ($serviceInstalled) {
    Try {
        $service = Get-WmiObject -Class Win32_Service -Filter "Name='$serviceName'"
        $service.StopService()
        Start-Sleep -s 1
        $service.delete()
        if ($console) {write-host }
        $serviceInstalled = $false
        Logger("Info", "Successfully removed existing $serviceName service.")
    }
    Catch {
        Logger("ERROR", "An error occured while removing the existing service, quitting.")
        exit
    }
}

# Create the destination
if ($createDest) {
    try {
        New-Item -Path $destination -ItemType Directory
        Logger("Info", "Service folder ($destination) created successfully.")
    }
    catch {
        Logger("ERROR", "Cannot create $destination, quitting.")
        exit
    }
}

# Copy files to destination
if ($copyFiles) {
    try {
        Copy-Item -Path "$source\*" -Destination $destination -recurse -Force
        Logger("Info", "Files successfully copied to $destination.")
    }
    catch {
        Logger("ERROR", "Problem copying files from $source to $destination, quitting.")
        exit
    }
}

# Create the executable from template
if ($32) {
    # use the 32-bit version
    if (Test-Path "$source\$($exeName)_32.exe") {
        try{
            Copy-Item -Path "$source\$($exeName)_32.exe" -Destination "$destination\$($exeName).exe" -force
            Logger("Info", "Created 32-bit executable.")
        }
        catch {
            Logger("ERROR", "Problem creating $exeName.exe from 32-bit executable, quitting.")
            exit
        }
    } else {
        Logger("ERROR", "Cannot find the source 32-bit executable, quitting.")
        exit
    }
} else {
    # use the 64-bit version
    if (Test-Path "$source\$($exeName)_64.exe") {
        try{
            Copy-Item -Path "$source\$($exeName)_64.exe" -Destination "$destination\$($exeName).exe" -force
            Logger("Info", "Created 64-bit executable.")
        }
        catch {
            Logger("ERROR", "Error creating $exeName.exe from 64-bit executable, quitting.")
            exit
        }
    } else {
        Logger("ERROR", "Cannot find the source 64-bit executable, quitting.")
        exit
    }
}

# Update the config file
if (Test-Path "$destination\$configFile") {
    ((Get-Content -path "$destination\$configFile" -Raw) -replace 'IPADDRESS', $ip) | Set-Content -Path "$destination\$configFile"
    ((Get-Content -path "$destination\$configFile" -Raw) -replace 'REDISPASS', $redispass) | Set-Content -Path "$destination\$configFile"
    Logger("Info", "Created $configFile from template.")
}

# Double-check the service status
if (Get-Service $serviceName -ErrorAction SilentlyContinue) {
    # this should never happen
    Logger("ERROR", "$serviceName service shows as installed, but it should not exist.  Quitting.")
    exit
}

# Install the new service
Try {
    New-Service -name $serviceName `
        -Description "Security On-Demand Windows Log Agent" `
        -displayName $serviceName `
        -binaryPathName "`"$destination\$exeName.exe`" --environment=windows_service -c `"$destination\$configFile`" --path.home `"$destination`" --path.data `"$dataDir`" --path.logs `"$dataDir\logs`" -E logging.files.redirect_stderr=true"
    Start-Sleep -s 1
    if (!(Test-Path "HKLM:\SYSTEM\CurrentControlSet\Services\SOD_Winlog")) {
        Logger("ERROR", "The $serviceName service was not created successfully, cannot continue.")
        exit
    }
    Logger("Info", "Successfully created $serviceName service.")
}
Catch {
    Logger("ERROR", "An error occured while creating $serviceName, cannot continue.")
    exit
}

# Configure the service
Try {
    # set the service to Delayed Autostart
    Logger("Info", "Configuring $serviceName.")
    sc.exe config "SOD_Winlog" start= delayed-auto 
    # validate the service was configured correctly
    if ((Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\SOD_Winlog" -Name "DelayedAutostart").DelayedAutostart -eq "1") {
        Logger("Info", "Service is set to Delayed Autostart.")
    } else {
        Logger("WARNING", "The $serviceName service could not be set to Delayed Autostart.  Please manually set the $serviceName service to Delayed Autostart and restart it.")
    }
}
Catch {
    Logger("WARNING", "An error occured while setting the $serviceName service to Delayed Autostart.  Please manually set the $serviceName service to Delayed Autostart and restart it.")
}

# Start the service
Try {
    Logger("Info", "Starting $serviceName.")
    Start-Service $serviceName -ErrorAction Stop
    Start-Sleep -s 2
    # validate the service started
    if ((Get-Service -Name "SOD_Winlog").Status -ne "Running") {
        Logger("WARNING", "The $serviceName service did not started.  Please start it manually.")
    } else {
        Logger("Info", "The $serviceName service started successfully.")
    }
}
Catch {
    Logger("ERROR", "There was an exception wile starting the $serviceName service: $_")
}

# Stop and disable the Snare service
if (-not ($keepsnare)) {
    if (Get-Service Snare -ErrorAction SilentlyContinue) {
        $state = (Get-Service -Name Snare).Status
        $start = (Get-Service -Name Snare).StartType
        if ($state -eq "Running") {
            try {
                Stop-Service -Name "Snare"
                Start-Sleep -s 2
                if ((Get-Service -Name "Snare").Status -eq "Running") {
                    Logger("WARNING", "Legacy Snare service could not be stopped.  Please stop and disable the service manually.")
                } else {
                    Logger("Info", "Legacy Snare service was stopped successfully.")
                }
            }
            catch {
                Logger("ERROR", "Exception while stopping the Snare service. Manual intervention is required.")
            }
        }
        if ($start -ne "Disabled") {
            try {
                Set-Service -StartupType Disabled "Snare"
                if ((Get-Service -Name "Snare").StartType -ne "Disabled") {
                    Logger("WARNING", "Legacy Snare service could not be disabled.  Please stop and disable the service manually.")
                } else {
                    Logger("Info", "Legacy Snare service is disabled.")
                }
            }
            catch {
                Logger("ERROR", "Exception while disabling the Snare service. Manual intervention is required.")
            }
        }
    }
}
Logger("Info", "Deployment complete.")
