diff --git a/.gitignore b/.gitignore index 8c93afb..1f328f9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,14 @@ log/* agent/* config/* cache/* + .vscode/ *.log +# JEA +RoleCapabilities/IcingaForWindows.psrc +IcingaForWindows.pssc +IcingaForWindowsTest.pssc + !cache/README.md -!cache/framework_cache.psm1 \ No newline at end of file +!cache/framework_cache.psm1 diff --git a/RoleCapabilities/.gitkeep b/RoleCapabilities/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/cache/framework_cache.psm1 b/cache/framework_cache.psm1 index dfd6284..30f8f78 100644 --- a/cache/framework_cache.psm1 +++ b/cache/framework_cache.psm1 @@ -13,3 +13,6 @@ $Global:Icinga = @{ }; Write-IcingaFrameworkCodeCache; + +Import-Module icinga-powershell-framework -Global -Force; +Import-Module icinga-powershell-framework -Force; diff --git a/doc/08-JEA-Profiles.md b/doc/08-JEA-Profiles.md new file mode 100644 index 0000000..32144b1 --- /dev/null +++ b/doc/08-JEA-Profiles.md @@ -0,0 +1,48 @@ +# JEA Profiles + +Starting with Icinga for Windows v1.6.0, we are supporting JEA profiles and provide all required tools to build a profile based on installed Icinga for Windows components. + +JEA stands for "Just Enough Administration" and you can read more about it on the [Microsoft Docs](https://docs.microsoft.com/de-de/powershell/scripting/learn/remoting/jea/overview). + +In short, JEA allows you to limit the access to certain Cmdlets, Functions and Binaries on the system. In addition, you can grant additional privileges to users to perform tasks, which are permitted to Administrators only in general. + +With JEA profiles, you can for example grant permission to certain users or group to restart a specific service, after starting a PowerShell with a specific JEA profile. You can limit the access only to this command to be executed in this elevated environment, while all other commands or services are still not manageable. + +## Requirements + +In order to use JEA profiles, you will require the following system requirements: + +* PowerShell 5.0 or later +* WinRM service configured + +## Why use JEA for Icinga for Windows + +Using JEA profiles will increase security in a certain way, while also ensuring that you no longer have to manage certain permissions for the monitoring user account. Instead of granting permissions to certain services, WMI objects or anything related, each command is executed within the `System` context. By defining profiles, you can ensure that fetching of these information is possible, but not modifying the system itself. + +For monitoring for example, certain `Scheduled Tasks` or even `Services` are not accessible by some users. To fetch the `vmms` service for Hyper-V for example, you need either to execute the checks in the context of `Hyper-V Administrators` or `LocalSystem`. Both are then unrestricted on how they can interact with Hyper-V, causing a possible security gap. + +## What can Icinga for Windows JEA and what can't it do + +Icinga for Windows provides `Cmdlets`, to automatically build a JEA profile based on your installed Icinga for Windows components. Each single used `Cmldet` is being analyzed and checked for commands being executed, to ensure plugins have access to all required tools to properly execute them and return the plugin information. + +### No hundred percent security + +By default, Icinga for Windows JEA profiles are created with the PowerShell language mode `FullLanguage`. This in general allows the execution of `ScriptBlocks` and other non-blocked Cmdlets, while `ConstrainedLanguage` is more restrictive on which commands can be executed by default, prohibiting `ScriptBlocks` and modifying `global variables` later on. + +If Icinga for Windows is used with the Icinga for Windows service, the `ConstrainedLanguage` flag will cause the the service to not work, as the service relies within the started PowerShell session to modify `global variables`, which is impossible in this mode. During development, we started to get rid of `ScriptBlocks` and user other methods for creating the internal threads. + +### No ScriptBlocks allowed + +Starting a JEA session with `FullLanguage`, will ensure that you can only execute commands you are permitted for. Any other command is not available and will be blocked. However, this changes once you create a `ScriptBlock`, because these will execute commands even when you should not be permitted to execute them. To mitigate this problem, Icinga for Windows will not add any command or module which ships with `ScriptBlocks` inside. + +### Increase Icinga for Windows security + +For better security, it is highly recommended to install the `Icinga PowerShell Framework` inside a context, that requires administrative privileges for making changes. By default, this would for example be `C:\Program Files\WindowsPowerShell\Modules\`. + +The JEA profile generator will lookup the root folder, in which the `Icinga PowerShell Framework` is installed into and only lookup Icinga for Windows components installed there. Any other Icinga for Windows module installed on the system is not included. + +This will ensure that you will require administrative privileges beforehand to modify these files, to later execute them inside the JEA context. + +## Getting Started + +To get started with the Icinga for Windows JEA profile, have a look on the [installation guide](jea/01-Installation.md). diff --git a/doc/developerguide/10-Custom-Daemons.md b/doc/developerguide/10-Custom-Daemons.md index 05883f3..4d08944 100644 --- a/doc/developerguide/10-Custom-Daemons.md +++ b/doc/developerguide/10-Custom-Daemons.md @@ -433,7 +433,7 @@ Register-IcingaBackgroundDaemon -Command 'Start-IcingaAgentServiceTest'; Once registered, you will have to restart the PowerShell service itselfs to apply the changes ```powershell -Restart-IcingaService 'icingapowershell'; +Restart-IcingaWindowsService; ``` Thats it! Now the daemon is loaded with every start, checking for the Agent state and restart it if it is not running. diff --git a/doc/developerguide/12-Custom-API-Endpoints.md b/doc/developerguide/12-Custom-API-Endpoints.md index d0f71bb..3d3574d 100644 --- a/doc/developerguide/12-Custom-API-Endpoints.md +++ b/doc/developerguide/12-Custom-API-Endpoints.md @@ -229,7 +229,7 @@ If our module is providing different endpoints, you will have to create multiple As everything is now ready, we can restart our Icinga PowerShell Framework service by using ```powershell -Restart-IcingaService 'icingapowershell'; +Restart-IcingaWindowsService; ``` and access our API endpoint by browsing to our API location (in our example we assume you use `5668` as default port): diff --git a/doc/experimental/01-Forward-checks-to-internal-API.md b/doc/experimental/01-Forward-checks-to-internal-API.md index 2b61437..7936b5e 100644 --- a/doc/experimental/01-Forward-checks-to-internal-API.md +++ b/doc/experimental/01-Forward-checks-to-internal-API.md @@ -54,7 +54,7 @@ To modify any REST-Api arguments, please follow the [REST-Api installation guide Last but not least restart the Icinga for Windows service: ```powershell -Restart-Service icingapowershell; +Restart-IcingaWindowsService; ``` ## Whitelist Check Commands @@ -112,9 +112,9 @@ Install-IcingaFrameworkComponent -Name restapi -Release; Install-IcingaFrameworkComponent -Name apichecks -Release; Register-IcingaBackgroundDaemon -Command 'Start-IcingaWindowsRESTApi'; -Restart-Service icingapowershell; - Add-IcingaRESTApiCommand -Command 'Invoke-IcingaCheck*' -Endpoint 'apichecks'; +Restart-IcingaWindowsService; + Enable-IcingaFrameworkApiChecks; ``` diff --git a/doc/jea/01-Installation.md b/doc/jea/01-Installation.md new file mode 100644 index 0000000..9cf01ec --- /dev/null +++ b/doc/jea/01-Installation.md @@ -0,0 +1,98 @@ +# Install JEA + +## Preparations + +Before we can use JEA profiles, we require to prepare our Windows machine for this. JEA profiles require a configured and running `WinRM` service, allowing PowerShell remove executions. + +The simple and easiest way, is to enable it with `Enable-PSRemoting`. + +**NOTE:** Please check your local security profiles and configurations before applying these changes. This installation step is not focussing on how to secure `WinRM` in your environment and just gives an example on how you can get started. You are responsible for yourself to properly secure `WinRM`, depending on your environment. + +Once `WinRM` is enabled and properly configured on your system, you can move on with installing the Icinga for Windows JEA profile + +## Install Icinga for Windows JEA profile + +We provide two ways on how Icinga for Windows is configured and JEA profiles are build. The easiest and most straight forward solution, is creating an own user which is managed by `Icinga for Windows` on the local system. The other option is to manually assign a user and create the profile this one. + +### JEA with Icinga for Windows managed User + +To fully automate the entire process and to ensure Icinga for Windows is executed with a dedicated user we run our JEA profile with, we can simply use the command `Install-IcingaSecurity`. + +This command will + +* install a user called `icinga` on the system +* create a JEA profile for this user + +You can modify the name of the user with the `-IcingaUser` argument, to create a managed user with a different name. + +```powershell +Install-IcingaSecurity -IcingaUser 'MyOwnIcingaUser'; +``` + +The user created by this command is not added to any user group and is only permitted to be used as service user. Local logins or RDP sessions are not forbidden. + +The user is created with a random, 60 digits password to ensure security. Each time the service is being modified with the user, the password is randomly re-created to ensure a valid login of the service user. The password is not stored anywhere on the Icinga for Windows context, besides the PowerShell session which is executed. However, once all actions the password is required for are completed, the variable is flushed from the memory. + +If present, both services `icinga2` and `icingapowershell` are updated to use the newly created user and being restarted afterwards. + +Once completed, Icinga for Windows will compile the JEA profile with the name `IcingaForWindows`. + +### JEA with non-managed user + +If you already use a monitoring user and create a user automatically, you can simply use `Install-IcingaJEAProfile`, by providing the user the profile is created for. The default user is set to `IcingaForWindows`, but can be overwritten. + +```powershell +Install-IcingaJEAProfile -IcingaUser 'MyOwnIcingaUser'; +``` + +This will create the JEA profile files and register them, but not modify any services or user data. + +## Additional Management + +There are additional arguments available for `Install-IcingaJEAProfile`, which can be used to change the behaviour a little. + +| Argument | Type | Description | +| --- | --- | --- | +| IcingaUser | String | The name of the user the JEA profile is created for | +| ConstrainedLanguage | Switch | Will create the JEA profile with language mode `ConstrainedLanguage` instead of `FullLanguage`, for increased security. Please note that the `Icinga for Windows service` will not work with this configuration | +| TestEnv | Switch | By enabling this flag, a second JEA test profile is created for the current using running the PowerShell for testing purpose. The profile is called `IcingaForWindowsTest` | + +## Creating Test Environment with existing Profile + +If you already created a profile with `Install-IcingaJEAProfile`, you can simply register a test environment for the current user, not requiring a full-rebuild of the JEA profile. + +```powershell +Register-IcingaJEAProfile -TestEnv +``` + +`Register-IcingaJEAProfile` supports the same arguments as listed above for `Install-IcingaJEAProfile`. + +## Update JEA Profiles + +To update your JEA profiles after you updated components or made modifications for yourself, you can rebuild the profile by using `Install-IcingaJEAProfile` with any of the above mentioned arguments or use the alias `Update-IcingaJEAProfile`, which does the same and is just named differently. + +```powershell +Update-IcingaJEAProfile -IcingaUser 'MyOwnIcingaUser'; +``` + +## Use JEA profile + +### Use test environment JEA + +If you used `TestEnv` to create a test environment for JEA for the current user, you can simply enter the PowerShell JEA session with this command: + +```powershell +powershell.exe -ConfigurationName 'IcingaForWindowsTest'; +``` + +This will open a new `remote` PowerShell session over `WinRM` on the local machine with the provided JEA profile 'IcingaForWindowsTest'. + +### Apply JEA to Icinga configuration + +Each plugin bundle shipped by the Icinga Team has new configuration `baskets` for the Icinga Director and `conf` files for Icinga 2 compiled with a new argument `-JEAProfile`. + +To make sure the Icinga Agent will execute plugins with the Icinga for Windows JEA context, you will have to add this to your CheckCommand or Service templates. + +The profile we create is called `IcingaForWindows` and can simply added to the CheckCommand definition for global rollout. + +**Note:** If you add this configuration in Icinga globally, each single node will fail it's checks if the JEA profile is not installed there. diff --git a/doc/jea/02-Uninstallation.md b/doc/jea/02-Uninstallation.md new file mode 100644 index 0000000..240dedd --- /dev/null +++ b/doc/jea/02-Uninstallation.md @@ -0,0 +1,31 @@ +# Uninstall JEA + +If you want to uninstall JEA profiles or even the managed user included, there are `Cmdlets` available for this. + +## Uninstall JEA with managed user + +To uninstall JEA profiles and the managed user, you can use `Uninstall-IcingaSecurity`. Like the installation counterpart, you can specify a custom user with `-IcingaUser`. + +However, users will only be removed if their description matches the Icinga for Windows managed user description. + +```powershell +Uninstall-IcingaSecurity -IcingaUser 'MyOwnIcingaUser'; +``` + +By default, it will remove the `icinga` user including unregistering the JEA profile. + +## Uninstall JEA Profile + +To simply uninstall the JEA profile and leave a possible managed user on the system, you can run `Uninstall-IcingaJEAProfile`. + +This will remove the created JEA profile and the JEA catalog for `IcingaForWindows` on the system. + +## Uninstall JEA test profile + +To simply remove the test environment of the JEA profile, you can use this command: + +```powershell +Unregister-PSSessionConfiguration -Name 'IcingaForWindowsTest'; +``` + +This will leave the catalog and the production system itself alone and only removes the test profile. diff --git a/doc/knowledgebase/IWKB000005.md b/doc/knowledgebase/IWKB000005.md index 94bf479..2bea3e3 100644 --- a/doc/knowledgebase/IWKB000005.md +++ b/doc/knowledgebase/IWKB000005.md @@ -83,7 +83,7 @@ Now open a new PowerShell session again and check of the new directory was added Once the directory is there, restart the `icingapowershell` service by running ```powershell -Restart-Service 'icingapowershell' +Restart-IcingaWindowsService; ``` Now the error should be resolved the the service should be running. @@ -119,7 +119,7 @@ Now open a new PowerShell session again and check of the new directory was added Once the directory is there, restart the `icingapowershell` service by running ```powershell -Restart-Service 'icingapowershell' +Restart-IcingaWindowsService; ``` Now the error should be resolved the the service should be running. diff --git a/doc/service/02-Register-Daemons.md b/doc/service/02-Register-Daemons.md index 8936bc3..68f0e86 100644 --- a/doc/service/02-Register-Daemons.md +++ b/doc/service/02-Register-Daemons.md @@ -17,7 +17,7 @@ The `Start-IcingaServiceCheckDaemon` is a directly integrated PowerShell functio Once you made changes, please remember to restart the PowerShell Service ```powershell -Restart-IcingaService 'icingapowershell'; +Restart-IcingaWindowsService; ``` List Enabled Daemons @@ -49,7 +49,7 @@ Unregister-IcingaBackgroundDaemon -BackgroundDaemon 'Start-IcingaServiceCheckDae Once you restart the PowerShell service the pending changes are applied ```powershell -Restart-IcingaService 'icingapowershell'; +Restart-IcingaWindowsService; ``` Write Custom Daemons diff --git a/doc/service/10-Register-Service-Checks.md b/doc/service/10-Register-Service-Checks.md index 69bba95..1e2f10d 100644 --- a/doc/service/10-Register-Service-Checks.md +++ b/doc/service/10-Register-Service-Checks.md @@ -17,7 +17,7 @@ Register-IcingaServiceCheck -CheckCommand 'Invoke-IcingaCheckCPU' -Interval 30 - Once you registered a service check, you will have to restart the PowerShell service ```powershell -Restart-IcingaService 'icingapowershell'; +Restart-IcingaWindowsService; ``` As collected metrics are written to disk as well, a restart will not flush previous data but load it again. A quick service restart will not cause missing data points. @@ -66,7 +66,7 @@ Register-IcingaServiceCheck -CheckCommand 'Invoke-IcingaCheckCPU' -Interval 60 - Once you modified a service check, you will have to restart the PowerShell service ```powershell -Restart-IcingaService 'icingapowershell'; +Restart-IcingaWindowsService; ``` Unregister Service Checks @@ -81,5 +81,5 @@ Unregister-IcingaServiceCheck -ServiceId 527521986464102122481142022477689145963 Once you removed a service check, you will have to restart the PowerShell service ```powershell -Restart-IcingaService 'icingapowershell'; +Restart-IcingaWindowsService; ``` diff --git a/icinga-powershell-framework.psd1 b/icinga-powershell-framework.psd1 index fb2696b..c4ed218 100644 --- a/icinga-powershell-framework.psd1 +++ b/icinga-powershell-framework.psd1 @@ -10,7 +10,7 @@ NestedModules = @( '.\cache\framework_cache.psm1' ) FunctionsToExport = @( '*' ) CmdletsToExport = @( '*' ) - VariablesToExport = '*' + VariablesToExport = @( '*' ) AliasesToExport = @( '*' ) PrivateData = @{ PSData = @{ diff --git a/icinga-powershell-framework.psm1 b/icinga-powershell-framework.psm1 index 7cef110..9e864ec 100644 --- a/icinga-powershell-framework.psm1 +++ b/icinga-powershell-framework.psm1 @@ -35,8 +35,6 @@ function Use-Icinga() if ($global:Icinga.ContainsKey('Minimal') -eq $FALSE) { $global:Icinga.Add('Minimal', $TRUE); } - - return; } # Ensure we autoload the Icinga Plugin collection, provided by the external @@ -46,10 +44,10 @@ function Use-Icinga() } if ($LibOnly -eq $FALSE) { - $global:IcingaThreads = [hashtable]::Synchronized(@{}); - $global:IcingaThreadContent = [hashtable]::Synchronized(@{}); - $global:IcingaThreadPool = [hashtable]::Synchronized(@{}); - $global:IcingaTimers = [hashtable]::Synchronized(@{}); + $global:IcingaThreads = [hashtable]::Synchronized(@{ }); + $global:IcingaThreadContent = [hashtable]::Synchronized(@{ }); + $global:IcingaThreadPool = [hashtable]::Synchronized(@{ }); + $global:IcingaTimers = [hashtable]::Synchronized(@{ }); $global:IcingaDaemonData = [hashtable]::Synchronized( @{ 'IcingaThreads' = $global:IcingaThreads; @@ -57,6 +55,7 @@ function Use-Icinga() 'IcingaThreadPool' = $global:IcingaThreadPool; 'IcingaTimers' = $global:IcingaTimers; 'FrameworkRunningAsDaemon' = $Daemon; + 'JEAContext' = $FALSE; 'DebugMode' = $DebugMode; } ); @@ -64,11 +63,14 @@ function Use-Icinga() # This will fix the debug mode in case we are only using Libs # without any other variable content and daemon handling if ($null -eq $global:IcingaDaemonData) { - $global:IcingaDaemonData = [hashtable]::Synchronized(@{}); + $global:IcingaDaemonData = [hashtable]::Synchronized(@{ }); } if ($global:IcingaDaemonData.ContainsKey('DebugMode') -eq $FALSE) { $global:IcingaDaemonData.DebugMode = $DebugMode; } + if ($global:IcingaDaemonData.ContainsKey('JEAContext') -eq $FALSE) { + $global:IcingaDaemonData.JEAContext = $FALSE; + } if ($global:IcingaDaemonData.ContainsKey('FrameworkRunningAsDaemon') -eq $FALSE) { $global:IcingaDaemonData.FrameworkRunningAsDaemon = $Daemon; } @@ -121,6 +123,8 @@ function Write-IcingaFrameworkCodeCache() $CacheContent += "Export-ModuleMember -Function @( '*' ) -Alias @( '*' ) -Variable @( '*' )"; Set-Content -Path $CacheFile -Value $CacheContent; + + Remove-IcingaFrameworkDependencyFile; } function Publish-IcingaEventlogDocumentation() diff --git a/lib/config/Test-IcingaPowerShellConfigItem.psm1 b/lib/config/Test-IcingaPowerShellConfigItem.psm1 index a301747..00aff21 100644 --- a/lib/config/Test-IcingaPowerShellConfigItem.psm1 +++ b/lib/config/Test-IcingaPowerShellConfigItem.psm1 @@ -21,10 +21,10 @@ function Test-IcingaPowerShellConfigItem() { - param( + param ( $ConfigObject, $ConfigKey ); - return ([bool]($ConfigObject.PSobject.Properties.Name -eq $ConfigKey) -eq $TRUE); + return ([bool]($ConfigObject.PSObject.Properties.Name -eq $ConfigKey) -eq $TRUE); } diff --git a/lib/core/docs/Publish-IcingaEventlogDocumentation.psm1 b/lib/core/docs/Publish-IcingaEventlogDocumentation.psm1 new file mode 100644 index 0000000..37285a0 --- /dev/null +++ b/lib/core/docs/Publish-IcingaEventlogDocumentation.psm1 @@ -0,0 +1,37 @@ +function Publish-IcingaEventlogDocumentation() +{ + param( + [string]$Namespace, + [string]$OutFile + ); + + [string]$DocContent = [string]::Format( + '# {0} Eventlog Documentation', + $Namespace + ); + $DocContent += New-IcingaNewLine; + $DocContent += New-IcingaNewLine; + $DocContent += "Below you will find a list of EventId's which are exported by this module. The short and detailed message are both written directly into the eventlog. This documentation shall simply provide a summary of available EventId's"; + + $SortedArray = $IcingaEventLogEnums[$Namespace].Keys.GetEnumerator() | Sort-Object; + + foreach ($entry in $SortedArray) { + $entry = $IcingaEventLogEnums[$Namespace][$entry]; + + $DocContent = [string]::Format( + '{0}{2}{2}## Event Id {1}{2}{2}| Category | Short Message | Detailed Message |{2}| --- | --- | --- |{2}| {3} | {4} | {5} |', + $DocContent, + $entry.EventId, + (New-IcingaNewLine), + $entry.EntryType, + $entry.Message, + $entry.Details + ); + } + + if ([string]::IsNullOrEmpty($OutFile)) { + Write-Output $DocContent; + } else { + Write-IcingaFileSecure -File $OutFile -Value $DocContent; + } +} diff --git a/lib/core/framework/Install-IcingaForWindowsService.psm1 b/lib/core/framework/Install-IcingaForWindowsService.psm1 index e3bd401..8bad904 100644 --- a/lib/core/framework/Install-IcingaForWindowsService.psm1 +++ b/lib/core/framework/Install-IcingaForWindowsService.psm1 @@ -47,11 +47,13 @@ function Install-IcingaForWindowsService() if ($ServiceStatus -eq 'Running') { Write-IcingaConsoleNotice 'Stopping Icinga PowerShell service'; - Stop-IcingaService 'icingapowershell'; + Stop-IcingaWindowsService; Start-Sleep -Seconds 1; } - Remove-ItemSecure -Path $Path -Force | Out-Null; + if (Test-Path $Path) { + Remove-ItemSecure -Path $Path -Force | Out-Null; + } Copy-ItemSecure -Path $UpdateFile -Destination $Path -Force | Out-Null; Remove-ItemSecure -Path $UpdateFile -Force | Out-Null; } @@ -73,7 +75,11 @@ function Install-IcingaForWindowsService() throw ([string]::Format('Failed to install Icinga PowerShell Service: {0}{1}', $ServiceCreation.Message, $ServiceCreation.Error)); } } else { - Write-IcingaConsoleWarning 'The Icinga PowerShell Service is already installed'; + $ServiceUpdate = Start-IcingaProcess -Executable 'sc.exe' -Arguments ([string]::Format('config icingapowershell binPath= "{0}"', $Path)); + + if ($ServiceUpdate.ExitCode -ne 0) { + throw ([string]::Format('Failed to update config for Icinga PowerShell Service: {0}{1}', $ServiceUpdate.Message, $ServiceUpdate.Error)); + } } # This is just a hotfix to ensure we setup the service properly before assigning it to @@ -81,9 +87,9 @@ function Install-IcingaForWindowsService() # will not start without this workaround. # Todo: Figure out the reason and fix it properly Set-IcingaAgentServiceUser -User 'LocalSystem' -Service 'icingapowershell' | Out-Null; - Restart-IcingaService 'icingapowershell'; + Restart-IcingaWindowsService; Start-Sleep -Seconds 1; - Stop-IcingaService 'icingapowershell'; + Stop-IcingaWindowsService; if ($ServiceStatus -eq 'Running') { Write-IcingaConsoleNotice 'Starting Icinga PowerShell service'; diff --git a/lib/core/framework/Install-IcingaFrameworkComponent.psm1 b/lib/core/framework/Install-IcingaFrameworkComponent.psm1 index 1056586..7978725 100644 --- a/lib/core/framework/Install-IcingaFrameworkComponent.psm1 +++ b/lib/core/framework/Install-IcingaFrameworkComponent.psm1 @@ -110,6 +110,11 @@ function Install-IcingaFrameworkComponent() # include the plugins Use-Icinga; + if ([string]::IsNullOrEmpty((Get-IcingaJEAContext)) -eq $FALSE) { + Write-IcingaConsoleNotice 'Updating Icinga JEA profile'; + Invoke-IcingaCommand { Install-IcingaJEAProfile }; + } + # Unload the module if it was loaded before Remove-Module $PluginDirectory -Force -ErrorAction SilentlyContinue; # Now import the module diff --git a/lib/core/framework/Install-IcingaFrameworkUpdate.psm1 b/lib/core/framework/Install-IcingaFrameworkUpdate.psm1 index 7d54aaf..04b18e5 100644 --- a/lib/core/framework/Install-IcingaFrameworkUpdate.psm1 +++ b/lib/core/framework/Install-IcingaFrameworkUpdate.psm1 @@ -57,7 +57,7 @@ function Install-IcingaFrameworkUpdate() if ($ServiceStatus -eq 'Running') { Write-IcingaConsoleNotice 'Stopping Icinga PowerShell service'; - Stop-IcingaService 'icingapowershell'; + Stop-IcingaWindowsService; Start-Sleep -Seconds 1; } if ($AgentStatus -eq 'Running') { @@ -78,12 +78,11 @@ function Install-IcingaFrameworkUpdate() Write-IcingaConsoleNotice 'Removing files from framework'; foreach ($ModuleFile in $Files) { - Remove-ItemSecure -Path $ModuleFile -Force | Out-Null; + Remove-ItemSecure -Path $ModuleFile.FullName -Force | Out-Null; } Remove-ItemSecure -Path (Join-Path $ModuleDirectory -ChildPath 'doc') -Recurse -Force | Out-Null; Remove-ItemSecure -Path (Join-Path $ModuleDirectory -ChildPath 'lib') -Recurse -Force | Out-Null; - Remove-ItemSecure -Path (Join-Path $ModuleDirectory -ChildPath 'manifests') -Recurse -Force | Out-Null; Write-IcingaConsoleNotice 'Copying new files to framework'; Copy-ItemSecure -Path (Join-Path $ModuleContent -ChildPath 'doc') -Destination $ModuleDirectory -Recurse -Force | Out-Null; @@ -102,6 +101,12 @@ function Install-IcingaFrameworkUpdate() Write-IcingaFrameworkCodeCache; } + if ([string]::IsNullOrEmpty((Get-IcingaJEAContext)) -eq $FALSE) { + Remove-IcingaFrameworkDependencyFile; + Write-IcingaConsoleNotice 'Updating Icinga JEA profile'; + Invoke-IcingaCommand { Install-IcingaJEAProfile }; + } + Write-IcingaConsoleNotice 'Framework update has been completed. Please start a new PowerShell instance now to complete the update'; Test-IcingaForWindowsService -ResolveProblems | Out-Null; diff --git a/lib/core/framework/Test-IcingaZipBinaryChecksum.psm1 b/lib/core/framework/Test-IcingaZipBinaryChecksum.psm1 index a0f401c..fd53513 100644 --- a/lib/core/framework/Test-IcingaZipBinaryChecksum.psm1 +++ b/lib/core/framework/Test-IcingaZipBinaryChecksum.psm1 @@ -36,7 +36,7 @@ function Test-IcingaZipBinaryChecksum() [string]$MD5Checksum = Get-Content $MD5Path; $MD5Checksum = ($MD5Checksum.Split(' ')[0]).ToLower(); - $FileHash = ((Get-FileHash $Path -Algorithm MD5).Hash).ToLower(); + $FileHash = ((Get-IcingaFileHash $Path -Algorithm MD5).Hash).ToLower(); if ($MD5Checksum -ne $FileHash) { return $FALSE; diff --git a/lib/core/framework/Uninstall-IcingaForWindows.psm1 b/lib/core/framework/Uninstall-IcingaForWindows.psm1 index 3e59e37..d0eb49b 100644 --- a/lib/core/framework/Uninstall-IcingaForWindows.psm1 +++ b/lib/core/framework/Uninstall-IcingaForWindows.psm1 @@ -11,6 +11,9 @@ Uninstalls every PowerShell module within the icinga-powershell-* namespace including the Icinga Agent with all components (like certificates) as well as the Icinga for Windows service and the Icinga PowerShell Framework. +.PARAMETER IcingaUser + In case the Icinga Security profile was installed with a defined user any other than + "icinga", you require to specify the user to remove it entirely .PARAMETER Force Suppress the question if you are sure to uninstall everything .INPUTS @@ -24,6 +27,7 @@ function Uninstall-IcingaForWindows() { param ( + $IcingaUser = 'icinga', [switch]$Force = $FALSE ); @@ -45,6 +49,8 @@ function Uninstall-IcingaForWindows() } Write-IcingaConsoleNotice 'Uninstalling Icinga for Windows from this host'; + Write-IcingaConsoleNotice 'Uninstalling Icinga Security configuration if applied'; + Uninstall-IcingaSecurity -IcingaUser $IcingaUser; Write-IcingaConsoleNotice 'Uninstalling Icinga Agent'; Uninstall-IcingaAgent -RemoveDataFolder | Out-Null; Write-IcingaConsoleNotice 'Uninstalling Icinga for Windows service'; diff --git a/lib/core/framework/Uninstall-IcingaForWindowsService.psm1 b/lib/core/framework/Uninstall-IcingaForWindowsService.psm1 index d1a9412..0c9cea5 100644 --- a/lib/core/framework/Uninstall-IcingaForWindowsService.psm1 +++ b/lib/core/framework/Uninstall-IcingaForWindowsService.psm1 @@ -24,7 +24,7 @@ function Uninstall-IcingaForWindowsService() $ServiceData = Get-IcingaForWindowsServiceData; - Stop-IcingaService 'icingapowershell'; + Stop-IcingaWindowsService; Start-Sleep -Seconds 1; $ServiceCreation = Start-IcingaProcess -Executable 'sc.exe' -Arguments 'delete icingapowershell'; diff --git a/lib/core/icingaagent/getters/Get-IcingaAgentHostCertificate.psm1 b/lib/core/icingaagent/getters/Get-IcingaAgentHostCertificate.psm1 index 027461d..81463cf 100644 --- a/lib/core/icingaagent/getters/Get-IcingaAgentHostCertificate.psm1 +++ b/lib/core/icingaagent/getters/Get-IcingaAgentHostCertificate.psm1 @@ -1,5 +1,13 @@ function Get-IcingaAgentHostCertificate() { + if (-Not (Test-Path -Path (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\var\lib\icinga2\certs\'))) { + return @{ + 'CertFile' = ''; + 'Subject' = ''; + 'Thumbprint' = ''; + }; + } + # Default for Icinga 2.8.0 and above [string]$CertDirectory = (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\var\lib\icinga2\certs\*'); $FolderContent = Get-ChildItem -Path $CertDirectory -Filter '*.crt' -Exclude 'ca.crt'; diff --git a/lib/core/icingaagent/getters/Get-IcingaServiceUser.psm1 b/lib/core/icingaagent/getters/Get-IcingaServiceUser.psm1 index 0cfa064..1a1d144 100644 --- a/lib/core/icingaagent/getters/Get-IcingaServiceUser.psm1 +++ b/lib/core/icingaagent/getters/Get-IcingaServiceUser.psm1 @@ -2,7 +2,10 @@ function Get-IcingaServiceUser() { $Services = Get-IcingaServices -Service 'icinga2'; if ($null -eq $Services) { - throw 'Icinga Service not installed'; + $Services = Get-IcingaServices -Service 'icingapowershell'; + if ($null -eq $Services) { + return $null; + } } $Services = $Services.GetEnumerator() | Select-Object -First 1; diff --git a/lib/core/icingaagent/misc/Start-IcingaAgentInstallWizard.psm1 b/lib/core/icingaagent/misc/Start-IcingaAgentInstallWizard.psm1 index da7ce4c..0e169c8 100644 --- a/lib/core/icingaagent/misc/Start-IcingaAgentInstallWizard.psm1 +++ b/lib/core/icingaagent/misc/Start-IcingaAgentInstallWizard.psm1 @@ -749,7 +749,7 @@ function Start-IcingaAgentInstallWizard() } Test-IcingaAgent; if ($InstallFrameworkService) { - Restart-IcingaService 'icingapowershell'; + Restart-IcingaWindowsService; } Restart-IcingaService 'icinga2'; } diff --git a/lib/core/icingaagent/setters/Set-IcingaAcl.psm1 b/lib/core/icingaagent/setters/Set-IcingaAcl.psm1 index 1a23f98..dfcaa6a 100644 --- a/lib/core/icingaagent/setters/Set-IcingaAcl.psm1 +++ b/lib/core/icingaagent/setters/Set-IcingaAcl.psm1 @@ -1,24 +1,38 @@ function Set-IcingaAcl() { param( - [string]$Directory + [string]$Directory, + [string]$IcingaUser = (Get-IcingaServiceUser), + [switch]$Remove = $FALSE ); if (-Not (Test-Path $Directory)) { - throw 'Failed to set Acl for directory. Directory does not exist'; + Write-IcingaConsoleWarning 'Unable to set ACL for directory "{0}". Directory does not exist' -Objects $Directory; return; } $DirectoryAcl = (Get-Item -Path $Directory).GetAccessControl('Access'); $DirectoryAccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule( - (Get-IcingaServiceUser), + $IcingaUser, 'Modify', 'ContainerInherit,ObjectInherit', 'None', 'Allow' ); - $DirectoryAcl.SetAccessRule($DirectoryAccessRule); + if ($Remove -eq $FALSE) { + $DirectoryAcl.SetAccessRule($DirectoryAccessRule); + } else { + foreach ($entry in $DirectoryAcl.Access) { + if (([string]($entry.IdentityReference)).ToLower() -like [string]::Format('*\{0}', $IcingaUser.ToLower())) { + $DirectoryAcl.RemoveAccessRuleSpecific($entry); + } + } + } + Set-Acl -Path $Directory -AclObject $DirectoryAcl; - Test-IcingaAcl -Directory $Directory -WriteOutput | Out-Null; + + if ($Remove -eq $FALSE) { + Test-IcingaAcl -Directory $Directory -WriteOutput | Out-Null; + } } diff --git a/lib/core/icingaagent/setters/Set-IcingaAgentServiceUser.psm1 b/lib/core/icingaagent/setters/Set-IcingaAgentServiceUser.psm1 index f8feae7..d104e4b 100644 --- a/lib/core/icingaagent/setters/Set-IcingaAgentServiceUser.psm1 +++ b/lib/core/icingaagent/setters/Set-IcingaAgentServiceUser.psm1 @@ -1,4 +1,4 @@ -function Set-IcingaAgentServiceUser() +function Set-IcingaServiceUser() { param ( [string]$User, @@ -12,6 +12,10 @@ function Set-IcingaAgentServiceUser() return $FALSE; } + if ($null -eq (Get-Service $Service -ErrorAction SilentlyContinue)) { + return; + } + if ($User.Contains('\') -eq $FALSE) { $User = [string]::Format('.\{0}', $User); } @@ -29,13 +33,16 @@ function Set-IcingaAgentServiceUser() if ($Output.ExitCode -eq 0) { if ($SetPermission) { + Set-IcingaAgentServicePermission | Out-Null; Set-IcingaUserPermissions; } - Write-IcingaConsoleNotice 'Service User successfully updated' + Write-IcingaConsoleNotice 'Service User "{0}" for service "{1}" successfully updated' -Objects $User, $Service; return $TRUE; } else { Write-IcingaConsoleError ([string]::Format('Failed to update the service user: {0}', $Output.Message)); return $FALSE; } } + +Set-Alias -Name 'Set-IcingaAgentServiceUser' -Value 'Set-IcingaServiceUser'; diff --git a/lib/core/icingaagent/setters/Set-IcingaUserPermissions.psm1 b/lib/core/icingaagent/setters/Set-IcingaUserPermissions.psm1 index e1123d0..87bb09c 100644 --- a/lib/core/icingaagent/setters/Set-IcingaUserPermissions.psm1 +++ b/lib/core/icingaagent/setters/Set-IcingaUserPermissions.psm1 @@ -1,7 +1,12 @@ function Set-IcingaUserPermissions() { - Set-IcingaAgentServicePermission | Out-Null; - Set-IcingaAcl "$Env:ProgramData\icinga2\etc"; - Set-IcingaAcl "$Env:ProgramData\icinga2\var"; - Set-IcingaAcl (Get-IcingaCacheDir); + param ( + [string]$IcingaUser = (Get-IcingaServiceUser), + [switch]$Remove = $FALSE + ); + + Set-IcingaAcl "$Env:ProgramData\icinga2\etc" -IcingaUser $IcingaUser -Remove:$Remove; + Set-IcingaAcl "$Env:ProgramData\icinga2\var" -IcingaUser $IcingaUser -Remove:$Remove; + Set-IcingaAcl (Get-IcingaCacheDir) -IcingaUser $IcingaUser -Remove:$Remove; + Set-IcingaAcl -Directory (Get-IcingaPowerShellConfigDir) -IcingaUser $IcingaUser -Remove:$Remove; } diff --git a/lib/core/icingaagent/tests/Test-IcingaAcl.psm1 b/lib/core/icingaagent/tests/Test-IcingaAcl.psm1 index 97f2d85..cc3ff46 100644 --- a/lib/core/icingaagent/tests/Test-IcingaAcl.psm1 +++ b/lib/core/icingaagent/tests/Test-IcingaAcl.psm1 @@ -2,15 +2,16 @@ function Test-IcingaAcl() { param( [string]$Directory, - [switch]$WriteOutput + [switch]$WriteOutput, + [string]$ServiceUser = (Get-IcingaServiceUser) ); if ([string]::IsNullOrEmpty($Directory) -Or -Not (Test-Path $Directory)) { - throw 'The specified directory was not found'; + Write-IcingaConsoleWarning 'The specified directory "{0}" was not found' -Objects $Directory; + return $FALSE; } $FolderACL = Get-Acl $Directory; - $ServiceUser = Get-IcingaServiceUser; $UserFound = $FALSE; $HasAccess = $FALSE; $ServiceUserSID = Get-IcingaUserSID $ServiceUser; diff --git a/lib/core/icingaagent/tests/Test-IcingaAgent.psm1 b/lib/core/icingaagent/tests/Test-IcingaAgent.psm1 index 1c6cbaf..9afb12c 100644 --- a/lib/core/icingaagent/tests/Test-IcingaAgent.psm1 +++ b/lib/core/icingaagent/tests/Test-IcingaAgent.psm1 @@ -1,18 +1,30 @@ function Test-IcingaAgent() { - if (Get-Service 'icinga2' -ErrorAction SilentlyContinue) { + $IcingaAgentData = Get-IcingaAgentInstallation; + $AgentServicePresent = Get-Service 'icinga2' -ErrorAction SilentlyContinue; + if ($IcingaAgentData.Installed -And $null -ne $AgentServicePresent) { Write-IcingaTestOutput -Severity 'Passed' -Message 'Icinga Agent service is installed'; - Test-IcingaAgentServicePermission | Out-Null; - Test-IcingaAcl "$Env:ProgramData\icinga2\etc" -WriteOutput | Out-Null; - Test-IcingaAcl "$Env:ProgramData\icinga2\var" -WriteOutput | Out-Null; - Test-IcingaAcl (Get-IcingaCacheDir) -WriteOutput | Out-Null; + } elseif ($IcingaAgentData.Installed -And $null -eq $AgentServicePresent) { + Write-IcingaTestOutput -Severity 'Failed' -Message 'Icinga Agent service is not installed'; + } elseif ($IcingaAgentData.Installed -eq $FALSE -And $null -ne $AgentServicePresent) { + Write-IcingaTestOutput -Severity 'Failed' -Message 'Icinga Agent service is still present, while Icinga Agent itself is not installed.'; + } elseif ($IcingaAgentData.Installed -eq $FALSE -And $null -eq $AgentServicePresent) { + Write-IcingaTestOutput -Severity 'Passed' -Message 'Icinga Agent is not installed and service is not present.'; + } + + Test-IcingaAgentServicePermission | Out-Null; + Test-IcingaAcl "$Env:ProgramData\icinga2\etc" -WriteOutput | Out-Null; + Test-IcingaAcl "$Env:ProgramData\icinga2\var" -WriteOutput | Out-Null; + Test-IcingaAcl (Get-IcingaCacheDir) -WriteOutput | Out-Null; + Test-IcingaAcl (Get-IcingaPowerShellConfigDir) -WriteOutput | Out-Null; + Test-IcingaAcl -Directory (Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'certificate') -WriteOutput | Out-Null;; + + if ($IcingaAgentData.Installed) { Test-IcingaAgentConfig | Out-Null; if (Test-IcingaAgentFeatureEnabled -Feature 'debuglog') { Write-IcingaTestOutput -Severity 'Warning' -Message 'The debug log of the Icinga Agent is enabled. Please keep in mind to disable it once testing is done, as a huge amount of data is generated' } else { Write-IcingaTestOutput -Severity 'Passed' -Message 'Icinga Agent debug log is disabled' } - } else { - Write-IcingaTestOutput -Severity 'Failed' -Message 'Icinga Agent service is not installed'; } } diff --git a/lib/core/installer/Install-Icinga.psm1 b/lib/core/installer/Install-Icinga.psm1 index 6de0164..203f34c 100644 --- a/lib/core/installer/Install-Icinga.psm1 +++ b/lib/core/installer/Install-Icinga.psm1 @@ -21,6 +21,7 @@ function Install-Icinga() 'DirectorRegisteredHost' = $FALSE; 'LastParent' = [System.Collections.ArrayList]@(); 'LastValues' = @(); + 'DisabledEntries' = @{ }; 'Config' = @{ }; 'ConfigSwap' = @{ }; 'ParentConfig' = $null; diff --git a/lib/core/installer/Start-IcingaForWindowsInstallation.psm1 b/lib/core/installer/Start-IcingaForWindowsInstallation.psm1 index 184da90..b3ac243 100644 --- a/lib/core/installer/Start-IcingaForWindowsInstallation.psm1 +++ b/lib/core/installer/Start-IcingaForWindowsInstallation.psm1 @@ -61,7 +61,7 @@ function Start-IcingaForWindowsInstallation() $PluginPackageSnapshot = $FALSE; if ([string]::IsNullOrEmpty($IcingaStableRepo) -eq $FALSE) { - Add-IcingaRepository -Name 'Icinga Stable' -RemotePath $IcingaStableRepo; + Add-IcingaRepository -Name 'Icinga Stable' -RemotePath $IcingaStableRepo -Force; } foreach ($endpoint in $IcingaEndpoints) { @@ -222,7 +222,7 @@ function Start-IcingaForWindowsInstallation() } if ($InstallService) { - Restart-IcingaService 'icingapowershell'; + Restart-IcingaWindowsService; } switch ($InstallJEAProfile) { diff --git a/lib/core/installer/menu/installation/director/RegisterHost.psm1 b/lib/core/installer/menu/installation/director/RegisterHost.psm1 index 91f7996..81dbccd 100644 --- a/lib/core/installer/menu/installation/director/RegisterHost.psm1 +++ b/lib/core/installer/menu/installation/director/RegisterHost.psm1 @@ -8,8 +8,6 @@ function Show-IcingaForWindowsManagementConsoleInstallationDirectorRegisterHost( [switch]$Advanced = $FALSE ); - $Advanced = $TRUE; - Show-IcingaForWindowsInstallerMenu ` -Header 'Do you want to register the host right now inside the Icinga Director? This will show missing configurations.' ` -Entries @( @@ -21,7 +19,7 @@ function Show-IcingaForWindowsManagementConsoleInstallationDirectorRegisterHost( @{ 'Caption' = 'Register host inside Icinga Director'; 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; - 'Help' = 'You can select this option to register the host within the Icinga Director right now, unlocking more advanced configurations for this host like "Parent Zone", "Parent Nodes" and "Parent Node Addresses"'; + 'Help' = 'You can select this option to register the host within the Icinga Director right now, unlocking more advanced configurations for this host like "Parent Zone", "Parent Nodes" and "Parent Node Addresses". Please note that the installation process will fail if you continue the installer, if you do not register it.'; 'Action' = @{ 'Command' = 'Resolve-IcingaForWindowsManagementConsoleInstallationDirectorTemplate'; 'Arguments' = @{ diff --git a/lib/core/installer/menu/installation/framework/IcingaRepository.psm1 b/lib/core/installer/menu/installation/framework/IcingaRepository.psm1 index 13b54e0..d708a95 100644 --- a/lib/core/installer/menu/installation/framework/IcingaRepository.psm1 +++ b/lib/core/installer/menu/installation/framework/IcingaRepository.psm1 @@ -1,7 +1,7 @@ function Show-IcingaForWindowsInstallationMenuStableRepository() { param ( - [array]$Value = @( 'https://packages.icinga.com/IcingaForWindows/stable' ), + [array]$Value = @( 'https://packages.icinga.com/IcingaForWindows/stable/ifw.repo.json' ), [string]$DefaultInput = 'c', [switch]$JumpToSummary = $FALSE, [switch]$Automated = $FALSE, @@ -13,7 +13,7 @@ function Show-IcingaForWindowsInstallationMenuStableRepository() -Entries @( @{ 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; - 'Help' = 'This is the stable repository from where all packages of Icinga for Windows are downloaded and installed from. Defaults to "https://packages.icinga.com/IcingaForWindows/stable"'; + 'Help' = 'This is the stable repository from where all packages of Icinga for Windows are downloaded and installed from. Defaults to "https://packages.icinga.com/IcingaForWindows/stable/ifw.repo.json"'; } ) ` -DefaultIndex $DefaultInput ` diff --git a/lib/core/installer/menu/installation/framework/InstallJEA.psm1 b/lib/core/installer/menu/installation/framework/InstallJEA.psm1 index 3713575..e1f803f 100644 --- a/lib/core/installer/menu/installation/framework/InstallJEA.psm1 +++ b/lib/core/installer/menu/installation/framework/InstallJEA.psm1 @@ -8,8 +8,8 @@ function Show-IcingaForWindowsInstallerMenuSelectInstallJEAProfile() [switch]$Advanced = $FALSE ); - if ($PSVersionTable.PSVersion -lt '5.0.0.0') { - return; + if ($PSVersionTable.PSVersion -lt (New-IcingaVersionObject -Version 5, 0)) { + Add-IcingaForWindowsInstallerDisabledEntry -Name 'IfW-InstallJEAProfile' -Reason ([string]::Format('PowerShell version "{0}" is lower than 5.0', $PSVersionTable.PSVersion.ToString(2))); } Show-IcingaForWindowsInstallerMenu ` @@ -21,9 +21,9 @@ function Show-IcingaForWindowsInstallerMenuSelectInstallJEAProfile() 'Help' = 'Installs the Icinga for Windows JEA profile for the specified service user'; }, @{ - 'Caption' = 'Install JEA Profile with managed user "IcingaForWindows"'; + 'Caption' = 'Install JEA Profile with managed user "icinga"'; 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; - 'Help' = 'Installs the Icinga for Windows JEA profile with a newly created, managed user "IcingaForWindows". This will override your service and service password configuration'; + 'Help' = 'Installs the Icinga for Windows JEA profile with a newly created, managed user "icinga". This will override your service and service password configuration'; }, @{ 'Caption' = 'Do not install JEA Profile'; diff --git a/lib/core/installer/menu/installation/general/ConfigurationSummary.psm1 b/lib/core/installer/menu/installation/general/ConfigurationSummary.psm1 index 9d45525..c9ed667 100644 --- a/lib/core/installer/menu/installation/general/ConfigurationSummary.psm1 +++ b/lib/core/installer/menu/installation/general/ConfigurationSummary.psm1 @@ -78,11 +78,21 @@ function Show-IcingaForWindowsInstallerConfigurationSummary() $Caption = ([string]::Format('{0}=> {1}', $PrintName, $EntryValue)); } + [bool]$EntryDisabled = $FALSE; + [string]$EntryDisabledReason = Get-IcingaForWindowsInstallerDisabledEntry -Name $RealCommand; + + if ([string]::IsNullOrEmpty($EntryDisabledReason) -eq $FALSE) { + $EntryDisabled = $TRUE; + $Caption = ([string]::Format('{0}=> Disabled: {1}', $PrintName, $EntryDisabledReason)); + } + $Entries += @{ - 'Caption' = $Caption; - 'Command' = $entry; - 'Arguments' = @{ '-JumpToSummary' = $TRUE }; - 'Help' = '' + 'Caption' = $Caption; + 'Command' = $entry; + 'Arguments' = @{ '-JumpToSummary' = $TRUE }; + 'Help' = ''; + 'Disabled' = $EntryDisabled; + 'DisabledReason' = $EntryDisabledReason } $global:Icinga.InstallWizard.HeaderPreview = ''; @@ -91,8 +101,6 @@ function Show-IcingaForWindowsInstallerConfigurationSummary() $CurrentIndex += 1; } - Write-Host 'Finished' - Disable-IcingaForWindowsInstallationHeaderPrint; Enable-IcingaForWindowsInstallationJumpToSummary; diff --git a/lib/core/installer/menu/manage/framework/ManageFramework.psm1 b/lib/core/installer/menu/manage/framework/ManageFramework.psm1 index 525d0c5..0b1d258 100644 --- a/lib/core/installer/menu/manage/framework/ManageFramework.psm1 +++ b/lib/core/installer/menu/manage/framework/ManageFramework.psm1 @@ -1,9 +1,14 @@ function Show-IcingaForWindowsManagementConsoleManageFramework() { - $FrameworkDebug = Get-IcingaFrameworkDebugMode; - $IcingaService = Get-Service 'icingapowershell' -ErrorAction SilentlyContinue; - $AdminShell = $global:Icinga.InstallWizard.AdminShell; - $ServiceStatus = $null; + $FrameworkDebug = Get-IcingaFrameworkDebugMode; + $IcingaService = Get-Service 'icingapowershell' -ErrorAction SilentlyContinue; + $AdminShell = $global:Icinga.InstallWizard.AdminShell; + $ServiceStatus = $null; + $JEADisabled = $FALSE; + + if ($PSVersionTable.PSVersion -lt (New-IcingaVersionObject -Version 5, 0) -Or $AdminShell -eq $FALSE) { + $JEADisabled = $TRUE; + } if ($null -ne $IcingaService) { $ServiceStatus = $IcingaService.Status; @@ -13,24 +18,31 @@ function Show-IcingaForWindowsManagementConsoleManageFramework() -Header 'Manage Icinga for Windows:' ` -Entries @( @{ - 'Caption' = 'Manage background daemons'; - 'Command' = 'Show-IcingaForWindowsManagementConsoleManageBackgroundDaemons'; - 'Help' = 'Allows you to manage Icinga for Windows background daemons'; - 'Disabled' = ($null -eq (Get-Service 'icingapowershell' -ErrorAction SilentlyContinue)); + 'Caption' = 'Manage background daemons'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageBackgroundDaemons'; + 'Help' = 'Allows you to manage Icinga for Windows background daemons'; + 'Disabled' = ($null -eq (Get-Service 'icingapowershell' -ErrorAction SilentlyContinue)); + 'DisabledReason' = 'Icinga for Windows service is not installed'; }, @{ 'Caption' = 'Manage Icinga Repositories'; 'Command' = 'Show-IcingaForWindowsManagementConsoleManageIcingaRepositories'; 'Help' = 'Allows you to manage Icinga for Windows repositories'; }, + @{ + 'Caption' = 'Manage JEA profile'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageJEA'; + 'Help' = 'Allows you to manage Icinga for Windows JEA profile'; + 'Disabled' = $JEADisabled; + 'DisabledReason' = ([string]::Format('PowerShell version "{0}" is lower than 5.0 or you are not inside an administrative shell', $PSVersionTable.PSVersion.ToString(2))); + }, @{ 'Caption' = ([string]::Format('Framework Debug Mode: {0}', (& { if ($FrameworkDebug) { 'Enabled' } else { 'Disabled' } } ))); 'Command' = 'Show-IcingaForWindowsManagementConsoleManageFramework'; 'Help' = 'Disable or enable the Icinga PowerShell Framework debug mode'; 'Disabled' = $FALSE; 'Action' = @{ - 'Command' = 'Invoke-IcingaForWindowsMangementConsoleToogleFrameworkDebug'; - 'Arguments' = @{ }; + 'Command' = 'Invoke-IcingaForWindowsMangementConsoleToogleFrameworkDebug'; } }, @{ @@ -38,8 +50,7 @@ function Show-IcingaForWindowsManagementConsoleManageFramework() 'Command' = 'Show-IcingaForWindowsManagementConsoleManageFramework'; 'Help' = 'Updates the Icinga PowerShell Framework Code Cache'; 'Action' = @{ - 'Command' = 'Write-IcingaFrameworkCodeCache'; - 'Arguments' = @{ }; + 'Command' = 'Write-IcingaFrameworkCodeCache'; } }, @{ @@ -48,8 +59,7 @@ function Show-IcingaForWindowsManagementConsoleManageFramework() 'Help' = 'Enables the Icinga untrusted certificate validation, allowing you to communicate with web servers which ships with a self-signed certificate not installed on this system. This applies only to this PowerShell session and is not permanent. Might be helpful in case you want to connect to the Icinga Director and the SSL is not trusted by this host'; 'Disabled' = $FALSE 'Action' = @{ - 'Command' = 'Enable-IcingaUntrustedCertificateValidation'; - 'Arguments' = @{ }; + 'Command' = 'Enable-IcingaUntrustedCertificateValidation'; } }, @{ @@ -59,31 +69,34 @@ function Show-IcingaForWindowsManagementConsoleManageFramework() 'Disabled' = $FALSE }, @{ - 'Caption' = 'Start Icinga for Windows Service'; - 'Command' = 'Show-IcingaForWindowsManagementConsoleManageFramework'; - 'Help' = 'Allows you to start the Icinga for Windows Service if the service is not running'; - 'Disabled' = ($null -eq $IcingaService -Or $ServiceStatus -eq 'Running' -Or (-Not $AdminShell)); - 'Action' = @{ + 'Caption' = 'Start Icinga for Windows Service'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageFramework'; + 'Help' = 'Allows you to start the Icinga for Windows Service if the service is not running'; + 'Disabled' = ($null -eq $IcingaService -Or $ServiceStatus -eq 'Running' -Or (-Not $AdminShell)); + 'DisabledReason' = 'The service is either not installed, already running or you are not inside an administrative shell'; + 'Action' = @{ 'Command' = 'Start-Service'; 'Arguments' = @{ '-Name' = 'icingapowershell'; }; } }, @{ - 'Caption' = 'Stop Icinga for Windows Service'; - 'Command' = 'Show-IcingaForWindowsManagementConsoleManageFramework'; - 'Help' = 'Allows you to stop the Icinga for Windows Service if the service is not running'; - 'Disabled' = ($null -eq $IcingaService -Or $ServiceStatus -ne 'Running' -Or (-Not $AdminShell)); - 'Action' = @{ + 'Caption' = 'Stop Icinga for Windows Service'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageFramework'; + 'Help' = 'Allows you to stop the Icinga for Windows Service if the service is not running'; + 'Disabled' = ($null -eq $IcingaService -Or $ServiceStatus -ne 'Running' -Or (-Not $AdminShell)); + 'DisabledReason' = 'The service is either not installed, already stopped or you are not inside an administrative shell'; + 'Action' = @{ 'Command' = 'Stop-Service'; 'Arguments' = @{ '-Name' = 'icingapowershell'; }; } }, @{ - 'Caption' = 'Restart Icinga for Windows Service'; - 'Command' = 'Show-IcingaForWindowsManagementConsoleManageFramework'; - 'Help' = 'Allows you to restart the Icinga for Windows Service if the service is installed'; - 'Disabled' = ($null -eq $IcingaService -Or (-Not $AdminShell)); - 'Action' = @{ + 'Caption' = 'Restart Icinga for Windows Service'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageFramework'; + 'Help' = 'Allows you to restart the Icinga for Windows Service if the service is installed'; + 'Disabled' = ($null -eq $IcingaService -Or (-Not $AdminShell)); + 'DisabledReason' = 'The service is either not installed or you are not inside an administrative shell'; + 'Action' = @{ 'Command' = 'Restart-Service'; 'Arguments' = @{ '-Name' = 'icingapowershell'; }; } diff --git a/lib/core/installer/menu/manage/framework/jea/ManageIcingaJEA.psm1 b/lib/core/installer/menu/manage/framework/jea/ManageIcingaJEA.psm1 new file mode 100644 index 0000000..de0b3c9 --- /dev/null +++ b/lib/core/installer/menu/manage/framework/jea/ManageIcingaJEA.psm1 @@ -0,0 +1,100 @@ +function Show-IcingaForWindowsManagementConsoleManageJEA() +{ + Show-IcingaForWindowsInstallerMenu ` + -Header 'Manage Icinga for Windows JEA configuration:' ` + -Entries @( + @{ + 'Caption' = 'Install JEA profile with managed user "icinga"'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageJEA'; + 'Help' = 'Will create a managed user called "icinga", updates the services "icinga2" and "icingapowershell" with the new user and creates a JEA profile based on installed modules'; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Install JEA profile with managed user "icinga"'; + '-Command' = 'Install-IcingaSecurity'; + } + } + }, + @{ + 'Caption' = 'Install/Update JEA profile'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageJEA'; + 'Help' = 'Installs or updates the JEA profile for Icinga for Windows for the current user assigned to the "icinga2" service'; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Install/Update JEA profile'; + '-Command' = 'Install-IcingaJEAProfile'; + } + } + }, + @{ + 'Caption' = 'Install JEA profile with "ConstrainedLanguage" and managed user "icinga"'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageJEA'; + 'Help' = 'Will create a managed user called "icinga", updates the services "icinga2" and "icingapowershell" with the new user and creates a JEA profile with "ConstrainedLanguage" based on installed modules. This is NOT recommended in case you are using the Icinga for Windows service, as it will not work in "ConstrainedLanguage" mode'; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Install JEA profile with "ConstrainedLanguage" and managed user "icinga"'; + '-Command' = 'Install-IcingaSecurity'; + '-CmdArguments' = @{ + '-ConstrainedLanguage' = $TRUE; + } + } + } + }, + @{ + 'Caption' = 'Install/Update JEA profile with "ConstrainedLanguage"'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageJEA'; + 'Help' = 'Installs or updates the JEA profile for Icinga for Windows for the current user assigned to the "icinga2" service with "ConstrainedLanguage". This is NOT recommended in case you are using the Icinga for Windows service, as it will not work in "ConstrainedLanguage" mode'; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Install/Update JEA profile with "ConstrainedLanguage"'; + '-Command' = 'Install-IcingaJEAProfile'; + '-CmdArguments' = @{ + '-ConstrainedLanguage' = $TRUE; + } + } + } + }, + @{ + 'Caption' = 'Uninstall JEA profile and managed user "icinga"'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageJEA'; + 'Help' = 'This will uninstall the JEA profile and remove the managed user "icinga" from the system. For the services the default user "NT Authority\NetworkService" will be applied'; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Uninstall JEA profile and managed user "icinga"'; + '-Command' = 'Uninstall-IcingaSecurity'; + } + } + }, + @{ + 'Caption' = 'Uninstall JEA profile'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageJEA'; + 'Help' = 'Uninstalls the JEA profile from this system'; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Uninstall JEA profile'; + '-Command' = 'Uninstall-IcingaJEAProfile'; + } + } + }, + @{ + 'Caption' = 'Rebuild Icinga PowerShell Framework dependency cache'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageJEA'; + 'Help' = 'Rebuilds the Icinga for Windows Framework dependency cache for JEA profile generation'; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Rebuild Icinga PowerShell Framework dependency cache'; + '-Command' = 'Get-IcingaJEAConfiguration'; + '-CmdArguments' = @{ + '-RebuildFramework' = $TRUE; + } + } + } + } + ); +} diff --git a/lib/core/installer/tools/AddDisabledEntry.psm1 b/lib/core/installer/tools/AddDisabledEntry.psm1 new file mode 100644 index 0000000..2e50b40 --- /dev/null +++ b/lib/core/installer/tools/AddDisabledEntry.psm1 @@ -0,0 +1,18 @@ +function Add-IcingaForWindowsInstallerDisabledEntry() +{ + param ( + [string]$Name = '', + [string]$Reason = '' + ); + + if ([string]::IsNullOrEmpty($Reason)) { + $Reason = 'Generic disable message'; + } + + if ($Global:Icinga.InstallWizard.DisabledEntries.ContainsKey($Name)) { + $Global:Icinga.InstallWizard.DisabledEntries[$Name] = $Reason; + return; + } + + $Global:Icinga.InstallWizard.DisabledEntries.Add($Name, $Reason); +} diff --git a/lib/core/installer/tools/GetDisabledEntry.psm1 b/lib/core/installer/tools/GetDisabledEntry.psm1 new file mode 100644 index 0000000..6d1c5af --- /dev/null +++ b/lib/core/installer/tools/GetDisabledEntry.psm1 @@ -0,0 +1,12 @@ +function Get-IcingaForWindowsInstallerDisabledEntry() +{ + param ( + [string]$Name = '' + ); + + if ($Global:Icinga.InstallWizard.DisabledEntries.ContainsKey($Name)) { + return ($Global:Icinga.InstallWizard.DisabledEntries[$Name]); + } + + return ''; +} diff --git a/lib/core/installer/tools/ShowInstallerMenu.psm1 b/lib/core/installer/tools/ShowInstallerMenu.psm1 index a03c4d2..28187ed 100644 --- a/lib/core/installer/tools/ShowInstallerMenu.psm1 +++ b/lib/core/installer/tools/ShowInstallerMenu.psm1 @@ -376,11 +376,12 @@ function Show-IcingaForWindowsInstallerMenu() }; } - $DisabledMenu = $FALSE; - $NextMenu = $null; - $NextArguments = @{ }; - $ActionCmd = $null; - $ActionArgs = $null; + [bool]$DisabledMenu = $FALSE; + [string]$DisabledReason = ''; + $NextMenu = $null; + $NextArguments = @{ }; + $ActionCmd = $null; + $ActionArgs = $null; if ([string]::IsNullOrEmpty($Result) -eq $FALSE) { if ($Result -eq 'c') { @@ -390,6 +391,9 @@ function Show-IcingaForWindowsInstallerMenu() $NextMenu = $Entries[0].Command; if ($null -ne $Entries[0].Disabled) { $DisabledMenu = $Entries[0].Disabled; + if ($null -ne $Entries[0].DisabledReason) { + $DisabledReason = $Entries[0].DisabledReason; + } } } $ActionCmd = $Entries[0].Action.Command; @@ -398,6 +402,9 @@ function Show-IcingaForWindowsInstallerMenu() $NextMenu = $Entries[$Result].Command; if ($null -ne $Entries[$Result].Disabled) { $DisabledMenu = $Entries[$Result].Disabled; + if ($null -ne $Entries[0].DisabledReason) { + $DisabledReason = $Entries[0].DisabledReason; + } } if ($Entries[$Result].ContainsKey('Arguments')) { $NextArguments = $Entries[$Result].Arguments; @@ -408,7 +415,10 @@ function Show-IcingaForWindowsInstallerMenu() } if ($DisabledMenu) { - $global:Icinga.InstallWizard.LastNotice = [string]::Format('This menu is not enabled: {0}', $Result); + if ([string]::IsNullOrEmpty($DisabledReason) -eq $FALSE) { + $DisabledReason = [string]::Format(' => Reason: {0}', $DisabledReason); + } + $global:Icinga.InstallWizard.LastNotice = [string]::Format('This menu is not enabled: {0}{1}', $Result, $DisabledReason); return; } diff --git a/lib/core/installer/tools/Write-IcingaManagementConsoleCommand.psm1 b/lib/core/installer/tools/Write-IcingaManagementConsoleCommand.psm1 index d26ed91..ba4f121 100644 --- a/lib/core/installer/tools/Write-IcingaManagementConsoleCommand.psm1 +++ b/lib/core/installer/tools/Write-IcingaManagementConsoleCommand.psm1 @@ -12,7 +12,7 @@ function Write-IcingaManagementConsoleCommand() if ($Entry.Action -And ($Entry.Action.ContainsKey('Command') -Or ($Entry.Action.ContainsKey('Arguments') -And $Entry.Action.Arguments.ContainsKey('-Command')))) { $PrintArguments = ''; $PrintCommand = '' - if ($Entry.Action.Arguments.ContainsKey('-CmdArguments')) { + if ($null -ne $Entry.Action.Arguments -And $Entry.Action.Arguments.ContainsKey('-CmdArguments')) { $PrintCommand = $Entry.Action.Arguments['-Command']; foreach ($cmdArg in $Entry.Action.Arguments['-CmdArguments'].Keys) { $PrintValue = $Entry.Action.Arguments['-CmdArguments'][$cmdArg]; @@ -34,23 +34,25 @@ function Write-IcingaManagementConsoleCommand() } } else { $PrintCommand = $Entry.Action.Command; - foreach ($cmdArg in $Entry.Action.Arguments.Keys) { - $PrintValue = $Entry.Action.Arguments[$cmdArg]; - [string]$StringArg = ([string]$cmdArg).Replace('-', ''); - if ($PrintValue.GetType().Name -eq 'Boolean') { - if ((Get-Command $PrintCommand).Parameters.$StringArg.ParameterType.Name -eq 'SwitchParameter') { - $PrintValue = ''; - } else { - if ($PrintValue) { - $PrintValue = '$TRUE'; + if ($null -ne $Entry.Action.Arguments) { + foreach ($cmdArg in $Entry.Action.Arguments.Keys) { + $PrintValue = $Entry.Action.Arguments[$cmdArg]; + [string]$StringArg = ([string]$cmdArg).Replace('-', ''); + if ($PrintValue.GetType().Name -eq 'Boolean') { + if ((Get-Command $PrintCommand).Parameters.$StringArg.ParameterType.Name -eq 'SwitchParameter') { + $PrintValue = ''; } else { - $PrintValue = '$FALSE'; + if ($PrintValue) { + $PrintValue = '$TRUE'; + } else { + $PrintValue = '$FALSE'; + } } + } elseif ($PrintValue.GetType().Name -eq 'String' -And $PrintValue.Contains(' ')) { + $PrintValue = (ConvertFrom-IcingaArrayToString -Array $PrintValue -AddQuotes); } - } elseif ($PrintValue.GetType().Name -eq 'String' -And $PrintValue.Contains(' ')) { - $PrintValue = (ConvertFrom-IcingaArrayToString -Array $PrintValue -AddQuotes); + $PrintArguments += ([string]::Format('{0} {1} ', $cmdArg, $PrintValue)); } - $PrintArguments += ([string]::Format('{0} {1} ', $cmdArg, $PrintValue)); } } @@ -60,7 +62,11 @@ function Write-IcingaManagementConsoleCommand() $PrintArguments = $PrintArguments.SubString(0, $PrintArguments.Length - 1); } - Write-IcingaConsolePlain ([string]::Format('PS> {0} {1};', $PrintCommand, $PrintArguments)) -ForeColor Magenta; + if ([string]::IsNullOrEmpty($PrintArguments) -eq $FALSE) { + $PrintArguments = [string]::Format(' {0}', $PrintArguments); + } + + Write-IcingaConsolePlain ([string]::Format('PS> {0}{1};', $PrintCommand, $PrintArguments)) -ForeColor Magenta; Write-IcingaConsolePlain ''; } } diff --git a/lib/core/jea/Get-IcingaCommandDependency.psm1 b/lib/core/jea/Get-IcingaCommandDependency.psm1 new file mode 100644 index 0000000..3883505 --- /dev/null +++ b/lib/core/jea/Get-IcingaCommandDependency.psm1 @@ -0,0 +1,51 @@ +function Get-IcingaCommandDependency() +{ + param ( + $DependencyList = (New-Object PSCustomObject), + [hashtable]$CompiledList = @{ }, + [string]$CmdName = '', + [string]$CmdType = '' + ); + + if ([string]::IsNullOrEmpty($CmdType)) { + return $CompiledList; + } + + if ($CompiledList.ContainsKey($CmdType) -eq $FALSE) { + $CompiledList.Add($CmdType, @{ }); + } + + if ($CompiledList[$CmdType].ContainsKey($CmdName)) { + $CompiledList[$CmdType][$CmdName] += 1; + return $CompiledList; + } + + $CompiledList[$CmdType].Add($CmdName, 0); + + if ((Test-PSCustomObjectMember -PSObject $DependencyList -Name $CmdName) -eq $FALSE) { + return $CompiledList; + } + + foreach ($CmdList in $DependencyList.$CmdName.PSObject.Properties.Name) { + $Cmd = $DependencyList.$CmdName.$CmdList; + + if ($CompiledList.ContainsKey($CmdList) -eq $FALSE) { + $CompiledList.Add($CmdList, @{ }); + } + + foreach ($entry in $Cmd.PSObject.Properties.Name) { + if ($CompiledList[$CmdList].ContainsKey($entry) -eq $FALSE) { + $CompiledList[$CmdList].Add($entry, 0); + + $CompiledList = Get-IcingaCommandDependency ` + -DependencyList $DependencyList ` + -CompiledList $CompiledList ` + -CmdName $entry; + } else { + $CompiledList[$CmdList][$entry] += 1; + } + } + } + + return $CompiledList; +} diff --git a/lib/core/jea/Get-IcingaFrameworkDependency.psm1 b/lib/core/jea/Get-IcingaFrameworkDependency.psm1 new file mode 100644 index 0000000..5eb3abe --- /dev/null +++ b/lib/core/jea/Get-IcingaFrameworkDependency.psm1 @@ -0,0 +1,44 @@ +function Get-IcingaFrameworkDependency() +{ + param ( + [string]$Command = $null, + $DependencyList = (New-Object PSCustomObject) + ); + + if (Test-PSCustomObjectMember -PSObject $DependencyList -Name $Command) { + return $DependencyList; + } + + $DependencyList | Add-Member -MemberType NoteProperty -Name ($Command) -Value (New-Object PSCustomObject); + + $CommandConfig = (Get-Command $Command); + $ModuleContent = $CommandConfig.ScriptBlock.ToString(); + $DeserializedFile = Read-IcingaPowerShellModuleFile -FileContent $ModuleContent; + [array]$CheckCmd = $DeserializedFile.CommandList + $DeserializedFile.FunctionList; + + foreach ($cmd in $CheckCmd) { + if ($cmd -eq $Command) { + continue; + } + + $CommandConfig = Get-Command $cmd -ErrorAction SilentlyContinue; + + if ($null -eq $CommandConfig) { + continue; + } + + [string]$CommandType = ([string]$CommandConfig.CommandType).Replace(' ', ''); + + if ((Test-PSCustomObjectMember -PSObject ($DependencyList.$Command) -Name $CommandType) -eq $FALSE) { + $DependencyList.$Command | Add-Member -MemberType NoteProperty -Name ($CommandType) -Value (New-Object PSCustomObject); + } + + if ((Test-PSCustomObjectMember -PSObject ($DependencyList.$Command.$CommandType) -Name $cmd) -eq $FALSE) { + $DependencyList.$Command.$CommandType | Add-Member -MemberType NoteProperty -Name ($cmd) -Value 0; + } + + $DependencyList.$Command.$CommandType.($cmd) += 1; + } + + return $DependencyList; +} diff --git a/lib/core/jea/Get-IcingaJEAConfiguration.psm1 b/lib/core/jea/Get-IcingaJEAConfiguration.psm1 new file mode 100644 index 0000000..1175769 --- /dev/null +++ b/lib/core/jea/Get-IcingaJEAConfiguration.psm1 @@ -0,0 +1,169 @@ +function Get-IcingaJEAConfiguration() +{ + param ( + [switch]$RebuildFramework = $FALSE, + [switch]$AllowScriptBlocks = $FALSE + ); + + # Prepare all variables and content we require for building the profile + $CommandList = Get-Command; + $PowerShellModules = Get-ChildItem -Path (Get-IcingaForWindowsRootPath) -Filter 'icinga-powershell-*'; + [array]$BlockedModules = @(); + $DependencyList = New-Object PSCustomObject; + [hashtable]$UsedCmdlets = @{ + 'Alias' = @{ }; + 'Cmdlet' = @{ }; + 'Function' = @{ }; + 'Modules' = ([System.Collections.ArrayList]@()); + }; + $ModuleContent = ''; + [bool]$DependencyCache = $FALSE; + + if ((Test-Path (Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'framework_dependencies.json')) -And $RebuildFramework -eq $FALSE) { + $DependencyList = ConvertFrom-Json -InputObject (Read-IcingaFileSecure -File (Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'framework_dependencies.json')); + $DependencyCache = $TRUE; + } + + # Lookup all PowerShell modules installed for Icinga for Windows inside the same folder as the Framework + # and fetch each single module file to list the used Cmdlets and Functions + # Add each file content to a big string file for better parsing + foreach ($module in $PowerShellModules) { + $Progress = Write-IcingaProgressStatus -Message 'Fetching Icinga for Windows Components' -CurrentValue $Progress -MaxValue $PowerShellModules.Count -Details; + if ($module.Name.ToLower() -eq 'icinga-powershell-framework') { + continue; + } + + if ($UsedCmdlets.Modules -NotContains $module.Name) { + $UsedCmdlets.Modules.Add($module.Name) | Out-Null; + } + + $ModuleFiles = Get-ChildItem -Path $module.FullName -Recurse -Include '*.psm1'; + $ModuleFileContent = ''; + + foreach ($PSFile in $ModuleFiles) { + $DeserializedFile = Read-IcingaPowerShellModuleFile -File $PSFile.FullName; + $RawModuleContent = $DeserializedFile.NormalisedContent; + + if ([string]::IsNullOrEmpty($RawModuleContent)) { + continue; + } + + $ModuleFileContent += $RawModuleContent; + $ModuleFileContent += "`r`n"; + $ModuleFileContent += "`r`n"; + + $SourceCode = $RawModuleContent.ToLower().Replace(' ', ''); + $SourceCode = $SourceCode.Replace("`r`n", ''); + $SourceCode = $SourceCode.Replace("`n", ''); + + # Lookup the entire command list and compare the source code behind if it contains any [ScriptBlocks] or Add-Types + # [ScriptBlocks] are forbidden and modules containing them will not be added, while Add-Type will print a warning + if ($null -ne (Select-String -InputObject $ModuleFileContent -Pattern '[scriptblock]' -SimpleMatch) -Or $null -ne (Select-String -InputObject $SourceCode -Pattern '={' -SimpleMatch) -Or $null -ne (Select-String -InputObject $SourceCode -Pattern 'return{' -SimpleMatch) -Or $null -ne (Select-String -InputObject $SourceCode -Pattern ';{' -SimpleMatch)) { + if ($AllowScriptBlocks -eq $FALSE) { + Write-IcingaConsoleError 'Unable to include module "{0}" into JEA profile. The file "{1}" is using one or more [ScriptBlock] variables which are forbidden in JEA context.' -Objects $module.Name, $PSFile.FullName; + $UsedCmdlets.Modules.RemoveAt($UsedCmdlets.Modules.IndexOf($module.Name)); + $BlockedModules += $module.Name; + $ModuleFileContent = ''; + break; + } else { + Write-IcingaConsoleWarning 'Module "{0}" is containing [ScriptBlock] like content inside file "{1}". Please validate the file before running it inside JEA context.' -Objects $module.Name, $PSFile.FullName; + } + } + + if ($null -ne (Select-String -InputObject $SourceCode -Pattern 'add-type' -SimpleMatch) -Or $null -ne (Select-String -InputObject $SourceCode -Pattern 'typedefinition@"' -SimpleMatch) -Or $null -ne (Select-String -InputObject $SourceCode -Pattern '@"' -SimpleMatch)) { + Write-IcingaConsoleWarning 'The module "{0}" is using "Add-Type" definitions for file "{1}". Ensure you validate the code before trusting this publisher.' -Objects $module.Name, $PSFile.FullName; + } + } + + $ModuleContent += $ModuleFileContent; + } + + if ($DependencyCache -eq $FALSE) { + # Now lets lookup every single Framework file and get all used Cmdlets and Functions so we know our dependencies + $FrameworkFiles = Get-ChildItem -Path (Get-IcingaFrameworkRootPath) -Recurse -Filter '*.psm1'; + + foreach ($ModuleFile in $FrameworkFiles) { + $Progress = Write-IcingaProgressStatus -Message 'Compiling Icinga PowerShell Framework Dependency List' -CurrentValue $Progress -MaxValue $FrameworkFiles.Count -Details; + + # Just ignore our cache file + if ($ModuleFile.FullName -eq (Get-IcingaFrameworkCodeCacheFile)) { + continue; + } + + $DeserializedFile = Read-IcingaPowerShellModuleFile -File $ModuleFile.FullName; + + foreach ($FoundFunction in $DeserializedFile.FunctionList) { + $DependencyList = Get-IcingaFrameworkDependency ` + -Command $FoundFunction ` + -DependencyList $DependencyList; + } + } + + Write-IcingaFileSecure -File (Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'framework_dependencies.json') -Value $DependencyList; + } + + $UsedCmdlets.Modules.Add('icinga-powershell-framework') | Out-Null; + + # Check all our configured background daemons and ensure we get all Cmdlets and Functions including the dependency list + $BackgroundDaemons = (Get-IcingaBackgroundDaemons).Keys; + + foreach ($daemon in $BackgroundDaemons) { + $Progress = Write-IcingaProgressStatus -Message 'Compiling Background Daemon Dependency List' -CurrentValue $Progress -MaxValue $BackgroundDaemons.Count -Details; + + $DaemonCmd = (Get-Command $daemon); + + if ($BlockedModules -Contains $DaemonCmd.Source) { + continue; + } + + $ModuleContent += [string]::Format('function {0} {{{1}{2}{1}}}', $daemon, "`r`n", $DaemonCmd.ScriptBlock.ToString()); + + [string]$CommandType = ([string]$DaemonCmd.CommandType).Replace(' ', ''); + + $UsedCmdlets = Get-IcingaCommandDependency ` + -DependencyList $DependencyList ` + -CompiledList $UsedCmdlets ` + -CmdName $DaemonCmd.Name ` + -CmdType $CommandType; + } + + # We need to add this function which is not used anywhere else and should still add the entire dependency tree + $UsedCmdlets = Get-IcingaCommandDependency ` + -DependencyList $DependencyList ` + -CompiledList $UsedCmdlets ` + -CmdName 'Exit-IcingaExecutePlugin' ` + -CmdType 'Function'; + + # We need to add this function for our background daemon we start with 'Start-IcingaPowerShellDaemon', + # as this function is called outside the JEA context + $UsedCmdlets = Get-IcingaCommandDependency ` + -DependencyList $DependencyList ` + -CompiledList $UsedCmdlets ` + -CmdName 'Add-IcingaForWindowsDaemon' ` + -CmdType 'Function'; + + # Finally loop through all commands again and build our JEA command list + $DeserializedFile = Read-IcingaPowerShellModuleFile -FileContent $ModuleContent; + [array]$JeaCmds = $DeserializedFile.CommandList + $DeserializedFile.FunctionList; + + foreach ($cmd in $JeaCmds) { + $Progress = Write-IcingaProgressStatus -Message 'Compiling JEA Profile Catalog' -CurrentValue $Progress -MaxValue $JeaCmds.Count -Details; + $CmdData = Get-Command $cmd -ErrorAction SilentlyContinue; + + if ($null -eq $CmdData) { + continue; + } + + $CommandType = ([string]$CmdData.CommandType).Replace(' ', ''); + + $UsedCmdlets = Get-IcingaCommandDependency ` + -DependencyList $DependencyList ` + -CompiledList $UsedCmdlets ` + -CmdName $cmd ` + -CmdType $CommandType; + } + + Disable-IcingaProgressPreference; + + return $UsedCmdlets; +} diff --git a/lib/core/jea/Get-IcingaJEAContext.psm1 b/lib/core/jea/Get-IcingaJEAContext.psm1 new file mode 100644 index 0000000..30e24b3 --- /dev/null +++ b/lib/core/jea/Get-IcingaJEAContext.psm1 @@ -0,0 +1,4 @@ +function Get-IcingaJEAContext() +{ + return (Get-IcingaPowerShellConfig -Path 'Framework.JEAProfile'); +} diff --git a/lib/core/jea/Get-IcingaJEAServicePid.psm1 b/lib/core/jea/Get-IcingaJEAServicePid.psm1 new file mode 100644 index 0000000..03179f8 --- /dev/null +++ b/lib/core/jea/Get-IcingaJEAServicePid.psm1 @@ -0,0 +1,11 @@ +function Get-IcingaJEAServicePid() +{ + [string]$JeaPidFile = (Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'jea.pid'); + [string]$JeaPid = Read-IcingaFileSecure -File $JeaPidFile; + + if ([string]::IsNullOrEmpty($JeaPid) -eq $FALSE) { + $JeaPid = $JeaPid.Replace("`r`n", '').Replace("`n", '').Replace(' ', ''); + } + + return $JeaPid; +} diff --git a/lib/core/jea/Get-IcingaJEASessionFile.psm1 b/lib/core/jea/Get-IcingaJEASessionFile.psm1 new file mode 100644 index 0000000..199bc57 --- /dev/null +++ b/lib/core/jea/Get-IcingaJEASessionFile.psm1 @@ -0,0 +1,10 @@ +function Get-IcingaJEASessionFile() +{ + [string]$Path = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'RoleCapabilities\IcingaForWindows.psrc'; + + if (Test-Path -Path $Path) { + return $Path; + } + + return ''; +} diff --git a/lib/core/jea/Install-IcingaJeaProfile.psm1 b/lib/core/jea/Install-IcingaJeaProfile.psm1 new file mode 100644 index 0000000..7279a18 --- /dev/null +++ b/lib/core/jea/Install-IcingaJeaProfile.psm1 @@ -0,0 +1,22 @@ +function Install-IcingaJEAProfile() +{ + param ( + [string]$IcingaUser = ((Get-IcingaServices).icinga2.configuration.ServiceUser), + [switch]$ConstrainedLanguage = $FALSE, + [switch]$TestEnv = $FALSE, + [switch]$RebuildFramework = $FALSE, + [switch]$AllowScriptBlocks = $FALSE + ); + + if ($PSVersionTable.PSVersion -lt '5.0.0.0') { + Write-IcingaConsoleError 'You cannot use JEA profiles on your system, as your installed PowerShell version "{0}" is lower than minimum required version "5.0"' -Objects $PSVersionTable.PSVersion; + return; + } + + Write-IcingaConsoleNotice 'Writing Icinga for Windows environment information as JEA profile' + Write-IcingaJEAProfile -RebuildFramework:$RebuildFramework -AllowScriptBlocks:$AllowScriptBlocks; + Write-IcingaConsoleNotice 'Registering Icinga for Windows JEA profile' + Register-IcingaJEAProfile -IcingaUser $IcingaUser -TestEnv:$TestEnv -ConstrainedLanguage:$ConstrainedLanguage; +} + +Set-Alias -Name 'Update-IcingaJEAProfile' -Value 'Install-IcingaJEAProfile'; diff --git a/lib/core/jea/Read-IcingaPowerShellModuleFile.psm1 b/lib/core/jea/Read-IcingaPowerShellModuleFile.psm1 new file mode 100644 index 0000000..a33bed8 --- /dev/null +++ b/lib/core/jea/Read-IcingaPowerShellModuleFile.psm1 @@ -0,0 +1,58 @@ +function Read-IcingaPowerShellModuleFile() +{ + param ( + [string]$File, + [string]$FileContent = '' + ); + + if (([string]::IsNullOrEmpty($File) -Or (Test-Path -Path $File) -eq $FALSE) -And [string]::IsNullOrEmpty($FileContent)) { + return ''; + } + + if ([string]::IsNullOrEmpty($FileContent)) { + $FileContent = Read-IcingaFileSecure -File $File; + } + + $PSParser = [System.Management.Automation.PSParser]::Tokenize($FileContent, [ref]$null); + [array]$Comments = @(); + [array]$RegexFilter = @(); + [string]$RegexPattern = ''; + [array]$CommandList = @(); + [array]$FunctionList = @(); + [hashtable]$CmdCache = @{ }; + [hashtable]$FncCache = @{ }; + [int]$Index = 0; + + foreach ($entry in $PSParser) { + if ($entry.Type -eq 'Comment') { + $Comments += Select-Object -InputObject $entry -ExpandProperty 'Content'; + } elseif ($entry.Type -eq 'Command') { + if ($CmdCache.ContainsKey($entry.Content) -eq $FALSE) { + $CommandList += [string]$entry.Content; + $CmdCache.Add($entry.Content, 0); + } + } elseif ($entry.Type -eq 'CommandArgument') { + if ($PSParser[$index - 1].Type -eq 'Keyword' -And $PSParser[$index - 1].Content.ToLower() -eq 'function') { + if ($FncCache.ContainsKey($entry.Content) -eq $FALSE) { + $FunctionList += [string]$entry.Content; + $FncCache.Add($entry.Content, 0); + } + } + } + + $Index += 1; + } + + foreach ($entry in $Comments) { + $RegexFilter += [regex]::Escape($entry); + } + + $RegexPattern = [string]::Join('|', $RegexFilter); + + return @{ + 'NormalisedContent' = ($FileContent -Replace $RegexPattern -Split '\r?\n' -NotMatch '^\s*$'); + 'RawContent' = $FileContent; + 'CommandList' = $CommandList; + 'FunctionList' = $FunctionList; + }; +} diff --git a/lib/core/jea/Register-IcingaJEAProfile.psm1 b/lib/core/jea/Register-IcingaJEAProfile.psm1 new file mode 100644 index 0000000..a9bfcb8 --- /dev/null +++ b/lib/core/jea/Register-IcingaJEAProfile.psm1 @@ -0,0 +1,49 @@ +function Register-IcingaJEAProfile() +{ + param ( + [string]$IcingaUser = ((Get-IcingaServices).icinga2.configuration.ServiceUser), + [switch]$ConstrainedLanguage = $FALSE, + [switch]$TestEnv = $FALSE + ); + + $JeaTemplate = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'templates\IcingaForWindows.pssc.template'; + $JeaProfile = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'IcingaForWindows.pssc'; + $JeaContent = Get-Content -Path $JeaTemplate -Raw; + $JeaName = 'IcingaForWindows'; + + if ($TestEnv) { + $IcingaUser = $ENV:USERNAME; + $JeaProfile = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'IcingaForWindowsTest.pssc'; + $JeaName = 'IcingaForWindowsTest'; + } + + if ([string]::IsNullOrEmpty($IcingaUser)) { + Write-IcingaConsoleError 'No user found to set the JEA profile to. By default the Icinga Agent user is used for this'; + return; + } + + $LanguageMode = 'FullLanguage'; + + if ($ConstrainedLanguage) { + $LanguageMode = 'ConstrainedLanguage'; + } + + $UserSID = Get-IcingaUserSID -User $IcingaUser; + $IcingaUser = Get-IcingaUsernameFromSID -SID $UserSID; + $JeaContent = $JeaContent.Replace('$ICINGAFORWINDOWSJEAUSER$', $IcingaUser); + $JeaContent = $JeaContent.Replace('$POWERSHELLLANGUAGEMODE$', $LanguageMode); + + Set-Content -Path $JeaProfile -Value $JeaContent; + + $Result = Register-PSSessionConfiguration -Name $JeaName -Path $JeaProfile -Force; + + if ($TestEnv -eq $FALSE) { + Set-IcingaPowerShellConfig -Path 'Framework.JEAProfile' -Value 'IcingaForWindows'; + } + + if ($null -ne $Result) { + Write-IcingaConsoleNotice 'JEA Profile "{0}" was successfully installed' -Objects $Result.Name; + } else { + Write-IcingaConsoleNotice 'Failed to install JEA profile'; + } +} diff --git a/lib/core/jea/Remove-IcingaFrameworkDependencyFile.psm1 b/lib/core/jea/Remove-IcingaFrameworkDependencyFile.psm1 new file mode 100644 index 0000000..35b7491 --- /dev/null +++ b/lib/core/jea/Remove-IcingaFrameworkDependencyFile.psm1 @@ -0,0 +1,10 @@ +function Remove-IcingaFrameworkDependencyFile() +{ + $DependencyFile = Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'framework_dependencies.json'; + + if (-Not (Test-Path $DependencyFile)) { + return; + } + + Remove-ItemSecure -Path $DependencyFile -Force | Out-Null; +} diff --git a/lib/core/jea/Test-IcingaJEAServiceRunning.psm1 b/lib/core/jea/Test-IcingaJEAServiceRunning.psm1 new file mode 100644 index 0000000..99e7821 --- /dev/null +++ b/lib/core/jea/Test-IcingaJEAServiceRunning.psm1 @@ -0,0 +1,21 @@ +function Test-IcingaJEAServiceRunning() +{ + param ( + [string]$JeaPid = $null + ); + + if ([string]::IsNullOrEmpty($JeaPid)) { + [string]$JeaPid = Get-IcingaJEAServicePid; + } + + $JeaPowerShellProcess = Get-Process -Id $JeaPid -ErrorAction SilentlyContinue; + if ($null -eq $JeaPowerShellProcess) { + return $FALSE; + } + + if ($JeaPowerShellProcess.ProcessName -ne 'wsmprovhost') { + return $FALSE; + } + + return $TRUE; +} diff --git a/lib/core/jea/Test-IcingaPowerShellCommandInCode.psm1 b/lib/core/jea/Test-IcingaPowerShellCommandInCode.psm1 new file mode 100644 index 0000000..a78c696 --- /dev/null +++ b/lib/core/jea/Test-IcingaPowerShellCommandInCode.psm1 @@ -0,0 +1,59 @@ +function Test-IcingaPowerShellCommandInCode() +{ + param ( + [string]$Code = '', + [string]$Command = '' + ); + + if ([string]::IsNullOrEmpty($Code) -Or [string]::IsNullOrEmpty($Command)) { + return $FALSE; + } + + [string]$SearchCmdSpace = [string]::Format('{0} ', $Command); + [string]$SearchCmdColon = [string]::Format('{0};', $Command); + [string]$SearchCmdCBClose = [string]::Format('{0})', $Command); + [string]$SearchCmdCBOpen = [string]::Format('{0}(', $Command); + [string]$SearchCmdSB = [string]::Format('{0}]', $Command); + [string]$SearchCmdBrace = [string]::Format('{0}}}', $Command); + [string]$SearchCmdSQ = [string]::Format("{0}'", $Command); + [string]$SearchCmdRN = [string]::Format('{0}{1}', $Command, "`r`n"); + [string]$SearchCmdNL = [string]::Format('{0}{1}', $Command, "`n"); + + if ($null -ne (Select-String -InputObject $ModuleContent -Pattern $SearchCmdSpace -SimpleMatch)) { + return $TRUE; + } + + if ($null -ne (Select-String -InputObject $ModuleContent -Pattern $SearchCmdColon -SimpleMatch)) { + return $TRUE; + } + + if ($null -ne (Select-String -InputObject $ModuleContent -Pattern $SearchCmdCBOpen -SimpleMatch)) { + return $TRUE; + } + + if ($null -ne (Select-String -InputObject $ModuleContent -Pattern $SearchCmdCBClose -SimpleMatch)) { + return $TRUE; + } + + if ($null -ne (Select-String -InputObject $ModuleContent -Pattern $SearchCmdSB -SimpleMatch)) { + return $TRUE; + } + + if ($null -ne (Select-String -InputObject $ModuleContent -Pattern $SearchCmdBrace -SimpleMatch)) { + return $TRUE; + } + + if ($null -ne (Select-String -InputObject $ModuleContent -Pattern $SearchCmdSQ -SimpleMatch)) { + return $TRUE; + } + + if ($null -ne (Select-String -InputObject $ModuleContent -Pattern $SearchCmdRN -SimpleMatch)) { + return $TRUE; + } + + if ($null -ne (Select-String -InputObject $ModuleContent -Pattern $SearchCmdNL -SimpleMatch)) { + return $TRUE; + } + + return $FALSE; +} diff --git a/lib/core/jea/Uninstall-IcingaJEAProfile.psm1 b/lib/core/jea/Uninstall-IcingaJEAProfile.psm1 new file mode 100644 index 0000000..c2f7538 --- /dev/null +++ b/lib/core/jea/Uninstall-IcingaJEAProfile.psm1 @@ -0,0 +1,20 @@ +function Uninstall-IcingaJEAProfile() +{ + $JeaProfile = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'IcingaForWindows.pssc'; + $JeaProfileRessource = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'RoleCapabilities\IcingaForWindows.psrc'; + + if (Test-Path $JeaProfile) { + Write-IcingaConsoleNotice 'Removing JEA profile'; + Remove-Item $JeaProfile -Force; + } + + if (Test-Path $JeaProfileRessource) { + Write-IcingaConsoleNotice 'Removing JEA profile ressource'; + Remove-Item $JeaProfileRessource -Force; + } + + Write-IcingaConsoleNotice 'Removing JEA profile registration'; + Unregister-PSSessionConfiguration -Name 'IcingaForWindows' -Force -ErrorAction SilentlyContinue; + + Set-IcingaPowerShellConfig -Path 'Framework.JEAProfile' -Value ''; +} diff --git a/lib/core/jea/Write-IcingaJEAProfile.psm1 b/lib/core/jea/Write-IcingaJEAProfile.psm1 new file mode 100644 index 0000000..0d7d287 --- /dev/null +++ b/lib/core/jea/Write-IcingaJEAProfile.psm1 @@ -0,0 +1,30 @@ +function Write-IcingaJEAProfile() +{ + param ( + [switch]$RebuildFramework = $FALSE, + [switch]$AllowScriptBlocks = $FALSE + ); + + [hashtable]$JeaConfig = Get-IcingaJEAConfiguration -RebuildFramework:$RebuildFramework -AllowScriptBlocks:$AllowScriptBlocks; + $JeaFile = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'templates\IcingaForWindows.psrc.template'; + $JeaString = Get-Content $JeaFile; + $NewJeaFile = ''; + + foreach ($line in $JeaString) { + if ($line -like '*ModulesToImport*') { + $NewJeaFile += [string]::Format(' ModulesToImport = {0}{1}', (ConvertFrom-IcingaArrayToString -Array $JeaConfig.Modules -AddQuotes), "`n"); + continue; + } + if ($line -like '*VisibleCmdlets*') { + $NewJeaFile += [string]::Format(' VisibleCmdlets = {0}{1}', (ConvertFrom-IcingaArrayToString -Array $JeaConfig.Cmdlet.Keys -AddQuotes), "`n"); + continue; + } + if ($line -like '*VisibleFunctions*') { + $NewJeaFile += [string]::Format(' VisibleFunctions = {0}{1}', (ConvertFrom-IcingaArrayToString -Array $JeaConfig.Function.Keys -AddQuotes), "`n"); + continue; + } + $NewJeaFile += $line + "`n"; + } + + Set-Content -Path (Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'RoleCapabilities\IcingaForWindows.psrc') -Value $NewJeaFile; +} diff --git a/lib/core/logging/Icinga_EventLog_Enums.psm1 b/lib/core/logging/Icinga_EventLog_Enums.psm1 index 9b9604b..671a9a8 100644 --- a/lib/core/logging/Icinga_EventLog_Enums.psm1 +++ b/lib/core/logging/Icinga_EventLog_Enums.psm1 @@ -14,6 +14,12 @@ if ($null -eq $IcingaEventLogEnums -Or $IcingaEventLogEnums.ContainsKey('Framewo 'Details' = 'The Framework or is components can issue generic debug message in case the debug log is enabled. Please ensure to disable it, if not used. You can do so with the command "Disable-IcingaFrameworkDebugMode"'; 'EventId' = 1000; }; + 1001 = @{ + 'EntryType' = 'Warning'; + 'Message' = 'Icinga for Windows deprecation warning'; + 'Details' = 'Icinga for Windows or one of its components executed a function or method, which is flagged as deprecated. Please modify your code or contact the responsible developer to update the component to no longer user this deprecated function or method.'; + 'EventId' = 1001; + }; 1100 = @{ 'EntryType' = 'Error'; 'Message' = 'Corrupt Icinga for Windows configuration'; @@ -32,6 +38,12 @@ if ($null -eq $IcingaEventLogEnums -Or $IcingaEventLogEnums.ContainsKey('Framewo 'Details' = 'Icinga for Windows could not read the specified file after several attempts, because another process is locking the file. Icinga for Windows terminated itself to prevent damage to this file.'; 'EventId' = 1102; }; + 1400 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Icinga for Windows background daemon not found'; + 'Details' = 'Icinga for Windows could not find the Function or Cmdlet for the specified background daemon. The daemon was not loaded.'; + 'EventId' = 1400; + }; 1500 = @{ 'EntryType' = 'Error'; 'Message' = 'Failed to securely establish a communication between this server and the client'; @@ -44,6 +56,30 @@ if ($null -eq $IcingaEventLogEnums -Or $IcingaEventLogEnums.ContainsKey('Framewo 'Details' = 'A client connection was terminated by the Framework because no secure SSL handshake could be established. This issue in general is followed by EventId 1500.'; 'EventId' = 1501; }; + 1502 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Unable to create PowerShell RunSpace in JEA context'; + 'Details' = 'A PowerShell RunSpace for background threads could not be created, as the required Icinga for Windows session configuration file could not be found. Use "Install-IcingaJEAProfile" to resolve this problem.'; + 'EventId' = 1502; + }; + 1503 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Unable to start Icinga for Windows service'; + 'Details' = 'Unable to start Icinga for Windows service, as the JEA session created by the service is still active. Run "Restart-IcingaWindowsService" to restart the Icinga for Windows service, while running in JEA context to prevent this issue.'; + 'EventId' = 1503; + }; + 1504 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Icinga for Windows JEA context vanished'; + 'Details' = 'The Icinga for Windows JEA session is no longer available. It might have either crashed or get terminated by user actions, like restarting the WinRM service.'; + 'EventId' = 1504; + }; + 1505 = @{ + 'EntryType' = 'Warning'; + 'Message' = 'Icinga for Windows JEA context not available'; + 'Details' = 'The Icinga for Windows JEA session is no longer available and is attempted to be restarted on the system. This could have either happenend due to a crash or a user action, like restarting the WinRM service.'; + 'EventId' = 1505; + }; 1550 = @{ 'EntryType' = 'Error'; 'Message' = 'Unsupported web authentication used'; @@ -80,6 +116,12 @@ if ($null -eq $IcingaEventLogEnums -Or $IcingaEventLogEnums.ContainsKey('Framewo 'Details' = 'A web client trying to authenticate failed as the provided user credentials could not be verified.'; 'EventId' = 1561; }; + 1600 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Exception on function calls in JEA context'; + 'Details' = 'An exception occurred while executing Icinga for Windows code inside a JEA context.'; + 'EventId' = 1600; + }; } }; } diff --git a/lib/core/logging/Write-IcingaDeprecated.psm1 b/lib/core/logging/Write-IcingaDeprecated.psm1 new file mode 100644 index 0000000..94427e8 --- /dev/null +++ b/lib/core/logging/Write-IcingaDeprecated.psm1 @@ -0,0 +1,28 @@ +function Write-IcingaDeprecated() +{ + param ( + [string]$Function, + [string]$Argument + ); + + if ([string]::IsNullOrEmpty($Function)) { + return; + } + + $Message = 'The called function or method "{0}" is deprecated. Please update your component or contact the developer to update the component accordingly.'; + + if ([string]::IsNullOrEmpty($Argument) -eq $FALSE) { + $Message = 'The function or method "{0}" is called with deprecated argument "{1}". Please update your component or contact the developer to update the component accordingly.'; + } + + Write-IcingaConsoleOutput ` + -Message $Message ` + -Objects $Function, $Argument ` + -ForeColor 'Cyan' ` + -Severity 'Deprecated'; + + Write-IcingaEventMessage -EventId 1001 -Namespace 'Framework' -Objects ` + ([string]::Format('Command or Method: {0}', $Function)), + ([string]::Format('Argument: {0}', $Argument)), + (Get-PSCallStack); +} diff --git a/lib/core/repository/Get-IcingaComponentList.psm1 b/lib/core/repository/Get-IcingaComponentList.psm1 index 8a1ec7f..69e95e6 100644 --- a/lib/core/repository/Get-IcingaComponentList.psm1 +++ b/lib/core/repository/Get-IcingaComponentList.psm1 @@ -16,7 +16,7 @@ function Get-IcingaComponentList() $SearchList | Add-Member -MemberType NoteProperty -Name 'Components' -Value @{ }; foreach ($entry in $Repositories) { - $RepoContent = Read-IcingaRepositoryFile -Name $entry.Name; + $RepoContent = Read-IcingaRepositoryFile -Name $entry.Name; if ($null -eq $RepoContent) { continue; @@ -28,6 +28,10 @@ function Get-IcingaComponentList() foreach ($repoEntry in $RepoContent.Packages.PSObject.Properties.Name) { + if ($repoEntry.ToLower() -eq 'kickstart') { + continue; + } + $RepoData = New-Object -TypeName PSObject; $RepoData | Add-Member -MemberType NoteProperty -Name 'Name' -Value $entry.Name; $RepoData | Add-Member -MemberType NoteProperty -Name 'RemoteSource' -Value $RepoContent.Info.RemoteSource; @@ -45,11 +49,15 @@ function Get-IcingaComponentList() continue; } + if ($Snapshot -eq $FALSE -And (Test-Numeric $package.Version.Replace('.', '')) -eq $FALSE) { + continue; + } + if ($SearchList.Components.ContainsKey($repoEntry) -eq $FALSE) { $SearchList.Components.Add($repoEntry, $package.Version); } - if ([version]($SearchList.Components[$repoEntry]) -lt [version]$package.Version) { + if ((Test-Numeric $package.Version.Replace('.', '')) -And [version]($SearchList.Components[$repoEntry]) -lt [version]$package.Version) { $SearchList.Components[$repoEntry] = $package.Version; } diff --git a/lib/core/repository/Install-IcingaComponent.psm1 b/lib/core/repository/Install-IcingaComponent.psm1 index b1a625c..d5ad124 100644 --- a/lib/core/repository/Install-IcingaComponent.psm1 +++ b/lib/core/repository/Install-IcingaComponent.psm1 @@ -138,7 +138,7 @@ function Install-IcingaComponent() } if ($ManifestFile.ModuleVersion -eq $InstallVersion -And $Force -eq $FALSE) { - Write-IcingaConsoleError ([string]::Format('The package "{0}" with version "{1}" is already installed. Use "-Force" to re-install the component', $Name.ToLower(), $ManifestFile.ModuleVersion)); + Write-IcingaConsoleWarning ([string]::Format('The package "{0}" with version "{1}" is already installed. Use "-Force" to re-install the component', $Name.ToLower(), $ManifestFile.ModuleVersion)); Start-Sleep -Seconds 2; Remove-Item -Path $DownloadDirectory -Recurse -Force; return; @@ -146,12 +146,13 @@ function Install-IcingaComponent() # These update steps only apply for the framework if ($Name.ToLower() -eq 'framework') { + Remove-IcingaFrameworkDependencyFile; $ServiceStatus = (Get-Service 'icingapowershell' -ErrorAction SilentlyContinue).Status; $AgentStatus = (Get-Service 'icinga2' -ErrorAction SilentlyContinue).Status; if ($ServiceStatus -eq 'Running') { Write-IcingaConsoleNotice 'Stopping Icinga for Windows service'; - Stop-IcingaService 'icingapowershell'; + Stop-IcingaWindowsService; Start-Sleep -Seconds 1; } if ($AgentStatus -eq 'Running') { @@ -168,7 +169,7 @@ function Install-IcingaComponent() $ComponentFileContent = Get-ChildItem -Path $ComponentFolder; foreach ($entry in $ComponentFileContent) { - if (($entry.Name -eq 'cache' -Or $entry.Name -eq 'config') -And $Name.ToLower() -eq 'framework') { + if (($entry.Name -eq 'cache' -Or $entry.Name -eq 'config' -Or $entry.Name -eq 'certificate') -And $Name.ToLower() -eq 'framework') { continue; } @@ -197,6 +198,12 @@ function Install-IcingaComponent() } Import-Module -Name $ComponentFolder -Force; + + # This will ensure that Framework functions will always win over third party functions, overwriting functionality + # of the Framework, which might cause problems during installation otherwise + Import-Module (Join-Path -Path (Get-IcingaForWindowsRootPath) -ChildPath 'icinga-powershell-framework') -Force; + Import-Module (Join-Path -Path (Get-IcingaForWindowsRootPath) -ChildPath 'icinga-powershell-framework') -Global -Force; + Write-IcingaConsoleNotice 'Installation of component "{0}" with version "{1}" was successful. Open a new PowerShell to apply the changes' -Objects $Name.ToLower(), $ManifestFile.ModuleVersion; } else { <# @@ -212,6 +219,7 @@ function Install-IcingaComponent() $ServiceData = Get-IcingaForWindowsServiceData; $ServiceDirectory = $ServiceData.Directory; $ServiceUser = $ServiceData.User; + [int]$Success = -1; if ([string]::IsNullOrEmpty($ConfigDirectory) -eq $FALSE) { $ServiceDirectory = $ConfigDirectory; @@ -219,6 +227,8 @@ function Install-IcingaComponent() if ([string]::IsNullOrEmpty($ConfigUser) -eq $FALSE) { $ServiceUser = $ConfigUser; + } else { + Set-IcingaPowerShellConfig -Path 'Framework.Icinga.ServiceUser' -Value $ServiceUser; } foreach ($binary in $FolderContent) { @@ -241,11 +251,8 @@ function Install-IcingaComponent() $NewService = Read-IcingaServicePackage -File $binary.FullName; if ($InstalledService.ProductVersion -eq $NewService.ProductVersion -And $null -ne $InstalledService -And $null -ne $NewService -And $Force -eq $FALSE) { - Write-IcingaConsoleError ([string]::Format('The package "service" with version "{0}" is already installed. Use "-Force" to re-install the component', $InstalledService.ProductVersion)); - Start-Sleep -Seconds 2; - Remove-Item -Path $DownloadDirectory -Recurse -Force; - - return; + $Success = 0; + break; } } @@ -254,11 +261,22 @@ function Install-IcingaComponent() Copy-ItemSecure -Path $binary.FullName -Destination $UpdateBin -Force; [void](Install-IcingaForWindowsService -Path $ServiceBin -User $ServiceUser -Password (Get-IcingaInternalPowerShellServicePassword)); + Update-IcingaServiceUser; Set-IcingaInternalPowerShellServicePassword -Password $null; - Start-Sleep -Seconds 2; + $Success = 1; + break; + } + + if ($Success -eq 0) { + Write-IcingaConsoleWarning ([string]::Format('The package "service" with version "{0}" is already installed. Use "-Force" to re-install the component', $InstalledService.ProductVersion)); Remove-Item -Path $DownloadDirectory -Recurse -Force; - Write-IcingaConsoleNotice 'Installation of component "service" was successful' + return; + } + + if ($Success -eq 1) { + Remove-Item -Path $DownloadDirectory -Recurse -Force; + Write-IcingaConsoleNotice 'Installation of component "service" was successful'; return; } @@ -269,7 +287,7 @@ function Install-IcingaComponent() return; } else { Write-IcingaConsoleError 'There was no manifest file found inside the package'; - Remove-Item -Path $DownloadDirectory -Recurse -Force; + Remove-ItemSecure -Path $DownloadDirectory -Recurse -Force; return; } } @@ -295,6 +313,8 @@ function Install-IcingaComponent() if ([string]::IsNullOrEmpty($ConfigUser) -eq $FALSE) { $ServiceUser = $ConfigUser; + } else { + Set-IcingaPowerShellConfig -Path 'Framework.Icinga.ServiceUser' -Value $ServiceUser; } [string]$InstallFolderMsg = $InstallTarget; @@ -313,7 +333,7 @@ function Install-IcingaComponent() $MSIData = & powershell.exe -Command { Use-Icinga; return Read-IcingaMSIMetadata -File $args[0] } -Args $DownloadDestination; if ($InstalledVersion.Full -eq $MSIData.ProductVersion -And $Force -eq $FALSE) { - Write-IcingaConsoleError 'The package "agent" with version "{0}" is already installed. Use "-Force" to re-install the component' -Objects $InstalledVersion.Full; + Write-IcingaConsoleWarning 'The package "agent" with version "{0}" is already installed. Use "-Force" to re-install the component' -Objects $InstalledVersion.Full; Remove-Item -Path $DownloadDirectory -Recurse -Force; return; @@ -343,6 +363,7 @@ function Install-IcingaComponent() } Set-IcingaAgentServiceUser -User $ServiceUser -SetPermission; + Update-IcingaServiceUser; Write-IcingaConsoleNotice 'Installation of component "agent" with version "{0}" was successful.' -Objects $MSIData.ProductVersion; } else { diff --git a/lib/core/repository/Show-Icinga.psm1 b/lib/core/repository/Show-Icinga.psm1 index 5c8ec09..3480e7f 100644 --- a/lib/core/repository/Show-Icinga.psm1 +++ b/lib/core/repository/Show-Icinga.psm1 @@ -58,6 +58,23 @@ function Show-Icinga() $IcingaForWindowsService = Get-IcingaForWindowsServiceData; $IcingaAgentService = Get-IcingaAgentInstallation; $WindowsInformation = Get-IcingaWindowsInformation Win32_OperatingSystem | Select-Object Version, BuildNumber, Caption; + $DefinedServiceUser = Get-IcingaPowerShellConfig -Path 'Framework.Icinga.ServiceUser'; + $JEAContext = Get-IcingaJEAContext; + $JEASessionFile = Get-IcingaJEASessionFile; + $IcingaForWindowsCert = Get-IcingaForWindowsCertificate; + + if ([string]::IsNullOrEmpty($DefinedServiceUser)) { + $DefinedServiceUser = ''; + } + if ([string]::IsNullOrEmpty($JEAContext)) { + $JEAContext = ''; + } + if ([string]::IsNullOrEmpty($JEASessionFile)) { + $JEASessionFile = ''; + } + if ($null -eq $IcingaForWindowsCert -Or [string]::IsNullOrEmpty($IcingaForWindowsCert)) { + $IcingaForWindowsCert = 'Not installed'; + } $Output += ''; $Output += 'Environment configuration'; @@ -67,9 +84,17 @@ function Show-Icinga() $Output += ([string]::Format('Icinga for Windows Service User => {0}', $IcingaForWindowsService.User)); $Output += ([string]::Format('Icinga Agent Path => {0}', $IcingaAgentService.RootDir)); $Output += ([string]::Format('Icinga Agent User => {0}', $IcingaAgentService.User)); + $Output += ([string]::Format('Defined Default User => {0}', $DefinedServiceUser)); + $Output += ([string]::Format('Icinga Managed User => {0}', (Test-IcingaManagedUser -IcingaUser (Get-IcingaPowerShellConfig -Path 'Framework.Icinga.ServiceUser')))); $Output += ([string]::Format('PowerShell Version => {0}', $PSVersionTable.PSVersion.ToString())); $Output += ([string]::Format('Operating System => {0}', $WindowsInformation.Caption)); $Output += ([string]::Format('Operating System Version => {0}', $WindowsInformation.Version)); + $Output += ([string]::Format('JEA Context => {0}', $JEAContext)); + $Output += ([string]::Format('JEA Session File => {0}', $JEASessionFile)); + $Output += ''; + $Output += 'Icinga for Windows Certificate'; + $Output += ''; + $Output += $IcingaForWindowsCert; $Output += ''; $Output += (Show-IcingaRepository); diff --git a/lib/core/thread/New-IcingaThreadHash.psm1 b/lib/core/thread/New-IcingaThreadHash.psm1 index 56e765d..179d384 100644 --- a/lib/core/thread/New-IcingaThreadHash.psm1 +++ b/lib/core/thread/New-IcingaThreadHash.psm1 @@ -1,7 +1,7 @@ function New-IcingaThreadHash() { param( - [ScriptBlock]$ShellScript, + [string]$ShellScript, [array]$Arguments ); diff --git a/lib/core/thread/New-IcingaThreadInstance.psm1 b/lib/core/thread/New-IcingaThreadInstance.psm1 index bf402ce..040edc2 100644 --- a/lib/core/thread/New-IcingaThreadInstance.psm1 +++ b/lib/core/thread/New-IcingaThreadInstance.psm1 @@ -1,9 +1,11 @@ function New-IcingaThreadInstance() { - param( + param ( [string]$Name, $ThreadPool, [ScriptBlock]$ScriptBlock, + [string]$Command, + [hashtable]$CmdParameters, [array]$Arguments, [Switch]$Start ); @@ -21,17 +23,53 @@ function New-IcingaThreadInstance() ) ); - $Shell = [PowerShell]::Create(); - $Shell.RunspacePool = $ThreadPool; - [void]$Shell.AddScript($ScriptBlock); - foreach ($argument in $Arguments) { - [void]$Shell.AddArgument($argument); + $Shell = [PowerShell]::Create(); + $Shell.RunSpacePool = $ThreadPool; + [string]$CodeHash = ''; + + if ([string]::IsNullOrEmpty($Command) -eq $FALSE) { + + [void]$Shell.AddCommand('Use-Icinga'); + [void]$Shell.AddParameter('-LibOnly', $TRUE); + [void]$Shell.AddParameter('-Daemon', $TRUE); + + [void]$Shell.AddCommand($Command); + + $CodeHash = $Command; + + foreach ($cmd in $CmdParameters.Keys) { + $Value = $CmdParameters[$cmd]; + + Write-IcingaDebugMessage -Message 'Adding new argument to thread command' -Objects $cmd, $value, $Command; + + [void]$Shell.AddParameter($cmd, $value); + + $Arguments += $cmd; + $Arguments += $value; + } + } + + if ($null -ne $ScriptBlock) { + Write-IcingaDeprecated -Function 'New-IcingaThreadInstance' -Argument 'ScriptBlock'; + $CodeHash = $ScriptBlock; + + [void]$Shell.AddScript($ScriptBlock); + foreach ($argument in $Arguments) { + [void]$Shell.AddArgument($argument); + } } $Thread = New-Object PSObject; Add-Member -InputObject $Thread -MemberType NoteProperty -Name Shell -Value $Shell; + if ($Start) { - Add-Member -InputObject $Thread -MemberType NoteProperty -Name Handle -Value ($Shell.BeginInvoke()); + Write-IcingaDebugMessage -Message 'Starting shell instance' -Objects $Command, $Shell, $Thread; + try { + $ShellData = $Shell.BeginInvoke(); + } catch { + Write-IcingaDebugMessage -Message 'Failed to start Icinga thread instance' -Objects $Command, $_.Exception.Message; + } + Add-Member -InputObject $Thread -MemberType NoteProperty -Name Handle -Value ($ShellData); Add-Member -InputObject $Thread -MemberType NoteProperty -Name Started -Value $TRUE; } else { Add-Member -InputObject $Thread -MemberType NoteProperty -Name Handle -Value $null; @@ -42,7 +80,7 @@ function New-IcingaThreadInstance() $global:IcingaDaemonData.IcingaThreads.Add($Name, $Thread); } else { $global:IcingaDaemonData.IcingaThreads.Add( - (New-IcingaThreadHash -ShellScript $ScriptBlock -Arguments $Arguments), + (New-IcingaThreadHash -ShellScript $CodeHash -Arguments $Arguments), $Thread ); } diff --git a/lib/core/thread/New-IcingaThreadPool.psm1 b/lib/core/thread/New-IcingaThreadPool.psm1 index f0a8a2b..b9f8703 100644 --- a/lib/core/thread/New-IcingaThreadPool.psm1 +++ b/lib/core/thread/New-IcingaThreadPool.psm1 @@ -5,10 +5,23 @@ function New-IcingaThreadPool() [int]$MaxInstances = 5 ); + $SessionConfiguration = $null; + $SessionFile = Get-IcingaJEASessionFile; + + if ([string]::IsNullOrEmpty((Get-IcingaJEAContext))) { + $SessionConfiguration = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault(); + } else { + if ([string]::IsNullOrEmpty($SessionFile)) { + Write-IcingaEventMessage -EventId 1502 -Namespace 'Framework'; + return $null; + } + $SessionConfiguration = [System.Management.Automation.Runspaces.InitialSessionState]::CreateFromSessionConfigurationFile($SessionFile); + } + $Runspaces = [RunspaceFactory]::CreateRunspacePool( $MinInstances, $MaxInstances, - [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault(), + $SessionConfiguration, $host ) diff --git a/lib/core/tools/Get-IcingaCheckCommandConfig.psm1 b/lib/core/tools/Get-IcingaCheckCommandConfig.psm1 index 20affe5..698c3f4 100644 --- a/lib/core/tools/Get-IcingaCheckCommandConfig.psm1 +++ b/lib/core/tools/Get-IcingaCheckCommandConfig.psm1 @@ -86,7 +86,7 @@ function Get-IcingaCheckCommandConfig() $CheckName = (Get-Command Invoke-IcingaCheck*).Name } - [int]$FieldID = 2; # Starts at '2', because '0' and '1' are reserved for 'Verbose' and 'NoPerfData' + [int]$FieldID = 4; # Starts at '4', because 0-3 are reserved for 'Verbose', 'NoPerfData', ExecutionPolicy and a placeholder [hashtable]$Basket = @{ }; # Define basic hashtable structure by adding fields: "Datafield", "DataList", "Command" @@ -98,21 +98,64 @@ function Get-IcingaCheckCommandConfig() $Basket.Command.Add( 'PowerShell Base', @{ - 'arguments' = @{ }; + 'arguments' = @{ + '-NoProfile' = @{ + 'order' = '-3'; + 'set_if' = $TRUE; + }; + '-NoLogo' = @{ + 'order' = '-2'; + 'set_if' = $TRUE; + }; + '-ExecutionPolicy' = @{ + 'order' = '-1'; + 'value' = '$IcingaPowerShellBase_String_ExecutionPolicy$'; + }; + }; 'command' = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'; 'disabled' = $FALSE; - 'fields' = @(); + 'fields' = @( + @{ + 'datafield_id' = 2; + 'is_required' = 'n'; + 'var_filter' = $NULL; + }; + ); 'imports' = @(); 'is_string' = $NULL; 'methods_execute' = 'PluginCheck'; 'object_name' = 'PowerShell Base'; 'object_type' = 'object'; 'timeout' = '180'; - 'vars' = @{ }; + 'vars' = @{ + 'IcingaPowerShellBase_String_ExecutionPolicy' = 'ByPass'; + }; 'zone' = $NULL; } ); + + Add-PowerShellDataList -Name 'PowerShell ExecutionPolicies' -Basket $Basket -Arguments @( 'AllSigned', 'Bypass', 'Default', 'RemoteSigned', 'Restricted', 'Undefined', 'Unrestricted' ); + + $Basket.Datafield.Add( + '2', @{ + 'varname' = 'IcingaPowerShellBase_String_ExecutionPolicy'; + 'caption' = 'PowerShell Execution Policy'; + 'description' = 'Defines with which Execution Policy the PowerShell is started'; + 'datatype' = 'Icinga\Module\Director\DataType\DataTypeDatalist'; + 'format' = $NULL; + 'originalId' = '2'; + } + ); + + $Basket.Datafield['2'].Add( + 'settings', @{ + 'datalist' = 'PowerShell ExecutionPolicies'; + 'data_type' = 'string'; + 'behavior' = 'strict'; + } + ); + $ThresholdIntervalArg = New-Object -TypeName PSObject; $ThresholdIntervalArg | Add-Member -MemberType NoteProperty -Name 'type' -Value (New-Object -TypeName PSObject); $ThresholdIntervalArg | Add-Member -MemberType NoteProperty -Name 'Description' -Value (New-Object -TypeName PSObject); @@ -158,13 +201,13 @@ function Get-IcingaCheckCommandConfig() '-C' = @{ 'value' = [string]::Format('try {{ Use-Icinga -Minimal; }} catch {{ Write-Output {1}The Icinga PowerShell Framework is either not installed on the system or not configured properly. Please check https://icinga.com/docs/windows for further details{1}; Write-Output {1}Error:{1} $$($$_.Exception.Message)Components:`r`n$$( Get-Module -ListAvailable {1}icinga-powershell-*{1} )`r`n{1}Module-Path:{1}`r`n$$($$Env:PSModulePath); exit 3; }}; Exit-IcingaExecutePlugin -Command {1}{0}{1} ', $Data.Name, "'"); 'order' = '0'; - } + }; } 'fields' = @(); 'imports' = @( 'PowerShell Base' ); 'object_name' = $Data.Name; 'object_type' = 'object'; - 'vars' = @{}; + 'vars' = @{ }; } ); @@ -351,7 +394,7 @@ function Get-IcingaCheckCommandConfig() $CheckParamList = @( $ThresholdIntervalArg ); foreach ($entry in $Data.parameters.parameter) { - $CheckParamList += (Convert-IcingaCheckArgumentToPSObject -Parameter $entry);; + $CheckParamList += (Convert-IcingaCheckArgumentToPSObject -Parameter $entry); } foreach ($parameter in $CheckParamList) { @@ -569,6 +612,21 @@ function Write-IcingaPlainConfigurationFiles() $PowerShellBase += [string]::Format(' "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"{0}', (New-IcingaNewLine)); $PowerShellBase += [string]::Format(' ]{0}', (New-IcingaNewLine)); $PowerShellBase += [string]::Format(' timeout = 3m{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' arguments += {{{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' "-ExecutionPolicy" = {{{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' order = -1{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' value = "$IcingaPowerShellBase_String_ExecutionPolicy$"{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' }}{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' "-NoLogo" = {{{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' order = -2{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' set_if = "1"{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' }}{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' "-NoProfile" = {{{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' order = -3{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' set_if = "1"{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' }}{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' }}{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' vars.IcingaPowerShellBase_String_ExecutionPolicy = "ByPass"{0}', (New-IcingaNewLine)); $PowerShellBase += '}'; Write-IcingaFileSecure -File (Join-Path -Path $ConfigDirectory -ChildPath 'PowerShell_Base.conf') -Value $PowerShellBase; diff --git a/lib/core/tools/Get-IcingaFileHash.psm1 b/lib/core/tools/Get-IcingaFileHash.psm1 new file mode 100644 index 0000000..b739d24 --- /dev/null +++ b/lib/core/tools/Get-IcingaFileHash.psm1 @@ -0,0 +1,32 @@ +function Get-IcingaFileHash() +{ + param ( + [string]$Path, + [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5')] + [string]$Algorithm = 'SHA256' + ); + + if ([string]::IsNullOrEmpty($Path) -Or ((Test-Path -Path $Path) -eq $FALSE)) { + Write-IcingaConsoleError 'Your path is either not specified or does not exist'; + return $null; + } + + $FileHasher = New-Object "System.Security.Cryptography.${Algorithm}CryptoServiceProvider"; + + if ($null -eq $FileHasher) { + Write-IcingaConsoleError 'Unable to create cryptography objects for algorithm "{0}"' -Objects $Algorithm; + return $null; + } + + # Read the file specified in $FilePath as a Byte array + [System.IO.Stream]$FileStream = [System.IO.File]::OpenRead($Path) + [Byte[]]$FileHash = $FileHasher.ComputeHash($FileStream) + [string]$HashString = [BitConverter]::ToString($FileHash).Replace('-', ''); + $RetValue = New-Object -TypeName PSObject; + + $RetValue | Add-Member -MemberType NoteProperty -Name 'Algorithm' -Value $Algorithm.ToUpper(); + $RetValue | Add-Member -MemberType NoteProperty -Name 'Hash' -Value $HashString; + $RetValue | Add-Member -MemberType NoteProperty -Name 'Path' -Value $Path; + + return $RetValue; +} diff --git a/lib/core/tools/Get-IcingaUserSID.psm1 b/lib/core/tools/Get-IcingaUserSID.psm1 index 7a9a1c0..e96602b 100644 --- a/lib/core/tools/Get-IcingaUserSID.psm1 +++ b/lib/core/tools/Get-IcingaUserSID.psm1 @@ -4,6 +4,10 @@ function Get-IcingaUserSID() [string]$User ); + if ([string]::IsNullOrEmpty($User)) { + return $null; + } + if ($User -eq 'LocalSystem') { $User = 'NT Authority\SYSTEM'; } @@ -14,7 +18,14 @@ function Get-IcingaUserSID() $NTUser = New-Object System.Security.Principal.NTAccount($UserData.Domain, $UserData.User); $SecurityData = $NTUser.Translate([System.Security.Principal.SecurityIdentifier]); } catch { - throw $_.Exception; + try { + # Try again but this time with our domain + $UserData.Domain = (Get-IcingaWindowsInformation -ClassName Win32_ComputerSystem).Domain; + $NTUser = New-Object System.Security.Principal.NTAccount($UserData.Domain, $UserData.User); + $SecurityData = $NTUser.Translate([System.Security.Principal.SecurityIdentifier]); + } catch { + throw $_.Exception; + } } if ($null -eq $SecurityData) { diff --git a/lib/core/tools/Get-IcingaUsernameFromSID.psm1 b/lib/core/tools/Get-IcingaUsernameFromSID.psm1 new file mode 100644 index 0000000..f3bfc47 --- /dev/null +++ b/lib/core/tools/Get-IcingaUsernameFromSID.psm1 @@ -0,0 +1,16 @@ +function Get-IcingaUsernameFromSID() +{ + param ( + [string]$SID + ); + + if ([string]::IsNullOrEmpty($SID)) { + Write-IcingaConsoleError 'You have to specify a SID'; + return $null; + } + + $UserData = New-Object System.Security.Principal.SecurityIdentifier $SID; + $UserObject = $UserData.Translate([System.Security.Principal.NTAccount]); + + return $UserObject.Value; +} diff --git a/lib/core/tools/New-IcingaVersionObject.psm1 b/lib/core/tools/New-IcingaVersionObject.psm1 new file mode 100644 index 0000000..686faa5 --- /dev/null +++ b/lib/core/tools/New-IcingaVersionObject.psm1 @@ -0,0 +1,12 @@ +function New-IcingaVersionObject() +{ + param ( + [array]$Version + ); + + if ($null -eq $Version -Or $Version.Count -gt 4) { + return (New-Object System.Version); + } + + return (New-Object System.Version $Version); +} diff --git a/lib/core/tools/Test-PSCustomObjectMember.psm1 b/lib/core/tools/Test-PSCustomObjectMember.psm1 index 1edeace..a69af03 100644 --- a/lib/core/tools/Test-PSCustomObjectMember.psm1 +++ b/lib/core/tools/Test-PSCustomObjectMember.psm1 @@ -9,5 +9,5 @@ function Test-PSCustomObjectMember() return $FALSE; } - return ([bool]($PSObject.PSobject.Properties.Name -eq $Name)); + return ([bool]($PSObject.PSObject.Properties.Name -eq $Name)); } diff --git a/lib/core/tools/Write-IcingaProgressStatus.psm1 b/lib/core/tools/Write-IcingaProgressStatus.psm1 new file mode 100644 index 0000000..922aacb --- /dev/null +++ b/lib/core/tools/Write-IcingaProgressStatus.psm1 @@ -0,0 +1,39 @@ +function Write-IcingaProgressStatus() +{ + param ( + [int]$CurrentValue = 0, + [int]$MaxValue = 1, + [string]$Message = 'Processing Icinga for Windows', + [string]$Status = "{0}% Complete", + [switch]$Details = $FALSE + ); + + if ($CurrentValue -le -99) { + $CurrentValue = 0; + return; + } + + if ($MaxValue -le 0) { + $MaxValue = 1; + } + + $ProgressValue = [math]::Round($CurrentValue / $MaxValue * 100, 0); + + if ($Details) { + $Message = [string]::Format('{0}: {1}/{2}', $Message, $CurrentValue, $MaxValue); + } + + $ProgressPreference = 'Continue'; + + if ($ProgressValue -ge 100) { + $ProgressValue = 100; + Write-Progress -Activity $Message -Status ([string]::Format($Status, $ProgressValue)) -PercentComplete $ProgressValue -Completed; + $CurrentValue = -99; + + return $CurrentValue; + } + + Write-Progress -Activity $Message -Status ([string]::Format($Status, $ProgressValue)) -PercentComplete $ProgressValue; + + return ($CurrentValue += 1); +} diff --git a/lib/core/windows/Clear-IcingaWindowsUserPassword.psm1 b/lib/core/windows/Clear-IcingaWindowsUserPassword.psm1 new file mode 100644 index 0000000..b9a5265 --- /dev/null +++ b/lib/core/windows/Clear-IcingaWindowsUserPassword.psm1 @@ -0,0 +1,12 @@ +function Clear-IcingaWindowsUserPassword() +{ + if ($null -eq $Global:Icinga) { + return; + } + + if ($Global:Icinga.ContainsKey('ServiceUserPassword') -eq $FALSE) { + return; + } + + $Global:Icinga.ServiceUserPassword = $null; +} diff --git a/lib/core/windows/Get-IcingaRandomChars.psm1 b/lib/core/windows/Get-IcingaRandomChars.psm1 new file mode 100644 index 0000000..addaf9d --- /dev/null +++ b/lib/core/windows/Get-IcingaRandomChars.psm1 @@ -0,0 +1,23 @@ +function Get-IcingaRandomChars() +{ + param ( + [int]$Count = 10, + [string]$Symbols = 'abcdefghiklmnoprstuvwxyzABCDEFGHKLMNOPRSTUVWXYZ1234567890!ยง$%&/()=?}][{@#*+' + ); + + $RandomChars = ''; + + if ([string]::IsNullOrEmpty($Symbols)) { + return $RandomChars; + } + + while ($Count -gt 0) { + + [int]$SymbolLength = $Symbols.Length; + $RandomValue = Get-Random -Minimum 0 -Maximum ($SymbolLength - 1); + $RandomChars += $Symbols[$RandomValue]; + $Count -= 1; + } + + return $RandomChars; +} diff --git a/lib/core/windows/Get-IcingaWindowsUserMetadata.psm1 b/lib/core/windows/Get-IcingaWindowsUserMetadata.psm1 new file mode 100644 index 0000000..9ac7c39 --- /dev/null +++ b/lib/core/windows/Get-IcingaWindowsUserMetadata.psm1 @@ -0,0 +1,7 @@ +function Get-IcingaWindowsUserMetadata() +{ + return @{ + 'Description' = 'Dedicated user for Icinga for Windows with limited privileges. The user is only allowed to be used as service user, while local login or RDP sessions are disabled. For monitoring, this user requires a valid JEA profile.'; + 'FullName' = 'Icinga for Windows Monitoring User'; + }; +} diff --git a/lib/core/windows/Install-IcingaSecurity.psm1 b/lib/core/windows/Install-IcingaSecurity.psm1 new file mode 100644 index 0000000..d201c14 --- /dev/null +++ b/lib/core/windows/Install-IcingaSecurity.psm1 @@ -0,0 +1,19 @@ +function Install-IcingaSecurity() +{ + param ( + [string]$IcingaUser = 'icinga', + [switch]$RebuildFramework = $FALSE, + [switch]$AllowScriptBlocks = $FALSE, + [switch]$ConstrainedLanguage = $FALSE + ); + + if ($PSVersionTable.PSVersion -lt (New-IcingaVersionObject -Version 5, 0)) { + Write-IcingaConsoleError 'You cannot use JEA profiles on your system, as your installed PowerShell version "{0}" is lower than minimum required version "5.0"' -Objects $PSVersionTable.PSVersion; + return; + } + + Install-IcingaServiceUser -IcingaUser $IcingaUser; + Install-IcingaJEAProfile -IcingaUser $IcingaUser -RebuildFramework:$RebuildFramework -AllowScriptBlocks:$AllowScriptBlocks -ConstrainedLanguage:$ConstrainedLanguage; + + Restart-IcingaWindowsService; +} diff --git a/lib/core/windows/Install-IcingaServiceUser.psm1 b/lib/core/windows/Install-IcingaServiceUser.psm1 new file mode 100644 index 0000000..9d1b862 --- /dev/null +++ b/lib/core/windows/Install-IcingaServiceUser.psm1 @@ -0,0 +1,33 @@ +function Install-IcingaServiceUser() +{ + param ( + $IcingaUser = 'icinga' + ); + + if ([string]::IsNullOrEmpty($IcingaUser)) { + Write-IcingaConsoleError 'The provided user cannot be empty.'; + return; + } + + Write-IcingaConsoleNotice 'Installing user "{0}"' -Objects $IcingaUser; + + $User = New-IcingaWindowsUser -IcingaUser $IcingaUser; + + Start-Sleep -Seconds 2; + + Set-IcingaPowerShellConfig -Path 'Framework.Icinga.ServiceUser' -Value $User.User; + + Set-IcingaServiceUser -User $IcingaUser -Password $Global:Icinga.ServiceUserPassword -Service 'icinga2' | Out-Null; + Set-IcingaServiceUser -User $IcingaUser -Password $Global:Icinga.ServiceUserPassword -Service 'icingapowershell' | Out-Null; + + Update-IcingaWindowsUserPermission -SID $User.SID; + + Set-IcingaUserPermissions -IcingaUser $IcingaUser; + + Restart-IcingaService 'icinga2'; + Restart-IcingaWindowsService; + + Clear-IcingaWindowsUserPassword; + + Write-IcingaConsoleNotice 'User "{0}" including permissions was successfully installed on this host' -Objects $IcingaUser; +} diff --git a/lib/core/windows/New-IcingaWindowsUser.psm1 b/lib/core/windows/New-IcingaWindowsUser.psm1 new file mode 100644 index 0000000..366af65 --- /dev/null +++ b/lib/core/windows/New-IcingaWindowsUser.psm1 @@ -0,0 +1,72 @@ +function New-IcingaWindowsUser() +{ + param ( + $IcingaUser = 'icinga' + ); + + if ((Test-AdministrativeShell) -eq $FALSE) { + Write-IcingaConsoleError 'For this command you require to run an Admin shell'; + + return @{ + 'User' = $null; + 'SID' = $null; + }; + } + + $UserMetadata = Get-IcingaWindowsUserMetadata; + $UserConfig = Get-IcingaWindowsInformation -Class 'Win32_UserAccount' | Where-Object { $_.Name -eq $IcingaUser }; + + if ($null -ne $UserConfig) { + + # User already exist -> override password - but only if the user is entirely managed by Icinga + if ($UserConfig.FullName -eq $UserMetadata.FullName -And $UserConfig.Description -eq $UserMetadata.Description) { + $Result = Start-IcingaProcess -Executable 'net' -Arguments ([string]::Format('user "{0}" "{1}"', $IcingaUser, (ConvertFrom-IcingaSecureString -SecureString (New-IcingaWindowsUserPassword)))); + + if ($Result.ExitCode -ne 0) { + Write-IcingaConsoleError 'Failed to update password for user "{0}": {1}' -Objects $IcingaUser, $Result.Error; + + return @{ + 'User' = $UserConfig.Caption; + 'SID' = $UserConfig.SID; + }; + } + + Write-IcingaConsoleNotice 'User updated successfully.'; + } + + return @{ + 'User' = $UserConfig.Caption; + 'SID' = $UserConfig.SID; + }; + } + + # Access our local Account Database + $AccountDB = [ADSI]"WinNT://$Env:COMPUTERNAME,Computer"; + $IcingaUserObject = $AccountDB.Create("User", $IcingaUser); + $IcingaUserObject.SetPassword((ConvertFrom-IcingaSecureString -SecureString (New-IcingaWindowsUserPassword))); + $IcingaUserObject.SetInfo(); + $IcingaUserObject.FullName = $UserMetadata.FullName; + $IcingaUserObject.SetInfo(); + $IcingaUserObject.Description = $UserMetadata.Description; + $IcingaUserObject.SetInfo(); + $IcingaUserObject.UserFlags = 65600; + $IcingaUserObject.SetInfo(); + + # Add to local user group + <# This is not required, but let's leave it here for possible later lookup on how this works + $SIDLocalGroup = New-Object System.Security.Principal.SecurityIdentifier ("S-1-5-32-545"); + $LocalGroup = ($SIDLocalGroup.Translate([System.Security.Principal.NTAccount])).Value.Split('\')[1]; + + $LocalUserGroup = [ADSI]"WinNT://$Env:COMPUTERNAME/$LocalGroup,group"; + $LocalUserGroup.Add("WinNT://$Env:COMPUTERNAME/$IcingaUser,user") + #> + + $UserConfig = Get-IcingaWindowsInformation -Class 'Win32_UserAccount' | Where-Object { $_.Name -eq $IcingaUser }; + + Write-IcingaConsoleNotice 'User was successfully created.'; + + return @{ + 'User' = $UserConfig.Caption; + 'SID' = $UserConfig.SID; + }; +} diff --git a/lib/core/windows/New-IcingaWindowsUserPassword.psm1 b/lib/core/windows/New-IcingaWindowsUserPassword.psm1 new file mode 100644 index 0000000..a73fe98 --- /dev/null +++ b/lib/core/windows/New-IcingaWindowsUserPassword.psm1 @@ -0,0 +1,17 @@ +function New-IcingaWindowsUserPassword() +{ + if ($null -eq $Global:Icinga) { + $Global:Icinga = @{ + 'ServiceUserPassword' = $null + }; + } + + if ($Global:Icinga.ContainsKey('ServiceUserPassword') -eq $FALSE) { + $Global:Icinga.Add('ServiceUserPassword', $null); + } + + [SecureString]$Password = ConvertTo-IcingaSecureString -String (Get-IcingaRandomChars -Count 60); + $Global:Icinga.ServiceUserPassword = $Password; + + return $Password; +} diff --git a/lib/core/windows/Remove-IcingaWindowsUser.psm1 b/lib/core/windows/Remove-IcingaWindowsUser.psm1 new file mode 100644 index 0000000..ab23a7a --- /dev/null +++ b/lib/core/windows/Remove-IcingaWindowsUser.psm1 @@ -0,0 +1,28 @@ +function Remove-IcingaWindowsUser() +{ + param ( + $IcingaUser = 'icinga' + ); + + $UserConfig = Get-IcingaWindowsInformation -Class 'Win32_UserAccount' | Where-Object { $_.Name -eq $IcingaUser }; + + if ((Test-IcingaManagedUser -IcingaUser $IcingaUser) -eq $FALSE) { + Write-IcingaConsoleNotice 'The user "{0}" is not present or not created by Icinga for Windows. Unable to remove user' -Objects $IcingaUser; + return; + } + + $Result = Start-IcingaProcess -Executable 'net' -Arguments ([string]::Format('user "{0}" /DELETE', $IcingaUser)); + + if ($Result.ExitCode -ne 0) { + Write-IcingaConsoleError 'Failed to delete user "{0}": {1}' -Objects $IcingaUser, $Result.Error; + } else { + # Delete Home Directory + $HomePath = Join-Path -Path ($ENV:HOMEDRIVE) -ChildPath (Join-Path -Path '\Users\' -ChildPath $IcingaUser); + Remove-ItemSecure -Path $HomePath -Recurse -Force; + } + + return @{ + 'User' = $UserConfig.Caption; + 'SID' = $UserConfig.SID; + }; +} diff --git a/lib/core/windows/Restart-IcingaWindowsService.psm1 b/lib/core/windows/Restart-IcingaWindowsService.psm1 new file mode 100644 index 0000000..41f4a1e --- /dev/null +++ b/lib/core/windows/Restart-IcingaWindowsService.psm1 @@ -0,0 +1,12 @@ +function Restart-IcingaWindowsService() +{ + [string]$JeaPid = Get-IcingaJEAServicePid; + + Stop-IcingaService -Service 'icingapowershell'; + + if ((Test-IcingaJEAServiceRunning -JeaPid $JeaPid)) { + Stop-Process -Id $JeaPid -Force; + } + + Restart-IcingaService -Service 'icingapowershell'; +} diff --git a/lib/core/windows/Stop-IcingaWindowsService.psm1 b/lib/core/windows/Stop-IcingaWindowsService.psm1 new file mode 100644 index 0000000..cb89eef --- /dev/null +++ b/lib/core/windows/Stop-IcingaWindowsService.psm1 @@ -0,0 +1,10 @@ +function Stop-IcingaWindowsService() +{ + [string]$JeaPid = Get-IcingaJEAServicePid; + + Stop-IcingaService -Service 'icingapowershell'; + + if ((Test-IcingaJEAServiceRunning -JeaPid $JeaPid)) { + Stop-Process -Id $JeaPid -Force; + } +} diff --git a/lib/core/windows/Test-IcingaManagedUser.psm1 b/lib/core/windows/Test-IcingaManagedUser.psm1 new file mode 100644 index 0000000..4a174d9 --- /dev/null +++ b/lib/core/windows/Test-IcingaManagedUser.psm1 @@ -0,0 +1,27 @@ +function Test-IcingaManagedUser() +{ + param ( + [string]$IcingaUser, + [string]$SID + ); + + $UserData = Get-IcingaWindowsInformation -Class 'Win32_UserAccount' | Where-Object { $_.Name -eq $IcingaUser }; + $FullUserData = Get-IcingaWindowsInformation -Class 'Win32_UserAccount' | Where-Object { $_.Caption.ToLower() -eq $IcingaUser.ToLower() }; + + if ($null -eq $FullUserData -And $null -eq $UserData -And [string]::IsNullOrEmpty($SID)) { + return $FALSE; + } + + if ([string]::IsNullOrEmpty($SID)) { + $SID = Get-IcingaUserSID -User $IcingaUser; + } + + $UserConfig = Get-IcingaWindowsInformation -Class 'Win32_UserAccount' | Where-Object { $_.SID -eq $SID }; + $UserMetadata = Get-IcingaWindowsUserMetadata; + + if ($null -eq $UserConfig -Or $UserConfig.FullName -ne $UserMetadata.FullName -Or $UserConfig.Description -ne $UserMetadata.Description) { + return $FALSE; + } + + return $TRUE; +} diff --git a/lib/core/windows/Uninstall-IcingaSecurity.psm1 b/lib/core/windows/Uninstall-IcingaSecurity.psm1 new file mode 100644 index 0000000..bf54872 --- /dev/null +++ b/lib/core/windows/Uninstall-IcingaSecurity.psm1 @@ -0,0 +1,9 @@ +function Uninstall-IcingaSecurity() +{ + param ( + $IcingaUser = 'icinga' + ); + + Uninstall-IcingaServiceUser -IcingaUser $IcingaUser; + Uninstall-IcingaJEAProfile; +} diff --git a/lib/core/windows/Uninstall-IcingaServiceUser.psm1 b/lib/core/windows/Uninstall-IcingaServiceUser.psm1 new file mode 100644 index 0000000..41bfec8 --- /dev/null +++ b/lib/core/windows/Uninstall-IcingaServiceUser.psm1 @@ -0,0 +1,31 @@ +function Uninstall-IcingaServiceUser() +{ + param ( + $IcingaUser = 'icinga' + ); + + if ([string]::IsNullOrEmpty($IcingaUser)) { + Write-IcingaConsoleError 'The provided user cannot be empty.'; + return; + } + + Write-IcingaConsoleNotice 'Uninstalling user "{0}"' -Objects $IcingaUser; + + Stop-IcingaService 'icinga2'; + Stop-IcingaWindowsService; + + Set-IcingaPowerShellConfig -Path 'Framework.Icinga.ServiceUser' -Value ''; + + Set-IcingaServiceUser -User 'NT Authority\NetworkService' -Service 'icinga2' | Out-Null; + Set-IcingaServiceUser -User 'NT Authority\NetworkService' -Service 'icingapowershell' | Out-Null; + + Set-IcingaUserPermissions -IcingaUser $IcingaUser -Remove; + + $UserConfig = Remove-IcingaWindowsUser -IcingaUser $IcingaUser; + Update-IcingaWindowsUserPermission -SID $UserConfig.SID -Remove; + + Restart-IcingaService 'icinga2'; + Restart-IcingaWindowsService; + + Write-IcingaConsoleNotice 'User "{0}" including permissions was removed from this host' -Objects $IcingaUser; +} diff --git a/lib/core/windows/Update-IcingaServiceUser.psm1 b/lib/core/windows/Update-IcingaServiceUser.psm1 new file mode 100644 index 0000000..e62b90b --- /dev/null +++ b/lib/core/windows/Update-IcingaServiceUser.psm1 @@ -0,0 +1,22 @@ +function Update-IcingaServiceUser() +{ + $IcingaUser = Get-IcingaPowerShellConfig -Path 'Framework.Icinga.ServiceUser'; + + if ([string]::IsNullOrEmpty($IcingaUser)) { + return; + } + + if ((Test-IcingaManagedUser -IcingaUser $IcingaUser) -eq $FALSE) { + return; + } + + $SID = Get-IcingaUserSID -User $IcingaUser; + $UserConfig = Get-IcingaWindowsInformation -Class 'Win32_UserAccount' | Where-Object { $_.SID -eq $SID }; + $User = New-IcingaWindowsUser -IcingaUser $UserConfig.Name; + + Set-IcingaServiceUser -User $IcingaUser -Password $Global:Icinga.ServiceUserPassword -Service 'icinga2' | Out-Null; + Set-IcingaServiceUser -User $IcingaUser -Password $Global:Icinga.ServiceUserPassword -Service 'icingapowershell' | Out-Null; + + Restart-IcingaService 'icinga2'; + Restart-IcingaWindowsService; +} diff --git a/lib/core/windows/Update-IcingaWindowsUserPermission.psm1 b/lib/core/windows/Update-IcingaWindowsUserPermission.psm1 new file mode 100644 index 0000000..040c035 --- /dev/null +++ b/lib/core/windows/Update-IcingaWindowsUserPermission.psm1 @@ -0,0 +1,74 @@ +function Update-IcingaWindowsUserPermission() +{ + param ( + [string]$SID = '', + [switch]$Remove = $FALSE + ); + + if ([string]::IsNullOrEmpty($SID)) { + Write-IcingaConsoleError 'You have to specify the SID of the user to set the security profile to'; + return; + } + + if ($SID.Length -le 16) { + Write-IcingaConsoleWarning 'It seems the provided SID "{0}" is a system SID. Skipping permission update' -Objects $SID; + return; + } + + if ((Test-IcingaManagedUser -SID $SID) -eq $FALSE) { + Write-IcingaConsoleWarning 'This user is not managed by Icinga directly. Skipping permission update'; + return; + } + + $UpdatedProfile = New-IcingaTemporaryFile; + $SystemOutput = Start-IcingaProcess -Executable 'secedit.exe' -Arguments ([string]::Format('/export /cfg "{0}.inf"', $UpdatedProfile)); + $NewSecurityProfile = @(); + + if ($SystemOutput.ExitCode -ne 0) { + throw ([string]::Format('Unable to fetch security profile: {0}', $SystemOutput.Message)); + return; + } + + $SecurityProfile = ''; + + if ($Remove -eq $FALSE) { + $SecurityProfile = Get-Content "$UpdatedProfile.inf"; + + foreach ($line in $SecurityProfile) { + if ($line -like '*SeServiceLogonRight*') { + $line = [string]::Format('{0},*{1}', $line, $SID); + } + if ($line -like '*SeDenyNetworkLogonRight*') { + $line = [string]::Format('{0},*{1}', $line, $SID); + } + if ($line -like '*SeDenyInteractiveLogonRight*') { + $line = [string]::Format('{0},*{1}', $line, $SID); + } + + $NewSecurityProfile += $line; + } + } else { + $SecurityProfile = Get-Content "$UpdatedProfile.inf" -Raw; + $SecurityProfile = $SecurityProfile.Replace([string]::Format(',*{0}', $SID), ''); + $SecurityProfile = $SecurityProfile.Replace([string]::Format('*{0},', $SID), ''); + $NewSecurityProfile = $SecurityProfile; + } + + Set-Content -Path "$UpdatedProfile.inf" -Value $NewSecurityProfile; + + $SystemOutput = Start-IcingaProcess -Executable 'secedit.exe' -Arguments ([string]::Format('/import /cfg "{0}.inf" /db "{0}.sdb"', $UpdatedProfile)); + + if ($SystemOutput.ExitCode -ne 0) { + throw ([string]::Format('Unable to import security profile: {0}', $SystemOutput.Message)); + return; + } + + $SystemOutput = Start-IcingaProcess -Executable 'secedit.exe' -Arguments ([string]::Format('/configure /cfg "{0}.inf" /db "{0}.sdb"', $UpdatedProfile)); + + if ($SystemOutput.ExitCode -ne 0) { + throw ([string]::Format('Unable to configure security profile: {0}', $SystemOutput.Message)); + return; + } + + Remove-Item $UpdatedProfile*; +} diff --git a/lib/daemon/Add-IcingaForWindowsDaemon.psm1 b/lib/daemon/Add-IcingaForWindowsDaemon.psm1 new file mode 100644 index 0000000..895bc95 --- /dev/null +++ b/lib/daemon/Add-IcingaForWindowsDaemon.psm1 @@ -0,0 +1,32 @@ +function Add-IcingaForWindowsDaemon() +{ + param ( + $IcingaDaemonData + ); + + Use-Icinga -LibOnly -Daemon; + $Global:IcingaDaemonData = $IcingaDaemonData; + + try { + $EnabledDaemons = Get-IcingaBackgroundDaemons; + + foreach ($daemon in $EnabledDaemons.Keys) { + Write-IcingaDebugMessage -Message 'Trying to enable background daemon' -Objects $daemon; + if (-Not (Test-IcingaFunction $daemon)) { + Write-IcingaEventMessage -EventId 1400 -Namespace 'Framework' $daemon; + continue; + } + + $daemonArgs = $EnabledDaemons[$daemon]; + Write-IcingaDebugMessage -Message 'Starting background daemon' -Objects $daemon, $daemonArgs; + + & $daemon @daemonArgs; + } + } catch { + # Todo: Add exception handling + } + + while ($TRUE) { + Start-Sleep -Seconds 1; + } +} diff --git a/lib/daemon/Start-IcingaPowerShellDaemon.psm1 b/lib/daemon/Start-IcingaPowerShellDaemon.psm1 index 1f1482c..4f95876 100644 --- a/lib/daemon/Start-IcingaPowerShellDaemon.psm1 +++ b/lib/daemon/Start-IcingaPowerShellDaemon.psm1 @@ -1,44 +1,96 @@ function Start-IcingaPowerShellDaemon() { - param( - [switch]$RunAsService + param ( + [switch]$RunAsService = $FALSE, + [switch]$JEARestart = $FALSE ); - $ScriptBlock = { - param($IcingaDaemonData); - - Use-Icinga -LibOnly -Daemon; - - try { - $EnabledDaemons = Get-IcingaBackgroundDaemons; - - foreach ($daemon in $EnabledDaemons.Keys) { - if (-Not (Test-IcingaFunction $daemon)) { - continue; - } - - $daemonArgs = $EnabledDaemons[$daemon]; - &$daemon @daemonArgs; - } - } catch { - # Todo: Add exception handling - } - - while ($TRUE) { - Start-Sleep -Seconds 1; - } - }; + Use-Icinga; $global:IcingaDaemonData.FrameworkRunningAsDaemon = $TRUE; - $global:IcingaDaemonData.Add('BackgroundDaemon', [hashtable]::Synchronized(@{})); - # Todo: Add config for active background tasks. Set it to 20 for the moment - $global:IcingaDaemonData.IcingaThreadPool.Add('BackgroundPool', (New-IcingaThreadPool -MaxInstances 20)); - $global:IcingaDaemonData.Add('Config', (Read-IcingaPowerShellConfig)); - New-IcingaThreadInstance -Name "Icinga_PowerShell_Background_Daemon" -ThreadPool $IcingaDaemonData.IcingaThreadPool.BackgroundPool -ScriptBlock $ScriptBlock -Arguments @( $global:IcingaDaemonData ) -Start; + [string]$MainServicePidFile = (Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'service.pid'); + [string]$JeaPidFile = (Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'jea.pid'); + [string]$JeaProfile = Get-IcingaPowerShellConfig -Path 'Framework.JEAProfile'; + [Security.Cryptography.X509Certificates.X509Certificate2]$Certificate = Get-IcingaForWindowsCertificate; + [string]$JeaPid = ''; + + if (Test-IcingaJEAServiceRunning) { + Write-IcingaEventMessage -EventId 1503 -Namespace 'Framework'; + exit 1; + } + + Write-IcingaFileSecure -File ($MainServicePidFile) -Value $PID; + + if ([string]::IsNullOrEmpty($JeaProfile)) { + Write-IcingaDebugMessage -Message 'Starting Icinga for Windows service without JEA context' -Objects $RunAsService, $JEARestart, $JeaProfile; + + $global:IcingaDaemonData.FrameworkRunningAsDaemon = $TRUE; + $global:IcingaDaemonData.Add('BackgroundDaemon', [hashtable]::Synchronized(@{ })); + # Todo: Add config for active background tasks. Set it to 20 for the moment + $global:IcingaDaemonData.IcingaThreadPool.Add('BackgroundPool', (New-IcingaThreadPool -MaxInstances 20)); + $global:IcingaDaemonData.Add('SSLCertificate', $Certificate); + + New-IcingaThreadInstance -Name "Icinga_PowerShell_Background_Daemon" -ThreadPool $IcingaDaemonData.IcingaThreadPool.BackgroundPool -Command 'Add-IcingaForWindowsDaemon' -CmdParameters @{ 'IcingaDaemonData' = $global:IcingaDaemonData } -Start; + } else { + Write-IcingaDebugMessage -Message 'Starting Icinga for Windows service inside JEA context' -Objects $RunAsService, $JEARestart, $JeaProfile; + & powershell.exe -NoProfile -NoLogo -ConfigurationName $JeaProfile -Command { + try { + Use-Icinga; + + Write-IcingaFileSecure -File ($args[1]) -Value $PID; + + $Global:IcingaDaemonData.JEAContext = $TRUE; + $global:IcingaDaemonData.FrameworkRunningAsDaemon = $TRUE; + $global:IcingaDaemonData.Add('BackgroundDaemon', [hashtable]::Synchronized(@{ })); + # Todo: Add config for active background tasks. Set it to 20 for the moment + $global:IcingaDaemonData.IcingaThreadPool.Add('BackgroundPool', (New-IcingaThreadPool -MaxInstances 20)); + $global:IcingaDaemonData.Add('SSLCertificate', ($args[0])); + + New-IcingaThreadInstance -Name "Icinga_PowerShell_Background_Daemon" -ThreadPool $IcingaDaemonData.IcingaThreadPool.BackgroundPool -Command 'Add-IcingaForWindowsDaemon' -CmdParameters @{ 'IcingaDaemonData' = $global:IcingaDaemonData } -Start; + + while ($TRUE) { + Start-Sleep -Seconds 100; + } + } catch { + $CallStack = @(); + foreach ($entry in (Get-PSCallStack)) { + $CallStack += [string]::Format('{0} => Line {1}', $entry.FunctionName, $entry.ScriptLineNumber); + } + Write-IcingaEventMessage -EventId 1600 -Namespace Framework -Objects $_.Exception.Message, $_.Exception.StackTrace, $CallStack; + } + } -Args $Certificate, $JeaPidFile; + } + + if ($JEARestart) { + return; + } if ($RunAsService) { + [int]$JeaRestartCounter = 1; while ($TRUE) { + if ([string]::IsNullOrEmpty($JeaProfile) -eq $FALSE) { + if ([string]::IsNullOrEmpty($JeaPid)) { + [string]$JeaPid = Get-IcingaJEAServicePid; + } + + if ((Test-IcingaJEAServiceRunning -JeaPid $JeaPid) -eq $FALSE) { + if ($JeaRestartCounter -gt 5) { + Write-IcingaEventMessage -EventId 1504 -Namespace Framework; + exit 1; + } + + Write-IcingaFileSecure -File $JeaPidFile -Value ''; + Write-IcingaEventMessage -EventId 1505 -Namespace Framework -Objects ([string]::Format('{0}/5', $JeaRestartCounter)); + Start-IcingaPowerShellDaemon -RunAsService:$RunAsService -JEARestart; + + $JeaRestartCounter += 1; + $JeaPid = ''; + } + + Start-Sleep -Seconds 5; + continue; + } Start-Sleep -Seconds 100; } } diff --git a/lib/daemons/ServiceCheckDaemon/Add-IcingaServiceCheckTask.psm1 b/lib/daemons/ServiceCheckDaemon/Add-IcingaServiceCheckTask.psm1 new file mode 100644 index 0000000..8649ea9 --- /dev/null +++ b/lib/daemons/ServiceCheckDaemon/Add-IcingaServiceCheckTask.psm1 @@ -0,0 +1,164 @@ +function Add-IcingaServiceCheckTask() +{ + param ( + $IcingaDaemonData, + $CheckCommand, + $Arguments, + $Interval, + $TimeIndexes, + $CheckId + ); + + Use-Icinga -LibOnly -Daemon; + + $PassedTime = 0; + $SortedResult = $null; + $PerfCache = @{ }; + $AverageCalc = @{ }; + [int]$MaxTime = 0; + + # Initialise some global variables we use to actually store check result data from + # plugins properly. This is doable from each thread instance as this part isn't + # shared between daemons + New-IcingaCheckSchedulerEnvironment; + + foreach ($index in $TimeIndexes) { + # Only allow numeric index values + if ((Test-Numeric $index) -eq $FALSE) { + continue; + } + if ($AverageCalc.ContainsKey([string]$index) -eq $FALSE) { + $AverageCalc.Add( + [string]$index, + @{ + 'Interval' = ([int]$index); + 'Time' = ([int]$index * 60); + 'Sum' = 0; + 'Count' = 0; + } + ); + } + if ($MaxTime -le [int]$index) { + $MaxTime = [int]$index; + } + } + + [int]$MaxTimeInSeconds = $MaxTime * 60; + + if (-Not ($global:Icinga.CheckData.ContainsKey($CheckCommand))) { + $global:Icinga.CheckData.Add($CheckCommand, @{ }); + $global:Icinga.CheckData[$CheckCommand].Add('results', @{ }); + $global:Icinga.CheckData[$CheckCommand].Add('average', @{ }); + } + + $LoadedCacheData = Get-IcingaCacheData -Space 'sc_daemon' -CacheStore 'checkresult_store' -KeyName $CheckCommand; + + if ($null -ne $LoadedCacheData) { + foreach ($entry in $LoadedCacheData.PSObject.Properties) { + $global:Icinga.CheckData[$CheckCommand]['results'].Add( + $entry.name, + @{ } + ); + foreach ($item in $entry.Value.PSObject.Properties) { + $global:Icinga.CheckData[$CheckCommand]['results'][$entry.name].Add( + $item.Name, + $item.Value + ); + } + } + } + + while ($TRUE) { + if ($PassedTime -ge $Interval) { + try { + & $CheckCommand @Arguments | Out-Null; + } catch { + # Just for debugging. Not required in production or usable at all + $ErrMsg = $_.Exception.Message; + Write-IcingaConsoleError $ErrMsg; + } + + try { + $UnixTime = Get-IcingaUnixTime; + + foreach ($result in $global:Icinga.CheckData[$CheckCommand]['results'].Keys) { + [string]$HashIndex = $result; + $SortedResult = $global:Icinga.CheckData[$CheckCommand]['results'][$HashIndex].GetEnumerator() | Sort-Object name -Descending; + Add-IcingaHashtableItem -Hashtable $PerfCache -Key $HashIndex -Value @{ } | Out-Null; + + foreach ($timeEntry in $SortedResult) { + + if ((Test-Numeric $timeEntry.Value) -eq $FALSE) { + continue; + } + + foreach ($calc in $AverageCalc.Keys) { + if (($UnixTime - $AverageCalc[$calc].Time) -le [int]$timeEntry.Key) { + $AverageCalc[$calc].Sum += $timeEntry.Value; + $AverageCalc[$calc].Count += 1; + } + } + if (($UnixTime - $MaxTimeInSeconds) -le [int]$timeEntry.Key) { + Add-IcingaHashtableItem -Hashtable $PerfCache[$HashIndex] -Key ([string]$timeEntry.Key) -Value ([string]$timeEntry.Value) | Out-Null; + } + } + + foreach ($calc in $AverageCalc.Keys) { + if ($AverageCalc[$calc].Count -ne 0) { + $AverageValue = ($AverageCalc[$calc].Sum / $AverageCalc[$calc].Count); + [string]$MetricName = Format-IcingaPerfDataLabel ( + [string]::Format('{0}_{1}', $HashIndex, $AverageCalc[$calc].Interval) + ); + + Add-IcingaHashtableItem ` + -Hashtable $global:Icinga.CheckData[$CheckCommand]['average'] ` + -Key $MetricName -Value $AverageValue -Override | Out-Null; + } + + $AverageCalc[$calc].Sum = 0; + $AverageCalc[$calc].Count = 0; + } + } + + # Flush data we no longer require in our cache to free memory + [array]$CheckStores = $global:Icinga.CheckData[$CheckCommand]['results'].Keys; + + foreach ($CheckStore in $CheckStores) { + [string]$CheckKey = $CheckStore; + [array]$CheckTimeStamps = $global:Icinga.CheckData[$CheckCommand]['results'][$CheckKey].Keys; + + foreach ($TimeSample in $CheckTimeStamps) { + if (($UnixTime - $MaxTimeInSeconds) -gt [int]$TimeSample) { + Remove-IcingaHashtableItem -Hashtable $global:Icinga.CheckData[$CheckCommand]['results'][$CheckKey] -Key ([string]$TimeSample); + } + } + } + + Set-IcingaCacheData -Space 'sc_daemon' -CacheStore 'checkresult' -KeyName $CheckCommand -Value $global:Icinga.CheckData[$CheckCommand]['average']; + # Write collected metrics to disk in case we reload the daemon. We will load them back into the module after reload then + Set-IcingaCacheData -Space 'sc_daemon' -CacheStore 'checkresult_store' -KeyName $CheckCommand -Value $PerfCache; + } catch { + # Just for debugging. Not required in production or usable at all + $ErrMsg = $_.Exception.Message; + Write-IcingaConsoleError 'Failed to handle check result processing: {0}' -Objects $ErrMsg; + } + + # Cleanup the error stack and remove not required data + $Error.Clear(); + + # Always ensure our check data is cleared regardless of possible + # exceptions which might occur + Get-IcingaCheckSchedulerPerfData | Out-Null; + Get-IcingaCheckSchedulerPluginOutput | Out-Null; + + $PassedTime = 0; + $SortedResult.Clear(); + $PerfCache.Clear(); + } + + $PassedTime += 1; + Start-Sleep -Seconds 1; + # Force PowerShell to call the garbage collector to free memory + [System.GC]::Collect(); + } +} diff --git a/lib/daemons/ServiceCheckDaemon/Start-IcingaServiceCheckDaemon.psm1 b/lib/daemons/ServiceCheckDaemon/Start-IcingaServiceCheckDaemon.psm1 index f650892..3dc127d 100644 --- a/lib/daemons/ServiceCheckDaemon/Start-IcingaServiceCheckDaemon.psm1 +++ b/lib/daemons/ServiceCheckDaemon/Start-IcingaServiceCheckDaemon.psm1 @@ -22,42 +22,45 @@ function Start-IcingaServiceCheckDaemon() { - $ScriptBlock = { - param($IcingaDaemonData); + New-IcingaThreadInstance -Name "Icinga_PowerShell_ServiceCheck_Scheduler" -ThreadPool $IcingaDaemonData.IcingaThreadPool.BackgroundPool -Command 'Add-IcingaServiceCheckDaemon' -CmdParameters @{ 'IcingaDaemonData' = $global:IcingaDaemonData } -Start; +} - Use-Icinga -LibOnly -Daemon; +function Add-IcingaServiceCheckDaemon() +{ + param ( + $IcingaDaemonData + ); - $IcingaDaemonData.IcingaThreadPool.Add('ServiceCheckPool', (New-IcingaThreadPool -MaxInstances (Get-IcingaConfigTreeCount -Path 'BackgroundDaemon.RegisteredServices'))); + Use-Icinga -LibOnly -Daemon; - while ($TRUE) { + $IcingaDaemonData.IcingaThreadPool.Add('ServiceCheckPool', (New-IcingaThreadPool -MaxInstances (Get-IcingaConfigTreeCount -Path 'BackgroundDaemon.RegisteredServices'))); - $RegisteredServices = Get-IcingaRegisteredServiceChecks; + while ($TRUE) { - foreach ($service in $RegisteredServices.Keys) { - [string]$ThreadName = [string]::Format('Icinga_Background_Service_Check_{0}', $service); - if ((Test-IcingaThread $ThreadName)) { - continue; - } + $RegisteredServices = Get-IcingaRegisteredServiceChecks; - [hashtable]$ServiceArgs = @{ }; - - if ($null -ne $RegisteredServices[$service].Arguments) { - foreach ($property in $RegisteredServices[$service].Arguments.PSObject.Properties) { - if ($ServiceArgs.ContainsKey($property.Name)) { - continue; - } - - $ServiceArgs.Add($property.Name, $property.Value) - } - } - - Start-IcingaServiceCheckTask -CheckId $service -CheckCommand $RegisteredServices[$service].CheckCommand -Arguments $ServiceArgs -Interval $RegisteredServices[$service].Interval -TimeIndexes $RegisteredServices[$service].TimeIndexes; + foreach ($service in $RegisteredServices.Keys) { + [string]$ThreadName = [string]::Format('Icinga_Background_Service_Check_{0}', $service); + if ((Test-IcingaThread $ThreadName)) { + continue; } - Start-Sleep -Seconds 1; - } - }; - New-IcingaThreadInstance -Name "Icinga_PowerShell_ServiceCheck_Scheduler" -ThreadPool $IcingaDaemonData.IcingaThreadPool.BackgroundPool -ScriptBlock $ScriptBlock -Arguments @( $global:IcingaDaemonData ) -Start; + [hashtable]$ServiceArgs = @{ }; + + if ($null -ne $RegisteredServices[$service].Arguments) { + foreach ($property in $RegisteredServices[$service].Arguments.PSObject.Properties) { + if ($ServiceArgs.ContainsKey($property.Name)) { + continue; + } + + $ServiceArgs.Add($property.Name, $property.Value) + } + } + + Start-IcingaServiceCheckTask -CheckId $service -CheckCommand $RegisteredServices[$service].CheckCommand -Arguments $ServiceArgs -Interval $RegisteredServices[$service].Interval -TimeIndexes $RegisteredServices[$service].TimeIndexes; + } + Start-Sleep -Seconds 1; + } } function Start-IcingaServiceCheckTask() @@ -72,161 +75,12 @@ function Start-IcingaServiceCheckTask() [string]$ThreadName = [string]::Format('Icinga_Background_Service_Check_{0}', $CheckId); - $ScriptBlock = { - param($IcingaDaemonData, $CheckCommand, $Arguments, $Interval, $TimeIndexes, $CheckId); - - Use-Icinga -LibOnly -Daemon; - $PassedTime = 0; - $SortedResult = $null; - $PerfCache = @{ }; - $AverageCalc = @{ }; - [int]$MaxTime = 0; - - # Initialise some global variables we use to actually store check result data from - # plugins properly. This is doable from each thread instance as this part isn't - # shared between daemons - New-IcingaCheckSchedulerEnvironment; - - foreach ($index in $TimeIndexes) { - # Only allow numeric index values - if ((Test-Numeric $index) -eq $FALSE) { - continue; - } - if ($AverageCalc.ContainsKey([string]$index) -eq $FALSE) { - $AverageCalc.Add( - [string]$index, - @{ - 'Interval' = ([int]$index); - 'Time' = ([int]$index * 60); - 'Sum' = 0; - 'Count' = 0; - } - ); - } - if ($MaxTime -le [int]$index) { - $MaxTime = [int]$index; - } - } - - [int]$MaxTimeInSeconds = $MaxTime * 60; - - if (-Not ($global:Icinga.CheckData.ContainsKey($CheckCommand))) { - $global:Icinga.CheckData.Add($CheckCommand, @{ }); - $global:Icinga.CheckData[$CheckCommand].Add('results', @{ }); - $global:Icinga.CheckData[$CheckCommand].Add('average', @{ }); - } - - $LoadedCacheData = Get-IcingaCacheData -Space 'sc_daemon' -CacheStore 'checkresult_store' -KeyName $CheckCommand; - - if ($null -ne $LoadedCacheData) { - foreach ($entry in $LoadedCacheData.PSObject.Properties) { - $global:Icinga.CheckData[$CheckCommand]['results'].Add( - $entry.name, - @{ } - ); - foreach ($item in $entry.Value.PSObject.Properties) { - $global:Icinga.CheckData[$CheckCommand]['results'][$entry.name].Add( - $item.Name, - $item.Value - ); - } - } - } - - while ($TRUE) { - if ($PassedTime -ge $Interval) { - try { - & $CheckCommand @Arguments | Out-Null; - } catch { - # Just for debugging. Not required in production or usable at all - $ErrMsg = $_.Exception.Message; - Write-IcingaConsoleError $ErrMsg; - } - - try { - $UnixTime = Get-IcingaUnixTime; - - foreach ($result in $global:Icinga.CheckData[$CheckCommand]['results'].Keys) { - [string]$HashIndex = $result; - $SortedResult = $global:Icinga.CheckData[$CheckCommand]['results'][$HashIndex].GetEnumerator() | Sort-Object name -Descending; - Add-IcingaHashtableItem -Hashtable $PerfCache -Key $HashIndex -Value @{ } | Out-Null; - - foreach ($timeEntry in $SortedResult) { - - if ((Test-Numeric $timeEntry.Value) -eq $FALSE) { - continue; - } - - foreach ($calc in $AverageCalc.Keys) { - if (($UnixTime - $AverageCalc[$calc].Time) -le [int]$timeEntry.Key) { - $AverageCalc[$calc].Sum += $timeEntry.Value; - $AverageCalc[$calc].Count += 1; - } - } - if (($UnixTime - $MaxTimeInSeconds) -le [int]$timeEntry.Key) { - Add-IcingaHashtableItem -Hashtable $PerfCache[$HashIndex] -Key ([string]$timeEntry.Key) -Value ([string]$timeEntry.Value) | Out-Null; - } - } - - foreach ($calc in $AverageCalc.Keys) { - if ($AverageCalc[$calc].Count -ne 0) { - $AverageValue = ($AverageCalc[$calc].Sum / $AverageCalc[$calc].Count); - [string]$MetricName = Format-IcingaPerfDataLabel ( - [string]::Format('{0}_{1}', $HashIndex, $AverageCalc[$calc].Interval) - ); - - Add-IcingaHashtableItem ` - -Hashtable $global:Icinga.CheckData[$CheckCommand]['average'] ` - -Key $MetricName -Value $AverageValue -Override | Out-Null; - } - - $AverageCalc[$calc].Sum = 0; - $AverageCalc[$calc].Count = 0; - } - } - - # Flush data we no longer require in our cache to free memory - [array]$CheckStores = $global:Icinga.CheckData[$CheckCommand]['results'].Keys; - - foreach ($CheckStore in $CheckStores) { - [string]$CheckKey = $CheckStore; - [array]$CheckTimeStamps = $global:Icinga.CheckData[$CheckCommand]['results'][$CheckKey].Keys; - - foreach ($TimeSample in $CheckTimeStamps) { - if (($UnixTime - $MaxTimeInSeconds) -gt [int]$TimeSample) { - Remove-IcingaHashtableItem -Hashtable $global:Icinga.CheckData[$CheckCommand]['results'][$CheckKey] -Key ([string]$TimeSample); - } - } - } - - Set-IcingaCacheData -Space 'sc_daemon' -CacheStore 'checkresult' -KeyName $CheckCommand -Value $global:Icinga.CheckData[$CheckCommand]['average']; - # Write collected metrics to disk in case we reload the daemon. We will load them back into the module after reload then - Set-IcingaCacheData -Space 'sc_daemon' -CacheStore 'checkresult_store' -KeyName $CheckCommand -Value $PerfCache; - } catch { - # Just for debugging. Not required in production or usable at all - $ErrMsg = $_.Exception.Message; - Write-IcingaConsoleError 'Failed to handle check result processing: {0}' -Objects $ErrMsg; - } - - # Cleanup the error stack and remove not required data - $Error.Clear(); - - # Always ensure our check data is cleared regardless of possible - # exceptions which might occur - Get-IcingaCheckSchedulerPerfData | Out-Null; - Get-IcingaCheckSchedulerPluginOutput | Out-Null; - - $PassedTime = 0; - $SortedResult.Clear(); - $PerfCache.Clear(); - } - - $PassedTime += 1; - Start-Sleep -Seconds 1; - # Force PowerShell to call the garbage collector to free memory - [System.GC]::Collect(); - } - }; - - New-IcingaThreadInstance -Name $ThreadName -ThreadPool $IcingaDaemonData.IcingaThreadPool.ServiceCheckPool -ScriptBlock $ScriptBlock -Arguments @( $global:IcingaDaemonData, $CheckCommand, $Arguments, $Interval, $TimeIndexes, $CheckId ) -Start; + New-IcingaThreadInstance -Name $ThreadName -ThreadPool $IcingaDaemonData.IcingaThreadPool.ServiceCheckPool -Command 'Add-IcingaServiceCheckTask' -CmdParameters @{ + 'IcingaDaemonData' = $global:IcingaDaemonData; + 'CheckCommand' = $CheckCommand; + 'Arguments' = $Arguments; + 'Interval' = $Interval; + 'TimeIndexes' = $TimeIndexes + 'CheckId' = $CheckId; + } -Start; } diff --git a/lib/icinga/exception/Exit-IcingaThrowCritical.psm1 b/lib/icinga/exception/Exit-IcingaThrowCritical.psm1 index 7abd875..c1f5883 100644 --- a/lib/icinga/exception/Exit-IcingaThrowCritical.psm1 +++ b/lib/icinga/exception/Exit-IcingaThrowCritical.psm1 @@ -25,7 +25,7 @@ function Exit-IcingaThrowCritical() Set-IcingaInternalPluginExitCode -ExitCode $IcingaEnums.IcingaExitCode.Critical; Set-IcingaInternalPluginException -PluginException $OutputMessage; - if ($null -eq $global:IcingaDaemonData -Or $global:IcingaDaemonData.FrameworkRunningAsDaemon -eq $FALSE) { + if ($null -eq $global:IcingaDaemonData -Or ($global:IcingaDaemonData.FrameworkRunningAsDaemon -eq $FALSE -And $global:IcingaDaemonData.JEAContext -eq $FALSE)) { Write-IcingaConsolePlain $OutputMessage; exit $IcingaEnums.IcingaExitCode.Critical; } diff --git a/lib/icinga/exception/Exit-IcingaThrowException.psm1 b/lib/icinga/exception/Exit-IcingaThrowException.psm1 index 86dfbc4..41922b9 100644 --- a/lib/icinga/exception/Exit-IcingaThrowException.psm1 +++ b/lib/icinga/exception/Exit-IcingaThrowException.psm1 @@ -110,7 +110,7 @@ function Exit-IcingaThrowException() Set-IcingaInternalPluginExitCode -ExitCode $IcingaEnums.IcingaExitCode.Unknown; Set-IcingaInternalPluginException -PluginException $OutputMessage; - if ($null -eq $global:IcingaDaemonData -Or $global:IcingaDaemonData.FrameworkRunningAsDaemon -eq $FALSE) { + if ($null -eq $global:IcingaDaemonData -Or ($global:IcingaDaemonData.FrameworkRunningAsDaemon -eq $FALSE -And $global:IcingaDaemonData.JEAContext -eq $FALSE)) { Write-IcingaConsolePlain $OutputMessage; exit $IcingaEnums.IcingaExitCode.Unknown; } diff --git a/lib/icinga/plugin/Exit-IcingaExecutePlugin.psm1 b/lib/icinga/plugin/Exit-IcingaExecutePlugin.psm1 index b25085d..098a907 100644 --- a/lib/icinga/plugin/Exit-IcingaExecutePlugin.psm1 +++ b/lib/icinga/plugin/Exit-IcingaExecutePlugin.psm1 @@ -4,17 +4,52 @@ function Exit-IcingaExecutePlugin() [string]$Command = '' ); + $JEAProfile = Get-IcingaJEAContext; + Invoke-IcingaInternalServiceCall -Command $Command -Arguments $args; try { - # Load the entire framework now, as we require to execute plugins locally - if ($null -eq $global:IcingaDaemonData) { - Use-Icinga; - } - Exit-IcingaPluginNotInstalled -Command $Command; - exit (& $Command @args); + if ([string]::IsNullOrEmpty($JEAProfile) -eq $FALSE) { + $ErrorHandler = '' + $JEARun = ( + & powershell.exe -ConfigurationName $JEAProfile -NoLogo -NoProfile -Command { + Use-Icinga; + + $global:IcingaDaemonData.JEAContext = $TRUE; + + $Command = $args[0]; + $Arguments = $args[1]; + $Output = ''; + + try { + $ExitCode = (& $Command @Arguments); + $Output = (Get-IcingaInternalPluginOutput); + $ExitCode = (Get-IcingaInternalPluginExitCode); + } catch { + $Output = [string]::Format('[UNKNOWN] Icinga Exception: Error while executing plugin in JEA context{0}{0}{1}', (New-IcingaNewLine), $_.Exception.Message); + $ExitCode = 3; + } + + return @{ + 'Output' = $Output; + 'PerfData' = (Get-IcingaCheckSchedulerPerfData) + 'ExitCode' = $ExitCode; + } + } -args $Command, $args + ) 2>$ErrorHandler; + + if ($LASTEXITCODE -ge 0) { + Write-IcingaPluginResult -PluginOutput $JEARun.Output -PluginPerfData $JEARun.PerfData; + exit $JEARun.ExitCode; + } else { + Write-IcingaConsolePlain '[UNKNOWN] Icinga Exception: Unable to start the PowerShell.exe with the provided JEA profile "{0}" for CheckCommand: {1}' -Objects $JEAProfile, $Command; + exit 3; + } + } else { + exit (& $Command @args); + } } catch { $ExMsg = $_.Exception.Message; $StackTrace = $_.ScriptStackTrace; diff --git a/lib/icinga/plugin/Get-IcingaInternalPluginExitCode.psm1 b/lib/icinga/plugin/Get-IcingaInternalPluginExitCode.psm1 new file mode 100644 index 0000000..2a19843 --- /dev/null +++ b/lib/icinga/plugin/Get-IcingaInternalPluginExitCode.psm1 @@ -0,0 +1,4 @@ +function Get-IcingaInternalPluginExitCode() +{ + return $Global:Icinga.PluginExecution.LastExitCode; +} diff --git a/lib/icinga/plugin/Get-IcingaInternalPluginOutput.psm1 b/lib/icinga/plugin/Get-IcingaInternalPluginOutput.psm1 new file mode 100644 index 0000000..fbde917 --- /dev/null +++ b/lib/icinga/plugin/Get-IcingaInternalPluginOutput.psm1 @@ -0,0 +1,8 @@ +function Get-IcingaInternalPluginOutput() +{ + if ([string]::IsNullOrEmpty($Global:Icinga.PluginExecution.PluginException) -eq $FALSE) { + return $Global:Icinga.PluginExecution.PluginException; + } + + return $Global:Icinga.CheckResults; +} diff --git a/lib/icinga/plugin/Write-IcingaPluginOutput.psm1 b/lib/icinga/plugin/Write-IcingaPluginOutput.psm1 index 21a1d05..2beffb1 100644 --- a/lib/icinga/plugin/Write-IcingaPluginOutput.psm1 +++ b/lib/icinga/plugin/Write-IcingaPluginOutput.psm1 @@ -4,7 +4,7 @@ function Write-IcingaPluginOutput() $Output ); - if ($global:IcingaDaemonData.FrameworkRunningAsDaemon -eq $FALSE) { + if ($global:IcingaDaemonData.FrameworkRunningAsDaemon -eq $FALSE -And $global:IcingaDaemonData.JEAContext -eq $FALSE) { if ($null -ne $global:Icinga -And $global:Icinga.Minimal) { Clear-Host; } diff --git a/lib/icinga/plugin/Write-IcingaPluginPerfData.psm1 b/lib/icinga/plugin/Write-IcingaPluginPerfData.psm1 index 40e27ad..20eade9 100644 --- a/lib/icinga/plugin/Write-IcingaPluginPerfData.psm1 +++ b/lib/icinga/plugin/Write-IcingaPluginPerfData.psm1 @@ -21,7 +21,7 @@ function Write-IcingaPluginPerfData() $CheckResultCache = $Global:Icinga.ThresholdCache[$CheckCommand]; - if ($global:IcingaDaemonData.FrameworkRunningAsDaemon -eq $FALSE) { + if ($global:IcingaDaemonData.FrameworkRunningAsDaemon -eq $FALSE -And $global:IcingaDaemonData.JEAContext -eq $FALSE) { [string]$PerfDataOutput = (Get-IcingaPluginPerfDataContent -PerfData $PerformanceData -CheckResultCache $CheckResultCache -IcingaCheck $IcingaCheck); Write-IcingaConsolePlain ([string]::Format('| {0}', $PerfDataOutput)); } else { diff --git a/lib/icinga/plugin/Write-IcingaPluginResult.psm1 b/lib/icinga/plugin/Write-IcingaPluginResult.psm1 new file mode 100644 index 0000000..ad28792 --- /dev/null +++ b/lib/icinga/plugin/Write-IcingaPluginResult.psm1 @@ -0,0 +1,18 @@ +function Write-IcingaPluginResult() +{ + param ( + [string]$PluginOutput = '', + [array]$PluginPerfData = @() + ); + + [string]$CheckResult = $PluginOutput; + + if ($PluginPerfData -ne 0) { + $CheckResult += "`n`r| "; + foreach ($PerfData in $PluginPerfData) { + $CheckResult += $PerfData; + } + } + + Write-IcingaConsolePlain $CheckResult; +} diff --git a/lib/webserver/Get-IcingaForWindowsCertificate.psm1 b/lib/webserver/Get-IcingaForWindowsCertificate.psm1 new file mode 100644 index 0000000..8beeb18 --- /dev/null +++ b/lib/webserver/Get-IcingaForWindowsCertificate.psm1 @@ -0,0 +1,11 @@ +function Get-IcingaForWindowsCertificate() +{ + [string]$CertificateFolder = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'certificate'; + [string]$CertificateFile = Join-Path -Path $CertificateFolder -ChildPath 'icingaforwindows.pfx'; + + if (-Not (Test-Path $CertificateFile)) { + return $null; + } + + return ([Security.Cryptography.X509Certificates.X509Certificate2]::CreateFromCertFile($CertificateFile)); +} diff --git a/lib/webserver/Install-IcingaForWindowsCertificate.psm1 b/lib/webserver/Install-IcingaForWindowsCertificate.psm1 new file mode 100644 index 0000000..64cbd57 --- /dev/null +++ b/lib/webserver/Install-IcingaForWindowsCertificate.psm1 @@ -0,0 +1,59 @@ +function Install-IcingaForWindowsCertificate() +{ + param ( + [string]$CertFile = '', + [string]$CertThumbprint = '' + ); + + [Security.Cryptography.X509Certificates.X509Certificate2]$Certificate = $null; + [string]$CertificateFolder = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'certificate'; + [string]$CertificateFile = Join-Path -Path $CertificateFolder -ChildPath 'icingaforwindows.pfx'; + [bool]$FoundCertificate = $FALSE; + + if (-Not (Test-Path $CertificateFolder)) { + New-Item -ItemType Directory -Path $CertificateFolder -Force | Out-Null; + } + + if (-Not (Test-IcingaAcl -Directory $CertificateFolder)) { + Set-IcingaAcl -Directory $CertificateFolder; + } + + if (Test-Path $CertificateFile) { + Remove-ItemSecure -Path $CertificateFile -Force | Out-Null; + } + + if ([string]::IsNullOrEmpty($CertFile) -eq $FALSE) { + if ([IO.Path]::GetExtension($CertFile) -ne '.pfx') { + ConvertTo-IcingaX509Certificate -CertFile $CertFile -OutFile $CertificateFile -Force | Out-Null; + } else { + Copy-ItemSecure -Path $CertFile -Destination $CertificateFile -Force | Out-Null; + } + } + + if ([string]::IsNullOrEmpty($CertThumbprint) -eq $FALSE) { + $Certificate = Get-ChildItem -Path 'cert:\*' -Include $CertThumbprint -Recurse + + if ($null -ne $Certificate) { + Export-Certificate -Cert $Certificate -FilePath $CertificateFile | Out-Null; + } + } + + if ([string]::IsNullOrEmpty($CertFile) -And [string]::IsNullOrEmpty($CertThumbprint)) { + $IcingaHostCertificate = Get-IcingaAgentHostCertificate; + + if ([string]::IsNullOrEmpty($IcingaHostCertificate.CertFile) -eq $FALSE) { + $LocalCert = ConvertTo-IcingaX509Certificate -CertFile $IcingaHostCertificate.CertFile -OutFile $CertificateFile -Force; + + Import-PfxCertificate -FilePath $CertificateFile -CertStoreLocation 'Cert:\LocalMachine\My\' -Exportable | Out-Null; + Remove-ItemSecure -Path $CertificateFile -Force | Out-Null; + $Certificate = Get-ChildItem -Path 'cert:\*' -Include $LocalCert.Thumbprint -Recurse + Export-Certificate -Cert $Certificate -FilePath $CertificateFile | Out-Null; + } + } + + if (Test-Path $CertificateFile) { + Write-IcingaConsoleNotice -Message 'Successfully installed Icinga for Windows certificate at "{0}"' -Objects $CertificateFile; + } else { + Write-IcingaConsoleError -Message 'Unable to install Icinga for Windows certificate, as with specified arguments and auto-lookup for Icinga Agent certificate, no certificate could be created' -Objects $CertificateFile; + } +} diff --git a/lib/wmi/Add-IcingaWmiPermissions.psm1 b/lib/wmi/Add-IcingaWmiPermissions.psm1 index dff4402..6dc6d05 100644 --- a/lib/wmi/Add-IcingaWmiPermissions.psm1 +++ b/lib/wmi/Add-IcingaWmiPermissions.psm1 @@ -100,7 +100,7 @@ function Add-IcingaWmiPermissions() $WmiSecurity.WmiArguments.Name = 'SetSecurityDescriptor'; $WmiSecurity.WmiArguments.Add('ArgumentList', $WmiSecurity.WmiAcl.PSObject.immediateBaseObject); $WmiArguments = $WmiSecurity.WmiArguments; - + $WmiSecurityData = Invoke-WmiMethod @WmiArguments; if ($WmiSecurityData.ReturnValue -ne 0) { Write-IcingaConsoleError 'Failed to set Wmi security descriptor information with error {0}' -Objects $WmiSecurityData.ReturnValue; diff --git a/lib/wmi/Remove-IcingaWmiPermissions.psm1 b/lib/wmi/Remove-IcingaWmiPermissions.psm1 index ae7c91f..bdc785f 100644 --- a/lib/wmi/Remove-IcingaWmiPermissions.psm1 +++ b/lib/wmi/Remove-IcingaWmiPermissions.psm1 @@ -57,7 +57,7 @@ function Remove-IcingaWmiPermissions() $WmiSecurity.WmiArguments.Name = 'SetSecurityDescriptor'; $WmiSecurity.WmiArguments.Add('ArgumentList', $WmiSecurity.WmiAcl.PSObject.immediateBaseObject); $WmiArguments = $WmiSecurity.WmiArguments - + $WmiSecurityData = Invoke-WmiMethod @WmiArguments; if ($WmiSecurityData.ReturnValue -ne 0) { Write-IcingaConsoleError 'Failed to set Wmi security descriptor information with error {0}' -Objects $WmiSecurityData.ReturnValue; diff --git a/templates/IcingaForWindows.psrc.template b/templates/IcingaForWindows.psrc.template new file mode 100644 index 0000000..d10d32d --- /dev/null +++ b/templates/IcingaForWindows.psrc.template @@ -0,0 +1,44 @@ + +@{ + # ID used to uniquely identify this document + GUID = '5d3b7a27-b88e-43b8-9d45-103ab701ff9c' + # Author of this document + Author = 'Lord Hepipud' + # Description of the functionality provided by these settings + Description = 'Icinga for Windows JEA Profile' + # Company associated with this document + CompanyName = 'Icinga GmbH' + # Copyright statement for this document + Copyright = '(c) 2021 Icinga GmbH | MIT' + # Modules to import when applied to a session + ModulesToImport = '' + # Cmdlets to make visible when applied to a session + VisibleCmdlets = '' + # Functions to make visible when applied to a session + VisibleFunctions = '' + # Aliases to be defined when applied to a session + AliasDefinitions = @( + @{ + Name = 'Select-Object'; + Value = 'Microsoft.PowerShell.Utility\Select-Object' + }, + @{ + Name = 'Add-Member'; + Value = 'Microsoft.PowerShell.Utility\Add-Member' + }, + @{ + Name = 'New-Object'; + Value = 'Microsoft.PowerShell.Utility\New-Object' + } + ) + VariableDefinitions = @( + @{ + 'Name' = 'WarningPreference' + 'Value' = 'SilentlyContinue' + }, + @{ + 'Name' = 'ProgressPreference' + 'Value' = 'SilentlyContinue' + } + ) +} diff --git a/templates/IcingaForWindows.pssc.template b/templates/IcingaForWindows.pssc.template new file mode 100644 index 0000000..b220aec --- /dev/null +++ b/templates/IcingaForWindows.pssc.template @@ -0,0 +1,27 @@ +@{ + # Version number of the schema used for this document + SchemaVersion = '2.0.0.0' + # ID used to uniquely identify this document + GUID = '8f47856d-9c17-403e-95fd-743bd15e5095' + # Author of this document + Author = 'Lord Hepipud' + # Description of the functionality provided by these settings + Description = 'Icinga for Windows JEA Profile' + # Company associated with this document + CompanyName = 'Icinga GmbH' + # Define the language the PowerShell JEA session will be started with + LanguageMode = '$POWERSHELLLANGUAGEMODE$' + # Session type defaults to apply for this session configuration. Can be 'RestrictedRemoteServer' (recommended), 'Empty', or 'Default' + SessionType = 'RestrictedRemoteServer' + # Directory to place session transcripts for this session configuration + # TranscriptDirectory = 'C:\Transcripts\' + # Whether to run this session configuration as the machine's (virtual) administrator account + RunAsVirtualAccount = $TRUE + # Scripts to run when applied to a session + # User roles (security groups), and the role capabilities that should be applied to them when applied to a session + RoleDefinitions = @{ + '$ICINGAFORWINDOWSJEAUSER$' = @{ + 'RoleCapabilities' = 'IcingaForWindows' + } + } +}