<#

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

PowerShell script to deploy the SOD_Filelog service

Description:
    The SOD_Filelog service is based on open source Filebeat from Elastic.  The config is generic to pull all events
    from DNS, DHCP, or IIS logs from their default locations.  Each log source can be enabled from the command line.
    
    The SOD collectors have a Redis service running to receive the output from SOD_Filelog.  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_Filelog service is successfully installed the legacy Epilog service will be
    stopped and disabled, unless the -keepEpilog switch is set, in which case the Epilog 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_Filelog 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_Filelog 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_Filelog service is already installed and -reinstall is not set, the script will simply exit.
    > If the SOD_Filelog service does not exist on the host, it will be installed.
    > If the SOD_Filelog 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_Filelog folder (typically, C:\ProgramData\SOD_Filelog).

Arguments:
    -source <"path to SOD_Filelog">  If argument is missing, script will look in "%ProgramFiles%\SOD_Filelog".  Use a network share when installing with GPO.
    -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
    -dns                             When present will enable DNS log forwarding.  Default (not present in command line) is disabled.
    -dhcp                            When present will enable DHCP log forwarding.  Default (not present in command line) is disabled.
    -iis                             When present will enable IIS log forwarding.  Default (not present in command line) is disabled.
    -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.
    -keepepilog                      If set, will ignore the existing Epilog service. Otherwise, if the Epilog service is found, it will be stopped and disabled.

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

param (
    [string] $source     = "",
    [string] $redispass  = "",
    [string] $ip         = "",
    [switch] $dns        = $false,
    [switch] $dhcp       = $false,
    [switch] $iis        = $false,
    [switch] $32         = $false,
    [switch] $reinstall  = $false,
    [switch] $console    = $false,
    [switch] $uninstall  = $false,
    [switch] $keepepilog = $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_Filelog.ps1 [-source <folderpath>] [-redispass <password>] [-ip <collectorIP>] [<options>]"
    write-host "`n  Options:"
    write-host "    -source <path to SOD_Filelog>  If argument is missing, script will look in <%ProgramFiles%\SOD_Filelog>."
    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 "    -dns                           When present will enable DNS log forwarding.  Default is disabled."
    write-host "    -dhcp                          When present will enable DHCP log forwarding.  Default is disabled."
    write-host "    -iis                           When present will enable IIS log forwarding.  Default is disabled."
    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 "    -keepepilog                    Will ignore the Epilog service. The default is to stop the service and disable it."
    write-host "`n  Example for installing DNS and DHCP logs, where the source zip file has been extracted to temp folder:"
    write-host "    powershell.exe -ExecutionPolicy UnRestricted -File c:\temp\Deploy_SOD_Filelog.ps1 -source c:\temp\SOD_Filelog -redispass xxxxxxxxxxxx -ip 10.10.0.20 -dns -dhcp -console"
    write-host "`n"
    exit
}

$serviceName        = "SOD_Filelog"
$exeName            = "filebeat"
$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 ($dns) {Logger("Info", "DNS logging is enabled")} else {Logger("Info", "DNS logging is disabled")}
if ($dhcp) {Logger("Info", "DHCP logging is enabled")} else {Logger("Info", "DHCP logging is disabled")}
if ($iis) {Logger("Info", "IIS logging is enabled")} else {Logger("Info", "IIS logging is disabled")}
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"
    ((Get-Content -path "$destination\$configFile" -Raw) -replace 'DNSENABLED',$dns.tostring().tolower()) | Set-Content -Path "$destination\$configFile"
    ((Get-Content -path "$destination\$configFile" -Raw) -replace 'DHCPENABLED',$dhcp.tostring().tolower()) | Set-Content -Path "$destination\$configFile"
    ((Get-Content -path "$destination\$configFile" -Raw) -replace 'IISENABLED',$iis.tostring().tolower()) | Set-Content -Path "$destination\$configFile"
    Logger("Info", "Created $configFile from template.")
} else {
    Logger("ERROR", "Cannot find $destination\$configFile, quitting.")
    exit
}

# 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 File 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"
    if (!(Test-Path "HKLM:\SYSTEM\CurrentControlSet\Services\SOD_Filelog")) {
        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_Filelog" start= delayed-auto 
    # validate the service was configured correctly
    if ((Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\SOD_Filelog" -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_Filelog").Status -ne "Running") {
        Logger("WARNING", "The $serviceName service did not start.  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 Epilog service
if (-not ($keepepilog)) {
    if (Get-Service Epilog -ErrorAction SilentlyContinue) {
        $state = (Get-Service -Name Epilog).Status
        $start = (Get-Service -Name Epilog).StartType
        if ($state -eq "Running") {
            try {
                Stop-Service -Name "Epilog"
                Start-Sleep -s 1
                if ((Get-Service -Name "Epilog").Status -eq "Running") {
                    Logger("WARNING", "Legacy Epilog service could not be stopped.  Please stop and disable the service manually.")
                } else {
                    Logger("Info", "Legacy Epilog service was stopped successfully.")
                }
            }
            catch {
                Logger("ERROR", "Exception while stopping the Epilog service. Manual intervention is required.")
            }
        }
        if ($start -ne "Disabled") {
            try {
                Set-Service -StartupType Disabled "Epilog"
                if ((Get-Service -Name "Epilog").StartType -ne "Disabled") {
                    Logger("WARNING", "Legacy Epilog service could not be disabled.  Please stop and disable the service manually.")
                } else {
                    Logger("Info", "Legacy Epilog service is disabled.")
                }
            }
            catch {
                Logger("ERROR", "Exception while disabling the Epilog service. Manual intervention is required.")
            }
        }
    }
}
Logger("Info", "Deployment complete.")
