diff --git a/community_scripts.json b/community_scripts.json index bcbd48ef..c2f4b855 100644 --- a/community_scripts.json +++ b/community_scripts.json @@ -242,6 +242,18 @@ ], "category": "TRMM (Win):TacticalRMM Related" }, + { + "guid": "65a82cdc-1e87-4956-8b43-e1e8a76ebf85", + "filename": "Win_TRMM_Troubleshooting_Agent.ps1", + "submittedBy": "https://github.com/silversword411", + "name": "TacticalRMM - Agent Troubleshooting Script TRMM and Mesh on Windows", + "description": "For troubleshooting problems. If TRMM agent is online you can run thru TRMM otherwise you can save as .ps1 file and run manually. It will create a timestamped log file", + "shell": "powershell", + "supported_platforms": [ + "windows" + ], + "category": "TRMM (Win):TacticalRMM Related" + }, { "guid": "b90fb6a1-cf53-48d4-9747-60dd333c7159", "filename": "Win_TRMM_Mesh_Install.ps1", @@ -902,9 +914,9 @@ "guid": "6c78eb04-57ae-43b0-98ed-cbd3ef9e2f80", "filename": "Win_Chocolatey_Manage_Apps_Bulk.ps1", "submittedBy": "https://github.com/silversword411", - "name": "Chocolatey - Install, Uninstall and Upgrade Software", - "description": "This script installs, uninstalls and updates software using Chocolatey with logic to slow tasks to minimize hitting community limits. Mode install/uninstall/upgrade Hosts x", - "syntax": "-$PackageName \n[-Hosts ]\n[-mode {(install) | upgrade | uninstall}]", + "name": "Chocolatey - Install, Uninstall, List and Upgrade Software", + "description": "This script installs, uninstalls and updates software using Chocolatey with logic to slow tasks to minimize hitting community limits. Mode install/uninstall/upgrade/upgrade-only-installed Hosts x", + "syntax": "-PackageName \n[-Hosts ]\n[-mode {(install) | upgrade | upgrade-only-installed | uninstall | list}]", "shell": "powershell", "category": "TRMM (Win):3rd Party Software>Chocolatey", "supported_platforms": [ @@ -918,7 +930,7 @@ "submittedBy": "https://github.com/dinger1986", "name": "Winget - Install, Uninstall and Upgrade Software", "description": "This script installs, uninstalls and updates software using winget. Mode install/uninstall/upgrade/search", - "syntax": "-$PackageName ]\n[-mode {install | search | upgrade | uninstall }]", + "syntax": "-PackageName ]\n[-mode {install | search | upgrade | uninstall }]", "shell": "powershell", "category": "TRMM (Win):3rd Party Software>WinGet", "supported_platforms": [ @@ -1028,6 +1040,20 @@ ], "default_timeout": 30 }, + { + "guid": "5bc815a0-d349-416f-8c3d-ac499d4da2e8", + "filename": "Win_Reboot.ps1", + "submittedBy": "https://github.com/silversword411", + "name": "Reboot/Restart Computer", + "description": "Reboots/Restarts the computer with an optional wait time before restarting.", + "syntax": "[-wait ]", + "shell": "powershell", + "category": "TRMM (Win):Other", + "supported_platforms": [ + "windows" + ], + "default_timeout": 86400 + }, { "guid": "f396dae2-c768-45c5-bd6c-176e56ed3614", "filename": "Win_Power_RestartorShutdown.ps1", @@ -1064,11 +1090,11 @@ "-serviceName {{client.ScreenConnectService}}", "-url {{client.ScreenConnectInstaller}}", "-clientname {{client.name}}", - "-sitename {{site.name}}", - "-action {(install) | uninstall | start | stop}" + "-sitename {{site.name}}" ], - "default_timeout": "90", + "default_timeout": "120", "shell": "powershell", + "syntax": "-serviceName \n-url \n-clientname \n-sitename \n-action {(install) | uninstall | start | stop}", "supported_platforms": [ "windows" ], diff --git a/community_scripts.schema.json b/community_scripts.schema.json index 4c700e1c..72ef7e22 100644 --- a/community_scripts.schema.json +++ b/community_scripts.schema.json @@ -22,6 +22,17 @@ "type": "string" } }, + "env": { + "description": "The script environmental variables listed as an array.", + "type": "array", + "items": { + "type": "string" + } + }, + "run_as_user": { + "description": "Run this script as the active user as opposed to System (Windows only)", + "type": "boolean" + }, "filename": { "description": "The filename of the script.", "type": "string" diff --git a/scripts/Win_Antivirus_Verify.ps1 b/scripts/Win_Antivirus_Verify.ps1 index a04974d6..a8c6657c 100644 --- a/scripts/Win_Antivirus_Verify.ps1 +++ b/scripts/Win_Antivirus_Verify.ps1 @@ -19,15 +19,16 @@ .NOTES Version 1.0 4/7/2021 silversword - https://mcpforlife.com/2020/04/14/how-to-resolve-this-state-value-of-av-providers/ - https://github.com/wortell/PSHelpers/blob/main/src/Public/Add-ProductStates.ps1 - Call with optional parameter "-antivirusName AntivirusNameHere" in order to check for a specific antivirus - antivirusName must match the "displayName" exactly - If no antivirusName parameter is specified, the tool returns success if there is any active up to date antivirus on the system + https://mcpforlife.com/2020/04/14/how-to-resolve-this-state-value-of-av-providers/ + https://github.com/wortell/PSHelpers/blob/main/src/Public/Add-ProductStates.ps1 + Call with optional parameter "-antivirusName AntivirusNameHere" in order to check for a specific antivirus + antivirusName must match the "displayName" exactly + If no antivirusName parameter is specified, the tool returns success if there is any active up to date antivirus on the system Version 1.1 10/15/2023 dinger1986 - Added in -customfield to write AV name to a customfield + Added in -customfield to write AV name to a customfield - OS Build must be greater than 14393 to support this script. If it's not it returns exit code 2 + OS Build must be greater than 14393 to support this script. If it's not it returns exit code 2 + Version 1.2 7/31/2025 silversword Removing extra text in -customField mode #> param($antivirusName = "*", [switch]$customField) @@ -58,7 +59,7 @@ param($antivirusName = "*", [switch]$customField) function Add-ProductStates { [CmdletBinding()] param ( - # This parameter can be passed from pipeline and can contain and array of collections that contain State or productstate members + # This parameter can be passed from pipeline and can contain and array of collections that contain State or productstate members [Parameter(ValueFromPipeline)] [Microsoft.Management.Infrastructure.CimInstance[]] $Products, @@ -120,18 +121,19 @@ if ([environment]::OSVersion.Version.Build -le 14393) { $return = Get-CimInstance -Namespace root/SecurityCenter2 -className AntivirusProduct | Where-Object { - ($_.displayName -like $antivirusName) -and - (($_.productState -band [ProductFlags]::ProductState) -eq [ProductState]::On) -and - (($_.productState -band [ProductFlags]::SignatureStatus) -eq [SignatureStatus]::UpToDate) + ($_.displayName -like $antivirusName) -and + (($_.productState -band [ProductFlags]::ProductState) -eq [ProductState]::On) -and + (($_.productState -band [ProductFlags]::SignatureStatus) -eq [SignatureStatus]::UpToDate) } -Write-Host "Antivirus selection: $antivirusName" if ($return) { if ($customField) { # Only output the name of the first antivirus $return[0].displayName exit 0 - } else { + } + else { + Write-Host "Antivirus selection: $antivirusName" Write-Host "Antivirus active and up to date" $return } diff --git a/scripts/Win_Bluescreen_Report.ps1 b/scripts/Win_Bluescreen_Report.ps1 index 140c3d16..e1b0662c 100644 --- a/scripts/Win_Bluescreen_Report.ps1 +++ b/scripts/Win_Bluescreen_Report.ps1 @@ -2,32 +2,43 @@ .Synopsis Bluescreen - Reports bluescreens .DESCRIPTION - This will check for Bluescreen events on your system. If parameter provided, goes back that number of days + This script checks for Bluescreen events on your system. If a parameter is provided, it goes back that number of days to check. .EXAMPLE 365 .NOTES v1 bbrendon 2/2021 - v1.1 silversword updating with parameters 11/2021 + v1.1 silversword updating with parameters 11/2021 + v1.2 dinger1986 Updated for improved filtering and structure 11/2024 #> +# Get the parameter (number of days to go back) +$DaysBack = $args[0] -$param1 = $args[0] +# Set error handling preference +$ErrorActionPreference = 'SilentlyContinue' -$ErrorActionPreference = 'silentlycontinue' +# Determine the time range based on the parameter if ($Args.Count -eq 0) { - $TimeSpan = (Get-Date) - (New-TimeSpan -Day 1) -} -else { - $TimeSpan = (Get-Date) - (New-TimeSpan -Day $param1) + $StartTime = (Get-Date).AddDays(-1) +} else { + $StartTime = (Get-Date).AddDays(-[int]$DaysBack) } +# Retrieve Bluescreen events +$BlueScreenEvents = Get-WinEvent -FilterHashtable @{ + LogName = 'Application'; + ID = 1001; + ProviderName = 'Windows Error Reporting'; + Level = 4; + StartTime = $StartTime +} | Where-Object { $_.Message -like "*BlueScreen*" } -if (Get-WinEvent -FilterHashtable @{LogName = 'application'; ID = '1001'; ProviderName = 'Windows Error Reporting'; Level = 4; Data = 'BlueScreen'; StartTime = $TimeSpan }) { - Write-Output "There has been bluescreen events detected on your system" - Get-WinEvent -FilterHashtable @{LogName = 'application'; ID = '1001'; ProviderName = 'Windows Error Reporting'; Level = 4; Data = 'BlueScreen'; StartTime = $TimeSpan } +# Check and output results +if ($BlueScreenEvents) { + Write-Output "There have been Bluescreen events detected on your system:" + $BlueScreenEvents | Format-List TimeCreated, Id, LevelDisplayName, Message exit 1 } else { - Write-Output "No bluescreen events detected in the past 24 hours." + Write-Output "No Bluescreen events detected in the past $((Get-Date) - $StartTime).Days days." exit 0 } - diff --git a/scripts/Win_Chocolatey_List_Installed.bat b/scripts/Win_Chocolatey_List_Installed.bat index 3b6e2bc3..d6e659dd 100644 --- a/scripts/Win_Chocolatey_List_Installed.bat +++ b/scripts/Win_Chocolatey_List_Installed.bat @@ -1,3 +1,5 @@ rem List apps installed by Chocolatey -choco list --local-only \ No newline at end of file +set "chocoExePath=%PROGRAMDATA%\chocolatey\choco.exe" + +"%chocoExePath%" list \ No newline at end of file diff --git a/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 b/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 index fbc18d53..27df7b40 100644 --- a/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 +++ b/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 @@ -1,71 +1,127 @@ <# - .SYNOPSIS - This will install software using the chocolatey, with rate limiting when run with Hosts parameter - .DESCRIPTION - For installing packages using chocolatey. If you're running against more than 10, include the Hosts parameter to limit the speed. If running on more than 30 agents at a time make sure you also change the script timeout setting. - .PARAMETER Mode - 3 options: install (default), uninstall, or upgrade. - .PARAMETER Hosts - Use this to specify the number of computer(s) you're running the command on. This will dynamically introduce waits to try and minimize the chance of hitting rate limits (20/min) on the chocolatey.org site: Hosts 20 - .PARAMETER PackageName - Use this to specify which software('s) to install eg: PackageName googlechrome. You can use multiple values using comma separated. - .EXAMPLE - -Hosts 20 -PackageName googlechrome - -Hosts 30 -PackageName googlechrome,vlc - .EXAMPLE - -Mode upgrade -Hosts 50 - .EXAMPLE - -Mode upgrade -Hosts 50 -PackageName chocolatey - .EXAMPLE - -Mode uninstall -PackageName googlechrome - .NOTES - 9/2021 v1 Initial release by @silversword411 and @bradhawkins - 11/14/2021 v1.1 Fixing typos and logic flow - #> + .SYNOPSIS + Installs, uninstalls, upgrades, or lists software with rate limiting when run with Hosts parameter + + .DESCRIPTION + This script uses Chocolatey to manage software packages. It introduces rate limiting when run on multiple hosts to avoid hitting rate limits at chocolatey.org. Use the Hosts parameter to specify the number of computers the script is running on. + + .PARAMETER Mode + 5 modes: 'install' (default), 'uninstall', 'upgrade', 'upgrade-only-installed' or 'list'. + Mode 'install' installs the software specified by "PackageName" + Mode 'uninstall' removes the software specified by "PackageName" + Mode 'upgrade' checks for newer version and upgrades the package(s). If package is not existing on system it gets installed (default behaviour of chocolatey). If no PackageName is given all installed packages are being updated. + Mode 'upgrade-only-installed' checks for newer version of the package(s) and upgrades it. It will _not_ install new software (by adding --failonnotinstalled to the choco-command). + Mode 'list' lists packages which are installed by chocolatey on the target + Mode 'list-upgradeable' lists packages which are installed by chocolatey on the target but have updates available + + .PARAMETER Hosts + Use this to specify the number of computer(s) you're running the command on. This will dynamically introduce waits to try and minimize the chance of hitting rate limits (20/min) on the chocolatey.org site: Hosts 20 + + .PARAMETER PackageName + Use this to specify which software('s) to install eg: PackageName googlechrome. You can use multiple values using comma separated. + + .EXAMPLE + -Hosts 20 -PackageName googlechrome + + .EXAMPLE + -Mode upgrade -Hosts 50 -PackageName chocolatey + + .EXAMPLE + -Mode upgrade-only-installed -Hosts 20 -PackageName googlechrome,firefox + + .EXAMPLE + -Mode list + + .NOTES + 9/2021 v1 Initial release by @silversword411 and @bradhawkins + 11/14/2021 v1.1 Fixing typos and logic flow + 12/8/2023 v1.3 Adding list, making choco full path + 2/22/2024 v1.4 Adding 'upgrade-only-installed' as mode by @derfladi + 3/5/2024 v1.5 silversword411 Adding --no-progress to minimize output + 5/21/2024 v1.6 silversword411 Adding list-upgradeable +#> param ( - [Int] $Hosts = "0", + [Parameter(Mandatory = $false)] + [int] $Hosts = 0, + + [Parameter(Mandatory = $false)] [string[]] $PackageName, + + [Parameter(Mandatory = $false)] + [ValidateSet("install", "uninstall", "upgrade", "upgrade-only-installed", "list", "list-upgradeable")] [string] $Mode = "install" ) -$ErrorCount = 0 +$chocoExePath = "$env:PROGRAMDATA\chocolatey\choco.exe" -if ($Mode -ne "upgrade" -and !$PackageName) { - write-output "No choco package name provided, please include Example: `"-PackageName googlechrome`" `n" +if (-not (Test-Path $chocoExePath)) { + Write-Host "Chocolatey is not installed." Exit 1 } -if ($Hosts -ne "0") { - $randrange = ($Hosts + 1) * 6 - # Write-Output "Calculating rnd" - # Write-Output "randrange $randrange" - $rnd = Get-Random -Minimum 1 -Maximum $randrange; - # Write-Output "rnd=$rnd" -} -else { - $rnd = "1" - # Write-Output "rnd set to 1 manually" - # Write-Output "rnd=$rnd" +$ErrorCount = 0 + +if ($Mode -ne "upgrade" -and $Mode -ne "upgrade-only-installed" -and $Mode -ne "list" -and $Mode -ne "list-upgradeable" -and -not $PackageName) { + Write-Host "Error: No package name provided. Please specify a package name, e.g., `-PackageName googlechrome`." + Exit 1 } -if ($Mode -eq "upgrade") { - # Write-Output "Starting Upgrade" - Start-Sleep -Seconds $rnd; - if (!$PackageName) { - choco upgrade -y all +# Calculate random delay based on the number of hosts +$randDelay = if ($Hosts -gt 0) { Get-Random -Minimum 1 -Maximum (($Hosts + 1) * 6) } else { 1 } + +Write-Host "Sleeping $randDelay seconds" +Start-Sleep -Seconds $randDelay + +switch ($Mode) { + "install" { + if ($PackageName) { + foreach ($package in $PackageName) { + & $chocoExePath install $package -y --no-progress + if ($LASTEXITCODE -ne 0) { $ErrorCount++ } + } + } + } + "uninstall" { + if ($PackageName) { + foreach ($package in $PackageName) { + & $chocoExePath uninstall $package -y + if ($LASTEXITCODE -ne 0) { $ErrorCount++ } + } + } + } + "upgrade" { + if ($PackageName) { + foreach ($package in $PackageName) { + & $chocoExePath upgrade $package -y --no-progress + if ($LASTEXITCODE -ne 0) { $ErrorCount++ } + } + } + else { + & $chocoExePath upgrade all -y --no-progress + } } - else { - foreach ($package in $PackageName) - { - choco upgrade $package -y + "upgrade-only-installed" { + if ($PackageName) { + foreach ($package in $PackageName) { + & $chocoExePath upgrade $package --failonnotinstalled -y + if ($LASTEXITCODE -ne 0) { $ErrorCount++ } + } } + else { + & $chocoExePath upgrade all --failonnotinstalled -y + } + } + "list" { + & $chocoExePath list } - # Write-Output "Running upgrade" - Exit 0 + "list-upgradeable" { + & $chocoExePath outdated + } +} + +if ($ErrorCount -gt 0) { + Write-Host "$ErrorCount errors occurred during the operation." } -# write-output "Running install/uninstall mode" -Start-Sleep -Seconds $rnd; -choco $Mode $PackageName -y Exit 0 diff --git a/scripts/Win_Duplicati_Status.ps1 b/scripts/Win_Duplicati_Status.ps1 index 6a883d41..fed467fc 100644 --- a/scripts/Win_Duplicati_Status.ps1 +++ b/scripts/Win_Duplicati_Status.ps1 @@ -32,17 +32,46 @@ # SET DSTATUS= $ErrorActionPreference = 'silentlycontinue' -$TimeSpan = (Get-Date) - (New-TimeSpan -Day 1) -if (Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = '202'; StartTime = $TimeSpan }) { - Write-Output "Duplicati Backup Ended with Errors" - Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = '205', '201', '202'; StartTime = $TimeSpan } - exit 1 +# Name of the service to check +$serviceName = 'Duplicati' # Update this to your specific service name if different + +# Check if the service exists +$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue +if (-not $service) { + Write-Output "The service $serviceName does not exist on this system." + $host.SetShouldExit(0) # Using exit code 0 for "service not found" + return +} + +# Define the time spans for the last 24 hours and the last 5 days +$Last24Hours = (Get-Date) - (New-TimeSpan -Days 1) +$Last10Days = (Get-Date) - (New-TimeSpan -Days 10) + +# Fetch error events from the last 24 hours +$errorEvents = Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = 202; StartTime = $Last24Hours} -ErrorAction SilentlyContinue | Sort-Object TimeCreated + +# Check for any errors in the last 24 hours first +if ($errorEvents) { + Write-Output "Error(s) found in Duplicati Backup within the last 24 hours." + foreach ($event in $errorEvents) { + Write-Output "Error at $($event.TimeCreated): $($event.Message)" + Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = '202', '200', '201' } + } + $host.SetShouldExit(1) # Exit code 1 for error + return } +# If no errors, check for successful backup events in the last 5 days +$successEvents = Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = '200', '201'; StartTime = $Last10Days} -ErrorAction SilentlyContinue | Sort-Object TimeCreated -else { - Write-Output "Duplicati Backup Is Working Correctly" - Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = '205', '200', '201' } - exit 0 +if ($successEvents) { + $lastSuccessfulEvent = $successEvents | Select-Object -Last 1 + Write-Output "Last successful Duplicati Backup was at $($lastSuccessfulEvent.TimeCreated)" + Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = '202', '200', '201' } + $host.SetShouldExit(0) # Exit code 0 for success +} else { + Write-Output "No successful Duplicati Backup found in the last 10 days." + Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = '202', '200', '201' } + $host.SetShouldExit(1) # Exit code 1 for error } diff --git a/scripts/Win_IIS_Check_SSL_Certs.ps1 b/scripts/Win_IIS_Check_SSL_Certs.ps1 index e97b5bcb..08146f19 100644 --- a/scripts/Win_IIS_Check_SSL_Certs.ps1 +++ b/scripts/Win_IIS_Check_SSL_Certs.ps1 @@ -10,9 +10,10 @@ .INSTRUCTIONS Add this as a script check to your Windows Server that has IIS installed. .NOTES - Version: 1.0 + Version: 1.1 Author: ebdavison (nalantha on discord) Creation Date: 2022-08-08 + Updated: 2024-07-25 styx-tdo #> param @@ -60,13 +61,21 @@ $CertState = foreach ($bind in $bindingslist) { if ($certFileWH.NotAfter) { if ($certFileWH.NotAfter -lt $Days) { - "$($bindsite) = $($certfileWH.FriendlyName) / $($certfileWH.thumbprint) will expire on $($certfileWH.NotAfter)" + if (certfileWH.FriendlyName) { + "$($bindsite) = $($certfileWH.FriendlyName) / $($certfileWH.thumbprint) will expire on $($certfileWH.NotAfter)" + }else{ + "$($bindsite) = $($certfileWH.Subject) / $($certfileWH.thumbprint) will expire on $($certfileWH.NotAfter)" + } } } if ($certFileMY.NotAfter) { if ($certFileMY.NotAfter -lt $Days) { - "$($bindsite) = $($certfileMY.FriendlyName) / $($certfileMY.thumbprint) will expire on $($certfileWMY.NotAfter)" + if (certfileMY.FriendlyName) { + "$($bindsite) = $($certfileMY.FriendlyName) / $($certfileMY.thumbprint) will expire on $($certfileMY.NotAfter)" + }else{ + "$($bindsite) = $($certfileMY.Subject) / $($certfileMY.thumbprint) will expire on $($certfileMY.NotAfter)" + } } } } @@ -78,4 +87,4 @@ if (!$certState){ } else { Write-Output $CertState exit 1 -} \ No newline at end of file +} diff --git a/scripts/Win_Reboot.ps1 b/scripts/Win_Reboot.ps1 new file mode 100644 index 00000000..6ea23c76 --- /dev/null +++ b/scripts/Win_Reboot.ps1 @@ -0,0 +1,28 @@ +<# +.SYNOPSIS + Reboots/Restarts the computer with an optional wait time before restarting. Max wait 24hrs + +.DESCRIPTION + This script restarts the computer forcefully. + +.PARAMETER Wait + Specifies the number of seconds to wait before restarting the computer. + +.EXAMPLE + -Wait 60 + Waits for 60 seconds and then restarts the computer. + +.NOTES + v1.0 5/17/2024 Created by silversword411 and dinger1986 +#> + +param( + [int]$Wait +) + +if ($Wait) { + shutdown -r -t $Wait +} +else { + Restart-Computer -Force +} diff --git a/scripts/Win_RecycleBin_Empty.ps1 b/scripts/Win_RecycleBin_Empty.ps1 index 4bd0699e..d4d7bdfc 100644 --- a/scripts/Win_RecycleBin_Empty.ps1 +++ b/scripts/Win_RecycleBin_Empty.ps1 @@ -1 +1,2 @@ -Clear-RecycleBin -Force \ No newline at end of file +# Must be "Run As User" +Clear-RecycleBin -Confirm:$false -ErrorAction SilentlyContinue \ No newline at end of file diff --git a/scripts/Win_RunAsUser_Example.ps1 b/scripts/Win_RunAsUser_Example.ps1 index 9c2fcf27..1daea9b7 100644 --- a/scripts/Win_RunAsUser_Example.ps1 +++ b/scripts/Win_RunAsUser_Example.ps1 @@ -8,11 +8,12 @@ .NOTES Change Log V1.0 6/25/2022 Initial release by silversword411 + v1.1 6/14/2024 silversword411 Adding -CaptureOutput #> # Make sure RunAsUser is installed if (Get-Module -ListAvailable -Name RunAsUser) { - # Write-Output "RunAsUser Already Installed" + Write-Output "RunAsUser Already Installed" } else { Write-Output "Installing RunAsUser" @@ -20,43 +21,31 @@ else { } # Make sure Tactical RMM temp script folder exists -If (!(test-path "c:\ProgramData\TacticalRMM\temp\")) { +If (!(Test-Path "c:\ProgramData\TacticalRMM\temp\")) { Write-Output "Creating c:\ProgramData\TacticalRMM\temp Folder" - New-Item "c:\ProgramData\TacticalRMM\temp" -itemType Directory + New-Item "c:\ProgramData\TacticalRMM\temp" -ItemType Directory } Write-Output "Hello from Systemland" -Invoke-AsCurrentUser -scriptblock { - +Invoke-AsCurrentUser -ScriptBlock { # Put all Userland code here - Write-Output "Hello from Userland" | Out-File -append -FilePath c:\ProgramData\TacticalRMM\temp\raulog.txt + $exit1Path = "c:\ProgramData\TacticalRMM\temp\exit1.txt" + + Write-Output "Hello from Userland" If (test-path "c:\temp\") { - Write-Output "Test for c:\temp\ folder passed which is Exit 0" | Out-File -append -FilePath c:\ProgramData\TacticalRMM\temp\raulog.txt + Write-Output "Test for c:\temp\ folder passed which is Exit 0" } else { - Write-Output "Test for c:\temp\ folder failed which is Exit 1" | Out-File -append -FilePath c:\ProgramData\TacticalRMM\temp\raulog.txt + Write-Output "Test for c:\temp\ folder failed which is Exit 1" # Writing exit1.txt for Userland Exit 1 passing to Systemland for returning to Tactical - Write-Output "Exit 1" | Out-File -append -FilePath c:\ProgramData\TacticalRMM\temp\exit1.txt + Write-Output "Exit 1" | Out-File -append -FilePath $exit1Path } - # End of all Userland code - -} - -# Get userland return info for Tactical Script History -$exitdata = Get-Content -Path "c:\ProgramData\TacticalRMM\temp\raulog.txt" -Write-Output $exitdata -# Cleanup raulog.txt File -Remove-Item -path "c:\ProgramData\TacticalRMM\temp\raulog.txt" +} -CaptureOutput # Checking for Userland Exit 1 -If (!(Test-Path -Path "c:\ProgramData\TacticalRMM\temp\exit1.txt" -PathType Leaf)) { - # No Exit 1 From Userland - Exit 0 -} -Else { +If (Test-Path -Path "c:\ProgramData\TacticalRMM\temp\exit1.txt" -PathType Leaf) { Write-Output 'Return Exit 1 to Tactical from Userland' - Remove-Item -path "c:\ProgramData\TacticalRMM\temp\exit1.txt" + Remove-Item -Path "c:\ProgramData\TacticalRMM\temp\exit1.txt" -ErrorAction SilentlyContinue Exit 1 } - diff --git a/scripts/Win_ScreenConnectAIO.ps1 b/scripts/Win_ScreenConnectAIO.ps1 index f654eefd..e36e67cc 100644 --- a/scripts/Win_ScreenConnectAIO.ps1 +++ b/scripts/Win_ScreenConnectAIO.ps1 @@ -1,23 +1,38 @@ <# Requires global variables for serviceName "ScreenConnectService" and url "ScreenConnectInstaller" -serviceName is the name of the ScreenConnect Service once it is installed EG: "ScreenConnect Client (1327465grctq84yrtocq)" -url is the path the download the exe version of the ScreenConnect Access installer +serviceName is the name of the ScreenConnect Service once it is installed EG: "ScreenConnect Client (1327465grctq84yrtocq)" or "ConnectWise Control Client (xxxxxx)" +url is the path to download the MSI version of the ScreenConnect Access installer Both variables values must start and end with " Also accepts uninstall variable to remove the installed instance if required. 2022-10-12: Added -action start and -action stop variables +2024-3-19 silversword411 - Adding debug. Fixing uninstall when .exe not running. +2024-06-18: Bomb - Updated for MSI-only support (.exe deprecated) #> param ( - [string] $serviceName, - [string] $url, - [string] $clientname, - [string] $sitename, - [string] $action + [string] $serviceName, + [string] $url, + [string] $clientname, + [string] $sitename, + [string] $action, + [switch] $debug ) -$clientname = $clientname.Replace(" ","%20") -$sitename = $sitename.Replace(" ","%20") -$url = $url.Replace("&t=&c=&c=&c=&c=&c=&c=&c=&c=","&t=&c=$clientname&c=$sitename&c=&c=&c=&c=&c=&c=") +# For setting debug output level. -debug switch will set $debug to true +if ($debug) { + $DebugPreference = "Continue" + $ErrorActionPreference = 'Continue' + Write-Debug "Debug mode enabled" +} +else { + $DebugPreference = "SilentlyContinue" + $ErrorActionPreference = 'silentlycontinue' + Write-Output "Regular mode enabled" +} + +$clientname = $clientname.Replace(" ", "%20") +$sitename = $sitename.Replace(" ", "%20") +$url = $url.Replace("&t=&c=&c=&c=&c=&c=&c=&c=&c=", "&t=&c=$clientname&c=$sitename&c=&c=&c=&c=&c=&c=") $ErrorCount = 0 if (!$serviceName) { @@ -25,128 +40,117 @@ if (!$serviceName) { $ErrorCount += 1 } if (!$url) { - write-output "Variable not specified ScreenConnectInstaller, please create a global custom field under Client called ScreenConnectInstaller, Example Value: `"https://myinstance.screenconnect.com/Bin/ConnectWiseControl.ClientSetup.exe?h=stupidlylongurlhere`" `n" + write-output "Variable not specified ScreenConnectInstaller, please create a global custom field under Client called ScreenConnectInstaller, Example Value: `"https://myinstance.screenconnect.com/Bin/ScreenConnect.ClientSetup.msi?h=stupidlylongurlhere`" `n" $ErrorCount += 1 } -if (!$ErrorCount -eq 0) { -exit 1 +if ($ErrorCount -ne 0) { + exit 1 } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + if ($action -eq "uninstall") { - $MyApp = Get-WmiObject -Class Win32_Product | Where-Object{$_.Name -eq "$serviceName"} + $MyApp = Get-WmiObject -Class Win32_Product | Where-Object { $_.Name -eq "$serviceName" } + Write-Debug "MyApp: $MyApp" + if ($MyApp) { $MyApp.Uninstall() -} elseif ($action -eq "stop") { + } else { + Write-Output "No matching ScreenConnect product found to uninstall." + } +} +elseif ($action -eq "stop") { If ((Get-Service $serviceName).Status -eq 'Running') { - Try - { + Try { Write-Output "Stopping $serviceName" Set-Service -Name $serviceName -Status stopped -StartupType disabled exit 0 - } - Catch - { - $ErrorMessage = $_.Exception.Message - $FailedItem = $_.Exception.ItemName - Write-Error -Message "$ErrorMessage $FailedItem" - exit 1 - } - Finally - { - } - } -} elseif ($action -eq "start") { -If ((Get-Service $serviceName).Status -ne 'Running') { - Try - { + Catch { + $ErrorMessage = $_.Exception.Message + $FailedItem = $_.Exception.ItemName + Write-Error -Message "$ErrorMessage $FailedItem" + exit 1 + } + } +} +elseif ($action -eq "start") { + If ((Get-Service $serviceName).Status -ne 'Running') { + Try { Write-Host "Starting $serviceName" Set-Service -Name $serviceName -Status running -StartupType automatic exit 0 - } - Catch - { - $ErrorMessage = $_.Exception.Message - $FailedItem = $_.Exception.ItemName - Write-Error -Message "$ErrorMessage $FailedItem" - exit 1 - } - Finally - { - } - } -} else { + Catch { + $ErrorMessage = $_.Exception.Message + $FailedItem = $_.Exception.ItemName + Write-Error -Message "$ErrorMessage $FailedItem" + exit 1 + } + } +} +else { If (Get-Service $serviceName -ErrorAction SilentlyContinue) { If ((Get-Service $serviceName).Status -eq 'Running') { - Try - { - Write-Output "Stopping $serviceName" - Set-Service -Name $serviceName -Status stopped -StartupType disabled - exit 0 + Try { + Write-Output "Stopping $serviceName" + Set-Service -Name $serviceName -Status stopped -StartupType disabled + exit 0 } - Catch - { + Catch { $ErrorMessage = $_.Exception.Message $FailedItem = $_.Exception.ItemName Write-Error -Message "$ErrorMessage $FailedItem" exit 1 - } - Finally - { - } - - } Else { - - Try - { - Write-Host "Starting $serviceName" - Set-Service -Name $serviceName -Status running -StartupType automatic - exit 0 } - Catch - { + } + Else { + Try { + Write-Host "Starting $serviceName" + Set-Service -Name $serviceName -Status running -StartupType automatic + exit 0 + } + Catch { $ErrorMessage = $_.Exception.Message $FailedItem = $_.Exception.ItemName Write-Error -Message "$ErrorMessage $FailedItem" exit 1 - } - Finally - { - } - + } } - } Else { + } + Else { $OutPath = $env:TMP - $output = "screenconnect.exe" + $output = "ScreenConnect.ClientSetup.msi" - Try - { - $start_time = Get-Date - $wc = New-Object System.Net.WebClient - $wc.DownloadFile("$url", "$OutPath\$output") - Write-Output "Time taken to download: $((Get-Date).Subtract($start_time).Seconds) second(s)" - - $start_time = Get-Date - $wc = New-Object System.Net.WebClient - Start-Process -FilePath $OutPath\$output -Wait - Write-Output "Time taken to install: $((Get-Date).Subtract($start_time).Seconds) second(s)" - exit 0 + Try { + $start_time = Get-Date + $wc = New-Object System.Net.WebClient + $wc.DownloadFile("$url", "$OutPath\$output") + Write-Debug "Time taken to download: $((Get-Date).Subtract($start_time).Seconds) second(s)" + + $start_time = Get-Date + # Install MSI silently + $proc = Start-Process -FilePath "msiexec.exe" -ArgumentList "/i `"$OutPath\$output`" /qn /norestart" -Wait -PassThru + Write-Debug "Time taken to install: $((Get-Date).Subtract($start_time).Seconds) second(s)" + if ($proc.ExitCode -eq 0) { + Write-Host "ScreenConnect installed successfully." + exit 0 + } else { + Write-Error "ScreenConnect install failed with exit code $($proc.ExitCode)." + exit $proc.ExitCode + } } - Catch - { + Catch { $ErrorMessage = $_.Exception.Message $FailedItem = $_.Exception.ItemName Write-Error -Message "$ErrorMessage $FailedItem" exit 1 } - Finally - { - Remove-Item -Path $OutPath\$output + Finally { + Remove-Item -Path "$OutPath\$output" -Force -ErrorAction SilentlyContinue } } diff --git a/scripts/Win_Speedtest.ps1 b/scripts/Win_Speedtest.ps1 index 2eb02c23..5717d069 100644 --- a/scripts/Win_Speedtest.ps1 +++ b/scripts/Win_Speedtest.ps1 @@ -1,3 +1,4 @@ + ## Measures the speed of the download, can only be ran on a PC running Windows 10 or a server running Server 2016+, plan is to add uploading also ## Majority of this script has been copied/butchered from https://www.ramblingtechie.co.uk/2020/07/13/internet-speed-test-in-powershell/ # MINIMUM ACCEPTED THRESHOLD IN mbps @@ -5,23 +6,54 @@ $mindownloadspeed = 20 $minuploadspeed = 4 # File to download you can find download links for other files here https://speedtest.flonix.net -$downloadurl = "https://files.xlawgaming.com/10mb.bin" -#$UploadURL = "http://ipv4.download.thinkbroadband.com/10MB.zip" +$downloadurls = @( + "https://raw.githubusercontent.com/jamesward/play-load-tests/master/public/10mb.txt" +) # SIZE OF SPECIFIED FILE IN MB (adjust this to match the size of your file in MB as above) -$size = 10 +$sizes = @( + 10, + 10 +) + # Name of Downloaded file $localfile = "SpeedTest.bin" # WEB CLIENT VARIABLES $webclient = New-Object System.Net.WebClient -#RUN DOWNLOAD & CALCULATE DOWNLOAD SPEED -$downloadstart_time = Get-Date -$webclient.DownloadFile($downloadurl, $localfile) -$downloadtimetaken = $((Get-Date).Subtract($downloadstart_time).Seconds) -$downloadspeed = ($size / $downloadtimetaken) * 8 -Write-Output "Time taken: $downloadtimetaken second(s) | Download Speed: $downloadspeed mbps" +# Variable to track if download was successful +$downloadSuccessful = $false + +for ($i = 0; $i -lt $downloadurls.Length; $i++) { + $downloadurl = $downloadurls[$i] + $size = $sizes[$i] + + # Write-Output "trying $downloadurl" + try { + #RUN DOWNLOAD & CALCULATE DOWNLOAD SPEED + $start_time = Get-Date + # $a = Measure-Command -Expression { + $webclient.DownloadFile($downloadurl, $localfile) + # } + $end_time = Get-Date + $downloadSuccessful = $true + break # exits for loop + } catch { + Write-Output "Failed to download: $downloadurl : Trying the next URL..." + } +} + +if (-not $downloadSuccessful) { + Write-Output "All download attempts failed." + exit 1 +} + + +$secs_taken = ($end_time - $start_time).TotalSeconds +$downloadspeed = ($size / $secs_taken) * 8 +Write-Output "Time taken: $([Math]::Round($secs_taken, 2)) seconds | Download Speed: $([Math]::Round($downloadspeed, 2)) mbps" + #RUN UPLOAD & CALCULATE UPLOAD SPEED #$uploadstart_time = Get-Date @@ -35,11 +67,9 @@ Remove-Item -path $localfile #SEND ALERTS IF BELOW MINIMUM THRESHOLD if ($downloadspeed -ge $mindownloadspeed) { - Write-Output "Speed is acceptable. Current download speed at is $downloadspeed mbps which is above the threshold of $mindownloadspeed mbps" + Write-Output "Speed is acceptable. Current download speed is above the threshold of $mindownloadspeed mbps" exit 0 -} - -else { - Write-Output "Current download speed at is $downloadspeed mbps which is below the minimum threshold of $mindownloadspeed mbps" +} else { + Write-Output "Current download speed is below the minimum threshold of $mindownloadspeed mbps" exit 1 } diff --git a/scripts/Win_Start_Cleanup.ps1 b/scripts/Win_Start_Cleanup.ps1 index 41266ab9..d08738e6 100644 --- a/scripts/Win_Start_Cleanup.ps1 +++ b/scripts/Win_Start_Cleanup.ps1 @@ -318,19 +318,6 @@ param( Write-Host "[DONE]" -ForegroundColor Green -BackgroundColor Black } - ## Starts cleanmgr.exe - Function Start-CleanMGR { - Try{ - Write-Host "Windows Disk Cleanup is running. " -NoNewline -ForegroundColor Green - Start-Process -FilePath Cleanmgr -ArgumentList '/sagerun:1' -Wait -Verbose - Write-Host "[DONE]" -ForegroundColor Green -BackgroundColor Black - } - Catch [System.Exception]{ - Write-host "cleanmgr is not installed! To use this portion of the script you must install the following windows features:" -ForegroundColor Red -NoNewline - Write-host "[ERROR]" -ForegroundColor Red -BackgroundColor black - } - } Start-CleanMGR - ## gathers disk usage after running the cleanup cmdlets. $After = Get-WmiObject Win32_LogicalDisk | Where-Object { $_.DriveType -eq "3" } | Select-Object SystemName, @{ Name = "Drive" ; Expression = { ( $_.DeviceID ) } }, @@ -362,32 +349,6 @@ Before: $Before" ## Sends the disk usage after running the cleanup script to the console for ticketing purposes. Write-Verbose "After: $After" - ## Prompt to scan for large ISO, VHD, VHDX files. - Function PromptforScan { - param( - $ScanPath, - $title = (Write-Host "Search for large files" -ForegroundColor Green), - $message = (Write-Host "Would you like to scan $ScanPath for ISOs or VHD(X) files?" -ForegroundColor Green) - ) - $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Scans $ScanPath for large files." - $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Skips scanning $ScanPath for large files." - $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) - $prompt = $host.ui.PromptForChoice($title, $message, $options, 0) - switch ($prompt) { - 0 { - Write-Host "Scanning $ScanPath for any large .ISO and or .VHD\.VHDX files per the Administrators request." -ForegroundColor Green - Write-Verbose ( Get-ChildItem -Path $ScanPath -Include *.iso, *.vhd, *.vhdx -Recurse -ErrorAction SilentlyContinue | - Sort-Object Length -Descending | Select-Object Name, Directory, - @{Name = "Size (GB)"; Expression = { "{0:N2}" -f ($_.Length / 1GB) }} | Format-Table | - Out-String -verbose ) - } - 1 { - Write-Host "The Administrator chose to not scan $ScanPath for large files." -ForegroundColor DarkYellow -Verbose - } - } - } - PromptforScan -ScanPath C:\ # end of function - ## Completed Successfully! Write-Host (Stop-Transcript) -ForegroundColor Green @@ -396,4 +357,4 @@ Script finished Write-Host "[DONE]" -ForegroundColor Green -BackgroundColor Black } -Start-Cleanup +Start-Cleanup \ No newline at end of file diff --git a/scripts/Win_TRMM_AV_Update_Exclusion.ps1 b/scripts/Win_TRMM_AV_Update_Exclusion.ps1 index 97cae9e8..5d659ea0 100644 --- a/scripts/Win_TRMM_AV_Update_Exclusion.ps1 +++ b/scripts/Win_TRMM_AV_Update_Exclusion.ps1 @@ -2,3 +2,4 @@ Add-MpPreference -ExclusionPath "C:\Program Files\Mesh Agent\*" Add-MpPreference -ExclusionPath "C:\Program Files\TacticalAgent\*" Add-MpPreference -ExclusionPath "C:\ProgramData\TacticalRMM\*" +Add-MpPreference -ExclusionProcess "C:\Windows\Temp\is-*.tmp\tacticalagent*" diff --git a/scripts/Win_TRMM_Troubleshooting_Agent.ps1 b/scripts/Win_TRMM_Troubleshooting_Agent.ps1 new file mode 100644 index 00000000..d889e78b --- /dev/null +++ b/scripts/Win_TRMM_Troubleshooting_Agent.ps1 @@ -0,0 +1,458 @@ +<# +.SYNOPSIS + Checks for all problems related to TRMM and Mesh Agent. + +.DESCRIPTION + This script checks for the presence of Mesh Agent service, folder, and executable file. If any of these components are missing, it returns an error code of 1. + +.PARAMETER debug + Switch parameter to enable debug output. + +.NOTES + Version: 1.0 Created 6/6/2023 by silversword411 + v1.2 5/15/2024 Adding default NIC info, TRMM registry data + v1.3 5/15/2024 Adding mesh server URL discovery, connection check to mesh and API, and checking for files and services + v1.4 5/15/2024 Rework and simplify. Write out logfile + v1.5 6/21/2024 Adding trmm agent to Check-Memorysize + v1.6 8/26/2024 checking mesh for CF proxy +#> + +param( + [String] $procname = "meshagent,tacticalrmm", + [Int] $warnwhenovermemsize = 100000000, + [switch]$debug +) + +if ($debug) { + $DebugPreference = "Continue" +} +else { + $DebugPreference = "SilentlyContinue" +} + +$logfile = "$(Get-Date -Format 'yyyy-MM-dd_HH-mm-ss')-trmmagenttroubleshooting.log" +Start-Transcript -Path $logfile -Append + +function Get-CloudflareIPRanges { + $ipv4Url = "https://www.cloudflare.com/ips-v4" + $ipv6Url = "https://www.cloudflare.com/ips-v6" + + try { + if ($Debug) { Write-Output "Downloading Cloudflare IPv4 ranges..." } + $ipv4Ranges = Invoke-WebRequest -Uri $ipv4Url -UseBasicParsing | Select-Object -ExpandProperty Content + + if ($Debug) { Write-Output "Downloading Cloudflare IPv6 ranges..." } + $ipv6Ranges = Invoke-WebRequest -Uri $ipv6Url -UseBasicParsing | Select-Object -ExpandProperty Content + + $global:CloudflareIPRanges = @() + $global:CloudflareIPRanges += $ipv4Ranges -split "`n" + $global:CloudflareIPRanges += $ipv6Ranges -split "`n" + + if ($Debug) { Write-Output "Cloudflare IP ranges downloaded successfully." } + } + catch { + Write-Output "Failed to download Cloudflare IP ranges. Please check your internet connection." + $global:CloudflareIPRanges = $null + } +} + +function ConvertTo-IPv4Integer { + param ([string]$ip) + + $ipBytes = [System.Net.IPAddress]::Parse($ip).GetAddressBytes() + [Array]::Reverse($ipBytes) # Convert to little-endian format + return [BitConverter]::ToUInt32($ipBytes, 0) +} + +function Test-IPv4InRange { + param ( + [string]$ip, + [string]$cidr + ) + + # Split the CIDR notation + $parts = $cidr -split '/' + $baseIP = $parts[0] + $subnetMask = [int]$parts[1] + + # Convert IP and base IP to 32-bit integers + $ipInt = ConvertTo-IPv4Integer -ip $ip + $baseIPInt = ConvertTo-IPv4Integer -ip $baseIP + + # Create the mask as a 32-bit unsigned integer + $mask = 0xFFFFFFFF -shl (32 - $subnetMask) + + # Compare the masked IP with the base IP + return (($ipInt -band $mask) -eq ($baseIPInt -band $mask)) +} + +function Test-CloudflareProxy { + if ($Debug) { Write-Output "Starting Cloudflare IP range retrieval..." } + Get-CloudflareIPRanges + + if ($Debug) { Write-Output "Resolving IP addresses for $global:MeshServerAddress..." } + + try { + $resolvedIPs = [System.Net.Dns]::GetHostAddresses($global:MeshServerAddress) + + if ($resolvedIPs.Count -eq 0) { + Write-Output "No IP addresses resolved for $global:MeshServerAddress." + return + } + else { + if ($Debug) { + Write-Output "Resolved IP addresses:" + foreach ($ip in $resolvedIPs) { + Write-Output " - $($ip.IPAddressToString)" + } + } + } + } + catch { + Write-Output "Failed to resolve IP addresses for $global:MeshServerAddress. Error: $_" + return + } + + $cloudflareDetected = $false + $matchedIP = $null + + foreach ($ip in $resolvedIPs) { + if ($ip.AddressFamily -eq "InterNetwork") { + # Only IPv4 + foreach ($range in $global:CloudflareIPRanges) { + if ($Debug) { Write-Output "Checking if IP $($ip.IPAddressToString) is in range $range..." } + if (Test-IPv4InRange -ip $ip.IPAddressToString -cidr $range) { + $cloudflareDetected = $true + $matchedIP = $ip.IPAddressToString + break + } + } + } + if ($cloudflareDetected) { break } + } + + if ($cloudflareDetected) { + if ($Debug) { + Write-Output "The IP address $matchedIP is within Cloudflare ranges." + } + else { + Write-Output "WARNING: $global:MeshServerAddress is using Cloudflare proxy IP $matchedIP." + } + } + else { + $notMatchedIP = $resolvedIPs | Where-Object { $_.AddressFamily -eq "InterNetwork" } | Select-Object -First 1 + if ($Debug) { + Write-Output "None of the resolved IPs are within Cloudflare ranges." + } + else { + Write-Output "The MeshServerAddress $global:MeshServerAddress is NOT using Cloudflare (IP $($notMatchedIP.IPAddressToString))." + } + } +} + +function Check-MemorySize { + if (!($procname)) { + Write-Output "No procname defined, and it is required. Exiting" + Stop-Transcript + Exit 1 + } + + if (!($warnwhenovermemsize)) { + Write-Output "No warnwhenovermemsize defined, and it is required. Exiting" + Stop-Transcript + Exit 1 + } + + Write-Debug "Warn when Memsize exceeds: $warnwhenovermemsize" + Write-Debug "#####" + + $procnameList = $procname -split ',' + + foreach ($proc in $procnameList) { + $proc = $proc.Trim() + Write-Debug "Checking process: $proc" + + $proc_pid = (get-process -Name $proc -ErrorAction SilentlyContinue).Id + + if ($null -eq $proc_pid) { + Write-Output "Process $proc not found." + continue + } + + $Processes = Get-WmiObject -Query "SELECT * FROM Win32_PerfFormattedData_PerfProc_Process WHERE IDProcess=$proc_pid" + + foreach ($Process in $Processes) { + $WS_MB = [math]::Round($Process.WorkingSetPrivate / 1MB, 2) + + if ($Process.WorkingSetPrivate -gt $warnwhenovermemsize) { + Write-Output "WARNING: $($WS_MB)MB: $($proc) has high memory usage" + Restart-service -name "Mesh Agent" + Stop-Transcript + Exit 1 + } + else { + Write-Output "$($WS_MB)MB: $($proc) is below the expected memory usage" + } + } + } +} + + +function Check-ForMeshComponents { + $serviceName = "Mesh Agent" + $ErrorCount = 0 + + if (!(Get-Service $serviceName -ErrorAction SilentlyContinue)) { + Write-Output "Mesh Agent Service Missing" + $ErrorCount += 1 + } + else { + Write-Output "Mesh Agent Service Found" + } + + if (!(Test-Path "c:\Program Files\Mesh Agent")) { + Write-Output "Mesh Agent Folder missing" + $ErrorCount += 1 + } + else { + Write-Output "Mesh Agent Folder exists" + } + + if (!(Test-Path "c:\Program Files\Mesh Agent\MeshAgent.exe")) { + Write-Output "Mesh Agent executable missing" + $ErrorCount += 1 + } + else { + Write-Output "Mesh Agent executable exists" + } + + if ($ErrorCount -ne 0) { + Stop-Transcript + exit 1 + } +} + +function Get-DefaultNetworkAdapter { + $networkConfigs = Get-NetIPConfiguration + $defaultRoutes = Get-NetRoute -DestinationPrefix '0.0.0.0/0' + + if ($defaultRoutes.Count -eq 0) { + Write-Output "No default route found." + return + } + + $defaultConfigs = @() + foreach ($route in $defaultRoutes) { + $config = $networkConfigs | Where-Object { $_.InterfaceIndex -eq $route.InterfaceIndex } + if ($config) { + $defaultConfigs += [PSCustomObject]@{ + InterfaceAlias = $config.InterfaceAlias + InterfaceMetric = $route.RouteMetric + $config.InterfaceMetric + IPv4Address = $config.IPv4Address.IPAddress + DefaultGateway = $route.NextHop + DnsServers = $config.DnsServer.ServerAddresses + } + } + } + + if ($defaultConfigs.Count -eq 0) { + Write-Output "No default network adapter found." + return + } + + $defaultConfig = $defaultConfigs | Sort-Object { $_.InterfaceMetric } | Select-Object -First 1 + + Write-Output "Default Network Adapter:" + Write-Output "Name : $($defaultConfig.InterfaceAlias)" + Write-Output "IP Address : $($defaultConfig.IPv4Address)" + Write-Output "Default Gateway : $($defaultConfig.DefaultGateway)" + Write-Output "DNS Servers : $($defaultConfig.DnsServers -join ', ')" +} + +function Get-TacticalRMMData { + $registryPath = "HKLM:\SOFTWARE\TacticalRMM" + $global:ApiURL = $null + + if (Test-Path $registryPath) { + $registryData = Get-ItemProperty -Path $registryPath + + foreach ($property in $registryData.PSObject.Properties) { + if ($property.Name -eq "AgentID" -or $property.Name -eq "Token") { + $truncatedValue = $property.Value.Substring(0, [Math]::Min(5, $property.Value.Length)) + "-snipped" + Write-Output "$($property.Name): $truncatedValue" + } + elseif ($property.Name -eq "ApiURL") { + $global:ApiURL = $property.Value + Write-Output "$($property.Name): $($property.Value)" + } + else { + Write-Output "$($property.Name): $($property.Value)" + } + } + } + else { + Write-Output "The registry key '$registryPath' does not exist." + } +} + +$global:MeshServerAddress = $null + +function Get-MeshServer { + param ( + [string]$filePath = "C:\Program Files\Mesh Agent\MeshAgent.msh" + ) + $global:MeshServerAddress = $null + + if (Test-Path $filePath) { + $content = Get-Content -Path $filePath + $meshServerLine = $content | Select-String -Pattern "MeshServer" + + if ($meshServerLine) { + $meshServer = $meshServerLine -replace "MeshServer=wss://", "" -replace ":.*", "" + $global:MeshServerAddress = $meshServer + } + else { + Write-Output "MeshServer not found in the file." + } + } + else { + Write-Output "File not found: $filePath" + } +} + +function Test-ServerConnections { + if ($global:MeshServerAddress) { + Write-Output "Pinging MeshServerAddress: $global:MeshServerAddress" + Test-Connection -ComputerName $global:MeshServerAddress -Count 2 | Format-Table -AutoSize + } + else { + Write-Output "MeshServerAddress is not set." + } + + if ($global:ApiURL) { + try { + if ($global:ApiURL -notmatch "^[a-zA-Z][a-zA-Z0-9+.-]*://") { + $global:ApiURL = "http://$global:ApiURL" + } + + $uri = [System.Uri]::new($global:ApiURL) + $hostname = $uri.Host + Write-Output "Pinging ApiURL: $hostname" + Test-Connection -ComputerName $hostname -Count 2 | Format-Table -AutoSize + } + catch { + Write-Output "Failed to parse ApiURL: $global:ApiURL" + Write-Output "Error: $_" + } + } + else { + Write-Output "ApiURL is not set." + } +} + +function Check-ServicesAndFiles { + param ( + [string]$MeshAgentPath = "C:\Program Files\Mesh Agent\MeshAgent.exe", + [string]$TacticalRmmPath = "C:\Program Files\TacticalAgent\tacticalrmm.exe", + [string]$MeshAgentService = "Mesh Agent", + [string]$TacticalRmmService = "tacticalrmm" + ) + + function Test-File { + param ( + [string]$FilePath + ) + return Test-Path -Path $FilePath + } + + function Test-Service { + param ( + [string]$ServiceName + ) + $service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue + if ($null -eq $service) { + Write-Output "PROBLEM: $ServiceName service does not exist." + return $false + } + elseif ($service.Status -ne 'Running') { + Write-Output "PROBLEM: $ServiceName service is not running. Attempting to start..." + Start-Service -Name $ServiceName + if ($?) { + Write-Output "OK: $ServiceName service started successfully." + return $true + } + else { + Write-Output "PROBLEM: Failed to start $ServiceName service." + return $false + } + } + else { + Write-Output "OK: $ServiceName service is running." + return $true + } + } + + if (Test-File -FilePath $MeshAgentPath) { + Write-Output "OK: MeshAgent.exe file exists." + } + else { + Write-Output "PROBLEM: MeshAgent.exe file does not exist." + } + + if (Test-File -FilePath $TacticalRmmPath) { + Write-Output "OK: tacticalrmm.exe file exists." + } + else { + Write-Output "PROBLEM: tacticalrmm.exe file does not exist." + } + + if (Test-Service -ServiceName $MeshAgentService) { + Write-Output "OK: $MeshAgentService service is verified." + } + else { + Write-Output "PROBLEM: $MeshAgentService service verification failed." + } + + if (Test-Service -ServiceName $TacticalRmmService) { + Write-Output "OK: $TacticalRmmService service is verified." + } + else { + Write-Output "PROBLEM: $TacticalRmmService service verification failed." + } +} + +Write-Output "******************** TRMM Registry Data ***********************" +Get-TacticalRMMData +Write-Output "" +Get-MeshServer + +Write-Output "" +Write-Output "********************** Usable Variables ***********************" +Write-Output "Global MeshServerAddress: $global:MeshServerAddress" +Write-Output "Global ApiURL: $global:ApiURL" +Write-Output "" + +Write-Output "**************** Check for files and services *****************" +Check-ServicesAndFiles +Write-Output "" + +Write-Output "************************ Default NIC *************************" +Get-DefaultNetworkAdapter +Write-Output "" + +Write-Output "************ Test Connectivity to Mesh and TRMM ***************" +Test-ServerConnections +Write-Output "" + +Write-Output "************ Checking if MeshServer is using Cloudflare *******" +Test-CloudflareProxy +Write-Output "" + +Write-Output "******************* Checking Mesh Agent ***********************" +Check-ForMeshComponents +Write-Output "" + +Write-Output "********************* Mesh Memory Size ************************" +Check-MemorySize + +Stop-Transcript \ No newline at end of file diff --git a/scripts/Win_Teamviewer_Get_ID.ps1 b/scripts/Win_Teamviewer_Get_ID.ps1 index 077f84be..c67c1d12 100644 --- a/scripts/Win_Teamviewer_Get_ID.ps1 +++ b/scripts/Win_Teamviewer_Get_ID.ps1 @@ -10,7 +10,7 @@ $Paths = @(foreach ($TeamViewerVersionsNum in $TeamViewerVersionsNums) { foreach ($Path in $Paths) { If (Test-Path $Path) { - $GoodPath = $Path + $GoodPath += $Path } } @@ -24,4 +24,4 @@ foreach ($FullPath in $GoodPath) { } -Write-Output $TeamViewerID \ No newline at end of file +Write-Output $TeamViewerID diff --git a/scripts/Win_Wifi_SSID_and_Password_Retrieval.ps1 b/scripts/Win_Wifi_SSID_and_Password_Retrieval.ps1 index d59f1251..7dd918ac 100644 --- a/scripts/Win_Wifi_SSID_and_Password_Retrieval.ps1 +++ b/scripts/Win_Wifi_SSID_and_Password_Retrieval.ps1 @@ -1,3 +1,30 @@ -# Query Windows 10 Saved SSID details outputs the WIFI name and password. -# Created by TechCentre with the help and assistance of the internet -(netsh wlan show profiles) | Select-String "\:(.+)$" | %{$name=$_.Matches.Groups[1].Value.Trim(); $_} | %{(netsh wlan show profile name="$name" key=clear)} | Select-String "Key Content\W+\:(.+)$" | %{$pass=$_.Matches.Groups[1].Value.Trim(); $_} | %{[PSCustomObject]@{ PROFILE_NAME=$name;PASSWORD=$pass }} | Format-Table -AutoSize \ No newline at end of file +<# +.NOTES + v1.1 8/23/2024 silversword411 complete refactor to add Connection mode column +#> + +# Get the list of saved SSIDs +$wifiProfiles = (netsh wlan show profiles) | Select-String "\:(.+)$" | % { $_.Matches.Groups[1].Value.Trim() } + +$results = @() + +foreach ($name in $wifiProfiles) { + $profileDetails = netsh wlan show profile name="$name" key=clear + + # Look for the "Connection mode" setting + $connectionModeMatch = $profileDetails | Select-String "Connection mode\W+\:(.+)$" + $connectionMode = if ($connectionModeMatch) { $connectionModeMatch.Matches.Groups[1].Value.Trim() } else { "Not found" } + + # Look for the password + $passwordMatch = $profileDetails | Select-String "Key Content\W+\:(.+)$" + $password = if ($passwordMatch) { $passwordMatch.Matches.Groups[1].Value.Trim() } else { "No password" } + + $results += [PSCustomObject]@{ + SSID = $name + PASSWORD = $password + CONNECTION_MODE = $connectionMode + } +} + +# Output the results in a table +$results | Format-Table -AutoSize diff --git a/scripts/Win_Win11_Ready.ps1 b/scripts/Win_Win11_Ready.ps1 index cbcf0658..11509dc9 100644 --- a/scripts/Win_Win11_Ready.ps1 +++ b/scripts/Win_Win11_Ready.ps1 @@ -4,9 +4,29 @@ #Returns 'Not Windows 11 Ready' if any of the checks fail, and returns 'Windows 11 Ready' if they all pass. #Useful if running in an automation policy and want to populate a custom field of all agents with their readiness. #This is a modified version of the official Microsoft script here: https://aka.ms/HWReadinessScript +#9/10/2025 silversword411 v1.2 Adding server output # #============================================================================================================================= +$osInfo = Get-WmiObject -Class Win32_OperatingSystem + +# Check if the OS is a server version +# ProductType=1 is Workstation, 2 is Domain Controller, 3 is Server. +if ($osInfo.ProductType -ne 1) { + Write-Output "$($osInfo.Caption)" + Exit 0 +} + +# Check Windows Version +$winVersion = [System.Version]$osInfo.Version + +if ($winVersion -ge [System.Version]::new(10, 0, 22000)) { + Write-Output "Already Windows 11" + Exit 0 +} + +# Continue with Windows 11 readiness check + $exitCode = 0 [int]$MinOSDiskSizeGB = 64 @@ -480,5 +500,6 @@ if (0 -eq $outObject.returncode) { "Windows 11 Ready" } else { - "Not Windows 11 Ready" + "Not Windows 11 Ready | " + Write-Output $outObject.returnReason } \ No newline at end of file diff --git a/scripts/Win_WinGet_Manage_Apps.ps1 b/scripts/Win_WinGet_Manage_Apps.ps1 index db5a3dd5..f92654a6 100644 --- a/scripts/Win_WinGet_Manage_Apps.ps1 +++ b/scripts/Win_WinGet_Manage_Apps.ps1 @@ -38,7 +38,7 @@ if ($Mode -eq "show") { Exit 0 } -if ($Mode -ne "upgrade" -and !$PackageName) { +if ($Mode -ne "update" -and !$PackageName) { write-output "No package name provided, please include Example: `"-PackageName google.chrome`" `n" Exit 1 } diff --git a/scripts_staging/Archives/Backup Veeam api v1_7.py b/scripts_staging/Archives/Backup Veeam api v1_7.py new file mode 100644 index 00000000..482c6ebb --- /dev/null +++ b/scripts_staging/Archives/Backup Veeam api v1_7.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 + +#old script archived & published for posterity was used with old veeam backup api v1_7. +#public + +import os +import sys +import requests +import xml.etree.ElementTree as ET +from datetime import datetime + +# Configuration and constants +VEEAM_API_URL = os.getenv("VEEAM_API_URL") +if not VEEAM_API_URL: + print("Error: VEEAM_API_URL environment variable is required.") + sys.exit(1) + +DEBUG_MODE = os.getenv('DEBUG_MODE', 'false').lower() in ['true', '1', 'yes'] + +def debug_print(message): + """Helper function to print debug messages.""" + if DEBUG_MODE: + print(f"[DEBUG] {message}") + +def authenticate(username, password): + """Authenticate and get session ID using Veeam's legacy session manager endpoint.""" + debug_print("Authenticating with Veeam API.") + auth_url = f"{VEEAM_API_URL}/sessionMngr/?v=v1_7" + headers = {"Content-Type": "application/json"} + + try: + response = requests.post(auth_url, auth=(username, password), headers=headers, verify=False) + response.raise_for_status() + + # Retrieve the session ID from response headers + session_id = response.headers['X-RestSvcSessionId'] + debug_print("Authentication successful. Session ID obtained.") + return session_id + except requests.exceptions.RequestException as e: + print(f"Authentication failed: {e}") + sys.exit(1) + +def parse_restore_points(xml_content): + """Parse XML and find the most recent restore point date per hostname.""" + root = ET.fromstring(xml_content) + hostname_restorepoints = {} + + for ref in root.findall('.//{http://www.veeam.com/ent/v1.0}Ref'): + # Extract the restore point date from the 'Name' attribute + name = ref.get('Name') + restore_date = extract_date_from_name(name) + + # Find hostname from backup job link within the same Ref element + backup_link = ref.find(".//{http://www.veeam.com/ent/v1.0}Link[@Type='BackupReference']") + if backup_link is not None: + hostname = backup_link.get('Name') + + # Update latest restore date for the hostname + if hostname not in hostname_restorepoints or restore_date > hostname_restorepoints[hostname]: + hostname_restorepoints[hostname] = restore_date + + # Print the most recent restore point per hostname + for hostname, date in hostname_restorepoints.items(): + print(f"Hostname: {hostname}, Most Recent Restore Date: {date}") + +def extract_date_from_name(name): + """Extract date from the 'Name' attribute in a specific format.""" + try: + return datetime.strptime(name, '%b %d %Y %I:%M%p') + except ValueError: + debug_print(f"Failed to parse date from name '{name}'") + return None + +def fetch_restore_points(session_id): + """Fetch raw restore points from the /restorePoints endpoint.""" + restore_points_url = f"{VEEAM_API_URL}/restorePoints" + headers = {"X-RestSvcSessionId": session_id} + + try: + response = requests.get(restore_points_url, headers=headers, verify=False) + response.raise_for_status() + + # Parse and link most recent restore points to hostnames + parse_restore_points(response.content) + + except requests.exceptions.RequestException as e: + print(f"Failed to retrieve restore points: {e}") + sys.exit(1) + +def main(): + # Get username and password from environment variables + username = os.getenv('USERNAME') + password = os.getenv('PASSWORD') + + if not (username and password): + print("Username (USERNAME) and password (PASSWORD) are required.") + sys.exit(1) + + # Authenticate and get session ID + session_id = authenticate(username, password) + + # Fetch and process restore points + fetch_restore_points(session_id) + +if __name__ == "__main__": + # Disable SSL warnings + requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) + main() diff --git a/scripts_staging/Backend/Mail notification password expiry.ps1 b/scripts_staging/Backend/Mail notification password expiry.ps1 new file mode 100644 index 00000000..3ad7a771 --- /dev/null +++ b/scripts_staging/Backend/Mail notification password expiry.ps1 @@ -0,0 +1,817 @@ +<# +.SYNOPSIS + Analyzes Active Directory user accounts for upcoming password expiration and optionally sends notifications. + +.DESCRIPTION + This script is configured entirely through environment variables and performs the following: + - Targets a specific Organizational Unit (OU) for user account analysis + - Uses configurable thresholds to classify accounts as warning or critical + - Optionally includes disabled accounts and accounts with passwords set to never expire + - Sends email reports to a list of administrator recipients or can generate reports only + - Supports customizable email signature and SMTP configuration for email delivery + + Accounts are classified based on password expiration: + - Warning: password is approaching expiration (WarningThreshold) + - Critical: password is close to expiring (CriticalThreshold) + +.NOTES + Dependency: + CallPowerShell7 snippet + Author: PQU + Date: 29/04/2025 + #public + +.EXAMPLE + # Example usage with environment variables set before running the script: + + TARGET_OU=OU=Employees,DC=example,DC=local + SMTP_SERVER=smtp.example.com + SMTP_PORT=587 + ADMIN_EMAIL=admin1@example.com,admin2@example.com + FROM_EMAIL=noreply@example.com + WARNING_THRESHOLD=14 + CRITICAL_THRESHOLD=7 + EMAIL_SIGNATURE=Best regards,
IT Department + INCLUDE_DISABLED=true + INCLUDE_NEVER_EXPIRES=false + GENERATE_REPORT_ONLY=false + +.CHANGELOG + 22.05.25 SAN – Added UTF8 encoding to resolve issues with Russian and French characters. + 06.06.25 PQU – Added support for multiple admin emails and centralized config. + 03.07.25 SAN - Update docs + +.TODO + Multiple Locale support + +#> + + +{{CallPowerShell7}} + +function Convert-ToBoolean($value) { + return $value -match '^(1|true|yes)$' +} + +$TargetOU = $env:TARGET_OU +$SmtpServer = $env:SMTP_SERVER +$SmtpPort = [int]$env:SMTP_PORT +$AdminEmails = $env:ADMIN_EMAIL -split '[,;]' | ForEach-Object { $_.Trim() } | Where-Object { $_ } +$FromEmail = $env:FROM_EMAIL +$WarningThreshold = [int]$env:WARNING_THRESHOLD +$CriticalThreshold = [int]$env:CRITICAL_THRESHOLD +$EmailSignature = $env:EMAIL_SIGNATURE +$IncludeDisabled = Convert-ToBoolean $env:INCLUDE_DISABLED +$IncludeNeverExpires = Convert-ToBoolean $env:INCLUDE_NEVER_EXPIRES +$GenerateReportOnly = Convert-ToBoolean $env:GENERATE_REPORT_ONLY + + + + +if ($env:SMTP_CREDENTIAL_USERNAME -and $env:SMTP_CREDENTIAL_PASSWORD) { + try { + $SecurePassword = ConvertTo-SecureString $env:SMTP_CREDENTIAL_PASSWORD -AsPlainText -Force + $SmtpCredential = New-Object System.Management.Automation.PSCredential ($env:SMTP_CREDENTIAL_USERNAME, $SecurePassword) + } catch { + Write-Error "Failed to create SMTP credentials: $_" + } +} + +function Test-Prerequisites { + $adFeature = Get-WindowsFeature -Name AD-Domain-Services -ErrorAction Stop + if ($adFeature.InstallState -ne 'Installed') { + Write-Error "AD Domain Services ne sont pas installés. Arrêt du script." + exit 1 + } + if (-not $SmtpServer -or -not $SmtpPort) { + Write-Error "Les variables `$SmtpServer et `$SmtpPort doivent être définies avant d'appeler cette fonction." + exit 1 + } + if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) { + Write-Error "Module ActiveDirectory non trouvé. Arrêt du script." + exit 1 + } + Import-Module ActiveDirectory -ErrorAction Stop + try { + $dc = Get-ADDomainController -Discover -ErrorAction Stop + Write-Host "Connexion réussie au contrôleur de domaine : $($dc.HostName)" + } + catch { + Write-Error "Impossible de se connecter au contrôleur de domaine. Arrêt du script." + exit 1 + } + try { + $tcpClient = New-Object System.Net.Sockets.TcpClient + $tcpClient.Connect($SmtpServer, $SmtpPort) + $tcpClient.Close() + Write-Host "Connexion réussie au serveur SMTP : $SmtpServer":"$SmtpPort" + } + catch { + Write-Error "Impossible de se connecter au serveur SMTP : $SmtpServer sur le port $SmtpPort. Arrêt du script." + exit 1 + } +} + +function Get-UserPasswordExpirationInfo { + param ( + $user, + $maxPasswordAge + ) + $result = [PSCustomObject]@{ + Name = $user.Name + SamAccountName = $user.SamAccountName + Email = $user.EmailAddress + ExpirationDate = $null + DaysLeft = $null + Status = "OK" + Enabled = $user.Enabled + PasswordNeverExpires = $user.PasswordNeverExpires + } + if ($user.PasswordLastSet -eq $null) { + $result.Status = "NeverLoggedIn" + return $result + } + if ($user.PasswordNeverExpires) { + $result.Status = "NeverExpires" + return $result + } + $passwordExpirationDate = $user.PasswordLastSet + $maxPasswordAge + $daysLeft = ($passwordExpirationDate - (Get-Date)).Days + $result.ExpirationDate = $passwordExpirationDate + $result.DaysLeft = $daysLeft + if ($daysLeft -lt 0) { + $result.Status = "Expired" + } + elseif ($daysLeft -le $CriticalThreshold) { + $result.Status = "Critical" + } + elseif ($daysLeft -le $WarningThreshold) { + $result.Status = "Warning" + } + return $result +} + +function ConvertTo-HtmlReport { + param ( + $expiredUsers, + $criticalUsers, + $warningUsers, + $neverExpiresUsers, + $neverLoggedInUsers, + $disabledUsers, + $targetOU, + $passwordPolicy, + $warningThreshold, + $criticalThreshold + ) + + $expiredSection = "" + if ($expiredUsers.Count -gt 0) { + $rows = $expiredUsers | ForEach-Object { + " + $($_.Name) + $($_.SamAccountName) + $($_.Email) + $($_.ExpirationDate.ToString('dd/MM/yyyy')) + $($_.DaysLeft) + $($_.Enabled) + " + } | Out-String + $expiredSection = @" +
+

Comptes expirés

+ + + + + + + + + + + + + $rows + +
NomSAM Account NameEmailDate d'expirationJours restantsActivé
+
+"@ + } + $criticalSection = "" + if ($criticalUsers.Count -gt 0) { + $rows = $criticalUsers | ForEach-Object { + " + $($_.Name) + $($_.SamAccountName) + $($_.Email) + $($_.ExpirationDate.ToString('dd/MM/yyyy')) + $($_.DaysLeft) + $($_.Enabled) + " + } | Out-String + $criticalSection = @" +
+

Comptes critiques

+ + + + + + + + + + + + + $rows + +
NomSAM Account NameEmailDate d'expirationJours restantsActivé
+
+"@ + } + $warningSection = "" + if ($warningUsers.Count -gt 0) { + $rows = $warningUsers | ForEach-Object { + " + $($_.Name) + $($_.SamAccountName) + $($_.Email) + $($_.ExpirationDate.ToString('dd/MM/yyyy')) + $($_.DaysLeft) + $($_.Enabled) + " + } | Out-String + $warningSection = @" +
+

Comptes en avertissement

+ + + + + + + + + + + + + $rows + +
NomSAM Account NameEmailDate d'expirationJours restantsActivé
+
+"@ + } + $neverExpiresSection = "" + if ($IncludeNeverExpires -and $neverExpiresUsers.Count -gt 0) { + $rows = $neverExpiresUsers | ForEach-Object { + " + $($_.Name) + $($_.SamAccountName) + $($_.Email) + $($_.Enabled) + " + } | Out-String + $neverExpiresSection = @" +
+

Comptes avec mot de passe n'expirant jamais

+ + + + + + + + + + + $rows + +
NomSAM Account NameEmailActivé
+
+"@ + } + $neverLoggedInSection = "" + if ($neverLoggedInUsers.Count -gt 0) { + $rows = $neverLoggedInUsers | ForEach-Object { + " + $($_.Name) + $($_.SamAccountName) + $($_.Email) + $($_.Enabled) + " + } | Out-String + $neverLoggedInSection = @" +
+

Comptes jamais connectés

+ + + + + + + + + + + $rows + +
NomSAM Account NameEmailActivé
+
+"@ + } + $disabledSection = "" + if ($IncludeDisabled -and $disabledUsers.Count -gt 0) { + $rows = $disabledUsers | ForEach-Object { + " + $($_.Name) + $($_.SamAccountName) + $($_.Email) + $($_.ExpirationDate.ToString('dd/MM/yyyy')) + $($_.DaysLeft) + " + } | Out-String + $disabledSection = @" +
+

Comptes désactivés

+ + + + + + + + + + + + $rows + +
NomSAM Account NameEmailDate d'expirationJours restants
+
+"@ + } + + $html = @" + + + + + + Rapport d'expiration des mots de passe + + + +
+

Rapport d'expiration des mots de passe

+
+

Politique de mot de passe du domaine

+

Durée maximale: $($passwordPolicy.MaxPasswordAge.Days) jours

+

Durée minimale: $($passwordPolicy.MinPasswordAge.Days) jours

+

Longueur minimale: $($passwordPolicy.MinPasswordLength) caractères

+

Complexité requise: $($passwordPolicy.ComplexityEnabled)

+

Historique: $($passwordPolicy.PasswordHistoryCount) mots de passe

+

Verrouillage: $($passwordPolicy.LockoutThreshold) tentatives (durée: $($passwordPolicy.LockoutDuration.Minutes) min)

+
+
+

Statistiques globales

+

+ Expirés: $($expiredUsers.Count) + Critiques: $($criticalUsers.Count) + Avertissements: $($warningUsers.Count) + Expirent jamais: $($neverExpiresUsers.Count) + Jamais connectés: $($neverLoggedInUsers.Count) + Désactivés: $($disabledUsers.Count) +

+
+ $expiredSection + $criticalSection + $warningSection + $neverExpiresSection + $neverLoggedInSection + $disabledSection + +
+ + +"@ + return $html +} + +function Get-EmailSignature { + if ($EmailSignature) { + return "" + } + return @" + +"@ +} + +function Send-EmailReport { + param( + [string[]]$Recipients, + [string]$Subject, + [string]$Body, + [string]$SmtpServer, + [int]$Port = 25, + [string]$FromAddress, + [string[]]$Attachments + ) + if ((Get-Date).DayOfWeek -ne 'Monday') { + Write-Host "Les emails ne sont envoyés que le lundi. Arrêt de l'envoi." + return + } + $signature = Get-EmailSignature + $bodyWithSignature = $Body + if ($Body -match '(?i)') { + $bodyWithSignature = $Body -replace '(?i)', "$signature" + } else { + $bodyWithSignature = "$Body$signature" + } + $mailMessage = New-Object System.Net.Mail.MailMessage + $mailMessage.From = $FromAddress + foreach ($recipient in $Recipients) { $mailMessage.To.Add($recipient) } + $mailMessage.Subject = $Subject + $mailMessage.Body = $bodyWithSignature + $mailMessage.IsBodyHtml = $true + if ($Attachments) { + foreach ($att in $Attachments) { + $mailMessage.Attachments.Add((New-Object System.Net.Mail.Attachment($att))) + } + } + $smtpClient = New-Object System.Net.Mail.SmtpClient($SmtpServer, $Port) + if ($SmtpCredential) { + $smtpClient.Credentials = $SmtpCredential + } + try { + $smtpClient.Send($mailMessage) + Write-Host "Email sent successfully." + } + catch { + Write-Error "Failed to send email: $_" + } +} + +function Send-UserNotification { + param( + [string]$Recipient, + [string]$Subject, + [string]$Body, + [string]$SmtpServer, + [int]$Port = 25, + [string]$FromAddress + ) + $signature = Get-EmailSignature + $bodyWithSignature = $Body + if ($Body -match '(?i)') { + $bodyWithSignature = $Body -replace '(?i)', "$signature" + } else { + $bodyWithSignature = @" + + + + + + + +
+$body +
+ + +"@ + } + $mailMessage = New-Object System.Net.Mail.MailMessage + $mailMessage.From = $FromAddress + $mailMessage.To.Add($Recipient) + $mailMessage.Subject = $subject + $mailMessage.Body = $bodyWithSignature + $mailMessage.IsBodyHtml = $true + $smtpClient = New-Object System.Net.Mail.SmtpClient($SmtpServer, $Port) + if ($SmtpCredential) { + $smtpClient.Credentials = $SmtpCredential + } + try { + $smtpClient.Send($mailMessage) + Write-Host "Notification sent to $Recipient." + } + catch { + Write-Error "Failed to send notification to ${Recipient}: $_" + } +} + +try { + $passwordPolicy = Get-ADDefaultDomainPasswordPolicy + $maxPasswordAge = $passwordPolicy.MaxPasswordAge + Write-Host "Politique de mot de passe du domaine:" + Write-Host " - Durée maximale: $($maxPasswordAge.Days) jours" + Write-Host " - Durée minimale: $($passwordPolicy.MinPasswordAge.Days) jours" + Write-Host " - Longueur minimale: $($passwordPolicy.MinPasswordLength) caractères" + Write-Host " - Complexité: $($passwordPolicy.ComplexityEnabled)" +} +catch { + Write-Error "Erreur lors de la récupération de la politique de mot de passe : $_" + exit 1 +} + +try { + $ouExists = Get-ADOrganizationalUnit -Identity $TargetOU -ErrorAction Stop +} +catch { + Write-Error "L'OU spécifiée n'existe pas ou est inaccessible : $TargetOU" + exit 1 +} + +$filter = "PasswordNeverExpires -eq `$false" +if ($IncludeDisabled) { + $filter = "($filter) -or (Enabled -eq `$false)" +} +if ($IncludeNeverExpires) { + $filter = "PasswordNeverExpires -eq `$true -or ($filter)" +} + +try { + Write-Host "Recherche des utilisateurs dans l'OU: $TargetOU" + $users = Get-ADUser -SearchBase $TargetOU -Filter * -Properties Name, SamAccountName, EmailAddress, PasswordLastSet, PasswordNeverExpires, Enabled | Where-Object { + if ($IncludeDisabled -and $IncludeNeverExpires) { $true } + elseif ($IncludeDisabled) { -not $_.PasswordNeverExpires } + elseif ($IncludeNeverExpires) { $_.Enabled } + else { $_.Enabled -and (-not $_.PasswordNeverExpires) } + } + Write-Host "Nombre d'utilisateurs trouvés: $($users.Count)" +} +catch { + Write-Error "Erreur lors de la récupération des utilisateurs : $_" + exit 1 +} + +if (-not $users) { + Write-Host "Aucun utilisateur trouvé dans l'OU spécifiée avec les critères actuels." + exit +} + +$reportData = foreach ($user in $users) { + if ($user.PasswordNeverExpires -or ($user.PasswordLastSet -eq $null -and -not $IncludeNeverExpires)) { + [PSCustomObject]@{ + Name = $user.Name + SamAccountName = $user.SamAccountName + Email = $user.EmailAddress + ExpirationDate = $null + DaysLeft = $null + Status = if ($user.PasswordNeverExpires) { "NeverExpires" } else { "NeverLoggedIn" } + Enabled = $user.Enabled + PasswordNeverExpires = $user.PasswordNeverExpires + } + } + else { + Get-UserPasswordExpirationInfo -user $user -maxPasswordAge $maxPasswordAge + } +} + +$expiredUsers = $reportData | Where-Object { $_.Status -eq "Expired" } | Sort-Object DaysLeft +$criticalUsers = $reportData | Where-Object { $_.Status -eq "Critical" } | Sort-Object DaysLeft +$warningUsers = $reportData | Where-Object { $_.Status -eq "Warning" } | Sort-Object DaysLeft +$neverExpiresUsers = $reportData | Where-Object { $_.Status -eq "NeverExpires" } +$neverLoggedInUsers = $reportData | Where-Object { $_.Status -eq "NeverLoggedIn" } +$disabledUsers = $reportData | Where-Object { $_.Enabled -eq $false } + +$reportFileName = "PasswordExpirationReport_$(Get-Date -Format 'yyyyMMdd_HHmm').html" +$htmlReport = ConvertTo-HtmlReport -expiredUsers $expiredUsers -criticalUsers $criticalUsers -warningUsers $warningUsers -neverExpiresUsers $neverExpiresUsers -neverLoggedInUsers $neverLoggedInUsers -disabledUsers $disabledUsers -targetOU $TargetOU -passwordPolicy $passwordPolicy -warningThreshold $WarningThreshold -criticalThreshold $CriticalThreshold +$htmlReport | Out-File $reportFileName -Encoding UTF8 +Write-Host "Rapport généré avec succès : $reportFileName" +Write-Host "Résumé :" +Write-Host " - Comptes expirés: $($expiredUsers.Count)" +Write-Host " - Comptes critiques: $($criticalUsers.Count)" +Write-Host " - Comptes en avertissement: $($warningUsers.Count)" +Write-Host " - Comptes expirant jamais: $($neverExpiresUsers.Count)" +Write-Host " - Comptes jamais connectés: $($neverLoggedInUsers.Count)" +Write-Host " - Comptes désactivés: $($disabledUsers.Count)" + +if ($GenerateReportOnly) { + Write-Host "Option GenerateReportOnly activée, rapport généré uniquement. Arrêt du script." + exit 0 +} + +foreach ($user in $reportData | Where-Object { $_.Status -in @("Warning", "Critical", "Expired") }) { + if ($user.Email) { + $expirationDate = if ($user.ExpirationDate) { $user.ExpirationDate.ToString("dd/MM/yyyy") } else { "N/A" } + $subject = "Avertissement: Expiration de votre mot de passe" + $body = @" + + + + + + + +
+

⚠️ Avertissement : Expiration de votre mot de passe

+

Bonjour $($user.Name),

+

Votre mot de passe est dans un état $($user.Status).

+

Date d'expiration: $expirationDate

+

Veuillez mettre à jour votre mot de passe dès que possible pour éviter tout problème d'accès.

+ + + + + + + + + + + + + + + + + + + + + + +
Nom$($user.Name)
SAM Account Name$($user.SamAccountName)
Email$($user.Email)
Date d'expiration$expirationDate
Jours restants$($user.DaysLeft)
+
+ + +"@ + Send-UserNotification -Recipient $user.Email -Subject $subject -Body $body -SmtpServer $SmtpServer -Port $SmtpPort -FromAddress $FromEmail + } + else { + Write-Warning "L'utilisateur $($user.Name) n'a pas d'adresse email définie dans Active Directory." + } +} + +if ($AdminEmails) { + if ($reportData.Count -gt 0) { + $smtpServer = $SmtpServer + $smtpPort = $SmtpPort + $fromAddress = $FromEmail + $subject = "Rapport hebdomadaire d'expiration des mots de passe" + $body = $htmlReport + Send-EmailReport -Recipients $AdminEmails -Subject $subject -Body $body -SmtpServer $smtpServer -Port $smtpPort -FromAddress $fromAddress -Attachments @() + } +} else { + Write-Warning "ADMIN_EMAIL n'est pas défini. Aucun email administrateur ne sera envoyé." +} \ No newline at end of file diff --git a/scripts_staging/Backend/Repo package updater.py b/scripts_staging/Backend/Repo package updater.py new file mode 100644 index 00000000..c54dea5a --- /dev/null +++ b/scripts_staging/Backend/Repo package updater.py @@ -0,0 +1,201 @@ +#!/usr/bin/python3 +import os +import re +import time +import requests +import subprocess +from fnmatch import fnmatch +from pathlib import Path + + +""" +.SYNOPSIS + This script automates the process of downloading and pushing Chocolatey packages to a local Chocolatey server. + It fetches the specified packages from the Chocolatey community repository, checks if the package version + already exists locally to avoid duplicates, and then pushes the package to a specified local Chocolatey server. + + It is internaly dubbed "poor's man packages internalizer" + + Usage: + - Set environment variables for the output directory, local Chocolatey server URL, API key, and base URL. + - Define the list of package names to download in the `package_names` list. + - Run the script, and it will download, save, and push the packages. + +.EXEMPLE + CHOCOLATEY_LOCAL_SERVER="https://XXXXXXX.XX/chocolatey" + CHOCOLATEY_OUTDIR=E:\XXXXX + CHOCOLATEY_API_KEY={{global.chocoapikey}} + CHOCOLATEY_BASE_URL=https://community.chocolatey.org/api/v2/package/ + DEBUG=True + +.NOTE + Author: SAN + Date: 01.01.24 + #public + +.TODO + Move package list out of the script + change logic for checking existing packages against push repo rather than folder check + 3 option for each package: + Download all versions available of tagged packages not only the latest + keep only the latest version of tagged packages not all since start. + as current download all version since start but not previous + automatisation of the list package_names based on choco log requests ? (tried download -> package added to list) + External webhook notification when update is done + +.CHANGELOG + 16.04.25 SAN big code cleanup & added a debug flag + +""" + +# List of package names to download +package_names = [ + "Chocolatey", + "chocolatey-compatibility.extension", + "chocolatey-core.extension", + "chocolatey-windowsupdate.extension", + "chocolatey.server", + "KB2919355", + "KB2919442", + "KB3118401", + "powershell-core", + "wazuh-agent", + "win-acme", + "7zip", + "7zip.install", + "accessenum", + "FirefoxESR", + "notepadplusplus", + "notepadplusplus.install", + "vmware-tools", + "windirstat", + "bleachbit", + "bleachbit.install", + "filezilla", + "firebird-odbc", + "nscp", + "syspin", + "adobereader", + "GoogleChrome", + "greenshot", + "keepass", + "keepass.install", + "registryworkshop", + "teamviewer", + "vcredist2010", + "autohotkey", + "autohotkey.install", + "chocolatey.server", + "dotnet", + "DotNet4.6", + "dotnet-8.0-runtime", + "dotnet-runtime", + "KB2999226", + "KB3033929", + "KB3035131", + "vcredist140", + "openssl", + "vcredist2015", + "nirlauncher", + "sysinternals", + "mysql", + "icinga2" +] + + + +# Retrieve and validate required environment variables +def get_env_var(name, default=None, required=True): + value = os.getenv(name, default) + if required and not value: + print(f"Error: {name} environment variable is not set.") + exit(1) + return value + +debug = os.getenv("DEBUG_MODE", "false").lower() == "true" +outdir = Path(get_env_var("CHOCOLATEY_OUTDIR")) +local_choco_server = get_env_var("CHOCOLATEY_LOCAL_SERVER") +api_key = get_env_var("CHOCOLATEY_API_KEY") +base_url = get_env_var("CHOCOLATEY_BASE_URL", "https://community.chocolatey.org/api/v2/package/") + +if not outdir.exists(): + print(f"Output directory '{outdir}' does not exist. Exiting.") + exit(1) + +error_occurred = False + +def extract_version_from_filename(filename): + match = re.search(r"(\d+\.\d+(?:\.\d+)?)", filename) + return match.group(1) if match else None + +def package_version_exists(package_name, version, directory): + return any(fnmatch(f.name, f"*{version}*") for f in directory.iterdir() if f.is_file()) + +def download_package(url): + try: + response = requests.get(url) + if response.status_code == 200: + return response + print(f"[ERROR] Failed to download from {url} — Status: {response.status_code}") + except requests.exceptions.RequestException as e: + print(f"[ERROR] Network error while downloading {url}: {e}") + return None + +def save_package_to_file(response, directory): + filename = os.path.basename(response.url) + filepath = directory / filename + with filepath.open("wb") as f: + f.write(response.content) + return filepath, filename + +def push_to_choco(filepath, server, api_key): + try: + subprocess.run( + ["choco", "push", str(filepath), f"--source={server}", f"--api-key={api_key}", "--force"], + check=True + ) + print(f"[INFO] Pushed: {filepath.name}") + return True + except subprocess.CalledProcessError as e: + print(f"[ERROR] Push failed: {e}") + return False + +def process_package(package_name): + time.sleep(10) + package_url = base_url + package_name + + for attempt in range(1, 4): + response = download_package(package_url) + if response: + filepath, filename = save_package_to_file(response, outdir) + version = extract_version_from_filename(filename) + + print(f"[INFO] Package '{package_name}': Filename = {filename}, Version = {version or 'Unknown'}") + + if version and package_version_exists(package_name, version, outdir): + print(f"[INFO] Package '{package_name}' version {version} already exists. Skipping.") + return True + + print(f"[INFO] Downloaded: {filename}") + + if push_to_choco(filepath, local_choco_server, api_key): + return True + + if attempt < 3: + print(f"[WARN] Retrying '{package_name}' in 1 minute... (Attempt {attempt + 1}/3)") + time.sleep(60) + + print(f"[FAIL] Failed to process '{package_name}' after 3 attempts.") + return False + +# Main loop +for idx, package_name in enumerate(package_names): + if not process_package(package_name): + error_occurred = True + + if debug: + print("[DEBUG] Dry run mode: exiting after first package.") + break + +if error_occurred: + exit(1) \ No newline at end of file diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py new file mode 100644 index 00000000..73709eda --- /dev/null +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -0,0 +1,811 @@ +#!/usr/bin/python3 +""" +.TITLE + Tactical RMM Script Sync with GIT Integration + + +.DESCRIPTION + This script was made to add some form of support to Tactical RMM for GIT sync of scripts and other code-based tools. + It is recommended to run this script regularly to keep everything updated, ideally at least once every hour. + The flags only prevent anything from being written to files or API; any possible outcome will still be displayed on the terminal. + + No script created on git side will be created in TRMM as they will be missing an id in the database and the json that goes with it + While possible no support to auto-create scripts in TRMM is planned as of now as this would also require to plan for multi-instance cases. + + This script can be executed on any device including the TRMM server itself as the only requirements are git + access to the API. + +.WORKFLOW + ------------------------------------------ + 0. /!\TO BE READY BEFORE RUNNING THE SCRIPT/!\: + ------------------------------------------ + The mapped folder should already be configured with git in the way you want to use it. + An api key for a dedicated user with the role including the permissions "List Scripts"+"Manage Scripts" + should be created in TRMM and added in the environements vars as per the exemples below. + + 1. Pull all the modifications from the git repo pre-configured for the folder via git commands + Any modification that would have been done on TRMM and git that would conflit will be overwriten by the GIT in priority. + + 2. Check for diff between the json and scripts; if there is a diff, write back to the API the changes. + + 3. Exports and overwrite all current scripts and scripts data to the 4 folders: + scripts: extracted script code from the API converted from json + scriptsraw: All json data from the API used for hash comparison and ID matches + snippets: extracted snippet code from the API converted from json + snippetsraw: All json data from the API used for hash comparison and ID matches + + 4. Push all the modifications to the git repo pre-configured for the folder via git commands + If there are no changes, no commit will be made. + +.EXEMPLE + MANDATORY: + DOMAIN=api-rmm.exemple.com + DOMAIN={{global.RMM_API_URL}} + API_TOKEN={{global.rmm_key_for_git_script}} + API_TOKEN=asdf1234 + SCRIPTPATH=/var/RMM-script-repo + + OPTIONAL: + ENABLE_GIT_PULL=False + ENABLE_GIT_PUSH=False + ENABLE_WRITEBACK=False + ENABLE_WRITETOFILE=False + GIT_PULL_BRANCH=BranchName + +.NOTES + #public + Original source not disclosed + +.CHANGELOG + v5.0 YYY Exports functional, adds script ID to from as "id - " + v5.a YYY "id - " for only raw folder. Fixed to use X-API-KEY + v5.1 YYY Sanitizing script names when has / in it + v5.2 YYY moving url and api token to .env file + v5.3 YYY Making script folders be subfolders of where export.py file is + v5.4 YYY making filenames utf-8 compliant + v5.5 11/7/2024 XXX Save PowerShell scripts with .ps1 and Python scripts with .py extensions + v5.6 11/7/2024 XXX Count the total number of scripts and print at the end + v5.7 11/7/2024 XXX Print a summary of all the different types of shells exported + v5.8 11/7/2024 XXX Add support for additional shell extension types + v5.9 11/7/2024 XXX Detect deleted scripts and delete them from both folders + v6 31/7/2024 SAN Add support for specifying the save folder via the SCRIPTPATH environment variable + v6.0.1 31/7/2024 SAN Add Git integration to push changes to the configured Git repository + v6.1 06/08/24 SAN add support for snippets + v6.1.1 06/08/24 SAN renamed scriptraw folder + v6.2 14/08/24 SAN Converted categories to folders + v6.2.1 14/08/24 SAN added a cleanup of old scripts + v6.2.2 14/08/24 SAN code cleanup and bug fixes + v9.0.0.1 16/08/24 SAN Added support for git pull for scripts + v9.0.0.2 16/08/24 SAN bug fixes and corrected some logic errors + v9.0.0.3 16/08/24 SAN bug fixe on huge payloads + v9.0.0.4 16/08/24 SAN bug fixe on huge payloads + v9.0.1.0 02/04/25 SAN Added dynamic commit messages + v9.0.1.0 02/04/25 SAN bug fix on commit messages + v9.0.1.1 07/04/25 SAN lots of code optimisation + v9.0.2.0 07/04/25 SAN Added support for snippets writeback, added counters and separators + v9.0.2.1 07/04/25 SAN small optimisations & added a var for changing the branch + v9.0.2.2 07/04/25 SAN better handeling of custom git setup + v9.0.2.3 10/04/25 SAN removed pathvalidate dependency + v9.0.2.4 10/04/25 SAN improvements in the git healthchecks and documentation + v9.0.2.5 11/04/25 SAN added more detailed checks before running and dummy proofing + v9.0.2.6 11/04/25 SAN improvements to sanitize, moved vars to global and fixed an issue that could delete all scripts from git randomly + v9.0.3.0 11/04/25 SAN improvements to the git healthchecks and git push, disabled deletetions if writetofile is false and moved alls toggle flags and branch to env + v9.0.3.1 14/04/25 SAN split step 2 into functions for easier upgrade + v9.0.3.2 24/04/25 SAN couple of pre-flight fixes + v9.0.3.3 24/04/25 SAN fix commit errors + v9.0.4.0 24/04/25 SAN New commit design, decreased importance of uncommited at git check, added emojis ✅, bugfix on git stdout + v9.0.4.1 28/04/25 SAN Paranoid check added to avoid random deletion, more verbose output on file deletion, moved folder check after git as git require an empty folder when first cloning, couple of pre-flight fixes + v9.0.4.2 29/04/25 SAN more explicit part 2 & 3 outputs, added RW check + v9.0.4.3 29/04/25 SAN more explicit outputs for git pull & fix other output + v9.0.4.4 30/04/25 SAN fix pull + +.TODO + move env setup to pre-flight + Review flow of step 3 for optimisations + Review the counters for step 3 + Revamp folder structure: + Move raws from "scriptsraw" to Category_name/raws/ + add "uncategorised" folder + remove "scripts" top level folder while keeping snippets and move snippets raws to snippets/raws/ + + Move ID from json to an array like this and make sure that this array is never overwriten to keep tracks of IDs across instances only add current instance in step 2 if missing: + "ids": [ + { + "server": "rmm.example.com", (this needs to be a hash of the domain not clear text) + "id": 123 + } + before writing to api the modifications in step 2 new function to check all .json for id missing to this instance if missing create script then step 2 will add it to the array + + Delete script support from git ? (dedicated function required at the end of step 2, if json exist but no script matches mark for delete json and use the id of the json to tell the api to delete in trmm) + Squash commit from minor update json with previous commit + Add reporting support + + +""" + +import subprocess +import sys +import os +import hashlib +import json +from collections import defaultdict +from pathlib import Path +import requests +import re +import socket +from requests.exceptions import RequestException, HTTPError + + +# Retrieve the git pull branch or default to 'master' +git_pull_branch = os.getenv('GIT_PULL_BRANCH', 'master') + +# Retrieve flags from environment variables (default to True unless set to 'false') +ENABLE_GIT_PULL = os.getenv('ENABLE_GIT_PULL', 'True').lower() != 'false' +ENABLE_GIT_PUSH = os.getenv('ENABLE_GIT_PUSH', 'True').lower() != 'false' +ENABLE_WRITEBACK = os.getenv('ENABLE_WRITEBACK', 'True').lower() != 'false' +ENABLE_WRITETOFILE = os.getenv('ENABLE_WRITETOFILE', 'True').lower() != 'false' +if not ENABLE_GIT_PULL: print("Git Pull is disabled.") +if not ENABLE_GIT_PUSH: print("Git Push is disabled.") +if not ENABLE_WRITEBACK: print("Writeback is disabled.") +if not ENABLE_WRITETOFILE: print("Write to file is disabled.") + + +def delete_obsolete_files(folder, current_scripts): + if not current_scripts: + print("❌ ERROR: No valid scripts provided by api. Aborting.") + sys.exit(1) + if not isinstance(current_scripts, set): + print("❌ ERROR: 'current_scripts' must be a set. Aborting.") + sys.exit(1) + + print(f"🧹 Cleaning {folder}...") + + all_paths = list(folder.rglob('*')) + obsolete = {f for f in all_paths if f.is_file() and f.relative_to(folder) not in current_scripts} + + if not obsolete: + print("✅ No files missing from the API but still present in the repo.") + else: + with open("deletion.log", "a") as log: + for f in obsolete: + action = "🗑️📄 Deleted" if ENABLE_WRITETOFILE else "🗑️📄 Simulated deletion of" + try: + if ENABLE_WRITETOFILE: + f.unlink() + print(f"{action} file no longer in the API: {f}") + log.write(f"{action}: {f}\n") + except Exception as e: + print(f"⚠️ Error deleting file {f}: {e}") + log.write(f"⚠️ Error deleting {f}: {e}\n") + + empty_dirs = [d for d in sorted(all_paths, key=lambda p: -len(p.parts)) if d.is_dir() and not any(d.iterdir())] + + if not empty_dirs: + print("✅ No empty directories to remove.") + else: + with open("deletion.log", "a") as log: + for d in empty_dirs: + action = "🗑️📁 Removed" if ENABLE_WRITETOFILE else "🗑️📁 Simulated removal of" + try: + if ENABLE_WRITETOFILE: + d.rmdir() + print(f"{action} empty directory: {d}") + log.write(f"{action}: {d}\n") + except Exception as e: + print(f"⚠️ Could not delete dir {d}: {e}") + log.write(f"⚠️ Could not delete dir {d}: {e}\n") + +def sanitize_filename(name: str) -> str: + removed_chars = [] + + if '\0' in name: + removed_chars.append("\\0") + name = name.replace('\0', '') + + invalid_chars = re.findall(r'[<>:"/\\|?*]', name) + if invalid_chars: + removed_chars.extend(invalid_chars) + name = re.sub(r'[<>:"/\\|?*]', '', name) + + if removed_chars: + print(f"Removed from file name: {', '.join(removed_chars)}") + + return name.strip() + +def process_scripts(scripts, script_folder, script_raw_folder, shell_summary, is_snippet=False): + print(f"Processing {'snippets' if is_snippet else 'scripts'}...") + current = set() + + for s in scripts: + sid = s.get('id') + name = sanitize_filename(s.get('name', 'Unnamed Script')) + cat = sanitize_filename(s.get('category', '').strip()) if s.get('category') else '' + folder = script_folder / cat if cat else script_folder + raw_folder = script_raw_folder / cat if cat else script_raw_folder + folder.mkdir(parents=True, exist_ok=True) + raw_folder.mkdir(parents=True, exist_ok=True) + + data = s if is_snippet else pull_from_api(f"{domain}/scripts/{sid}/download/?with_snippets=false") + if not data: continue + + code = data.get('code') + shell = s.get('shell') + ext = {'powershell': '.ps1', 'python': '.py', 'cmd': '.bat', 'shell': '.sh', 'nushell': '.nu'}.get(shell, '.txt') + if not is_snippet: shell_summary[shell] += 1 + + fname = f"{name}{ext}" + save_file(folder / fname, code) + raw_name = f"{sid} - {name}.json" + save_file(raw_folder / raw_name, {**data, **s}, is_json=True) + + current.add((folder / fname).relative_to(script_folder)) + current.add((raw_folder / raw_name).relative_to(script_raw_folder)) + + print(f"Processed {len(current)} {'snippets' if is_snippet else 'scripts'}.\n") + return current + +def compute_hash(file_path): + try: + with open(file_path, 'rb') as f: + return hashlib.sha256(f.read()).hexdigest() + except FileNotFoundError: + return None + +def save_file(path, content, is_json=False): + data = json.dumps(content, indent=4, ensure_ascii=False) if is_json else content + if ENABLE_WRITETOFILE: + path.write_text(data, encoding="utf-8") + print(f"File saved: {path.relative_to(base_dir) if base_dir else path}") + else: + print(f"File would be saved (simulation): {path.relative_to(base_dir) if base_dir else path}") + + +def pull_from_api(url): + try: + print(f"Fetching: {url}") + r = requests.get(url, headers=headers) + r.raise_for_status() + return r.json() if r.ok else [] + except RequestException as e: + print(f"Request failed: {e}") + sys.exit(1) + except ValueError as e: + print(f"Error decoding JSON: {e}") + sys.exit(1) + +def compare_files_and_hashes(match, raw_path): + try: + file_hash = compute_hash(match) + except Exception as e: + print(f"Error computing hash for file {match}: {e}") + return None, None, None + + try: + with raw_path.open(encoding='utf-8') as f: + raw_data = json.load(f) + except (FileNotFoundError, json.JSONDecodeError) as e: + print(f"Error reading JSON file {raw_path}: {e}") + return None, None, None + except Exception as e: + print(f"Unexpected error reading file {raw_path}: {e}") + return None, None, None + + code = raw_data.get('code', '') + try: + code_hash = hashlib.sha256(code.encode('utf-8')).hexdigest() + except Exception as e: + print(f"Error generating hash for code in {raw_path}: {e}") + return None, None, None + + return file_hash, code_hash, raw_data + +def update_api_if_needed(match, raw_data, is_snippet): + try: + with match.open(encoding='utf-8') as f: + updated_payload = {**raw_data, 'code': f.read()} + except (FileNotFoundError, IOError) as e: + print(f"Error reading script file {match}: {e}") + return False + except Exception as e: + print(f"Unexpected error reading file {match}: {e}") + return False + + try: + if ENABLE_WRITEBACK: + print(f"Updating API for {'snippet' if is_snippet else 'script'} {match}...") + update_to_api(raw_data.get('id'), updated_payload, is_snippet) + return True + else: + print(f"Simulated push for {'snippet' if is_snippet else 'script'} {match}:") + updated_payload['script_body'] = updated_payload.pop('code') + print(json.dumps(updated_payload, indent=4)) + sys.stdout.flush() + return False + except (ConnectionError, TimeoutError) as e: + print(f"Network error while updating API for {'snippet' if is_snippet else 'script'} {match}: {e}") + except Exception as e: + print(f"Unexpected error while updating API for {'snippet' if is_snippet else 'script'} {match}: {e}") + + return False + + +def write_modifications_to_api(base_dir, folders): + print("Comparing script files with JSON files...") + mismatches = [] + + total_files_checked = 0 + total_matches = 0 + total_mismatches = 0 + total_updated = 0 + total_not_updated = 0 + + for folder_key, folder in folders.items(): + is_snippet = folder_key == 'snippetsraw' + folder_name = 'snippets' if is_snippet else 'scripts' + + for raw_path in folder.rglob('*.json'): + total_files_checked += 1 + raw_name = re.sub(r'^\d+ - ', '', raw_path.stem).lower() + try: + match = next((p for p in folders[folder_name].rglob('*') + if p.is_file() and p.stem.lower() == raw_name), None) + except Exception as e: + print(f"Error matching file for {raw_path}: {e}") + total_not_updated += 1 + continue + + if not match: + print(f"No match for {'snippet' if is_snippet else 'script'}: {raw_path.relative_to(base_dir)}") + total_not_updated += 1 + continue + + print(f"Matched {'snippet' if is_snippet else 'script'}: {match.relative_to(base_dir)} <-> {raw_path.relative_to(base_dir)}") + total_matches += 1 + + file_hash, code_hash, raw_data = compare_files_and_hashes(match, raw_path) + + if file_hash and code_hash and file_hash != code_hash: + total_mismatches += 1 + print(f"\n--- {'Snippet' if is_snippet else 'Script'} (first 10 lines) ---") + try: + with match.open(encoding='utf-8') as f: + for i, line in enumerate(f): + if i >= 10: break + print(line.strip()) + except Exception as e: + print(f"Error reading file {match}: {e}") + + print(f"\n--- JSON Code (first 10 lines) ---") + for line in raw_data.get('code', '').splitlines()[:10]: + print(line.strip()) + + updated = update_api_if_needed(match, raw_data, is_snippet) + + if updated: + total_updated += 1 + else: + total_not_updated += 1 + + print("\n🔍 Comparison Complete:") + + print(f"🧾 Total files checked: {total_files_checked}") + if total_matches > 0: + print(f"↔️ Total matches: {total_matches}") + if total_mismatches > 0: + print(f"🧩 Total mismatches to update: {total_mismatches}") + if total_updated > 0: + print(f"✅ Total updated: {total_updated}") + if total_not_updated > 0: + print(f"❌ Total errors: {total_not_updated}") + + if total_matches == total_files_checked: + print("✅ Everything is up to date in the api") + + +def update_to_api(item_id, payload, is_snippet=False): + """Update the API with the provided item ID and payload.""" + + if is_snippet: + payload['code'] = payload.pop('code', '') + endpoint = f"{domain}/scripts/snippets/{item_id}/" + else: + payload['script_body'] = payload.pop('code', '') + endpoint = f"{domain}/scripts/{item_id}/" + + body = payload['code'] if is_snippet else payload['script_body'] + + print(f"Updating {'snippet' if is_snippet else 'script'} {item_id}, length: {len(body)}, preview: {body[:1000]}{'...' if len(body) > 1000 else ''}") + + try: + res = requests.put(endpoint, headers=headers, json=payload, timeout=120) + print(f"{item_id} update: {res.status_code} {res.reason}") + if res.status_code != 200: + print(res.text) + except requests.exceptions.RequestException as e: + print(f"Request error for {'snippet' if is_snippet else 'script'} {item_id}: {e}") + +def git_pull(base_dir): + """Force pull latest changes from the git repository if there are changes, discarding local changes.""" + + print("Starting pull process...", flush=True) + try: + print(f"Branch to pull: '{git_pull_branch}'") + print("Fetching latest changes from remote...", flush=True) + subprocess.check_call(['git', '-C', base_dir, 'fetch', 'origin'], stdout=sys.stdout, stderr=sys.stderr) + + print("Checking for incoming changes...", flush=True) + result = subprocess.run( + ['git', '-C', base_dir, 'log', f'HEAD..origin/{git_pull_branch}', '--oneline'], + capture_output=True, text=True + ) + + if not result.stdout.strip(): + print("No changes to pull. Repository is up to date.", flush=True) + return + + print("Incoming commits:") + print(result.stdout, flush=True) + + print(f"Resetting local branch to match 'origin/{git_pull_branch}'...", flush=True) + subprocess.check_call(['git', '-C', base_dir, 'reset', '--hard', f'origin/{git_pull_branch}'], stdout=sys.stdout, stderr=sys.stderr) + + print(f"Force-pull completed from 'origin/{git_pull_branch}'.", flush=True) + except subprocess.CalledProcessError as e: + print("An error occurred during git operations.", flush=True) + print(f"Error details: {e}", flush=True) + sys.exit(1) + + print("Git pull process completed.", flush=True) + +def generate_commit_message(base_dir, max_files=5, skip_raw_dirs=True, group_by_dir=False, use_emojis=True): + """Generate a commit message based on staged changes with optional enhancements.""" + result = subprocess.run( + ['git', '-C', base_dir, 'diff', '--cached', '--name-status'], + capture_output=True, text=True, check=True + ) + + changes = { + "created": [], + "modified": [], + "deleted": [], + "renamed": [] + } + + emoji_map = { + "created": "➕", + "modified": "📝", + "deleted": "🗑️", + "renamed": "🔁" + } + + for line in result.stdout.strip().split("\n"): + if not line: + continue + + parts = line.split("\t") + status = parts[0] + + if status.startswith("R") and len(parts) == 3: + old, new = parts[1], parts[2] + if skip_raw_dirs and (old.startswith("scriptsraw/") or old.startswith("snippetsraw/")): + continue + changes["renamed"].append(f"{old} -> {new}") + elif len(parts) >= 2: + file = parts[1] + if skip_raw_dirs and (file.startswith("scriptsraw/") or file.startswith("snippetsraw/")): + continue + if status.startswith("A"): + changes["created"].append(file) + elif status.startswith("M"): + changes["modified"].append(file) + elif status.startswith("D"): + changes["deleted"].append(file) + + if not any(changes.values()): + return "Minor update" + + parts = [] + for change_type, files in changes.items(): + if not files: + continue + icon = emoji_map[change_type] + " " if use_emojis else "" + + if group_by_dir: + grouped = defaultdict(list) + for f in files: + grouped[f.split(os.sep)[0]].append(f) + detail = "; ".join(f"{k} ({len(v)})" for k, v in grouped.items()) + else: + detail = ", ".join(files[:max_files]) + ("..." if len(files) > max_files else "") + + parts.append(f"{icon}{change_type} {len(files)}: {detail}") + + return "; ".join(parts) + +def git_push(base_dir): + """Push local changes to the git repository.""" + try: + # Get staged changes if none do nothing + status_result = subprocess.run( + ['git', '-C', base_dir, 'status', '--porcelain'], + capture_output=True, text=True + ) + if status_result.stdout: + subprocess.check_call(['git', '-C', base_dir, 'add', '.']) + + commit_message = generate_commit_message(base_dir) + + # Commit & Push changes + subprocess.check_call(['git', '-C', base_dir, 'commit', '-m', commit_message]) + print(f"Committed changes: {commit_message}") + subprocess.check_call(['git', '-C', base_dir, 'push', 'origin', git_pull_branch]) + print(f"Changes pushed to branch '{git_pull_branch}'") + + else: + print("✅ No changes to commit.") + except subprocess.CalledProcessError as e: + print(f"Git operation failed: {e}") + +def check_git_health(base_dir): + """Check the health of the Git repository.""" + + # Check the rights to read/write in the directory + try: + if not os.access(base_dir, os.R_OK | os.W_OK): + print(f"❌ Error: No read/write permissions for the directory '{base_dir}'.") + return False + except Exception as e: + print(f"❌ Error: Failed to check permissions for the directory '{base_dir}'. {e}") + return False + + # Check if 'git' command is available + try: + subprocess.check_call(['git', '--version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError: + print("❌ Error: The 'git' command is not available.") + return False + + # Check if the directory is a valid Git repository + try: + subprocess.check_call(['git', '-C', base_dir, 'rev-parse', '--is-inside-work-tree'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError: + print(f"❌ Error: '{base_dir}' is not a valid Git repository.") + return False + + # Check if the Git index is locked + try: + index_lock = Path(base_dir) / '.git' / 'index.lock' + if index_lock.exists(): + print("❌ Error: Git index is locked. Possibly due to a failed operation.") + return False + except Exception as e: + print(f"❌ Error: Failed to check index lock - {e}") + return False + + # Check if a rebase is in progress + try: + rebase_in_progress = subprocess.run( + ['git', '-C', base_dir, 'rebase', '--show-current-patch'], + capture_output=True, text=True + ).returncode == 0 + if rebase_in_progress: + print("❌ Error: Rebase in progress. Complete or abort it.") + return False + except subprocess.CalledProcessError: + print("❌ Error: Failed to check rebase status.") + return False + + # Check for unresolved merge conflicts + try: + merge_conflicts = subprocess.check_output(['git', '-C', base_dir, 'ls-files', '--unmerged']).decode().strip() + if merge_conflicts: + print("❌ Error: There are unresolved merge conflicts.") + return False + except subprocess.CalledProcessError: + print("❌ Error: Failed to check for merge conflicts.") + return False + + # Check for uncommitted changes + try: + status = subprocess.check_output(['git', '-C', base_dir, 'status', '--porcelain']).decode().strip() + if status: + print("⚠️ Warning: There are uncommitted changes in the Git repository.") + except subprocess.CalledProcessError: + print("❌ Error: Failed to check Git status.") + return False + + # Check for untracked files + try: + untracked_files = subprocess.check_output(['git', '-C', base_dir, 'ls-files', '--others', '--exclude-standard']).decode().strip() + if untracked_files: + print("❌ Error: There are untracked files in the Git repository.") + return False + except subprocess.CalledProcessError: + print("❌ Error: Failed to check for untracked files.") + return False + + # Check the current Git branch + try: + current_branch = subprocess.check_output(['git', '-C', base_dir, 'symbolic-ref', '--short', 'HEAD']).decode().strip() + if current_branch != git_pull_branch: + print(f"❌ Warning: You're not on the expected branch '{git_pull_branch}'. Current branch is '{current_branch}'.") + return False + except subprocess.CalledProcessError: + print("❌ Error: Unable to determine the current Git branch.") + return False + + # Check for remote repository configuration + try: + remote_info = subprocess.check_output(['git', '-C', base_dir, 'remote', 'show', 'origin']).decode().strip() + if not remote_info: + print("❌ Error: No remote repository is configured.") + return False + except subprocess.CalledProcessError: + print("❌ Error: Failed to retrieve remote repository information.") + return False + + # Check if there are commits behind the remote + try: + commits_behind = subprocess.check_output(['git', '-C', base_dir, 'rev-list', '--count', f'HEAD..origin/{git_pull_branch}']).decode().strip() + if int(commits_behind) > 0: + print(f"❌ Error: You are {commits_behind} commits behind the remote branch.") + return False + except subprocess.CalledProcessError: + print("❌ Error: Failed to check commit history.") + return False + + return True + + +def pre_flight(): + global domain, headers, base_dir + + domain = os.getenv('DOMAIN') + api_token = os.getenv('API_TOKEN') + scriptpath = os.getenv('SCRIPTPATH') + + missing = [name for name, val in [('DOMAIN', domain), ('API_TOKEN', api_token), ('SCRIPTPATH', scriptpath)] if not val] + if missing: + print(f"❌ Error: Missing environment variable(s): {', '.join(missing)}") + for var in missing: + if var == 'DOMAIN': print(f" - DOMAIN: The URL of your RMM API. (e.g. api-rmm.example.com)") + if var == 'API_TOKEN': print(f" - API_TOKEN: An API token for a user with permission to access and write scripts.") + if var == 'SCRIPTPATH': print(f" - SCRIPTPATH: The local folder path for Git commands.") + sys.exit(1) + + #Build headers + headers = {"X-API-KEY": api_token} + #Build base_dir path + base_dir = Path(scriptpath).resolve() + + # no http for tcp test or any trailing slash + domain_for_connection = domain.replace("https://", "").replace("http://", "").rstrip("/") + + try: + socket.create_connection((domain_for_connection, 443), timeout=5) + print(f"✅ Connectivity to {domain} on port 443 OK.") + except Exception as e: + obfuscated = api_token[:3] + '*' * (len(api_token) - 6) + api_token[-3:] + print(f"❌ Error: Unable to connect to {domain} on port 443 - {e} (Obfuscated API Token: {obfuscated})") + sys.exit(1) + + # Make sure domain starts with https:// and remove any trailing slash + if not domain.startswith("http://") and not domain.startswith("https://"): + domain = "https://" + domain + domain = domain.rstrip("/") + + #Test api token for read, it is currently not possible to test for write as any request to the api would write empty file. + obfuscated = api_token[:3] + '*' * (len(api_token) - 6) + api_token[-3:] + try: + response = requests.get(f"{domain}/scripts/", headers=headers, timeout=5) + if response.status_code == 200: + print(f"✅ Token valid for read access: {obfuscated}") + else: + print(f"❌ Token read access denied (status {response.status_code}) - Obfuscated Token: {obfuscated}") + sys.exit(1) + except Exception as e: + print(f"❌ Token read access check failed: {e} - Obfuscated Token: {obfuscated}") + sys.exit(1) + + return + + +def check_and_create_folders(base_path, subfolders): + try: + if not base_path.exists(): + base_path.mkdir(parents=True, exist_ok=True) + print(f"✅ Root folder created at {base_path.resolve()}.") + else: + print(f"✅ Root folder exists at {base_path.resolve()}.") + + for folder_path in subfolders.values(): + if folder_path.exists(): + print(f"✅ Folder '{folder_path.name}' exists.") + else: + folder_path.mkdir(parents=True, exist_ok=True) + print(f"✅ Folder '{folder_path.name}' created at {folder_path.resolve()}.") + except Exception as e: + print(f"❌ Error: Failed to create folder(s).") + print(f"Error: {e}") + sys.exit(1) + +def main(): + + # 0. Prep: Verify Dependencies, Set Up Environment, and Git Health Check + print("\n===== Step 0: General Prep =====") + + + # ENV vars & network checks & prep vars + pre_flight() + + # Check the health of the Git repo + if ENABLE_GIT_PULL or ENABLE_GIT_PUSH: + if check_git_health(base_dir): + print("✅ Git repo is healthy.") + else: + print("❌ Error: Git folder is not healthy.") + sys.exit(1) + else: + print("Skipping Git health check because both pull and push are disabled.") + + # Folder structure check + folders = { + "scripts": base_dir / "scripts", + "scriptsraw": base_dir / "scriptsraw", + "snippets": base_dir / "snippets", + "snippetsraw": base_dir / "snippetsraw" + } + check_and_create_folders(base_dir, folders) + print("✅ All folders created and verified.") + + print("===== End of Step 0: General Prep =====\n") + + # 1. Git Pull + print("\n===== Step 1: Git Pull =====") + if ENABLE_GIT_PULL: + git_pull(base_dir) + else: + print("Git pull is disabled.") + print("===== End of Step 1 =====\n") + + # 2. Write modifications to the API + print("\n===== Step 2: Write Modifications to API =====") + write_modifications_to_api(base_dir, folders) + print("===== End of Step 2 =====\n") + + # 3. Fetch and process scripts + print("\n===== Step 3: Fetch and Process Scripts and Snippets =====") + # Initialize counters and sets + shell_summary, current_scripts = defaultdict(int), set() + print("Fetching script list...") + user_defined_scripts = pull_from_api(f"{domain}/scripts/?showHiddenScripts=true") + user_defined_scripts = [item for item in user_defined_scripts if item.get('script_type') == 'userdefined'] + current_scripts.update(process_scripts(user_defined_scripts, folders["scripts"], folders["scriptsraw"], shell_summary)) + + # Fetch and process snippets + print("Fetching snippets list...") + snippets = pull_from_api(f"{domain}/scripts/snippets/") + current_scripts.update(process_scripts(snippets, folders["snippets"], folders["snippetsraw"], shell_summary, is_snippet=True)) + + # Output the total number of scripts exported and provide a summary of the shell counts + print(f"Total number of scripts exported: {len(current_scripts)}") + print("Shell summary:") + for shell, count in shell_summary.items(): + print(f"{shell.strip()}: {count}") + + # Remove any obsolete files that are no longer existing in the api + print("\nRemove any obsolete files") + for folder in folders.values(): + delete_obsolete_files(folder, current_scripts) + + print("===== End of Step 3 =====\n") + + # 4. Git Push + print("\n===== Step 4: Git Push =====") + if ENABLE_GIT_PUSH: + git_push(base_dir) + else: + print("Git push is disabled.") + print("===== End of Step 4 =====\n") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts_staging/Backend/Uptime Kuma Monitoring For Tactical.py b/scripts_staging/Backend/Uptime Kuma Monitoring For Tactical.py new file mode 100644 index 00000000..e8938e2f --- /dev/null +++ b/scripts_staging/Backend/Uptime Kuma Monitoring For Tactical.py @@ -0,0 +1,542 @@ +#!/usr/bin/python3 +#public +''' +.SYNOPSIS + Python script designed to automatically update the interface of Uptime-Kuma based online machines for Tactical. + +.DESCRIPTION + This script operates in two parts. The first part retrieves information from the field and the Agent ID from the Tactical Swagger + After fetching the information, it checks whether the websites still exist in Tactical. If they don't, the script removes them from the dashboard. + Additionally, it verifies if the sites are already present; if not, it creates them, specifying the name, URL, and Agent ID in the description. + +.ADDITIONAL INFORMATIONS + API : https://uptime-kuma-api.readthedocs.io/en/latest/index.html + Docker-Compose : uptime-kuma on dockge + Version : 1.5.2 + +.NOTE + Author: MSA/SAN + Date: 17.08.24 + +.EXEMPLE +endpoint_uptimekuma=UPTIME URL +user_uptimekuma=UPTIME USER +password_uptimekuma={{global.uptimepassword}} +rmm_key_for_uptime={{global.rmm_key_for_uptime_script}} +rmm_url=https://RMM API URL/agents +CustomFieldID=11111111 + +.TODO + When a hostname is removed/moved, this script doesn't automatically delete it. Need to be fix. + The HTTP protocol is automatically replaced by HTTPS. This should be adjusted to retain HTTP when specific keywords are used. + Remove the URL from the display name. +''' + + +# Import standard modules +import sys +import subprocess +import re +import os +import requests +import time + +# Function to install missing packages +def install(package): + subprocess.check_call([sys.executable, "-m", "pip", "install", package]) + +# Attempt to import the 'uptime_kuma_api' module +try: + import uptime_kuma_api +except ImportError: + print("Module 'uptime_kuma_api' not found. Installing...") + install("uptime_kuma_api") + +# Import additional modules needed for interacting with Uptime-Kuma API +from uptime_kuma_api import UptimeKumaApi, MonitorType + +# Initialise connection to the Uptime-Kuma API +api = UptimeKumaApi(os.environ.get('endpoint_uptimekuma')) +api.login(os.environ.get('user_uptimekuma'), os.environ.get('password_uptimekuma')) + +# Define API key and URL from environment variables +api_key = os.getenv('rmm_key_for_uptime') +url = os.getenv('rmm_url') +custom_field_id = int(os.getenv('CustomFieldID')) + +# Define headers for the API request +headers = { + "X-API-KEY": api_key, + "Accept": "application/json" +} + +try: + # Send a GET request to the specified URL + response = requests.get(url, headers=headers) + + if response.status_code == 200: + # Parse the JSON response + data = response.json() + + if isinstance(data, list): + for agent in data: + # Check if 'custom_fields' is present in the agent data + if 'custom_fields' in agent: + # Extract values from custom fields where the field ID is custom_field_id + filtered_values = [cf['value'] for cf in agent['custom_fields'] if cf.get('field') == custom_field_id and cf.get('value')] + + # Process agents with at least one relevant custom field + if filtered_values: + + # Extract agent details + agent_id = agent.get('agent_id', 'N/A') + default_hostname = agent.get('hostname', 'N/A') + site_name = agent.get('site_name', 'N/A') + client_name = agent.get('client_name', 'N/A') + public_ip = agent.get('public_ip', 'N/A') + + # Get 5 first character + agent_id_5_char = agent_id[:5] + + # Hostname full name + hostname = f"{default_hostname} [{agent_id_5_char}]" + + # Space in order to have an output more clearly + print() + + # Check and deploy client monitor + monitors = api.get_monitors() + client_monitor = next((monitor for monitor in monitors if monitor.get('name') == client_name), None) + + if client_monitor: + print(f"{client_name} already exists") + else: + api.add_monitor( + type=MonitorType.GROUP, + name=client_name, + description="Client" + ) + print(f"Client {client_name} has been created") + + # Check and deploy site monitor under the client + monitors = api.get_monitors() + client_monitor = next((monitor for monitor in monitors if monitor.get('name') == client_name), None) + + if any(monitor.get('name') == site_name and monitor.get('parent') == client_monitor.get('id') for monitor in monitors): + print(f"{site_name} already exists on {client_name}") + else: + api.add_monitor( + type=MonitorType.GROUP, + name=site_name, + parent=client_monitor.get('id'), + description="Site" + ) + print(f"Site {site_name} has been created on {client_name}") + + # Check and deploy hostname monitor under the site + monitors = api.get_monitors() + site_monitor = next((monitor for monitor in monitors if monitor.get('name') == site_name and monitor.get('parent') == client_monitor.get('id')), None) + + if site_monitor: + if any(monitor.get('name') == hostname and monitor.get('parent') == site_monitor.get('id') for monitor in monitors): + print(f"{hostname} already exists on {client_name} / {site_name}") + else: + api.add_monitor( + type=MonitorType.GROUP, + name=hostname, + parent=site_monitor.get('id'), + description="Hostname" + ) + print(f"Hostname {hostname} - {agent_id} has been created on {client_name} / {site_name}") + + # Space in order to have an output more clearly + print() + + # Add specific monitors based on filtered values + monitors = api.get_monitors() + monitor_id = None + + # Find monitor ID for hostname + for monitor in monitors: + if monitor.get('name') == hostname: + monitor_id = monitor.get('id') + + # Get relevant monitors that are children of the hostname monitor + relevant_monitors = [monitor for monitor in monitors if monitor.get('parent') == monitor_id] + + for value in filtered_values: + + # Add TCP port monitors with IP addresses + tcp_ports_with_ip_matches = re.findall(r'(\d+):(\d+\.\d+\.\d+\.\d+)', value) + + for port, ip in tcp_ports_with_ip_matches: + if port.isdigit(): + port_int = int(port) + + monitor_name = f"{port_int} - {ip} [{agent_id_5_char}]" + + if any(monitor.get('name') == monitor_name for monitor in relevant_monitors): + print(f"{monitor_name} already exists on {client_name} / {site_name} / {hostname}") + else: + api.add_monitor( + type=MonitorType.PORT, + name=monitor_name, + port=port_int, + interval=60, + retryInterval=20, + maxretries=20, + parent=monitor_id, + description=f"Agent ID: {agent_id}", + hostname=ip + ) + print(f"Monitoring TCP for {monitor_name} has been created on {client_name} / {site_name} / {hostname}") + + # Add TCP port monitors with default IP addresses + value = re.sub(r'\d+:\d+\.\d+\.\d+\.\d+', '', value) + tcp_ports_no_ip_matches = re.findall(r'\b\d{1,5}\b', value) + + for port in tcp_ports_no_ip_matches: + if port.isdigit(): + port_int = int(port) + + monitor_name = f"{port_int} - {public_ip} [{agent_id_5_char}]" + + if any(monitor.get('name') == monitor_name for monitor in relevant_monitors): + print(f"{monitor_name} already exists on {client_name} / {site_name} / {hostname}") + else: + api.add_monitor( + type=MonitorType.PORT, + name=monitor_name, + port=port_int, + interval=60, + retryInterval=20, + maxretries=20, + parent=monitor_id, + description=f"Agent ID: {agent_id}", + hostname=public_ip + ) + print(f"Monitoring TCP for {monitor_name} has been created on {client_name} / {site_name} / {hostname}") + + # Add HTTP monitors for URLs + http_section_match = re.search(r'HTTP:\s*((?:(?!TCP:|KEYWORD:)[\s\S])*)', value) + if http_section_match: + http_section = http_section_match.group(1).strip() + http_urls = [url.strip() for url in http_section.split('\n') if url.strip()] + + for url in http_urls: + original_url = url + + url = re.sub(r'^(https?:\/\/)+', '', url) + url = re.sub(r'^\/+', '', url) + url = re.sub(r'\s+', ' ', url) + url = url.strip() + + if original_url.lower().startswith('http:'): + protocol = 'http://' + elif original_url.lower().startswith('https:'): + protocol = 'https://' + else: + protocol = 'https://' + + full_url = f"{protocol}{url}" + monitor_name = f"{full_url} [{agent_id_5_char}]" + + if re.match(r'^https?:\/\/[a-zA-Z0-9-._~:/?#\[\]@!$&\'()*+,;%=]+$', full_url): + if any(monitor.get('name') == monitor_name for monitor in relevant_monitors): + print(f"{monitor_name} already exists on {client_name} / {site_name} / {hostname}") + else: + api.add_monitor( + type=MonitorType.HTTP, + name=monitor_name, + url=full_url, + interval=60, + retryInterval=20, + maxretries=20, + timeout=15, + expiryNotification=True, + parent=monitor_id, + description=f"Agent ID: {agent_id}", + hostname=public_ip + ) + print(f"Monitoring HTTP for {monitor_name} has been created on {client_name} / {site_name} / {hostname}") + else: + print(f"Invalid HTTP URL: {full_url}") + + # Add KEYWORD monitors for keyword-based URLs + keyword_urls_matches = re.findall(r'KEYWORD:\s*((?:[^\n]+(?:\n(?!TCP:|HTTP:))?)+)', value, re.DOTALL) + if keyword_urls_matches: + for match in keyword_urls_matches: + urls = match.strip().split('\n') + for url in urls: + url = url.strip() + + if ':' in url: + base_url, keyword = url.rsplit(':', 1) + else: + base_url = url + keyword = 'test' + + original_protocol = '' + if base_url.lower().startswith('http://'): + original_protocol = 'http://' + elif base_url.lower().startswith('https://'): + original_protocol = 'https://' + + base_url = re.sub(r'^(https?:\/\/)+', '', base_url) + base_url = re.sub(r'^\/+', '', base_url) + base_url = re.sub(r'\s+', ' ', base_url) + base_url = base_url.strip() + + if original_protocol: + base_url = f"{original_protocol}{base_url}" + elif not base_url.startswith(('http://', 'https://')): + base_url = f"https://{base_url}" + + keyword = keyword.strip() + monitor_name = f"{base_url} - {keyword} [{agent_id_5_char}]" + if any(monitor.get('name') == monitor_name for monitor in relevant_monitors): + print(f"{monitor_name} already exists on {client_name} / {site_name} / {hostname}") + else: + api.add_monitor( + type=MonitorType.KEYWORD, + name=monitor_name, + url=base_url, + keyword=keyword, + interval=60, + retryInterval=20, + maxretries=20, + timeout=15, + expiryNotification=True, + parent=monitor_id, + description=f"Agent ID: {agent_id}", + hostname=public_ip + ) + print(f"Monitoring KEYWORD for {monitor_name} has been created on {client_name} / {site_name} / {hostname}") + + # Space in order to have an output more clearly + print() + + # Reset default values + monitors = api.get_monitors() + monitor_id = None + + # Find monitor ID for hostname + for monitor in monitors: + if monitor.get('name') == hostname: + monitor_id = monitor.get('id') + + # Get relevant monitors that are children of the hostname monitor + relevant_monitors = [monitor for monitor in monitors if monitor.get('parent') == monitor_id] + + # Check and remove TCP port monitors with default IP if no longer relevant + for monitor in relevant_monitors: + if monitor.get('type') == 'port': + monitor_name = monitor.get('name') + monitor_id = monitor.get('id') + exists_in_value = False + + # Extract port, IP, and agent_id from monitor name + port_ip_match = re.match(r'(\d+) - ([\d\.]+) \[(.*?)\]', monitor_name) + if port_ip_match: + monitor_port, monitor_ip, monitor_agent_id = port_ip_match.groups() + + for value in filtered_values: + # Check for ports with specific IP + if re.search(rf'{monitor_port}:{monitor_ip}', value): + exists_in_value = True + break + + # Check for ports with public IP + if monitor_ip == public_ip: + tcp_ports_no_ip = re.sub(r'\d+:\d+\.\d+\.\d+\.\d+', '', value) + tcp_ports = re.findall(r'\b(\d+)\b', tcp_ports_no_ip) + if monitor_port in tcp_ports: + exists_in_value = True + break + + # Check if the monitor belongs to the current agent + if monitor_agent_id != agent_id_5_char: + exists_in_value = True # Don't delete monitors from other agents + + if not exists_in_value: + print(f"{monitor_name} does not exist anymore on the agent and has been deleted on {client_name} / {site_name} / {hostname}") + api.delete_monitor(monitor_id) + + # Check and remove HTTP monitors if no longer relevant + for monitor in relevant_monitors: + if monitor.get('type') == 'http': + monitor_name = monitor.get('name') + monitor_id = monitor.get('id') + exists_in_value = False + + for value in filtered_values: + http_urls_matches = re.findall(r'HTTP:\s*((?:[^\n]+\n?)+)', value, re.DOTALL) + if http_urls_matches: + for match in http_urls_matches: + urls = match.strip().split('\n') + for url in urls: + url = url.strip() + + url = re.sub(r'^(https?:\/\/)+', '', url) + url = re.sub(r'^\/+', '', url) + url = re.sub(r'\s+', ' ', url) + url = url.strip() + + if url.lower().startswith('https:'): + protocol = 'https://' + url = url[6:] + elif url.lower().startswith('http:'): + protocol = 'http://' + url = url[5:] + else: + protocol = 'https://' + + full_url = f"{protocol}{url}" + expected_name = f"{full_url} [{agent_id_5_char}]" + + if monitor_name == expected_name: + exists_in_value = True + break + if exists_in_value: + break + if exists_in_value: + break + if exists_in_value: + break + + if not exists_in_value: + print(f"{monitor_name} does not exist anymore on the agent and has been deleted on {client_name} / {site_name} / {hostname}") + api.delete_monitor(monitor_id) + + # Check and remove KEYWORD monitors if no longer relevant + for monitor in relevant_monitors: + if monitor.get('type') == 'keyword': + monitor_name = monitor.get('name') + monitor_id = monitor.get('id') + exists_in_value = False + + for value in filtered_values: + keyword_urls_matches = re.findall(r'KEYWORD:\s*((?:[^\n]+\n?)+)', value, re.DOTALL) + if keyword_urls_matches: + for match in keyword_urls_matches: + urls = match.strip().split('\n') + for url in urls: + url = url.strip() + + if ':' in url: + base_url, keyword = url.rsplit(':', 1) + else: + base_url = url + keyword = 'test' + + base_url = re.sub(r'^(https?:\/\/)+', '', base_url) + base_url = re.sub(r'^\/+', '', base_url) + base_url = re.sub(r'\s+', ' ', base_url) + base_url = base_url.strip() + + if not base_url.startswith(('http://', 'https://')): + base_url = f"https://{base_url}" + + keyword = keyword.strip() + + expected_name = f"{base_url} - {keyword} [{agent_id_5_char}]" + if monitor_name == expected_name: + exists_in_value = True + break + if exists_in_value: + break + if exists_in_value: + break + if exists_in_value: + break + + if not exists_in_value: + print(f"{monitor_name} does not exist anymore on the agent and has been deleted on {client_name} / {site_name} / {hostname}") + api.delete_monitor(monitor_id) + + # Space in order to have an output more clearly + print() + + # Additional wait to ensure API is fully synced + time.sleep(1) + + + # Get custom fields with the field ID from the environment variable that have no value for the given agent + empty_values = [cf for cf in agent['custom_fields'] if cf.get('field') == custom_field_id and not cf.get('value')] + + # Proceed only if there are custom fields that are empty + if empty_values: + # Fetch agent details with fallback defaults if values are missing + # Extract agent details + agent_id = agent.get('agent_id', 'N/A') + default_hostname = agent.get('hostname', 'N/A') + site_name = agent.get('site_name', 'N/A') + client_name = agent.get('client_name', 'N/A') + + # Get 5 first character + agent_id_5_char = agent_id[:5] + + # Hostname full name + hostname = f"{default_hostname} [{agent_id_5_char}]" + + # Get the list of all relevant monitors via API + relevant_monitors = api.get_monitors() + + # Loop through each monitor to check for matches with the agent's hostname + for monitor in relevant_monitors: + monitor_name = monitor.get('name') + monitor_id = monitor.get('id') + monitor_child = monitor.get('childrenIDs', []) + + # If the monitor's name matches the agent's hostname, proceed + if monitor_name == hostname: + + # Loop through child monitors of the matched monitor + for child in monitor_child: + # Get the name of the child monitor, with a fallback to 'Unknown' if not found + child_monitor_name = api.get_monitor(child).get('name', 'Unknown') + + # Log a message about the child monitor being deleted + print(f"{child_monitor_name} does not exist anymore on the agent and has been deleted on {client_name} / {site_name} / {hostname}") + + # Delete the child monitor via API + api.delete_monitor(child) + + # Additional wait to ensure API is fully synced + time.sleep(1) + + # Check and remove group monitors with no children + NoSubMonitor = True + + while NoSubMonitor: + relevant_monitors = api.get_monitors() + NoSubMonitor = False + + for monitor in relevant_monitors: + if monitor.get('type') == 'group': + monitor_name = monitor.get('name') + monitor_id = monitor.get('id') + children_ids = monitor.get('childrenIDs', []) + + if not children_ids: + print(f"{monitor_name} does not have any children") + api.delete_monitor(monitor_id) + NoSubMonitor = True # Continue loop if any monitor was deleted + + # Additional wait to ensure API is fully synced + time.sleep(1) + + time.sleep(2) # Sleep to avoid rapid API calls + + else: + print("Unexpected data format received.") + + else: + print(f"Request failed. Status code: {response.status_code}") + print(f"Error message: {response.text}") + +except requests.exceptions.RequestException as e: + print(f"An error occurred during the request: {e}") + +# Disconnect from the API service +api.disconnect() \ No newline at end of file diff --git a/scripts_staging/Build/Change NTP target to company.ps1 b/scripts_staging/Build/Change NTP target to company.ps1 new file mode 100644 index 00000000..9d6c6f65 --- /dev/null +++ b/scripts_staging/Build/Change NTP target to company.ps1 @@ -0,0 +1,71 @@ +<# +.SYNOPSIS + Configures the system's time synchronization with an NTP server if the computer is not part of a domain or is a Domain Controller. + +.DESCRIPTION + This script checks the domain membership status of the machine. + If the device is either not part of a domain or is a Domain Controller, it configures the Windows Time service (`w32time`) to synchronize with an NTP server specified in the `NTPTARGET` environment variable. The script updates registry settings related to time synchronization, ensures the correct time zone is set, and forces a time resynchronization. + +.PARAMETER NTPTARGET + The NTP server address that the machine will use for time synchronization. + This can be specified through the environment variable `NTPTARGET`. + +.EXAMPLE + NTPTARGET=pool.ntp.org + This will configure the system to synchronize its time with `pool.ntp.org`. + +.NOTES + Author: SAN + Date: 01.01.2024 + #public + +.CHANGELOG + + +#> + + + +try { + $computerSystem = Get-WmiObject -Class Win32_ComputerSystem + $domain = $computerSystem.PartOfDomain + $isDomainController = $computerSystem.DomainRole -eq 4 -or $computerSystem.DomainRole -eq 5 +} catch { + Write-Host "Error determining domain membership status. Exiting script." + exit +} + +$ntpTarget = $env:NTPTARGET +if (-not $ntpTarget) { + Write-Host "NTPTARGET environment variable is not set. Exiting script." + exit +} + +if (-not $domain -or $isDomainController) { + Write-Host "Device is not a member of a domain or is a Domain Controller. Proceeding with time configuration." + + Start-Service w32time + + w32tm /config /manualpeerlist:"$ntpTarget,0x8" /syncfromflags:manual /reliable:yes /update + + try { + New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpClient" -Name "SpecialPollInterval" -Value 3600 -PropertyType DWord -Force + New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DateTime\Servers" -Name "0" -Value "$ntpTarget" -PropertyType String -Force + New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DateTime\Servers" -Name "(default)" -Value "0" -PropertyType String -Force + } catch { + Write-Host "Error setting registry values. Exiting script." + exit + } + + Set-Service W32Time -StartupType "Automatic" + + Stop-Service w32time + Start-Service w32time + Set-TimeZone -Name "W. Europe Standard Time" + + w32tm /resync + + Write-Host "Time configuration done." +} else { + Write-Host "Device is a member of a domain and is not a Domain Controller. Skipping time configuration." +} \ No newline at end of file diff --git a/scripts_staging/Build/Change default chocolatey repo to internal.ps1 b/scripts_staging/Build/Change default chocolatey repo to internal.ps1 new file mode 100644 index 00000000..25216e19 --- /dev/null +++ b/scripts_staging/Build/Change default chocolatey repo to internal.ps1 @@ -0,0 +1,54 @@ +<# +.SYNOPSIS + Updates Chocolatey package sources by removing existing repositories and adding new ones with specified priorities. + +.DESCRIPTION + This script removes the specified Chocolatey package sources and adds new sources based on environment variables. + It sets the priority for the new source to a specified value and ensures that the default Chocolatey source is added with a lower priority or removed. + +.EXAMPLE + NEW_URL="https://myrepo.com/chocolatey/" + NEW_NAME="myrepo" + keepDefaultRepo=0 + +.NOTES + Author: SAN + Date: 01.01.2024 + #public + +.CHANGELOG + SAN 11.12.24 Moved new info to env + SAN 03.05.25 Added a flag to keep or not the default repo + SAN 18.06.25 Added outputs + +#> + +$newUrl = $env:NEW_URL +$newPriority = 5 +$newName = $env:NEW_NAME + +$defaultUrl = "https://chocolatey.org/api/v2/" +$defaultPriority = 10 +$defaultName = "chocolatey" + +# Default to keeping the default repo unless explicitly set to "0" +$keepDefaultRepo = ($env:keepDefaultRepo -ne '0') + +# Always remove both sources to ensure clean state and updated priority +Write-Output "Removing Chocolatey source: $newName (if exists)" +choco source remove -n $newName -y + +Write-Output "Removing Chocolatey source: $defaultName (if exists)" +choco source remove -n $defaultName -y + +# Add the new internal Chocolatey repository +Write-Output "Adding new Chocolatey source: $newName with URL $newUrl and priority $newPriority" +choco source add -n $newName -s $newUrl --priority $newPriority + +# Conditionally re-add the default repository +if ($keepDefaultRepo) { + Write-Output "Re-adding default Chocolatey source: $defaultName with priority $defaultPriority" + choco source add -n $defaultName -s $defaultUrl --priority $defaultPriority +} else { + Write-Output "Skipping re-adding default Chocolatey source" +} diff --git a/scripts_staging/Build/Create generic admin account.ps1 b/scripts_staging/Build/Create generic admin account.ps1 new file mode 100644 index 00000000..9b159c3d --- /dev/null +++ b/scripts_staging/Build/Create generic admin account.ps1 @@ -0,0 +1,74 @@ +<# +.SYNOPSIS + This script checks if an admin user exists, and if so, changes the password and ensures the user is added to the Administrators group. + +.DESCRIPTION + The script retrieves the admin username from the environment variable `adminusername` and generates a passphrase. + It checks if the user exists on the system, then either updates the password for an existing user or creates the user if they do not exist. + It also ensures the user is added to both the 'Administrators' and 'Administrateurs' local groups and disables the password expiration. + +.PARAMETER adminusername + The environment variable `adminusername` should be set with the desired username for the admin account. + +.EXAMPLE + adminusername=adminUser + +.NOTES + Author: SAN + Date: 01.01.24 + Dependencies: + GeneratedPassphrase snippet + #public + +.CHANGELOG + + + +#> + + +{{GeneratedPassphrase}} + +# Get admin username and password +$adminUsername = $env:adminusername +$adminPassword = $GeneratedPassphrase + +# Check if the admin username is provided +if (-not $adminUsername) { + Write-Output "adminusername environment variable is not set. Exiting script." + exit 1 +} + +# Check if the user already exists +$existingUser = & net user $adminUsername 2>&1 +if ($LASTEXITCODE -eq 0) { + # User already exists + Write-Output "The user '$adminUsername' already exists." + try { + # Change password + & net user $adminUsername $adminPassword + & wmic UserAccount where "Name='$adminUsername'" set PasswordExpires=False + & net localgroup Administrators $adminUsername /add + & net localgroup Administrateurs $adminUsername /add + Write-Output "The password for user '$adminUsername' has been changed." + } + catch { + Write-Output "Failed to change the password for user '$adminUsername'." + } +} +else { + # User doesn't exist + Write-Output "The user '$adminUsername' does not exist." + try { + # Create user + & net user $adminUsername $adminPassword /add /Y + Write-Output "The user '$adminUsername' has been created with the password '$adminPassword'." + & net localgroup Administrators $adminUsername /add + & net localgroup Administrateurs $adminUsername /add + Write-Output "The user '$adminUsername' has been added to the Administrators group." + & wmic UserAccount where "Name='$adminUsername'" set PasswordExpires=False + } + catch { + Write-Output "Failed to create the user '$adminUsername'." + } +} \ No newline at end of file diff --git a/scripts_staging/Build/Forward HTTP Traffic To Company Website.ps1 b/scripts_staging/Build/Forward HTTP Traffic To Company Website.ps1 new file mode 100644 index 00000000..7f4d087a --- /dev/null +++ b/scripts_staging/Build/Forward HTTP Traffic To Company Website.ps1 @@ -0,0 +1,73 @@ +<# +.SYNOPSIS + Incoming traffic for HTTP and HTTPS destined for the domain controlleris forwarded to the company's website + +.DESCRIPTION + This script is designed to set up port forwarding on a domain controller to forward traffic from port 80 (HTTP) + and port 443 (HTTPS) to a website associated with the domain. + This is done to allow having the same AD domain as the company website. + + The script performs the following actions: + 1. Checks if the script is running on a domain controller. + 2. Retrieves the domain name of the device. + 3. Resolves the public IP address of the domain using a specified DNS server. + 4. Configures port proxy rules to forward traffic from port 80 and port 443 on the domain controller + to the resolved IP address. + 5. Creates a single Windows Firewall rule to allow inbound traffic on ports 80 and 443 from the local subnet only. + +.EXEMPLE + DNS_SERVER=x.x.x.x + +.NOTES + Author: SAN + Date: 15.08.24 + #public + +.CHANGELOG + 11.12.24 SAN Added a var for DNS srv + +#> + +# Check if the machine is a domain controller +$isDomainController = (Get-WmiObject -Class Win32_ComputerSystem).DomainRole -eq 5 + +if (-not $isDomainController) { + Write-Output "Error: This script can only be run on a domain controller." + exit 1 +} + +# Get the domain name of the device +$domainName = (Get-WmiObject -Class Win32_ComputerSystem).Domain +Write-Output "Local domain: $domainName" +# Resolve the main local IP address (excluding loopback and other non-primary IPs) +$localIP = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object {$_.InterfaceAlias -ne "Loopback Pseudo-Interface 1"} | Sort-Object -Property AddressFamily,PrefixLength -Descending | Select-Object -First 1).IPAddress +Write-Output "Local ip: $localIP" + +# Get the DNS server from the environment variable, defaulting to 9.9.9.9 if not set +$dnsServer = [System.Environment]::GetEnvironmentVariable('DNS_SERVER') +if (-not $dnsServer) { + $dnsServer = '9.9.9.9' +} +Write-Output "Using DNS server: $dnsServer" + +# Resolve the IP address of the domain using the specified DNS server +$connectIP = Resolve-DnsName -Name $domainName -Server $dnsServer | Where-Object { $_.QueryType -eq "A" } | Select-Object -ExpandProperty IPAddress +Write-Output "Resolved public ip: $connectIP" +if (-not $connectIP) { + Write-Output "Error: Could not resolve the IP address for $domainName." + exit 1 +} + +# Apply the port proxy for HTTPS (port 443) +Write-Output "netsh interface portproxy add v4tov4 listenport=443 listenaddress=$localIP connectport=443 connectaddress=$connectIP" +netsh interface portproxy add v4tov4 listenport=443 listenaddress=$localIP connectport=443 connectaddress=$connectIP + +# Apply the port proxy for HTTP (port 80) +Write-Output "netsh interface portproxy add v4tov4 listenport=80 listenaddress=$localIP connectport=80 connectaddress=$connectIP" +netsh interface portproxy add v4tov4 listenport=80 listenaddress=$localIP connectport=80 connectaddress=$connectIP + +Write-Output "Configuration of the firewall" +# Apply a single firewall rule for both ports 80 and 443 allowing traffic from the local subnet only +New-NetFirewallRule -DisplayName 'Open Ports 80 and 443 (LocalSubnet)' -Direction Inbound -LocalPort 80,443 -Protocol TCP -Action Allow -RemoteAddress LocalSubnet + +Write-Output "Port proxy configuration and firewall rules have been applied successfully." diff --git a/scripts_staging/Build/TRMM agent deployment.ps1 b/scripts_staging/Build/TRMM agent deployment.ps1 new file mode 100644 index 00000000..ac65bb6d --- /dev/null +++ b/scripts_staging/Build/TRMM agent deployment.ps1 @@ -0,0 +1,175 @@ +<# +.SYNOPSIS + Checks for connectivity to the rmm, when functional installs Tactical RMM. + +.DESCRIPTION + This script is made to be packaged into a standard ISO and run with or after sysprep and not run from the RMM itself. + It syncronise the system time to avoid SSL issues, checks for connectivity to 443 of the rmm server, + installs Tactical RMM when the network link is up. (retries every 30 seconds) + If Windows Defender is active, it adds exclusions for Tactical RMM-related paths. + The log are optionals + + +.NOTES + Author: SAN + Date: 01.10.2024 + #public + +.EXEMPLE + $DeploymentURL = "https://api-rmm-xxxxxxx.xxxxxxxx.xxx/clients/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/deploy/" + +.CHANGELOG + SAN 02.05.25 Cleaned the code for publication and removed sensitive data + SAN 02.05.25 Use only the domain of the deployement url for the network check, changed to a json query rather than tcp check, added optional max tires and lots of other tweaks + +.TODO + +#> + +$DeploymentURL = "" # Provide Deployment URL + +$logDirectory = "" # Provide OPTIONAL log directory +$MaxTries = $null # Set to a number for limited attempts, or leave as $null for infinite tries +$DownloadPath = "C:\ProgramData\TacticalRMM\temp" # This is the default recommanded folder by TRMM +$SleepBeforeExit = 20 # Timeout to leave some time to read the terminal output on the device +$TryEvery = 30 # Duration between trials + +# Function to log messages +function Write-Log { + param ([string]$message) + + $timestamp = Get-Date -Format "dd-MM-yyyy HH:mm:ss" + $logMessage = "$timestamp - $message" + + Write-Host $logMessage + + if ($logFile) { + Add-Content -Path $logFile -Value $logMessage + } +} + +# Function to extract FQDN from Deployment URL +function Get-FQDNFromURL { + param ([string]$url) + + if (-not [System.Uri]::IsWellFormedUriString($url, [System.UriKind]::Absolute)) { + Write-Log "Invalid DeploymentURL: '$url'. Must be a well-formed absolute URI." + Start-Sleep -Seconds $SleepBeforeExit + exit 1 + } + + try { + $uri = [Uri]$url + $fqdn = $uri.Host + + # Simple check for valid domain or IP address + if ($fqdn -match '^(([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|(\d{1,3}\.){3}\d{1,3})$') { + return "$($uri.Scheme)://$($uri.Host)" + } else { + Write-Log "The host part of the URL ('$fqdn') is not a valid domain or IP." + Start-Sleep -Seconds $SleepBeforeExit + exit 1 + } + } catch { + Write-Log "Failed to parse DeploymentURL '$url': $($_.Exception.Message)" + Start-Sleep -Seconds $SleepBeforeExit + exit 1 + } +} + +# Function to check the availability of the TRMM instance +function Check-RMM-uplink { + $baseURL = Get-FQDNFromURL -url $DeploymentURL + try { + Write-Log "Sending GET request to $baseURL..." + $response = Invoke-RestMethod -Uri $baseURL -UseBasicParsing -ErrorAction Stop + + if ($null -eq $response) { + Write-Log "ERROR Received empty response from $baseURL." + return $false + } + + if ($response.PSObject.Properties.Name -contains "status") { + $statusValue = $response.status + if ($statusValue -eq "ok") { + Write-Log "TRMM check succeeded. Status is: $statusValue" + return $true + } else { + Write-Log "ERROR TRMM responded, but status is not OK: $statusValue" + return $false + } + } else { + Write-Log "ERROR Response does not contain a 'status' field." + return $false + } + } catch { + Write-Log "ERROR Error occurred during TRMM check: $($_.Exception.Message)" + return $false + } +} + +# Ensure the log directory exists if set +if ($logDirectory -and -not (Test-Path -Path $logDirectory)) { + New-Item -ItemType Directory -Path $logDirectory | Out-Null +} +$logFile = if ($logDirectory) { Join-Path -Path $logDirectory -ChildPath "deploy_log_$(Get-Date -Format 'ddMMyyyy').log" } else { $null } + +# Synchronize time +Write-Log "Synchronizing the system time..." +w32tm /resync +Restart-Service w32time + +# Retry loop with optional max attempts +$attempt = 0 +do { + $rmmReady = Check-RMM-uplink + if ($rmmReady) { break } + + $attempt++ + if ($MaxTries -ne $null -and $attempt -ge $MaxTries) { + Write-Log "Maximum retry attempts ($MaxTries) reached. Exiting..." + Start-Sleep -Seconds $SleepBeforeExit + exit 1 + } + + Write-Log "Retrying in $TryEvery seconds... (Attempt #$attempt)" + Start-Sleep -Seconds $TryEvery +} until ($rmmReady) + +# Check if TRMM is already installed +$tacticalInstalled = Get-WmiObject -Query "SELECT Name FROM Win32_Service WHERE Name LIKE 'tacticalrmm'" | Select-Object -ExpandProperty Name + +if (-not $tacticalInstalled) { + Write-Log "Tactical RMM agent not found. Proceeding with installation..." + Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process -Force + + # Check if Windows Defender is active before adding exclusions + $defenderActive = Get-MpComputerStatus | Select-Object -ExpandProperty AMServiceEnabled + if ($defenderActive) { + Write-Log "Windows Defender is active. Adding path exclusions..." + Add-MpPreference -ExclusionPath "C:\Program Files\TacticalAgent\*" + Add-MpPreference -ExclusionPath "C:\Program Files\Mesh Agent\*" + Add-MpPreference -ExclusionPath "C:\ProgramData\TacticalRMM\*" + } else { + Write-Log "Third-party antivirus detected. Skipping exclusion rules." + } + + #Create download destination + if (-not (Test-Path -Path $DownloadPath)) { + New-Item -ItemType Directory -Path $DownloadPath | Out-Null + } + + # Download and run the installer + Write-Log "Downloading Tactical RMM installer..." + Invoke-WebRequest -Uri $DeploymentURL -OutFile "$DownloadPath\tactical.exe" + Write-Log "Launching installer..." + Start-Process -FilePath "$DownloadPath\tactical.exe" -NoNewWindow -Wait + Write-Log "Installation completed. Exiting..." + Start-Sleep -Seconds $SleepBeforeExit + exit 0 + +} else { + Write-Log "Tactical RMM agent is already installed. Exiting..." + Start-Sleep -Seconds $SleepBeforeExit + exit 0 +} diff --git a/scripts_staging/Build/Update TRMM agent.ps1 b/scripts_staging/Build/Update TRMM agent.ps1 new file mode 100644 index 00000000..ca264829 --- /dev/null +++ b/scripts_staging/Build/Update TRMM agent.ps1 @@ -0,0 +1,162 @@ +<# +.SYNOPSIS + Downloads and installs the latest or specified version of the Tactical RMM agent, with support for signed and unsigned downloads. + +.DESCRIPTION + This script retrieves the latest version of the Tactical RMM agent from GitHub or downloads a specified version based on the input environment variables. + It supports downloading a signed version using a provided token, or an unsigned version directly from GitHub. + If the specified version is set to "latest," the script fetches the most recent release information. + Before downloading, it checks the locally installed version from the software list and skips the download if it matches the desired version. + +.PARAMETER version + Specifies the version to download. If set to "latest," the script retrieves the latest version available on GitHub. + This should be specified through the environment variable `version`. + +.PARAMETER signedDownloadToken + The token used for authenticated signed downloads. This should be set in the environment variable `trmm_sign_download_token`. + If this token is provided, the script will download the signed version. + +.PARAMETER trmm_api_target + The API target required for signed downloads. This should be specified in the environment variable `trmm_api_target`. + This is only necessary if using a signed download. + +.EXEMPLE + trmm_sign_download_token={{global.trmm_sign_download_token}} + version=latest + version=2.7.0 + trmm_api_target=api.exemple.com + +.NOTES + Author: SAN + Date: 29.10.24 + #public + +.CHANGELOG + 29.10.24 SAN Initial script with signed and unsigned download support. + 21.12.24 SAN updated the script to not require "issigned" + 22.12.24 SAN default to latest when no version is set + +.TODO + integrate to our monthly update runs + test if api target is really needed + +#> +# Variables +$version = $env:version # Specify a version manually, or leave as "latest" to get the latest version from GitHub +$signedDownloadToken = $env:trmm_sign_download_token # Token used for signed downloads only +$apiTarget = $env:trmm_api_target # Environment variable for the API target URL + +# Define GitHub API URL for the RMMAgent repository +$repoUrl = "https://api.github.com/repos/amidaware/rmmagent/releases/latest" + +# Function to get the currently installed version of the Tactical RMM agent from the software list +function Get-InstalledVersion { + $appName = "Tactical RMM Agent" # Adjust if the application's display name differs left this in case whitelabel changes the name of the app + $installedSoftware = Get-CimInstance -ClassName Win32_Product | Where-Object { $_.Name -like "*$appName*" } + + if ($installedSoftware) { + return $installedSoftware.Version + } else { + # Check the uninstall registry key for a more complete list + $uninstallKeys = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" + ) + + foreach ($key in $uninstallKeys) { + $installedSoftware = Get-ItemProperty $key | Where-Object { $_.DisplayName -like "*$appName*" } + if ($installedSoftware) { + return $installedSoftware.DisplayVersion + } + } + + return $null + } +} + +try { + # Set up headers for GitHub API request + $headers = @{ + "User-Agent" = "PowerShell Script" + } + + # If version is not set, default to "latest" + if (-not $version) { + $version = "latest" + } + + # If version is set to "latest", fetch the latest release information from GitHub + if ($version -eq "latest") { + Write-Output "Fetching the latest version information from GitHub..." + $response = Invoke-RestMethod -Uri $repoUrl -Headers $headers -Method Get -ErrorAction Stop + $version = $response.tag_name.TrimStart('v') # Remove 'v' prefix if exists + Write-Output "Latest version found: $version" + } else { + Write-Output "Using specified version: $version" + } + + # Check if the installed version matches the desired version + $installedVersion = Get-InstalledVersion + if ($installedVersion) { + Write-Output "Installed version of 'Tactical RMM Agent': $installedVersion" + if ($installedVersion -eq $version) { + Write-Output "The installed version matches the desired version. No download required." + exit 0 + } else { + Write-Output "The installed version ($installedVersion) does not match the desired version ($version). Proceeding with download." + } + } else { + Write-Output "'Tactical RMM Agent' is not installed on this system. Checking installed software..." + } + + # Define the temp directory for downloading + $tempDir = [System.IO.Path]::GetTempPath() + $outputFile = Join-Path -Path $tempDir -ChildPath "tacticalagent-v$version.exe" + + # Determine the download URL based on the presence of $signedDownloadToken + if ($signedDownloadToken) { + if (-not $apiTarget) { + Write-Output "Error: Missing API target for signed downloads. Exiting..." + exit 1 + } + + # Download the signed agent using the token + $downloadUrl = "https://agents.tacticalrmm.com/api/v2/agents?version=$version&arch=amd64&token=$signedDownloadToken&plat=windows&api=$apiTarget" + } else { + # Download the unsigned agent directly from GitHub releases + $downloadUrl = "https://github.com/amidaware/rmmagent/releases/download/v$version/tacticalagent-v$version-windows-amd64.exe" + } + + Write-Output "Downloading from: $downloadUrl" + + # Download the agent file + try { + Invoke-WebRequest -Uri $downloadUrl -OutFile $outputFile -ErrorAction Stop + Write-Output "Download completed: $outputFile" + } catch { + Write-Output "Failed to download the agent. Error: $($_.Exception.Message)" + exit 1 + } + + # Run the downloaded file in a new context (using cmd) + $processStartInfo = New-Object System.Diagnostics.ProcessStartInfo + $processStartInfo.FileName = $outputFile + $processStartInfo.Arguments = "/VERYSILENT" + $processStartInfo.UseShellExecute = $true # Allows the executable to run independently + $processStartInfo.CreateNoWindow = $true # Prevents a new window from being created + + Write-Output "Starting installation..." + + # Start the process without attempting to cast the result + try { + [System.Diagnostics.Process]::Start($processStartInfo) + Write-Output "Installation started. The process is running in the background." + } catch { + Write-Output "Failed to start the installation process. Error: $($_.Exception.Message)" + exit 1 + } +} catch { + # Handle unexpected errors with output + Write-Output "An unexpected error occurred: $($_.Exception.Message)" + exit 1 +} diff --git a/scripts_staging/Build/Upgrade OS to Windows Server X Standard.ps1 b/scripts_staging/Build/Upgrade OS to Windows Server X Standard.ps1 new file mode 100644 index 00000000..bddd129b --- /dev/null +++ b/scripts_staging/Build/Upgrade OS to Windows Server X Standard.ps1 @@ -0,0 +1,180 @@ +<# +.SYNOPSIS +This script performs an in-place upgrade of a Windows Server machine by downloading and extracting the ISO file from a specified Nextcloud share. + +.DESCRIPTION +The script downloads the ISO from a Nextcloud share, verifies its checksum, extracts it using 7-Zip, and then initiates an in-place upgrade of the server. +The Nextcloud share URL format should be as follows: +https://nextcloud.xxx.xxx/s/xxxxxxxxx/download?path=%2F&files= +All keys are valid for initial installation and from https://learn.microsoft.com/en-us/windows-server/get-started/kms-client-activation-keys?tabs=server2016%2Cwindows1110ltsc%2Cversion1803%2Cwindows81 + +.EXAMPLE + TARGETED_VERSION=2019 + Download_Source=https://nextcloud.xxx.xxx/s/xxxxxxxxx/download?path=%2F&files= + + +.NOTE + Author: SAN + Date: 14.11.24 + #Public + +.CHANGELOG + + 27.03.25 SAN Full code refactorisation for more locale support & checksum verification & transfer repo to a single NC share + 03.04.25 SAN exit if missing version + 07.04.25 SAN Added timestamps to messages + +.TODO + find solutions for automated DC server upgrades + Add password on the nextcloud repo and make the script use it + Find a way to use UUP to download the ISO of all windows versions + +#> + + +# Windows Server Versions Metadata +$serverVersions = @{ + "2016" = @{ + "en" = @{ + "file" = "en_windows_server_2016_vl_x64_dvd_11636701.iso" + "checksum" = "47919CE8B4993F531CA1FA3F85941F4A72B47EBAA4D3A321FECF83CA9D17E6B8" # pragma: allowlist-secret #trufflehog:ignore + "licenseKey" = "WC2BQ-8NRM3-FDDYY-2BFGV-KHKQY" + } + "fr" = @{ + "file" = "fr_windows_server_2016_vl_x64_dvd_11636729.iso" + "checksum" = "81B809A9782C046A48D461AAEBFCD33D07A566C5A990373D0A36CDA1E08EA6F0" # pragma: allowlist-secret #trufflehog:ignore + "licenseKey" = "WC2BQ-8NRM3-FDDYY-2BFGV-KHKQY" + } + } + "2019" = @{ + "en" = @{ + "file" = "en-us_windows_server_2019_x64_dvd_f9475476.iso" + "checksum" = "EA247E5CF4DF3E5829BFAAF45D899933A2A67B1C700A02EE8141287A8520261C" # pragma: allowlist-secret #trufflehog:ignore + "licenseKey" = "N69G4-B89J2-4G8F4-WWYCC-J464C" + } + "fr" = @{ + "file" = "fr-fr_windows_server_2019_x64_dvd_f6f6acf6.iso" + "checksum" = "E0C6958E94F41163AA1EA9500825B8523136E1B8C5FC03CB7E3900858C7134AD" # pragma: allowlist-secret #trufflehog:ignore + "licenseKey" = "N69G4-B89J2-4G8F4-WWYCC-J464C" + } + } + "2022" = @{ + "en" = @{ + "file" = "en-us_windows_server_2022_updated_nov_2024_x64_dvd_4e34897c.iso" + "checksum" = "0C388FE9D0A524AC603945F5CFFB7CC600A73432BCCCEA3E95274BF851973C96" # pragma: allowlist-secret #trufflehog:ignore + "licenseKey" = "VDYBN-27WPP-V4HQT-9VMD4-VMK7H" + } + "fr" = @{ + "file" = "fr-fr_windows_server_2022_updated_nov_2024_x64_dvd_4e34897c.iso" + "checksum" = "CCF7FF49503C652E59EE87DE5E66260739F5B20BFB448B3D68411455C291F423" # pragma: allowlist-secret #trufflehog:ignore + "licenseKey" = "VDYBN-27WPP-V4HQT-9VMD4-VMK7H" + } + } + "2025" = @{ + "en" = @{ + "file" = "en-us_windows_server_2025_x64_dvd_b7ec10f3.iso" + "checksum" = "854109E1F215A29FC3541188297A6CA97C8A8F0F8C4DD6236B78DFDF845BF75E" # pragma: allowlist-secret #trufflehog:ignore + "licenseKey" = "TVRH6-WHNXV-R9WG3-9XRFY-MY832" + } + "fr" = @{ + "file" = "fr-fr_windows_server_2025_x64_dvd_bd6be507.iso" + "checksum" = "45384960A3F430D26454955D1198A6E38E7AA98C9E3906AC1AE9367229C103D0" # pragma: allowlist-secret #trufflehog:ignore + "licenseKey" = "TVRH6-WHNXV-R9WG3-9XRFY-MY832" + } + } +} + + +# Function to compute SHA256 checksum +function Get-FileChecksum { + param ([string]$filePath) + $hashAlgorithm = [System.Security.Cryptography.SHA256]::Create() + $fileStream = [System.IO.File]::OpenRead($filePath) + $checksum = [BitConverter]::ToString($hashAlgorithm.ComputeHash($fileStream)).Replace("-", "").ToUpper() + $fileStream.Close() + return $checksum +} + +# Function to verify the checksum of the downloaded file +function Verify-Checksum { + param ([string]$filePath, [string]$expectedChecksum) + if (-not (Test-Path $filePath)) { return $false } + return (Get-FileChecksum -filePath $filePath) -eq $expectedChecksum +} + +# Function to check requirements +function Check-Requirements { + param ([string]$targetedVersion, [string]$baseUrl) + + if (-not $targetedVersion -or -not $baseUrl) { Write-Log "Missing parameters. Exiting."; exit 1 } + if (-not $serverVersions.ContainsKey($targetedVersion)) { Write-Log "Invalid version: $targetedVersion. Exiting."; exit 1 } + + $systemLocale = (Get-WinSystemLocale).Name.Substring(0,2).ToLower() + if (-not $serverVersions[$targetedVersion].ContainsKey($systemLocale)) { Write-Log "Unsupported language: $systemLocale. Exiting."; exit 1 } + + $sevenZipPath = (Get-Command 7z.exe -ErrorAction SilentlyContinue).Source + if (-not $sevenZipPath -and -not (Test-Path ($sevenZipPath = "C:\Program Files\7-Zip\7z.exe"))) { Write-Log "7-Zip not found. Exiting."; exit 1 } + + if ((Get-PSDrive C).Free -lt 12GB) { Write-Log "Not enough disk space. Exiting."; exit 1 } + + return $systemLocale, $sevenZipPath +} + +# Function to write log with timestamp +function Write-Log { + param ([string]$message) + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + Write-Host "[$timestamp] $message" +} + +# Function to perform in-place upgrade +function Perform-InPlaceUpgrade { + param ([string]$setupPath, [string]$licenseKey) + $upgradeArgs = "/auto upgrade /quiet /dynamicupdate disable /imageindex 2 /eula accept /pkey $licenseKey" + Write-Log "Starting in-place upgrade..." + Start-Process -FilePath $setupPath -ArgumentList $upgradeArgs -Wait -NoNewWindow + Write-Log "Upgrade process initiated." +} + +# Main Execution +$targetedVersion = [Environment]::GetEnvironmentVariable("TARGETED_VERSION") +$baseUrl = $env:Download_Source + +# Perform requirements check +$checkResult = Check-Requirements -targetedVersion $targetedVersion -baseUrl $baseUrl +$language = $checkResult[0] +$sevenZipPath = $checkResult[1] + +# Fetch metadata +$metadata = $serverVersions[$targetedVersion][$language] +$isoFile = "C:\\Windows\\Temp\\$($metadata.file)" +$extractFolder = "C:\\Windows\\Temp\\windows_server_extract" + +# Validate or download ISO +if (!(Verify-Checksum -filePath $isoFile -expectedChecksum $metadata.checksum)) { + Write-Log "Downloading ISO..." + Invoke-WebRequest -Uri "$baseUrl$($metadata.file)" -OutFile $isoFile + if (!(Verify-Checksum -filePath $isoFile -expectedChecksum $metadata.checksum)) { + Write-Log "Checksum verification failed. Exiting." + exit 1 + } +} + +# Clean and extract ISO +if (Test-Path $extractFolder) { Remove-Item -Recurse -Force $extractFolder } +Write-Log "Extracting ISO..." +Start-Process -FilePath $sevenZipPath -ArgumentList "x `"$isoFile`" -o`"$extractFolder`" -y" -Wait + +# Delete ISO file to free up space +Write-Log "Deleting ISO file to free up space..." +Remove-Item -Path $isoFile -Force + +# Locate and execute setup.exe +$setupPath = Get-ChildItem -Path $extractFolder -Recurse -Filter "setup.exe" -File | Select-Object -First 1 +if ($setupPath) { + Perform-InPlaceUpgrade -setupPath $setupPath.FullName -licenseKey $metadata.licenseKey +} else { + Write-Log "setup.exe not found. Exiting." + exit 1 +} + diff --git a/scripts_staging/Checks/AD Connect health.ps1 b/scripts_staging/Checks/AD Connect health.ps1 new file mode 100644 index 00000000..11561a9e --- /dev/null +++ b/scripts_staging/Checks/AD Connect health.ps1 @@ -0,0 +1,72 @@ +<# +.SYNOPSIS + Check Azure AD Connect Sync. + +.DESCRIPTION + Check Azure AD Connect Sync status and returns output and code. + +.PARAMETER Hours + Hours since the last synchronization. + Default: 3 + +.EXEMPLE + SYNC_HOURS=6 + +.NOTES + Author: Juan Granados + #public + +.CHANGELOG + 04.09.2024 SAN Problems corrections + 11.12.24 SAN moved hours to env +#> + + +# Check if the environment variable is set, otherwise default to 3 +$Hours = [int]$env:SYNC_HOURS +if (-not $Hours) { + $Hours = 3 +} + +# Check if ADSync module (Azure AD Connect) is installed +if (-not (Get-Module -Name ADSync -ListAvailable)) { + Write-Host "Azure AD Connect is not installed. Exiting." + exit 0 +} + +$Output = "" +$ExitCode = 0 + +$pingEvents = Get-EventLog -LogName "Application" -Source "Directory Synchronization" -InstanceId 654 -After (Get-Date).AddHours(-$($Hours)) -ErrorAction SilentlyContinue | + Sort-Object { $_.Time } -Descending +if ($null -ne $pingEvents) { + $Output = "Latest heart beat event (within last $($Hours) hours). Time $($pingEvents[0].TimeWritten)." +} else { + $Output = "No ping event found within last $($Hours) hours." + $ExitCode = 1 +} + +$ADSyncScheduler = Get-ADSyncScheduler +if (!$ADSyncScheduler.SyncCycleEnabled) { + $ExitCode = 2 +} + +if ($ADSyncScheduler.StagingModeEnabled) { + $Output = "Server is in stand by mode. $($Output)" +} else { + $Output = "Server is in active mode. $($Output)" +} + +if ($ExitCode -eq 0) { + Write-Host "OK: Azure AD Connect Sync is up and running." + Write-Host "$($Output)" +} elseif ($ExitCode -eq 1) { + Write-Host "WARNING: Azure AD Connect Sync is enabled, but not syncing." + Write-Host "$($Output)" +} elseif ($ExitCode -eq 2) { + Write-Host "CRITICAL: Azure AD Connect Sync is disabled." + Write-Host "$($Output)" +} + +$host.SetShouldExit($ExitCode) +Exit($ExitCode) diff --git a/scripts_staging/Checks/AD link health.ps1 b/scripts_staging/Checks/AD link health.ps1 new file mode 100644 index 00000000..f5f3f738 --- /dev/null +++ b/scripts_staging/Checks/AD link health.ps1 @@ -0,0 +1,169 @@ +<# +.SYNOPSIS + This script performs connectivity tests for Active Directory Domain Controllers, + checking various services and protocols to ensure proper functionality. + It includes DNS resolution, and service port checks for LDAP, SMB, RPC, and Kerberos authentication. + +.DESCRIPTION + The script first checks if the local machine is part of a domain. + It then discovers all domain controllers in the domain and performs connectivity tests on each. + Results are logged, and the script exits with a status code indicating success or failure based on the results. + +.NOTE + Author: SAN + Date: 04.10.24 + #public + +.CHANGELOG + 26.11.24 SAN big code cleanup, bug fix, removal of debug to help with cleanup + 17.12.24 SAN fixed couting issue, added a fallback in case tnc does not work + +.TODO + Make ldap rpc smb followup querries to test that the protocol works + re-implement debug + +#> + + +# Define ports commonly used by Active Directory services +$portsToCheck = @{ + 'DNS' = 53 + 'RPC Endpoint Mapper' = 135 + 'SMB' = 445 + 'LDAP' = 389 + 'LDAP (SSL)' = 636 + 'Kerberos' = 88 + 'Kerberos Entra' = 464 + 'Global Catalog LDAP' = 3268 + 'Global Catalog LDAP (SSL)' = 3269 +# 'NetBIOS Name Service' = 137 +# 'NetBIOS Datagram Service' = 138 +# 'NetBIOS Session Service' = 139 +} + + +# Function to perform DNS resolution test +function Test-DnsResolution { + param ( + [string]$ADDomainController + ) + try { + $dnsResult = [System.Net.Dns]::GetHostAddresses($ADDomainController) + if ($dnsResult) { + $status = "OK" + } else { + $status = "KO" + } + } catch { + $status = "KO" + } + + [PSCustomObject]@{ + TestName = "DNS Resolution" + Status = $status + TargetDC = $ADDomainController + } +} + +# Function to test a specific port connection +function Test-PortConnection { + param ( + [string]$ADDomainController, + [int]$Port, + [string]$ServiceName + ) + + # Try Test-NetConnection first + try { + $connection = Test-NetConnection -ComputerName $ADDomainController -Port $Port -WarningAction SilentlyContinue + $status = if ($connection.TcpTestSucceeded) { "OK" } else { "KO" } + } catch { + # Fallback to System.Net.Sockets.TcpClient + $tcpClient = New-Object System.Net.Sockets.TcpClient + try { + $tcpClient.Connect($ADDomainController, $Port) + $status = "OK" + } catch { + $status = "KO" + } finally { + $tcpClient.Close() + } + } + + # Return the result + [PSCustomObject]@{ + TestName = "Port $Port ($ServiceName)" + Status = $status + TargetDC = $ADDomainController + } +} + + +# Function to perform Kerberos authentication test +function Test-KerberosAuthentication { + param ( + [string]$ADDomainController + ) + $kerbTicket = klist + $status = if ($kerbTicket) { "OK" } else { "KO" } + + [PSCustomObject]@{ + TestName = "Kerberos Authentication" + Status = $status + TargetDC = $ADDomainController + } +} + +function Test-ADConnection { + param ( + [string[]]$ADDomainControllers, + [hashtable]$PortsToCheck + ) + $results = @() + + foreach ($ADDomainController in $ADDomainControllers) { + # DNS resolution test + $results += Test-DnsResolution -ADDomainController $ADDomainController + + # Kerberos authentication test + $results += Test-KerberosAuthentication -ADDomainController $ADDomainController + + # Port tests + foreach ($service in $PortsToCheck.GetEnumerator()) { + $results += Test-PortConnection -ADDomainController $ADDomainController -Port $service.Value -ServiceName $service.Key + } + + # Add a separator + $results += [PSCustomObject]@{ + TestName = "--------" + Status = "" + TargetDC = "--------" + } + } + + # Count and handle failures + $failedCount = ($results | Where-Object { $_.Status -eq "KO" -and $_.Status }) | Measure-Object | Select-Object -ExpandProperty Count + + Write-Host "$failedCount tests failed." + Write-Host "" + + # Output the results table + $results | Format-Table -AutoSize + + if ($failedCount -gt 0) { + exit 1 + } +} + + +# Discover all domain controllers in the current domain +$domain = (Get-WmiObject Win32_ComputerSystem).Domain +if ($domain -and $domain -ne 'WORKGROUP') { + $domainControllers = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().DomainControllers + $dcNames = $domainControllers | ForEach-Object { $_.Name } + + # Run the tests and display results + Test-ADConnection -ADDomainControllers $dcNames -PortsToCheck $portsToCheck +} else { + Write-Host "This machine is not part of a domain." +} diff --git a/scripts_staging/Checks/Activation status.ps1 b/scripts_staging/Checks/Activation status.ps1 new file mode 100644 index 00000000..f7123824 --- /dev/null +++ b/scripts_staging/Checks/Activation status.ps1 @@ -0,0 +1,37 @@ +<# +.SYNOPSIS + Checks the Windows activation status and exits with the appropriate code. + +.DESCRIPTION + This script checks the activation status of the Windows operating system. + It uses the WMI query to determine if Windows is activated and exits with + status code 0 if activated, or 1 if not activated. + +.NOTES + Author: SAN + Date : 13.11.24 + #public + +.CHANGELOG + 09.04.25 SAN move to Get-CimInstance and other improvements + +#> + + +try { + $activationStatus = Get-CimInstance -Query "SELECT * FROM SoftwareLicensingProduct WHERE LicenseStatus = 1 AND PartialProductKey IS NOT NULL" -ErrorAction Stop + + if ($activationStatus) { + foreach ($product in $activationStatus) { + Write-Host "OK: Activated - $($product.Name) [$($product.Description)]" + } + exit 0 + } else { + Write-Host "KO: Windows is not activated." + exit 1 + } +} catch { + Write-Host "ERROR: Failed to check activation status. $_" + exit 1 +} + diff --git a/scripts_staging/Checks/Active Directory Health.ps1 b/scripts_staging/Checks/Active Directory Health.ps1 new file mode 100644 index 00000000..ea45ffca --- /dev/null +++ b/scripts_staging/Checks/Active Directory Health.ps1 @@ -0,0 +1,218 @@ +<# +.SYNOPSIS + This script performs Active Directory (AD) diagnostics and compares Group Policy Object (GPO) version numbers between Sysvol and Active Directory. + +.DESCRIPTION + The script performs a series of Active Directory tests using DCDIAG, checks for discrepancies in GPO versions between Sysvol and AD, and outputs the results. + It also checks if the Active Directory Domain Services (AD-DS) feature is installed on the system before performing these tests. + If any test fails, the exit code is incremented. The script provides detailed output for each test and comparison, indicating success or failure. + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.CHANGELOG + 17.07.25 SAN Big cleanup of bug fixes for the dcdiag function, fixes of error codes, output in stderr of all errors for readability + +.TODO + Do a breakdown at the top of the output for easy read with ok/ko returns from functions + +#> + +# Initialize exit code +$global:exitCode = 0 + +# Function to perform Active Directory tests +function CheckAD { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [string[]]$Tests, + + [Parameter()] + [hashtable]$SuccessPatterns = @{ + 'en' = @('passed test') + 'fr' = @('a réussi', 'a reussi', 'a russi', 'ussi') + }, + + [Parameter()] + [int]$MinimumMatches = 2 + ) + + $DebugMode = $false + $global:exitCode = 0 + + # Combine all success patterns from all languages into a single list + $allPatterns = @() + foreach ($lang in $SuccessPatterns.Keys) { + $allPatterns += $SuccessPatterns[$lang] + } + + if ($DebugMode) { + Write-Host "`n[DEBUG] Loaded Success Patterns:" + foreach ($p in $allPatterns) { + Write-Host " - $p" + } + Write-Host "" + } + + $results = @{} + + foreach ($test in $Tests) { + Write-Host "`nRunning DCDIAG test: $test" + + # Start dcdiag process and redirect output + $startInfo = New-Object System.Diagnostics.ProcessStartInfo + $startInfo.FileName = "dcdiag.exe" + $startInfo.Arguments = "/test:$test" + $startInfo.RedirectStandardOutput = $true + $startInfo.RedirectStandardError = $true + $startInfo.UseShellExecute = $false + $startInfo.CreateNoWindow = $true + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $startInfo + $process.Start() | Out-Null + $stream = $process.StandardOutput.BaseStream + $memoryStream = New-Object System.IO.MemoryStream + $buffer = New-Object byte[] 4096 + while (($read = $stream.Read($buffer, 0, $buffer.Length)) -gt 0) { + $memoryStream.Write($buffer, 0, $read) + } + $process.WaitForExit() + + $bytes = $memoryStream.ToArray() + $output = [System.Text.Encoding]::GetEncoding(1252).GetString($bytes) + + if ($DebugMode) { + $preview = if ($output.Length -gt 800) { $output.Substring(0,800) + "`n..." } else { $output } + Write-Host "[DEBUG] DCDIAG Output Preview:" + Write-Host $preview + Write-Host "" + } + + $matchCount = 0 + foreach ($pattern in $allPatterns) { + $count = ([regex]::Matches($output, [regex]::Escape($pattern))).Count + $matchCount += $count + + if ($DebugMode) { + Write-Host "[DEBUG] Pattern '$pattern' matched $count time(s)." + } + } + + if ($DebugMode) { + Write-Host "[DEBUG] Total success match count: $matchCount`n" + } + + if ($matchCount -ge $MinimumMatches) { + $results[$test] = "OK" + } else { + $results[$test] = "Failed!" + Write-Error "$results[$test] = Failed!" + $global:exitCode++ + } + + Write-Host "DCDIAG Test: $test Result: $($results[$test])" + } + + return $results +} + +# Function to compare GPO version numbers +function Compare-GPOVersions { + [CmdletBinding()] + param () + + process { + Import-Module GroupPolicy + + Get-GPO -All | ForEach-Object { + # Retrieve GPO information (GUID and Name) + $GPOId = $_.Id + $GPOName = $_.DisplayName + + # Version GPO User + $NumUserSysvol = (Get-Gpo -Guid $GPOId).User.SysvolVersion + $NumUserAD = (Get-Gpo -Guid $GPOId).User.DSVersion + + # Version GPO Machine + $NumComputerSysvol = (Get-Gpo -Guid $GPOId).Computer.SysvolVersion + $NumComputerAD = (Get-Gpo -Guid $GPOId).Computer.DSVersion + + # USER - Compare version numbers + if ($NumUserSysvol -ne $NumUserAD) { + Write-Host "$GPOName ($GPOId) : USER Versions différentes (Sysvol : $NumUserSysvol | AD : $NumUserAD)" + Write-Error "$GPOName ($GPOId) : USER Versions différentes (Sysvol : $NumUserSysvol | AD : $NumUserAD)" + $global:exitCode++ + } else { + Write-Host "$GPOName : USER Versions identiques" + } + + # COMPUTER - Compare version numbers + if ($NumComputerSysvol -ne $NumComputerAD) {Health + Write-Host "$GPOName ($GPOId) : COMPUTER Versions différentes (Sysvol : $NumComputerSysvol | AD : $NumComputerAD)" + Write-Error "$GPOName ($GPOId) : COMPUTER Versions différentes (Sysvol : $NumComputerSysvol | AD : $NumComputerAD)" + $global:exitCode++ + } else { + Write-Host "$GPOName : COMPUTER Versions identiques" + } + } + Write-Host "GPO USER/COMPUTER Version OK" + } +} + +# Function to check if the Recycle Bin in enabled +function Check-ADRecycleBin { + $recycleFeatures = Get-ADOptionalFeature -Filter {name -like "recycle bin feature"} + + foreach ($feature in $recycleFeatures) { + if ($null -ne $feature.EnabledScopes) { + Write-Host "OK: Recycle Bin enabled" + } else { + Write-Host "KO: Recycle Bin disabled" + Write-Error "KO: Recycle Bin disabled" + $global:exitCode++ + } + } +} + +# Check if Active Directory Domain Services feature is installed +try { + $adFeature = Get-WindowsFeature -Name AD-Domain-Services -ErrorAction Stop + + if ($adFeature.InstallState -eq "Installed") { + + # function with the AD tests + $tests = ("Advertising", "FrsSysVol", "MachineAccount", "Replications", "RidManager", "Services", "FsmoCheck", "SysVolCheck") + Write-Host "DCDIAG tests: $tests" + $testResults = CheckAD -Tests $tests + $failedTests = $testResults.GetEnumerator() | Where-Object { $_.Value -eq "Failed!" } + if ($failedTests) { + Write-Error "Some Active Directory tests failed." + } else { + Write-Host "All Active Directory tests passed successfully." + } + Write-Host "" + + # function to compare GPO versions + Write-Host "GPO Versions checks" + Compare-GPOVersions + Write-Host "" + + # function to check the Recycle Bin + Write-Host "Recycle Bin checks" + Check-ADRecycleBin + Write-Host "" + + } else { + Write-Host "Active Directory Domain Services feature is not installed or not in the 'Installed' state." + exit + } +} catch { + Write-Error "Failed to retrieve information about Active Directory Domain Services feature: $_" + $global:exitCode++ +} + +$host.SetShouldExit($global:exitCode) +exit $global:exitCode \ No newline at end of file diff --git a/scripts_staging/Checks/Backup Veeam SPC.py b/scripts_staging/Checks/Backup Veeam SPC.py new file mode 100644 index 00000000..84481991 --- /dev/null +++ b/scripts_staging/Checks/Backup Veeam SPC.py @@ -0,0 +1,245 @@ +#!/usr/bin/python +""" +Synopsis: + This script monitors the backup status of a specified VM or Computer + by interfacing with the API of the Veeam Service Provider Console to + retrieve and analyze restore points. + + It checks if the latest backup is within a user-defined threshold + and outputs a detailed restoration status report. + +EXEMPLE: + Mandatory: + host={{agent.hostname}} + apikey={{global.VeaamSPCapi}} + apiurl=https://vspc.XXXXXX.XXXX:XXXX + + Optional + THRESHOLD_HOURS=48 + force={{agent.Hostname_Override}} + DEBUG=1 + force=DISABLEDBACKUPCHECK +NOTE: + Author: SAN + Date: 18.12.24 + #public + +Outputs: + - "OK" or "CRITICAL" status indicating backup health. + - Detailed restoration status report if the backup check is successful. + - Debug logs if DEBUG is set to True in the environment variables. + - Disabled if DISABLEDBACKUPCHECK is set in FORCE + +Changelog: + + 27.03.25 SAN added more debug + 15.04.25 SAN big code cleanup + publication + +TODO: + better flow for the "force" + set fallback to get localhostname if hosts is not specified + avoid redundant calls to os.getenv + more function decomposition + graceful handling of missing keys in json responses + use more descriptive variable names + better error handling for missing data + optimize vm filtering logic + early exit for empty backed_up_vms + +""" + +import os +import sys +import json +import time +import math +import requests +from datetime import datetime, timedelta + +# === Utility Functions === +def log_debug(msg): + """Logs debug information if debugging is enabled.""" + if env_vars['DEBUG']: + print(msg) + +def convert_size(bytes_): + """Converts a size in bytes to a human-readable format.""" + if bytes_ == 0: + return "0B" + i = int(math.log(bytes_, 1024)) + return f"{round(bytes_ / (1024 ** i), 2)} {('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')[i]}" + +def expiry_days(expiry): + """Calculates the number of days since a given expiry date.""" + expiry_date = datetime.strptime(expiry[:26], "%Y-%m-%dT%H:%M:%S.%f").date() + return (datetime.today().date() - expiry_date).days + +def get_timestamp(date_str): + """Converts a date string to a Unix timestamp.""" + return time.mktime(datetime.strptime(date_str[:26], "%Y-%m-%dT%H:%M:%S.%f").timetuple()) + +def api_call_with_retries(url, method='GET', data=None, headers=None, retries=5, wait=60): + """Makes an API call with retries for handling HTTP 429 responses.""" + for attempt in range(retries): + try: + res = requests.request(method, url, data=data, headers=headers) + if res.status_code == 429 and attempt < retries - 1: + print(f"HTTP 429 received. Retrying in {wait} seconds... (Attempt {attempt + 1}/{retries})") + time.sleep(wait) + continue + res.raise_for_status() + return res + except requests.exceptions.RequestException as e: + if attempt == retries - 1: + print(f"API call failed after {retries} attempts: {e}") + sys.exit(1) + time.sleep(wait) + +def get_auth_headers(): + """Returns the headers required for authenticated API requests.""" + return { + "Connection": "close", + "Authorization": f"Bearer {env_vars['APIKEY']}", + "Content-Type": "application/json", + "accept": "application/json", + } + +def apiGet_BackedUpVMs(): + """Retrieves a list of backed-up virtual machines.""" + url = f"{env_vars['APIURL']}/api/v3/protectedWorkloads/virtualMachines?limit=500&select=[{{'propertyPath':'name'}},{{'propertyPath':'instanceUid'}},{{'propertyPath':'backupServerUid'}}]" + return api_call_with_retries(url, method='GET', headers=get_auth_headers()) + +def apiGet_VMbackups(vmUID): + """Retrieves the list of backups for a specific VM.""" + url = f"{env_vars['APIURL']}/api/v3/protectedWorkloads/virtualMachines/{vmUID}/backups?limit=500" + return api_call_with_retries(url, method='GET', headers=get_auth_headers()) + +def apiGet_BackedUpComputers(): + """Retrieves a list of computers that are backed up.""" + url = f"{env_vars['APIURL']}/api/v3/protectedWorkloads/computersManagedByBackupServer?limit=500" + return api_call_with_retries(url, method='GET', headers=get_auth_headers()) + +def apiGet_ComputersRestorePoints(): + """Retrieves restore points for computers managed by the backup server.""" + url = f"{env_vars['APIURL']}/api/v3/protectedWorkloads/computersManagedByBackupServer/restorePoints?limit=500" + return api_call_with_retries(url, method='GET', headers=get_auth_headers()) + +# === Environment and Constants === +vmUID, lastRPoint, vmBkp_bkpSrvUID, tmpName, strSchedule = ("",) * 5 +nameNotFound = True +isComputer = False + +env_vars = { + 'HOST': None, + 'FORCE': None, + 'DEBUG': False, + 'APIKEY': None, + 'APIURL': None, + 'THRESHOLD_HOURS': 48 +} +env_vars.update({k: os.getenv(k, v) for k, v in env_vars.items()}) +env_vars['DEBUG'] = str(env_vars['DEBUG']).lower() in ("true", "1") + +# === Exit Early Conditions === +if not env_vars['APIKEY'] or not env_vars['APIURL']: + print("CRITICAL: 'APIURL' and 'APIKEY' must be set.") + sys.exit(2) + +if env_vars['FORCE'] and "DISABLEDBACKUPCHECK" in env_vars['FORCE']: + print("Backup check is disabled because 'FORCE' contains 'DISABLEDBACKUPCHECK'.") + sys.exit(0) + +def main(): + try: + log_debug("Parsed Environment Variables:") + log_debug(f" HOST: {env_vars['HOST']}") + log_debug(f" FORCE: {env_vars['FORCE']}") + log_debug(f" DEBUG: {env_vars['DEBUG']}") + api_key = env_vars['APIKEY'] + masked_api_key = f"{api_key[:3]}{'*' * (len(api_key) - 6)}{api_key[-3:]}" + log_debug(f" APIKEY: {masked_api_key}") + log_debug(f" APIURL: {env_vars['APIURL']}") + log_debug(f" THRESHOLD_HOURS: {env_vars['THRESHOLD_HOURS']}\n") + + log_debug("INFO: Fetching the list of all backed-up VMs...") + response = apiGet_BackedUpVMs().json() + backed_up_vms = response["data"] + + for vm in backed_up_vms: + log_debug(f"VM Name: {vm['name']}") + + host_arg = ( + env_vars['FORCE'] + if env_vars.get('FORCE') and "Manual" not in env_vars['FORCE'] + else env_vars['HOST'] + ) + + if env_vars.get('FORCE'): + matching_vms = [vm for vm in backed_up_vms if host_arg == vm["name"]] + else: + matching_vms = [vm for vm in backed_up_vms if host_arg in vm["name"]] + + if not matching_vms and not env_vars.get('FORCE'): + host_arg_lower = host_arg.lower() + matching_vms = [vm for vm in backed_up_vms if host_arg_lower in vm["name"]] + + if not matching_vms: + print(f"KO: VM or Computer '{host_arg}' not found in the backup list.") + sys.exit(2) + elif len(matching_vms) > 1: + log_debug(f"WARNING: Multiple matches found for '{host_arg}':") + for vm in matching_vms: + log_debug(f" - Name: {vm['name']}, VM UID: {vm['instanceUid']}") + print("Exiting to avoid mismatches.") + sys.exit(2) + + global vmUID, tmpName + vmUID = matching_vms[0]["instanceUid"] + tmpName = matching_vms[0]["name"] + log_debug(f"INFO: Selected VM: {tmpName} (UID: {vmUID})") + + try: + restore_points_response = ( + apiGet_ComputersRestorePoints() if isComputer else apiGet_VMbackups(vmUID) + ) + except requests.exceptions.RequestException as e: + print("API CALL FAILED: Unable to fetch restore points.") + log_debug(str(e)) + sys.exit(2) + + restore_points = restore_points_response.json()["data"] + + latest_restore_point = next( + (p['creationTimeUtc'] for p in restore_points if 'creationTimeUtc' in p), + next((p['latestRestorePointDate'] for p in restore_points if 'latestRestorePointDate' in p), None) + ) + + if not latest_restore_point: + print("KO: No valid restore points found.") + sys.exit(2) + + restore_point_time = datetime.strptime(latest_restore_point[:26], "%Y-%m-%dT%H:%M:%S.%f") + time_since_last_backup = datetime.utcnow() - restore_point_time + + threshold_hours = int(env_vars['THRESHOLD_HOURS']) + backup_age_limit = timedelta(hours=threshold_hours) + + if time_since_last_backup <= backup_age_limit: + print(f"OK: The latest backup was {time_since_last_backup} ago, within the threshold of {threshold_hours} hours.") + else: + print(f"KO: The latest backup was {time_since_last_backup} ago, exceeding the threshold of {threshold_hours} hours.") + sys.exit(2) + + total_restore_point_size = sum(p.get('totalRestorePointSize', 0) for p in restore_points) + total_restore_point_size_readable = convert_size(total_restore_point_size) + + print(f"Restoration Status Report:\n- VM or Computer: {tmpName}\n- Latest Restore Point Date/Time: {latest_restore_point}\n- Number of Restore Points Available: {len(restore_points)}\n- Total Size of Restore Points: {total_restore_point_size_readable}") + + + except requests.exceptions.RequestException as e: + print("KO: API call failed.") + log_debug("API CALL FAILED: " + str(e)) + sys.exit(2) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts_staging/Checks/Backup Veeam agent.ps1 b/scripts_staging/Checks/Backup Veeam agent.ps1 new file mode 100644 index 00000000..62da85d0 --- /dev/null +++ b/scripts_staging/Checks/Backup Veeam agent.ps1 @@ -0,0 +1,105 @@ +<# +.SYNOPSIS + This script checks the status of the Veeam Backup Agent by: + 1. Searching for the most recent `.Backup.log` file in the specified directory. + 2. Extracting the job status and completion time from the log file. + 3. Verifying whether the job was successful and if the log entry is within a specified threshold period (default is 48 hours). + 4. Outputs a simplified result + +.DESCRIPTION + The script is intended to monitor the status of Veeam backup jobs by checking the latest log + file in the Veeam Endpoint backup folder. + +.NOTE + Author: SAN + Date: 10/08/24 + #public + +.CHANGELOG + 15/04/25 SAN Code Cleaup & Publication + +.TODO + Var to env + get latest logs in case of error and output the logs + +#> + +$RootDirectory = "C:\ProgramData\Veeam\Endpoint" +$ThresholdHours = 48 +$DateFormat = "dd.MM.yyyy HH:mm:ss" +$LogPattern = "Job session '.*' has been completed, status: '(.*?)'," + +function Get-RecentLogFile { + try { + $logFile = Get-ChildItem -Path $RootDirectory -Filter "*.Backup.log" -Recurse | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 + return $logFile + } catch { + Write-Output "KO: Error accessing files: $_" + exit 1 + } +} + +function Get-JobStatusFromLog { + param ($logFile) + try { + $recentLine = Select-String -Path $logFile.FullName -Pattern $LogPattern | + Select-Object -Last 1 + + if ($recentLine -and $recentLine.Line -match "\[(.*?)\] .* Job session '.*' has been completed, status: '(.*?)',") { + $dateTime = $matches[1] + $status = $matches[2] + return @{ DateTime = $dateTime; Status = $status } + } else { + Write-Output "KO: No matching lines found in the log file." + exit 1 + } + } catch { + Write-Output "KO: Error processing the log file: $_" + exit 1 + } +} + +function Check-JobStatus { + param ( + [string]$dateTime, + [string]$status + ) + + try { + $logDate = [datetime]::ParseExact($dateTime, $DateFormat, $null) + $timeSpan = New-TimeSpan -Start $logDate -End (Get-Date) + + if ($status -ne "Success") { + Write-Output "KO: Job status is not 'Success'." + exit 1 + } elseif ($timeSpan.TotalHours -gt $ThresholdHours) { + Write-Output "KO: Log entry is older than $ThresholdHours hours." + exit 1 + } else { + Write-Output "OK: Job Status: $status, Date and Time: $dateTime" + exit 0 + } + } catch { + Write-Output "KO: Error checking job status: $_" + exit 1 + } +} + +try { + $logFile = Get-RecentLogFile + + if ($logFile) { + $jobInfo = Get-JobStatusFromLog -logFile $logFile + if ($jobInfo) { + Check-JobStatus -dateTime $jobInfo.DateTime -status $jobInfo.Status + } + } else { + Write-Output "KO: No .Backup.log files found in the directory or subdirectories." + exit 1 + } +} catch { + Write-Output "KO: Unexpected error: $_" + exit 1 +} diff --git a/scripts_staging/Checks/Boot mode.ps1 b/scripts_staging/Checks/Boot mode.ps1 new file mode 100644 index 00000000..b77e31df --- /dev/null +++ b/scripts_staging/Checks/Boot mode.ps1 @@ -0,0 +1,29 @@ +<# +.SYNOPSIS + Checks if the system is booted in Safe Mode. + +.DESCRIPTION + This script confirms the system is booted in Safe Mode and exits with a code 1. Otherwise, it indicates + that the system is not in Safe Mode and exits with a code 0. + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.CHANGELOG + 12.12.24 SAN Changed outputs + +#> + +# Check if the system is booted in Safe Mode +$regPath = "HKLM:\SYSTEM\CurrentControlSet\Control\SafeBoot\Option" +$safeModeKeyExists = Test-Path $regPath + +if ($safeModeKeyExists) { + Write-Host "KO: System is booted in Safe Mode." + exit 1 +} else { + Write-Host "OK: System is not booted in Safe Mode." + exit 0 +} \ No newline at end of file diff --git a/scripts_staging/Checks/Certificates expiry.ps1 b/scripts_staging/Checks/Certificates expiry.ps1 new file mode 100644 index 00000000..41a5b4f4 --- /dev/null +++ b/scripts_staging/Checks/Certificates expiry.ps1 @@ -0,0 +1,209 @@ +<# +.SYNOPSIS + Display information about certificates in specified stores and optionally delete expired certificates. + +.DESCRIPTION + This script retrieves certificates from specified certificate stores, displays information about each certificate, and categorizes them based on their expiration status. + If the script is run with the `-DeleteExpired` parameter, it will also delete certificates that have already expired. + +.PARAMETER DeleteExpired + If present, the script will delete certificates that have already expired. + +.PARAMETER OutputAll + If present, the output will be more verbose. + +.EXEMPLE + -DeleteExpired + -OutputAll + WARN_THRESHOLD_DAYS=20 + ERROR_THRESHOLD_DAYS=2 + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.CHANGELOG + 29.08.24 SAN Display the full list if none is warn or error, Made a lot of variables for future proofing + 04.09.2024 SAN Problems corrections + 30.09.2024 SAN changed outputs layouts + 11.12.24 SAN added errorThresholdDays to help change the status when close to expiry, moved threshold to env + 07.01.25 SAN Bugfix in env var + +.TODO + Make the output messages more readable + move all flags and var to env + + +#> + +# Get values from environment variables, defaulting if unset +if ($env:WARN_THRESHOLD_DAYS -ne $null) { + $warnThresholdDays = [int]$env:WARN_THRESHOLD_DAYS +} else { + $warnThresholdDays = 20 +} + +if ($env:ERROR_THRESHOLD_DAYS -ne $null) { + $errorThresholdDays = [int]$env:ERROR_THRESHOLD_DAYS +} else { + $errorThresholdDays = 5 +} + +# Configuration Variables +$certificateStores = @("Cert:\LocalMachine\My", "Cert:\LocalMachine\WebHosting", "Cert:\LocalMachine\Remote Desktop") +$excludeByName = @() +$excludeByThumbprint = @() +$exitCodeSuccess = 0 +$exitCodeWarning = 1 +$exitCodeError = 2 + +# Initialize warn and error counters +$global:warnCount = 0 +$global:errorCount = 0 + +# Check if -DeleteExpired and -OutputAll parameters are present +$deleteExpired = $false +$outputAll = $false +if ($args -contains "-DeleteExpired") { + $deleteExpired = $true +} +if ($args -contains "-OutputAll") { + $outputAll = $true +} + +# Function to display certificate information and categorize +function DisplayCertificateInfoAndCategorize($cert, $deleteExpired, $outputAll) { + # Skip excluded certificates + if ($excludeByName -contains $cert.FriendlyName -or $excludeByThumbprint -contains $cert.Thumbprint) { + return + } + + # Check if Subject and Issuer are the same (self-signed) + if ($cert.Subject -eq $cert.Issuer) { + return + } + + $today = Get-Date + $daysToExpiration = ($cert.NotAfter - $today).Days + + # Display certificate info if expiration is within the threshold or if -OutputAll is true + if ($daysToExpiration -le $warnThresholdDays -or $outputAll) { + Write-Host "Certificate Details:" + Write-Host "--------------------" + Write-Host "Path : $($cert.PSPath)" + Write-Host "Subject : $($cert.Subject)" + Write-Host "Issuer : $($cert.Issuer)" + Write-Host "Expiration : $($cert.NotAfter)" + + # Conditionally display Friendly Name + if (-not [string]::IsNullOrEmpty($cert.FriendlyName)) { + Write-Host "Friendly Name : $($cert.FriendlyName)" + } + + Write-Host "Thumbprint : $($cert.Thumbprint)" + + if ($daysToExpiration -gt 0) { + if ($daysToExpiration -le $warnThresholdDays) { + Write-Host "Status : Warn (Expires in $daysToExpiration days)" + $global:warnCount++ + } else { + Write-Host "Status : Valid (Expires in $daysToExpiration days)" + } + } else { + if ($daysToExpiration -le -$errorThresholdDays) { + Write-Host "Status : Error (Expired for more than $errorThresholdDays days)" + $global:errorCount++ + } else { + Write-Host "Status : Error (Expired $(-$daysToExpiration) days ago)" + } + + if ($deleteExpired -eq $true) { + Write-Host "Deleting expired certificate..." + Remove-Item -Path $cert.PSPath + } else { + Write-Host "Run me with -DeleteExpired to remove this cert" + } + } + + Write-Host "-----------------------------" + } +} + +# Function to handle specific exceptions +function HandleException($exception) { + if ($exception.Exception -is [System.UnauthorizedAccessException]) { + Write-Host "Access Denied: Cannot access the certificate store. Please run the script with elevated privileges." + } else { + Write-Host "An error occurred: $($exception.Exception.Message)" + } +} + +# Main script logic +foreach ($storePath in $certificateStores) { + # Only display "Checking" message if -OutputAll is called + if ($outputAll) { + Write-Host "Checking certificates in ${storePath}..." + } + + try { + $certificates = Get-ChildItem -Path $storePath + if ($null -ne $certificates -and $certificates.Count -gt 0) { + if ($outputAll) { + Write-Host "Certificates found in ${storePath}:" + Write-Host "-----------------------------" + } + foreach ($cert in $certificates) { + DisplayCertificateInfoAndCategorize $cert $deleteExpired $outputAll + } + } elseif ($outputAll) { + Write-Host "No certificates found in ${storePath}." + Write-Host "-----------------------------" + } + } catch { + HandleException $_ + } +} + +# Display messages based on warn and error counts +if ($global:errorCount -gt 0) { + Write-Host "There are $($global:errorCount) certificate(s) in error status." + $host.SetShouldExit($exitCodeError) + exit $exitCodeError +} + +if ($global:warnCount -gt 0) { + Write-Host "There are $($global:warnCount) certificate(s) in warning status." + $host.SetShouldExit($exitCodeWarning) + exit $exitCodeWarning +} + +# If no errors or warnings were found and -OutputAll was not used +if ($global:warnCount -eq 0 -and $global:errorCount -eq 0 -and -not $outputAll) { + # Calculate the next expiry date + $nextExpiryDays = [int]::MaxValue # Start with a high number + foreach ($storePath in $certificateStores) { + try { + $certificates = Get-ChildItem -Path $storePath + if ($null -ne $certificates -and $certificates.Count -gt 0) { + foreach ($cert in $certificates) { + $daysToExpiration = ($cert.NotAfter - (Get-Date)).Days + if ($daysToExpiration -gt 0 -and $daysToExpiration -lt $nextExpiryDays) { + $nextExpiryDays = $daysToExpiration + } + } + } + } catch { + HandleException $_ + } + } + Write-Host "OK Next expiry in $nextExpiryDays days." + $host.SetShouldExit($exitCodeSuccess) + exit $exitCodeSuccess +} + +# If OutputAll was used, we need to end the script with a success code without additional output +if ($outputAll) { + $host.SetShouldExit($exitCodeSuccess) + exit $exitCodeSuccess +} diff --git a/scripts_staging/Checks/DFS replication.ps1 b/scripts_staging/Checks/DFS replication.ps1 new file mode 100644 index 00000000..a44797f5 --- /dev/null +++ b/scripts_staging/Checks/DFS replication.ps1 @@ -0,0 +1,133 @@ +<# +.SYNOPSIS + Monitors DFS Replication backlog and generates status based on the file count in the backlog for specified replication groups. + +.DESCRIPTION + This script checks the DFS Replication backlog for specified replication groups using WMI queries and the 'dfsrdiag' command. + It generates success, warning, or error statuses based on the backlog file count, helping to monitor replication health. + +.PARAMETER ReplicationGroupList + An array of DFS Replication Group names to monitor. If not specified, all groups will be checked. + This can be specified through the variable `ReplicationGroupList`. + +.EXAMPLE + ReplicationGroupList = @("Group1", "Group2") + This will check the backlog for "Group1" and "Group2" replication groups. + +.NOTES + Author: matty-uk + Date: ???? + Usefull links: + https://exchange.nagios.org/directory/Addons/Monitoring-Agents/DFSR-Replication-and-BackLog/details + #public + +.CHANGELOG + 01.01.24 SAN Re-implementation for rmm + 12.12.24 SAN code cleanup + +.TODO + Add additional options for backlog threshold customization. + move list to env + +#> + + + +# Define parameter for specifying replication groups (default is an empty array) +Param ( + [String[]]$ReplicationGroupList = @("") # Default is no specific group +) + +# Retrieve all DFS Replication Group configurations via WMI +$ReplicationGroups = Get-WmiObject -Namespace "root\MicrosoftDFS" -Query "SELECT * FROM DfsrReplicationGroupConfig" + +# Filter replication groups if specific group names are provided +if ($ReplicationGroupList) { + $FilteredReplicationGroups = @() + foreach ($ReplicationGroup in $ReplicationGroupList) { + $FilteredReplicationGroups += $ReplicationGroups | Where-Object { $_.ReplicationGroupName -eq $ReplicationGroup } + } + + # Exit with UNKNOWN status if no groups match + if ($FilteredReplicationGroups.Count -eq 0) { + Write-Host "UNKNOWN: None of the specified group names were found." + exit 3 + } else { + $ReplicationGroups = $FilteredReplicationGroups + } +} + +# Initialize counters for success, warning, and error +$SuccessCount = 0 +$WarningCount = 0 +$ErrorCount = 0 + +# Initialize an array to store output messages +$OutputMessages = @() + +# Iterate through each DFS Replication Group +foreach ($ReplicationGroup in $ReplicationGroups) { + # Query for DFS Replicated Folder configurations for the current replication group + $ReplicatedFoldersQuery = "SELECT * FROM DfsrReplicatedFolderConfig WHERE ReplicationGroupGUID='" + $ReplicationGroup.ReplicationGroupGUID + "'" + $ReplicatedFolders = Get-WmiObject -Namespace "root\MicrosoftDFS" -Query $ReplicatedFoldersQuery + + # Query for DFS Replication Connection configurations for the current replication group + $ReplicationConnectionsQuery = "SELECT * FROM DfsrConnectionConfig WHERE ReplicationGroupGUID='" + $ReplicationGroup.ReplicationGroupGUID + "'" + $ReplicationConnections = Get-WmiObject -Namespace "root\MicrosoftDFS" -Query $ReplicationConnectionsQuery + + # Iterate through each DFS Replication Connection for the current replication group + foreach ($ReplicationConnection in $ReplicationConnections) { + $ConnectionName = $ReplicationConnection.PartnerName + + # Check if the connection is enabled + if ($ReplicationConnection.Enabled -eq $True) { + # Iterate through each DFS Replicated Folder for the current connection + foreach ($ReplicatedFolder in $ReplicatedFolders) { + $ReplicationGroupName = $ReplicationGroup.ReplicationGroupName + $ReplicatedFolderName = $ReplicatedFolder.ReplicatedFolderName + + # Execute the 'dfsrdiag' command to get backlog information + $BacklogCommand = "dfsrdiag Backlog /RGName:'$ReplicationGroupName' /RFName:'$ReplicatedFolderName' /SendingMember:$ConnectionName /ReceivingMember:$env:ComputerName" + $BacklogOutput = Invoke-Expression -Command $BacklogCommand + + $BacklogFileCount = 0 + # Parse the 'dfsrdiag' output to retrieve the backlog file count + foreach ($Item in $BacklogOutput) { + if ($Item -ilike "*Backlog File count*") { + $BacklogFileCount = [int]$Item.Split(":")[1].Trim() + } + } + + # Generate status messages based on backlog file count and update counters + if ($BacklogFileCount -eq 0) { + $OutputMessages += "OK: $BacklogFileCount files in backlog for $ConnectionName->$env:ComputerName in $ReplicationGroupName" + $SuccessCount++ + } elseif ($BacklogFileCount -lt 10) { + $OutputMessages += "WARNING: $BacklogFileCount files in backlog for $ConnectionName->$env:ComputerName in $ReplicationGroupName" + $WarningCount++ + } else { + $OutputMessages += "CRITICAL: $BacklogFileCount files in backlog for $ConnectionName->$env:ComputerName in $ReplicationGroupName" + $ErrorCount++ + } + } + } + } +} + +# Generate the final status based on the success, warning, and error counters +if ($ErrorCount -gt 0) { + Write-Host "CRITICAL: $ErrorCount errors, $WarningCount warnings, and $SuccessCount successful replications." + Write-Host "$OutputMessages" + $host.SetShouldExit(2) + exit 2 +} elseif ($WarningCount -gt 0) { + Write-Host "WARNING: $WarningCount warnings, and $SuccessCount successful replications." + Write-Host "$OutputMessages" + $host.SetShouldExit(1) + exit 1 +} else { + Write-Host "OK: $SuccessCount successful replications." + Write-Host "$OutputMessages" + $host.SetShouldExit(0) + exit 0 +} diff --git a/scripts_staging/Checks/Disk Free Space.ps1 b/scripts_staging/Checks/Disk Free Space.ps1 new file mode 100644 index 00000000..4ba8b717 --- /dev/null +++ b/scripts_staging/Checks/Disk Free Space.ps1 @@ -0,0 +1,126 @@ +<# +.SYNOPSIS + Disk Space Check Script + +.DESCRIPTION + This PowerShell script checks the free disk space on local drives, excluding network drives + and optionally specified drives to ignore, + and exits with different codes based on warning and error thresholds. + +.PARAMETER warningThreshold + The percentage of free disk space at which a warning is issued. Default is 10%. + +.PARAMETER errorThreshold + The percentage of free disk space at which an error is issued. Default is 5%. + +.PARAMETER ignoreDisks + An array of drive letters representing the disks to ignore during the disk space check. + +.EXAMPLE + -warningThreshold 15 -errorThreshold 10 + Checks disk space with custom warning (15%) and error (10%) thresholds. + + -ignoreDisks "D:", "E:" + Checks disk space excluding drives D: and E: from the check. + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.CHANGELOG + 17.07.25 SAN Added debug flag, taken into account cases where all drives are ignored. + + +.TODO + move flags to env + +#> + +param( + [int]$warningThreshold = 10, + [int]$errorThreshold = 5, + [string[]]$ignoreDisks = @(), + [bool]$DebugOutput = $false +) + +function CheckDiskSpace { + [CmdletBinding()] + param() + + # Get all local drives excluding network drives and the ones specified to ignore + $allDrives = Get-WmiObject Win32_LogicalDisk | Where-Object { $_.DriveType -eq 3 } + $drives = $allDrives | Where-Object { $_.DeviceID -notin $ignoreDisks } + + if ($drives.Count -eq 0) { + Write-Host "OK: disks $($ignoreDisks -join ', ') are ignored" + if ($DebugOutput) { + Write-Host "[DEBUG] Total drives found: $($allDrives.Count)" + Write-Host "[DEBUG] Ignored drives: $($ignoreDisks -join ', ')" + } + $host.SetShouldExit(0) + return + } + + $failedDrives = @() + $warningDrives = @() + + foreach ($drive in $drives) { + $freeSpacePercent = [math]::Round(($drive.FreeSpace / $drive.Size) * 100, 2) + + if ($freeSpacePercent -lt $errorThreshold) { + $failedDrives += $drive + } + elseif ($freeSpacePercent -lt $warningThreshold) { + $warningDrives += $drive + } + } + + foreach ($drive in $drives) { + $freeSpacePercent = [math]::Round(($drive.FreeSpace / $drive.Size) * 100, 2) + + if ($failedDrives -contains $drive) { + Write-Host "ERROR: $($drive.DeviceID) has less than $($errorThreshold)% free space ($freeSpacePercent%)." + } + elseif ($warningDrives -contains $drive) { + Write-Host "WARNING: $($drive.DeviceID) has less than $($warningThreshold)% free space ($freeSpacePercent%)." + } + else { + Write-Host "OK: $($drive.DeviceID) has $($freeSpacePercent)% free space." + } + } + + if ($DebugOutput) { + if ($failedDrives.Count -gt 0) { + Write-Host "DEBUG: The following drives failed:" + $failedDrives | ForEach-Object { + $p = [math]::Round(($_.FreeSpace / $_.Size) * 100, 2) + Write-Host "DEBUG: $($_.DeviceID): $p%" + } + } elseif ($warningDrives.Count -gt 0) { + Write-Host "DEBUG: The following drives are in warning:" + $warningDrives | ForEach-Object { + $p = [math]::Round(($_.FreeSpace / $_.Size) * 100, 2) + Write-Host "DEBUG: $($_.DeviceID): $p%" + } + } else { + Write-Host "DEBUG: All drives have sufficient free space." + } + } + + if ($failedDrives.Count -gt 0) { + if ($DebugOutput) { Write-Host "DEBUG: exit code 2" } + $host.SetShouldExit(2) + } + elseif ($warningDrives.Count -gt 0) { + if ($DebugOutput) { Write-Host "DEBUG: exit code 1" } + $host.SetShouldExit(1) + } + else { + if ($DebugOutput) { Write-Host "DEBUG: exit code 0" } + $host.SetShouldExit(0) + } +} + +# Execute the function +CheckDiskSpace diff --git a/scripts_staging/Checks/Disk RW.ps1 b/scripts_staging/Checks/Disk RW.ps1 new file mode 100644 index 00000000..94dc5258 --- /dev/null +++ b/scripts_staging/Checks/Disk RW.ps1 @@ -0,0 +1,97 @@ +<# +.SYNOPSIS + Tests disk read and write speeds using a specified test file, with thresholds configurable via environment variables. + +.DESCRIPTION + The Test-DiskSpeed function creates a 1GB test file (or size specified by the user) at the location specified by the + environment variable 'target_file', measures the write and read speeds, and returns the results. The script then + checks these speeds against predefined thresholds which can be overridden by environment variables + +.EXEMPLE + READ_WARN_THRESHOLD_MBPS=2000 + READ_ERROR_THRESHOLD_MBPS=1500 + WRITE_WARN_THRESHOLD_MBPS=80 + WRITE_ERROR_THRESHOLD_MBPS=50 + TARGET_FILE=C:\TestdiskRWspeed.tmp + +.OUTPUTS + Outputs the write and read speeds in MB/s. + Exits with: + - 0: All speeds are above the defined thresholds. + - 1: At least one speed is below the warning threshold or if the target file environment variable is empty. + - 2: At least one speed is below the error threshold. + +.NOTES + Author: SAN + Date: 07.10.24 + #public + +.CHANGELOG + SAN 11.12.24 Moved vars to env + SAN 17.04.25 Default to temp dir if no value provided and code cleanup +#> + +# Set thresholds from environment or fallback to defaults +$ReadWarnThresholdMBps = [int]($env:READ_WARN_THRESHOLD_MBPS || 2000) +$ReadErrorThresholdMBps = [int]($env:READ_ERROR_THRESHOLD_MBPS || 1500) +$WriteWarnThresholdMBps = [int]($env:WRITE_WARN_THRESHOLD_MBPS || 80) +$WriteErrorThresholdMBps = [int]($env:WRITE_ERROR_THRESHOLD_MBPS || 50) + +# Function to test disk speed +function Test-DiskSpeed { + param ( + [string]$TestFile = $(if ($env:target_file) { $env:target_file } else { "C:\Windows\Temp\disk_test.tmp" }), + [int]$FileSizeInMB = 1024 + ) + + $buffer = New-Object byte[] (1MB) + $rnd = New-Object Random + + # Write test + $writeStart = Get-Date + $stream = [System.IO.File]::Create($TestFile) + for ($i = 0; $i -lt $FileSizeInMB; $i++) { + $rnd.NextBytes($buffer) + $stream.Write($buffer, 0, $buffer.Length) + } + $stream.Close() + $writeDuration = (Get-Date) - $writeStart + $writeSpeedMBps = $FileSizeInMB / $writeDuration.TotalSeconds + + # Read test + $readStart = Get-Date + $stream = [System.IO.File]::OpenRead($TestFile) + while ($stream.Read($buffer, 0, $buffer.Length)) { } + $stream.Close() + $readDuration = (Get-Date) - $readStart + $readSpeedMBps = $FileSizeInMB / $readDuration.TotalSeconds + + Remove-Item -Force $TestFile + + return [pscustomobject]@{ + WriteSpeedMBps = [math]::Round($writeSpeedMBps, 2) + ReadSpeedMBps = [math]::Round($readSpeedMBps, 2) + TestFile = $TestFile + } +} + +# Run and evaluate +$speedResults = Test-DiskSpeed + +Write-Output "W: $($speedResults.WriteSpeedMBps) MB/s" +Write-Output "R: $($speedResults.ReadSpeedMBps) MB/s" +Write-Output "T: $($speedResults.TestFile)" + +if ( + $speedResults.WriteSpeedMBps -lt $WriteErrorThresholdMBps -or + $speedResults.ReadSpeedMBps -lt $ReadErrorThresholdMBps +) { + exit 2 +} elseif ( + $speedResults.WriteSpeedMBps -lt $WriteWarnThresholdMBps -or + $speedResults.ReadSpeedMBps -lt $ReadWarnThresholdMBps +) { + exit 1 +} else { + exit 0 +} diff --git a/scripts_staging/Checks/Eset Status.ps1 b/scripts_staging/Checks/Eset Status.ps1 new file mode 100644 index 00000000..aaa352a4 --- /dev/null +++ b/scripts_staging/Checks/Eset Status.ps1 @@ -0,0 +1,49 @@ +<# +.SYNOPSIS + This script checks the protection status of ESET Security using the `ermm.exe` command-line tool + and provides an appropriate exit code based on the status. + +.DESCRIPTION + The script executes the ESET Security command to retrieve the protection status. + If the system is protected, it exits with a code of 0. If the license is about to expire, + it exits with a code of 1, and if the protection status is not found or any other error occurs, + it exits with a code of 2. Debug output is provided in cases where the protection status or + license expiration is detected but the command's output is unexpected. + +.NOTES + Author: SAN + Usefull links: + https://help.eset.com/eea/12/en-US/rmm_command_line.html?idh_config_ermm.html + #public + +.TODO + Get the output and convert to json to use it and output more details + +#> + +try { + $commandOutput = & "C:\Program Files\ESET\ESET Security\ermm.exe" get protection-status 2>&1 + + if ($commandOutput -match "You are protected") { + Write-Host "You are protected" + $host.SetShouldExit(0) + exit 0 + } elseif ($commandOutput -match "License expires") { + Write-Host "License expires" + Write-Host "Debug output:" + Write-Host "$commandOutput" + $host.SetShouldExit(1) + exit 1 + } else { + Write-Host "Protection status not found" + Write-Host "Debug output:" + Write-Host "$commandOutput" + $host.SetShouldExit(2) + exit 2 + } +} catch { + Write-Host "Error executing the command: $_" + Write-Host "ESET is not installed" + $host.SetShouldExit(2) + exit 2 +} \ No newline at end of file diff --git a/scripts_staging/Checks/Exchange Health.ps1 b/scripts_staging/Checks/Exchange Health.ps1 new file mode 100644 index 00000000..cfe0c2ed --- /dev/null +++ b/scripts_staging/Checks/Exchange Health.ps1 @@ -0,0 +1,166 @@ +<# +.SYNOPSIS + This PowerShell script performs various checks on an Exchange server and reports status information. + +.DESCRIPTION + This script combines multiple functions to check the health and status of an Exchange server. + It checks the submission queue, MAPI connectivity, mailbox databases, DAG index state, and certificate expiration. + +.NOTES + Author: SAN + Date: 24.04.24 + #public + +.CHANGELOG + 23.10.24 SAN Bug fix on the counter + 11.12.24 SAN Code cleanup + +#> + + +$ExchangeServices = Get-Service | Where-Object { $_.DisplayName -like "Microsoft Exchange*" } + +if ($ExchangeServices -eq $null) { + Write-Host "No Exchange services found. Exiting." + exit 0 +} else { + Write-Host "Exchange services found." +} + +# Add Exchange PowerShell Snapin +Add-PSSnapin Microsoft.Exchange.Management.PowerShell.SnapIn + +function CheckQueue { + $warningThreshold = 500 + $criticalThreshold = 1000 + + $queueCount = (Get-Queue -Server $(hostname) -Filter {Identity -eq "Submission"}).MessageCount + + if ($queueCount -eq $null) { + Write-Host "CRITICAL: Unable to retrieve Submission Queue count." + return 2 + } elseif ($queueCount -ge $criticalThreshold) { + Write-Host "CRITICAL: Submission Queue count is $queueCount" + return 2 + } elseif ($queueCount -ge $warningThreshold) { + Write-Host "WARNING: Submission Queue count is $queueCount" + return 1 + } else { + Write-Host "OK: Submission Queue count is $queueCount" + return 0 + } +} + +function CheckMAPIs { + $exitCode = 0 + $resultOK = "" + $resultKO = "" + + Get-MailboxDatabase | Where-Object {$_.Server -match $(hostname.exe)} | ForEach-Object { + $testResult = Test-MapiConnectivity -Database $_ + if (($testResult.Result -notmatch "Success") -and ($testResult.Result -notmatch "ussite")) { + $exitCode = 2 + $resultKO += " $($_.Name)" + } else { + $resultOK += " $($_.Name)" + } + } + + if ($exitCode -eq 0) { + Write-Host "OK: MAPI databases: $resultOK" + } else { + Write-Host "KO: MAPI databases: KO: $resultKO OK: $resultOK" + } + + return $exitCode +} + +function CheckDatabases { + $statusCode = 0 + $statusMessage = "" + + ForEach ($db in Get-MailboxDatabase -Server $(hostname)) { + $dbStatus = Get-MailboxDatabaseCopyStatus -Identity "$($db.Name)\$(hostname)" + + foreach ($status in $dbStatus) { + if ($status.Status -ne "Mounted" -and $status.Status -ne "Healthy") { + $statusCode = 2 + if ($statusMessage) { + $statusMessage += ", " + } + $statusMessage += "$($status.Name) is $($status.Status)" + } + } + } + + if ($statusCode -eq 0) { + Write-Host "OK: All Mailbox Databases are mounted and healthy." + } else { + Write-Host "KO: $statusMessage" + } + + return $statusCode +} + + +function CheckIndexState { + $exitCode = 0 + + foreach ($index in Get-MailboxDatabaseCopyStatus) { + if ($index.ContentIndexState -eq "NotApplicable") { + Write-Host "OK: Index state not applicable." + } elseif ($index.ContentIndexState -ne "Healthy") { + $exitCode = 2 + break + } + } + + if ($exitCode -eq 2) { + Write-Host "CRITICAL: Index state error." + } else { + Write-Host "OK: Index state is healthy." + } + + return $exitCode +} + +function CheckCertValidity { + $validCert = (Get-ExchangeCertificate | Where-Object {$_.Services -match "IIS" -and $_.Status -match "Valid" -and $_.IsSelfSigned -eq $False}).NotAfter.AddDays(-30) + + if ((Get-Date) -gt $validCert) { + Write-Host "CRITICAL: The Exchange certificate expires in less than 30 days." + return 2 + } else { + Write-Host "OK: Exchange certificate is valid." + return 0 + } +} + +# Main Script + +function GetStatus { + param ( + [scriptblock]$CheckFunction + ) + + return & $CheckFunction +} + +$queueStatus = GetStatus { CheckQueue } +$mapiStatus = GetStatus { CheckMAPIs } +$dbStatus = GetStatus { CheckDatabases } +$indexStatus = GetStatus { CheckIndexState } +$certStatus = GetStatus { CheckCertValidity } + +# Collect all status codes into an array of integers +$statusArray = @($queueStatus, $mapiStatus, $dbStatus, $indexStatus, $certStatus) + +# Calculate the maximum status +$maxStatus = $statusArray | Measure-Object -Maximum | Select-Object -ExpandProperty Maximum + +# Output the final status +Write-Host "Final Exit Code: $maxStatus" + +# Ensure that $maxStatus is an integer before exiting +$host.SetShouldExit([int]$maxStatus) +exit [int]$maxStatus \ No newline at end of file diff --git a/scripts_staging/Checks/Internet uplink.ps1 b/scripts_staging/Checks/Internet uplink.ps1 new file mode 100644 index 00000000..891d59bf --- /dev/null +++ b/scripts_staging/Checks/Internet uplink.ps1 @@ -0,0 +1,89 @@ +<# +.SYNOPSIS + Tests connectivity to a predefined list of IP addresses either randomly or all at once. + +.DESCRIPTION + This script checks network connectivity by pinging a list of predefined IP addresses. + The user can choose to test all the IP addresses or a randomly selected one. + If a ping fails, the script exits with a status code of 1. + +.PARAMETER TestAll + A switch parameter to test all IP addresses in the list. + If not specified, the script selects a random IP address for testing. + +.EXAMPLE + -TestAll + +.NOTES + Author: SAN + Date: 01.01.25 + #public + +.CHANGELOG + 25.03.25 SAN Format output +.TODO + Include customizable input for the list of IP addresses. + Enhance error handling for unreachable hosts. + move test all to env + tnc has some relability issue maybe use normal ping as fallback + +#> + + +param ( + [switch]$TestAll +) + +# List of IP addresses with their respective owners +$ipAddresses = @( + @{ IP="8.8.8.8"; Owner="Google DNS" }, + @{ IP="8.8.4.4"; Owner="Google DNS" }, + @{ IP="1.1.1.1"; Owner="Cloudflare DNS" }, + @{ IP="1.0.0.1"; Owner="Cloudflare DNS" }, + @{ IP="208.67.222.222"; Owner="OpenDNS" }, + @{ IP="208.67.220.220"; Owner="OpenDNS" }, + @{ IP="9.9.9.9"; Owner="Quad9 DNS" }, + @{ IP="149.112.112.112"; Owner="Quad9 DNS" }, + @{ IP="13.107.42.14"; Owner="Microsoft Azure" }, + @{ IP="20.190.160.1"; Owner="Microsoft Azure" }, + @{ IP="54.239.28.85"; Owner="Amazon AWS" }, + @{ IP="205.251.242.103"; Owner="Amazon AWS" } +) + +$pingFailed = $false + +if ($TestAll) { + # Test all IP addresses + foreach ($entry in $ipAddresses) { + $ip = $entry.IP + $owner = $entry.Owner + $pingResult = Test-Connection -ComputerName $ip -Count 1 -Quiet + + if (-not $pingResult) { + Write-Host "KO: Ping to $ip ($owner) failed." + $pingFailed = $true + } else { + Write-Host "OK: Ping to $ip ($owner) succeeded." + } + } + + if ($pingFailed) { + exit 1 + } +} else { + # Randomly select an IP address + $randomEntry = $ipAddresses | Get-Random + $randomIp = $randomEntry.IP + $owner = $randomEntry.Owner + + # Ping the selected IP address + $pingResult = Test-Connection -ComputerName $randomIp -Count 1 -Quiet + + # Check the result of the ping and exit with status code 1 if it fails + if (-not $pingResult) { + Write-Host "KO: Ping to $randomIp ($owner) failed." + exit 1 + } else { + Write-Host "OK: Ping to $randomIp ($owner) succeeded." + } +} \ No newline at end of file diff --git a/scripts_staging/Checks/Is TCP port open.ps1 b/scripts_staging/Checks/Is TCP port open.ps1 new file mode 100644 index 00000000..b46b3e31 --- /dev/null +++ b/scripts_staging/Checks/Is TCP port open.ps1 @@ -0,0 +1,87 @@ +<# +.SYNOPSIS + Checks if a TCP port is open on the local machine based on the environment variable "TCP_PORT". + +.DESCRIPTION + This script checks if the TCP port defined by the environment variable "TCP_PORT" is open using the `Test-NetConnection` cmdlet. + If `Test-NetConnection` is not available, it falls back to using the `System.Net.Sockets.TcpClient` class to perform the check. + Additionally, it will display the executable and process information that is holding the port open. + If the application is linked to a service, the service name and status will be displayed. + The script will exit with a status code of 1 if the port is closed or if the environment variable is not set. + +.EXEMPLE + TCP_PORT=3435 + +.NOTES + Author: SAN + Date: 01.10.2024 + #public + +.CHANGELOG + +#> + +$portStr = [System.Environment]::GetEnvironmentVariable("TCP_PORT") + +# Initialize the port variable +$port = 0 + +# Check if the environment variable is set and valid +if (-not $portStr -or -not [int]::TryParse($portStr, [ref]$port) -or $port -lt 1) { + Write-Output "Error: Environment variable 'TCP_PORT' is not set or is invalid." + exit 1 +} + +$address = "localhost" + +Write-Output "Checking connectivity to $address on port $port..." + +# Try Test-NetConnection if available +if (Get-Command Test-NetConnection -ErrorAction SilentlyContinue) { + $tcpConnection = Test-NetConnection -ComputerName $address -Port $port + if ($tcpConnection.TcpTestSucceeded) { + Write-Output "Success: Port $port on $address is open." + } else { + Write-Output "Failure: Port $port on $address is not open." + Write-Output "Details: TCP connection test failed." + exit 1 + } +} else { + # Fallback using TcpClient + try { + $tcpClient = New-Object System.Net.Sockets.TcpClient + $tcpClient.Connect($address, $port) + Write-Output "Success: Port $port on $address is open." + $tcpClient.Close() + } catch { + Write-Output "Failure: Port $port on $address is not open." + Write-Output "Details: TCP connection test threw an exception." + exit 1 + } +} + +# Find the process holding the port open for incoming connections only +$netstatOutput = netstat -ano | Select-String ":$port\s" | ForEach-Object { $_.Line } | Where-Object { $_ -match 'LISTENING' -and $_ -match '0.0.0.0|127.0.0.1' } +if ($netstatOutput) { + $portPID = $netstatOutput -replace '^.*\s+(\d+)$', '$1' + $process = Get-Process -Id $portPID -ErrorAction SilentlyContinue + + if ($process) { + Write-Output "The port $port is being used by the process '$($process.ProcessName)' (PID: $portPID)." + Write-Output "Executable Path: $($process.Path)" + + # Check if the process is linked to a service + $service = Get-WmiObject Win32_Service | Where-Object { $_.ProcessId -eq $portPID } + if ($service) { + Write-Output "This process is linked to the service: '$($service.Name)'" + Write-Output "Service Display Name: $($service.DisplayName)" + Write-Output "Service Status: $($service.State)" + } else { + Write-Output "This process is not linked to any service." + } + } else { + Write-Output "Unable to retrieve the process details for PID $portPID." + } +} else { + Write-Output "No process is currently using port $port for incoming connections." +} \ No newline at end of file diff --git a/scripts_staging/Checks/Last errors logs.ps1 b/scripts_staging/Checks/Last errors logs.ps1 new file mode 100644 index 00000000..55e1f306 --- /dev/null +++ b/scripts_staging/Checks/Last errors logs.ps1 @@ -0,0 +1,156 @@ +<# +.SYNOPSIS + This script retrieves and processes error events from the Windows Event Log within the last 48 and 12 hours. + +.DESCRIPTION + This script is useful for monitoring and alerting on error events in the Windows Event Log. + The script processes error logs from the 'System' log only, only critical errors are counted and displayed. + + 1. Retrieves the last 20 error events from the 'System' log in the last 48 hours, excluding specified event IDs. + 2. Counts and displays the number of error events found in the last 48 hours (after filtering out ignored events). + 3. Retrieves error events from the last 12 hours and checks if there are 4 or more errors. + 4. If 4 or more errors are found in the last 12 hours, the script exits with an error code (1). + 5. If fewer than 4 errors are found, the script exits with a success code (0). + +.EXEMPLE + debug=true + FILTER_ID=1111,22222,3333 + FILTER_KEYWORD=keyword1,keyword2 + +.NOTES + Author: SAN + Date: 24.10.2024 + #public + + 10016 safe to ignore + https://learn.microsoft.com/en-us/troubleshoot/windows-client/application-management/event-10016-logged-when-accessing-dcom + 36874 to ignore + Fixing the issue would be more dangerous than leaving it be it would require blocking tls 1.2 and forcing 1.1 with unsafe cyphers and loosing connection to devices that do not support 1.1 + +.CHANGELOG + 04.12.24 SAN added id to ignore in comma separeted variable + 12.12.24 SAN adding keyword filters, added filter addition via env var + +.TODO + Set 20 Error Events and 48 hours in vars same for 4 and 12 + Re-thing the thresholds to add info warn error limits + +#> + +$defaultEventIds = @(10016,36874) +$defaultKeywords = @("gupdate","anotherkeyword") + +$debug = [System.Environment]::GetEnvironmentVariable("DEBUG") +$filterIdEnv = [System.Environment]::GetEnvironmentVariable("FILTER_ID") +$filterKeywordEnv = [System.Environment]::GetEnvironmentVariable("FILTER_KEYWORD") + +$ignoredEventIds = if ($filterIdEnv) { + $filterIdEnv.Split(",") + $defaultEventIds +} else { + $defaultEventIds +} + +$ignoredKeywords = if ($filterKeywordEnv) { + $filterKeywordEnv.Split(",") + $defaultKeywords +} else { + $defaultKeywords +} + +$start48h = (Get-Date).AddHours(-48) +$start12h = (Get-Date).AddHours(-12) + +$allErrors48h = Get-WinEvent -FilterHashtable @{LogName='System'; Level=2; StartTime=$start48h} -ErrorAction SilentlyContinue + +$eventsWithIdFilter = $allErrors48h | Where-Object { $ignoredEventIds -contains $_.Id.ToString() } + +$eventsWithKeywordFilter = $allErrors48h | Where-Object { + $eventData = $_.Properties -join " " + $eventData += " " + $_.Message + $keywordMatches = $false + $ignoredKeywords | ForEach-Object { + $keyword = $_.Trim() + if ($eventData -match "(?i)\b$($keyword)\b") { + $keywordMatches = $true + } + } + $keywordMatches +} + +if ($debug -eq "true") { + Write-Output "DEBUG: Filtering events with the following parameters:" + Write-Output "DEBUG: Filtered Event IDs: $ignoredEventIds" + Write-Output "DEBUG: Filtered Keywords: $ignoredKeywords" + + if ($eventsWithIdFilter.Count -gt 0) { + Write-Output "Filtered Events by Event ID in the last 48 hours:" + $eventsWithIdFilter | ForEach-Object { + Write-Output "TimeCreated: $($_.TimeCreated)" + Write-Output "Event ID: $($_.Id)" + Write-Output "Message: $($_.Message)" + Write-Output "----------------------------------------" + } + } else { + Write-Output "No events found matching the specified Event IDs in the last 48 hours." + } + + if ($eventsWithKeywordFilter.Count -gt 0) { + Write-Output "Filtered Events by Keyword in the last 48 hours:" + $eventsWithKeywordFilter | ForEach-Object { + Write-Output "TimeCreated: $($_.TimeCreated)" + Write-Output "Event ID: $($_.Id)" + Write-Output "Message: $($_.Message)" + Write-Output "----------------------------------------" + } + } else { + Write-Output "No events found matching the specified Keywords in the last 48 hours." + } +} + +$remainingErrors48h = $allErrors48h | Where-Object { + $eventIdMatches = $ignoredEventIds -contains $_.Id.ToString() + $eventData = $_.Properties -join " " + $eventData += " " + $_.Message + $keywordMatches = $false + $ignoredKeywords | ForEach-Object { + $keyword = $_.Trim() + if ($eventData -match "(?i)\b$($keyword)\b") { + $keywordMatches = $true + } + } + if ($eventIdMatches -or $keywordMatches) { + $false + } else { + $true + } +} + +if ($remainingErrors48h.Count -gt 0) { + Write-Output "Remaining Error Events in the last 48 hours (after filtering out ignored Event IDs and Keywords):" + $remainingErrors48h | ForEach-Object { + Write-Output "TimeCreated: $($_.TimeCreated)" + Write-Output "Event ID: $($_.Id)" + Write-Output "Message: $($_.Message)" + Write-Output "----------------------------------------" + } +} + +$errors12h = $remainingErrors48h | Where-Object { $_.TimeCreated -gt $start12h } + +if ($errors12h.Count -ge 4) { + Write-Output "Error: 4 or more error events found in the last 12 hours." + Write-Output "Error Events in the last 12 hours (excluding ignored event IDs and keywords):" + $errors12h | ForEach-Object { + Write-Output "TimeCreated: $($_.TimeCreated)" + Write-Output "Event ID: $($_.Id)" + Write-Output "Message: $($_.Message)" + Write-Output "----------------------------------------" + } + exit 1 +} else { + if ($errors12h.Count -eq 0) { + Write-Output "OK: No error events found in the last 12 hours." + } else { + Write-Output "OK: Less than 4 error events found in the last 12 hours." + } + exit 0 +} diff --git a/scripts_staging/Checks/Maximum UpTime.ps1 b/scripts_staging/Checks/Maximum UpTime.ps1 new file mode 100644 index 00000000..f24dce87 --- /dev/null +++ b/scripts_staging/Checks/Maximum UpTime.ps1 @@ -0,0 +1,47 @@ +<# +.SYNOPSIS + This script calculates the uptime of a computer and compares it to a specified maximum time. + +.DESCRIPTION + The script retrieves the LastBootUpTime of the computer and calculates the current uptime. + If the uptime exceeds the maximum time, the script exits with an exit code of 1. + If the uptime is within the allowed range, the script exits with an exit code of 0. + +.PARAMETER MaxTime + Specifies the maximum allowed uptime in days. + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.TODO + move var to env + +.CHANGELOG + 12.12.24 SAN Changed outputs + +#> + +param ( + [Parameter(Mandatory = $true, HelpMessage = "Specify the maximum allowed uptime in days.")] + [int]$MaxTime +) + +# Calculate the uptime +$uptime = (Get-Date) - (Get-CimInstance -Class Win32_OperatingSystem).LastBootUpTime +$uptimeDays = $uptime.Days +$uptimeTimeSpan = $uptime.ToString("hh\:mm\:ss") +$formattedUptime = "{0} days, {1}" -f $uptimeDays, $uptimeTimeSpan + +# Compare the uptime with the maximum time +if ($uptimeDays -gt $MaxTime) { + Write-Output "The computer has an uptime of $formattedUptime." + Write-Output "The computer has an uptime greater than $MaxTime days." + exit 1 +} else { + Write-Output "OK: Uptime is not above max" + #Write-Output "The computer has an uptime of $formattedUptime." + #Write-Output "The computer has an uptime lower than $MaxTime days." + exit 0 +} \ No newline at end of file diff --git a/scripts_staging/Checks/Ping monitoring.ps1 b/scripts_staging/Checks/Ping monitoring.ps1 new file mode 100644 index 00000000..6b4eee86 --- /dev/null +++ b/scripts_staging/Checks/Ping monitoring.ps1 @@ -0,0 +1,91 @@ +<# +.SYNOPSIS + A PowerShell script to check the reachability and response time of specified hosts or IP addresses using ping. + +.DESCRIPTION + This script checks if a list of hosts or IP addresses (specified in the PING_TARGETS environment variable) is reachable by sending a single ping request. + It outputs "OK" with the latency in milliseconds if the host is reachable, or "KO" if it is not. + +.PARAMETER PING_TARGETS + Environment variable that holds a comma-separated list of IP addresses or hostnames to ping. + +.PARAMETER PING_ERROR_THRESHOLD + The threshold in milliseconds for a warning. If the ping response time exceeds this threshold, the script will output a warning and exit with code 1. + +.PARAMETER PING_ERROR_THRESHOLD + The threshold in milliseconds for an error. If the ping response time exceeds this threshold, the script will output an error and exit with code 2. + +.EXEMPLE + PING_TARGETS=8.8.8.8,1.1.1.1,example.com + PING_ERROR_THRESHOLD=200 + PING_WARN_THRESHOLD=500 + +.NOTES + Author: SAN + Created: 08.11.24 + #public + +.CHANGELOG + 13.11.24 SAN Changed from tnc to ping tnc was not trustworthy + + +#> + +# Set default threshold values (in ms) +$DefaultWarnThreshold = 300 # Default warning threshold in milliseconds +$DefaultErrorThreshold = 600 # Default error threshold in milliseconds + +# Check for environment variables to override the default thresholds +$WarnThreshold = if ($env:PING_WARN_THRESHOLD) { [int]$env:PING_WARN_THRESHOLD } else { $DefaultWarnThreshold } +$ErrorThreshold = if ($env:PING_ERROR_THRESHOLD) { [int]$env:PING_ERROR_THRESHOLD } else { $DefaultErrorThreshold } + +# Get the list of targets from the environment variable +$Targets = $env:PING_TARGETS -split "," # Split comma-separated values into an array + +if (-not $Targets) { + Write-Output "No targets specified in the environment variable 'PING_TARGETS'. Exiting." + exit 3 +} + +$ExitCode = 0 # Default exit code is 0 (success) + +foreach ($Target in $Targets) { + $Target = $Target.Trim() + try { + # Run the ping command and capture the output + $PingResult = & ping -n 1 -w 1000 $Target 2>&1 + + # Process each line in $PingResult to look for the response time + $ResponseTime = $PingResult | Select-String -Pattern "time=(\d+)ms" | ForEach-Object { + if ($_ -match "time=(\d+)ms") { + [int]$matches[1] + } + } + + if ($ResponseTime -ne $null) { + if ($ResponseTime -gt $ErrorThreshold) { + Write-Output "ERR $Target $ResponseTime ms" + $ExitCode = [math]::max($ExitCode, 2) + } elseif ($ResponseTime -gt $WarnThreshold) { + Write-Output "WARN $Target $ResponseTime ms" + $ExitCode = [math]::max($ExitCode, 1) + } else { + Write-Output "OK $Target $ResponseTime ms" + } + } elseif ($PingResult -match "Request timed out") { + Write-Output "KO $Target (Timeout)" + $ExitCode = [math]::max($ExitCode, 3) + } else { + Write-Output "KO $Target (Ping command failed or unexpected output)" + $ExitCode = [math]::max($ExitCode, 3) + } + } + catch { + Write-Output "KO $Target (Error: $_)" + $ExitCode = [math]::max($ExitCode, 3) + } +} + +# Exit with the determined exit code (warn = 1, error = 2, fail = 3) +$host.SetShouldExit($ExitCode) +exit $ExitCode diff --git a/scripts_staging/Checks/Rdcms size.ps1 b/scripts_staging/Checks/Rdcms size.ps1 new file mode 100644 index 00000000..eecc6347 --- /dev/null +++ b/scripts_staging/Checks/Rdcms size.ps1 @@ -0,0 +1,45 @@ +<# +.SYNOPSIS + Checks the size of MDF and LDF files and returns an exit code based on file size. + +.DESCRIPTION + This script checks the size of the specified MDF file (e.g., SQL Server database file) and compares it against a defined size threshold. If the file size exceeds the threshold (80MB in this case), it outputs a critical message and returns an exit code of 1. If the file size is within the threshold, it returns an exit code of 0. If the file does not exist, the script skips the check and returns an exit code of 0. + If the MDF file becomes too large, especially in the context of an RDS (Remote Desktop Services) server, it can reach a critical size and prevent the RDS server from functioning properly. This can cause system errors or failures related to the database, which might lead to disruptions in RDS operations. + +.NOTES + Author: SAN + #public + +.CHANGELOG + +#> + +$MDFFilePath = 'C:\Windows\rdcbDb\Rdcms.mdf' +$fileSizeThreshold = 80MB + +try { + if (!(Test-Path $MDFFilePath -PathType 'Leaf')) { + Write-Output "File not found. Skipping file check." + exit 0 + } + + $mdfSize = (Get-Item $MDFFilePath).Length + + if ($mdfSize -gt $fileSizeThreshold) { + Write-Output "File size exceeded the threshold. MDF Size: $($mdfSize / 1MB) MB." + Write-Output "Critical the RDS server is about to stop" + Write-Output "Check the links bellow for more informations:" + Write-Output "https://learn.microsoft.com/fr-fr/sql/relational-databases/logs/troubleshoot-a-full-transaction-log-sql-server-error-9002?view=sql-server-ver16" + Write-Output "https://learn.microsoft.com/en-us/troubleshoot/windows-server/performance/esent-event-327-326" + exit 1 + + } + else { + Write-Output "File size is within the threshold. MDF Size: $($mdfSize / 1MB) MB." + exit 0 + } +} +catch { + Write-Output "An error occurred: $($_.Exception.Message)" + exit 1 +} \ No newline at end of file diff --git a/scripts_staging/Checks/SQL Health.ps1 b/scripts_staging/Checks/SQL Health.ps1 new file mode 100644 index 00000000..b051ec24 --- /dev/null +++ b/scripts_staging/Checks/SQL Health.ps1 @@ -0,0 +1,296 @@ +<# +.SYNOPSIS + This script performs health checks on a machine with SQL Server installed. + +.DESCRIPTION + The script checks various aspects of SQL Server health, including version, blocked requests, + and availability group synchronization. It provides a modular approach with separate functions + for each check. + +.NOTES + Author: SAN + Date: 01.01.2024 + #public + +.CHANGELOG + SAN 12.12.2023 Changed outputs + SAN 14.03.2024 Added availability group checks + SAN 02.07.2025 Centralised querries executions + +.TODO + Improve error handling + + +#> +function Get-SqlInstances { + $computername = $env:COMPUTERNAME + $instances = (Get-ItemProperty -Path "HKLM:\Software\Microsoft\Microsoft SQL Server" -Name "InstalledInstances").InstalledInstances + $serverInstances = @() + + foreach ($instance in $instances) { + if ($instance -eq "MSSQLSERVER") { + $serverInstances += "localhost" + } else { + try { + # Try without port first + $serverInstances += "$computername\$instance" + # Optionally, also add with port to test + $port = (Get-ItemProperty -Path "HKLM:\Software\Microsoft\Microsoft SQL Server\$instance\MSSQLServer\SuperSocketNetLib\Tcp" -Name "TcpPort" -ErrorAction SilentlyContinue).TcpPort + if ($port) { + $serverInstances += "$computername\$instance,$port" + } + } catch { + Write-Host "Warning: Could not retrieve port for instance $instance" + } + } + } + return $serverInstances +} +function Ensure-SqlCmdAvailable { + if (-not (Get-Command Invoke-Sqlcmd -ErrorAction SilentlyContinue)) { + try { + Add-PSSnapin SqlServerCmdletSnapin100 -ErrorAction Stop + Add-PSSnapin SqlServerProviderSnapin100 -ErrorAction Stop + } catch { + throw "SQL Cmdlets are not available and could not be loaded." + } + } +} + +function Run-SqlQuery { + param ( + [string]$Query, + [string]$Description, + [switch]$ReturnResults + ) + + Write-Host "Running: $Description" + $serverInstances = Get-SqlInstances + $errorEncountered = $false + $allResults = @() + + Ensure-SqlCmdAvailable + + foreach ($serverInstance in $serverInstances) { + try { + $result = Invoke-Sqlcmd -ServerInstance $serverInstance -Query $Query -QueryTimeout 30 -ErrorAction Stop + if (-not $result) { + Write-Host "OK: $serverInstance - No results" + } else { + Write-Host "OK: $serverInstance - Results:" + $result | Format-Table -AutoSize + } + if ($ReturnResults) { + $allResults += [pscustomobject]@{ + ServerInstance = $serverInstance + Result = if ($result) { $result } else { @() } + } + } + } catch { + Write-Host "Error: $($_.Exception.Message) for $serverInstance" + $errorEncountered = $true + } + } + if ($errorEncountered) { + return "Error" + } else { + if ($ReturnResults) { + return $allResults + } + return "OK" + } +} + + +function Get-SqlServerVersion { + $query = "SELECT @@VERSION;" + Write-Host "Running: Get SQL Server Version" + + $serverInstances = Get-SqlInstances + $errorEncountered = $false + + Ensure-SqlCmdAvailable + + foreach ($serverInstance in $serverInstances) { + Write-Host "`nQuerying instance: $serverInstance" + try { + $result = Invoke-Sqlcmd -ServerInstance $serverInstance -Query $query -QueryTimeout 30 -ErrorAction Stop + if (-not $result) { + #Write-Host "DEBUG: No results returned from $serverInstance." + $errorEncountered = $true + } else { + #Write-Host "DEBUG: Raw result object type: $($result.GetType().FullName)" + #Write-Host "DEBUG: Result count: $($result.Count)" + #Write-Host "DEBUG: Result content:" + #$result | Format-List | Out-String | Write-Host + Write-Host "OK: $serverInstance SQL Version: $($result.Column1)" + } + } catch { + Write-Host "ERROR: Exception querying $serverInstance : $($_.Exception.Message)" + $errorEncountered = $true + } + } + + if ($errorEncountered) { + return "Error" + } else { + return "OK" + } +} + + + +function Get-BlockedSqlRequests { + $query = @" +USE master +SELECT db.name AS DBName, + tl.request_session_id, + wt.blocking_session_id, + OBJECT_NAME(p.OBJECT_ID) AS BlockedObjectName, + tl.resource_type, + h1.TEXT AS RequestingText, + h2.TEXT AS BlockingText, + tl.request_mode +FROM sys.dm_tran_locks AS tl +JOIN sys.databases db ON db.database_id = tl.resource_database_id +JOIN sys.dm_os_waiting_tasks AS wt ON tl.lock_owner_address = wt.resource_address +JOIN sys.partitions AS p ON p.hobt_id = tl.resource_associated_entity_id +JOIN sys.dm_exec_connections ec1 ON ec1.session_id = tl.request_session_id +JOIN sys.dm_exec_connections ec2 ON ec2.session_id = wt.blocking_session_id +CROSS APPLY sys.dm_exec_sql_text(ec1.most_recent_sql_handle) AS h1 +CROSS APPLY sys.dm_exec_sql_text(ec2.most_recent_sql_handle) AS h2 +"@ + return Run-SqlQuery -Query $query -Description "Get Blocked SQL Requests" +} +function Get-SqlAgSyncStatus { + Write-Host "Running: Get SQL Availability Group Sync Status" + + $instanceName = (Get-Item 'HKLM:\Software\Microsoft\Microsoft SQL Server\Instance Names\SQL').Property[0] + $server = "$($env:COMPUTERNAME)\$instanceName" + + Write-Host "Detected SQL Server Instance: $server" + + $agQuery = @" +SELECT c.name, s.synchronization_health +FROM sys.availability_groups_cluster c +JOIN sys.dm_hadr_availability_group_states s ON c.group_id = s.group_id +WHERE LOWER(s.primary_replica) = LOWER('$server') + OR LOWER('$server') IN ( + SELECT LOWER(replica_server_name) + FROM sys.availability_replicas + ); +"@ + + $agResults = Run-SqlQuery -Query $agQuery -Description "Get Availability Group Synchronization Status" -ReturnResults + + if ($agResults -eq "Error") { + Write-Host "Failed to retrieve Availability Group synchronization status." + return "Error" + } + + $agData = $agResults | Where-Object { $_.ServerInstance -eq $server } + + if (-not $agData -or $agData.Result.Count -eq 0) { + Write-Host "OK: $server AG SYNCHRO - No Availability Groups found." + return "OK" + } + + $description = "" + $statusLevel = 0 + + foreach ($row in $agData.Result) { + switch ($row.synchronization_health) { + 0 { + $statusLevel = [Math]::Max($statusLevel, 2) + $description += "$($row.name): Not Healthy " + } + 1 { + $statusLevel = [Math]::Max($statusLevel, 1) + $description += "$($row.name): Partially Healthy " + } + 2 { + $description += "$($row.name): Healthy " + } + } + } + + $status = @("OK", "WARNING", "CRITICAL")[$statusLevel] + Write-Host "${status}: $server AG SYNCHRO - $description" + return $status +} + +function Check-SqlServerInstallation { + if (Get-Command Invoke-Sqlcmd -ErrorAction SilentlyContinue) { + return $true + } else { + Write-Host "SQL Server is not installed on this device" + return $false + } +} + +function Get-SqlCurrentUser { + + $query = "SELECT SUSER_NAME() AS LoginName, USER_NAME() AS UserName;" + + $results = Run-SqlQuery -Query $query -Description "Get current SQL user" -ReturnResults + + if ($results -eq "Error" -or $results.Count -eq 0) { + Write-Host "Failed to retrieve current user information." + return "Error" + } + + foreach ($res in $results) { + if (-not $res.ServerInstance -or -not $res.Result) { + # Skip empty results + continue + } + + $loginName = "" + $userName = "" + + if ($res.Result -is [System.Data.DataRow]) { + $loginName = $res.Result.Item("LoginName") + $userName = $res.Result.Item("UserName") + } + elseif ($res.Result -is [System.Data.DataTable]) { + if ($res.Result.Rows.Count -gt 0) { + $row = $res.Result.Rows[0] + $loginName = $row.Item("LoginName") + $userName = $row.Item("UserName") + } + } + elseif ($res.Result -is [System.Collections.IEnumerable]) { + $firstRow = $res.Result | Select-Object -First 1 + if ($firstRow) { + $loginName = $firstRow.LoginName + $userName = $firstRow.UserName + } + } + + Write-Host "Connected as: $loginName ($userName) on $($res.ServerInstance)" + } +} + + +# MAIN EXECUTION BLOCK +if (Check-SqlServerInstallation) { + + Get-SqlCurrentUser + + $result1 = Get-SqlServerVersion + $result2 = Get-BlockedSqlRequests + $result3 = Get-SqlAgSyncStatus + + if ($result1 -eq "OK" -and $result2 -eq "OK" -and $result3 -eq "OK") { + Write-Host "OK: All components are functioning properly" + } else { + $errorComponents = @() + if ($result1 -ne "OK") { $errorComponents += "SQL Server Version Check" } + if ($result2 -ne "OK") { $errorComponents += "Blocked SQL Requests Check" } + if ($result3 -ne "OK") { $errorComponents += "AG Synchronization Check" } + + $errorList = $errorComponents -join ", " + Write-Host "KO: Some components encountered errors. Errors in: $errorList" + Exit 1 + } +} diff --git a/scripts_staging/Checks/Swap health.ps1 b/scripts_staging/Checks/Swap health.ps1 new file mode 100644 index 00000000..f51d1107 --- /dev/null +++ b/scripts_staging/Checks/Swap health.ps1 @@ -0,0 +1,81 @@ +<# +.SYNOPSIS + This script checks the virtual memory settings on a Windows system, comparing them against recommended guidelines. + +.DESCRIPTION + This script retrieves information about physical and virtual memory on the system, calculates recommended minimum and maximum virtual memory sizes, + and compares them with the settings configured on the system. It provides warnings and errors if the configured settings do not meet the recommended + criteria. + +.NOTES + Author: SAN + Date: 01.01.24 + Usefull links: + https://learn.microsoft.com/en-us/troubleshoot/windows-client/performance/how-to-determine-the-appropriate-page-file-size-for-64-bit-versions-of-windows + #public + +.TODO + Implement fully the recomendations of the script + +.CHANGELOG + v1.1 9/12/2024 silversword411 Adding GB to output + v1.2 10/30/2024 SAN change output layout for readability + +#> + + +# Helper function to convert bytes to gigabytes +function ConvertTo-GB { + param ([double]$bytes) + return [math]::Round($bytes / 1GB, 2) +} + +# Get the virtual memory information +$virtualMemoryInfo = Get-WmiObject -Query "SELECT * FROM Win32_OperatingSystem" + +# Extract the Max Size and Available values +$MaxSize = $virtualMemoryInfo.TotalVirtualMemorySize * 1024 +$Available = $virtualMemoryInfo.FreeVirtualMemory * 1024 + +# Get the minimum size set on the system +$minimumSize = $virtualMemoryInfo.TotalVisibleMemorySize * 1024 + +# Calculate the minimum size based on RAM ÷ 8, max 32 GB +$physicalMemory = (Get-WmiObject -Class Win32_ComputerSystem).TotalPhysicalMemory +$calculatedMinimumSize = [Math]::Min(($physicalMemory / 8), 32GB) + +# Calculate the max size based on 3 times the RAM or 4 GB, whichever is larger +$calculatedMaxSize = [Math]::Max(($physicalMemory * 3), 4GB) + +# Required Available memory (10% of Max Size) +$requiredAvailable = $MaxSize * 0.05 + + +if ($Available -ge $requiredAvailable) { + Write-Output "Available meets the requirement." +} else { + Write-Output "Available does not meet the requirement (should be at least 10% of Max Size). (Error)" + $host.SetShouldExit(2) +} +Write-Output ("Available: {0} GB" -f (ConvertTo-GB $Available)) +Write-Output ("Required Available: {0} GB" -f (ConvertTo-GB $requiredAvailable)) +Write-Output "---------------" + +if ($minimumSize -ge $calculatedMinimumSize) { + Write-Output "Minimum Size meets the requirement." +} else { + Write-Output "Minimum Size does not meet the requirement (should be at least RAM divided by 8, max 32 GB). (Warn)" + #$host.SetShouldExit(1) +} +Write-Output ("Minimum Size set on the system: {0} GB" -f (ConvertTo-GB $minimumSize)) +Write-Output ("Calculated Minimum Size: {0} GB" -f (ConvertTo-GB $calculatedMinimumSize)) +Write-Output "---------------" + +if ($MaxSize -ge $calculatedMaxSize) { + Write-Output "Max Size meets the requirement." +} else { + Write-Output "Max Size does not meet the requirement (should be at least 3 times the RAM or 4 GB, whichever is larger). (Warn)" + #$host.SetShouldExit(1) +} +Write-Output ("Max Size: {0} GB" -f (ConvertTo-GB $MaxSize)) +Write-Output ("Calculated Max Size: {0} GB" -f (ConvertTo-GB $calculatedMaxSize)) diff --git a/scripts_staging/Checks/Task Scheduler scanner.ps1 b/scripts_staging/Checks/Task Scheduler scanner.ps1 new file mode 100644 index 00000000..82382c0f --- /dev/null +++ b/scripts_staging/Checks/Task Scheduler scanner.ps1 @@ -0,0 +1,147 @@ +<# +.SYNOPSIS + This script retrieves scheduled tasks and filters out those that match specific ignore conditions based on task folder, name, or user. It identifies rogue tasks that do not meet the ignore criteria. + +.DESCRIPTION + The script retrieves all scheduled tasks on the system and checks each task against predefined conditions to ignore certain tasks. + It filters tasks based on their folder path, task name, and user ID. If a task does not match any of the ignore criteria, its details (folder, name, and user) are collected. + The script provides a debug mode for verbose output during task processing. If rogue tasks are found, they are displayed in a table, and the script exits with a non-zero status code. + + +.EXAMPLE + company_name={{global.Company_Name}} + company_name=Consonto + + +.NOTES + Author: SAN + Date: 01.01.25 + #public + +.CHANGELOG + 02.07.25 SAN Added company name to the folders + 17.07.25 SAN added powertoys + +.TODO + Use a flag for debug + set ignore value from env + +#> + + + +# Set the debug flag +$debug = 0 + +# Retrieve all scheduled tasks +$tasks = Get-ScheduledTask + +# Initialize an array to hold the task details +$taskDetails = @() + +# Define ignore conditions +$ignoreFolders = @( + "\Mozilla\", + "\Microsoft\Office\", + "\Microsoft\Windows\", + "\MySQL\Installer\", + "\PowerToys\" +) + +# If it exists and is not empty, add it to the ignore list +$companyName = $env:company_name +if (-not [string]::IsNullOrEmpty($companyName)) { + $ignoreFolders += "\$companyName\" +} + +$ignoreNames = @( + "Optimize Start Menu Cache", + "DropboxUpdateTaskUserS", + "GoogleUpdate", + "User_Feed_Synchronization", + "Adobe Acrobat", + "RMM", + "edgeupdate", + "OneDrive Reporting Task", + "ZoomUpdateTaskUser", + "OneDrive Standalone Update Task", + "OneDrive Startup Task", + "CreateExplorerShellUnelevatedTask" +) +$ignoreUsers = @( + "*svc*", + "*Systme*", + "*Système*", + "*Syst*", + "SYSTEM" +) + +# Loop through each scheduled task +foreach ($task in $tasks) { + $taskFolder = $task.TaskPath + $taskName = $task.TaskName + $principalUserId = $task.Principal.UserId + + # Check if triggers are null and handle accordingly + if ($task.Triggers) { + # Commented out because Triggers are not needed + # $taskTriggers = $task.Triggers | ForEach-Object { $_.ToString() } + $taskTriggers = "Triggers present" + } else { + $taskTriggers = "No triggers" + } + + if ($debug -eq 1) { + # Debug: Print the current task details + Write-Output "Checking Task: Folder='$taskFolder', Name='$taskName', UserID='$principalUserId'" + # Commented out because Triggers are not needed + # Write-Output "Triggers: $($taskTriggers -join ', ')" + } + + # Check ignore conditions + $folderIgnored = $ignoreFolders | Where-Object { $taskFolder -like "*$_*" } | Measure-Object | Select-Object -ExpandProperty Count + $nameIgnored = $ignoreNames | Where-Object { $taskName -like "*$_*" } | Measure-Object | Select-Object -ExpandProperty Count + $userIgnored = $ignoreUsers | Where-Object { $principalUserId -like $_ } | Measure-Object | Select-Object -ExpandProperty Count + + if ($debug -eq 1) { + # Debug: Print ignore conditions + Write-Output "Folder Ignored Count: $folderIgnored" + Write-Output "Name Ignored Count: $nameIgnored" + Write-Output "User Ignored Count: $userIgnored" + } + + # Determine if the task should be ignored + $shouldIgnore = ($folderIgnored -gt 0) -or ($nameIgnored -gt 0) -or ($userIgnored -gt 0) + + if ($debug -eq 1) { + # Debug: Print ignore decision + Write-Output "Should Ignore: $shouldIgnore" + } + + if (-not $shouldIgnore) { + # Get the task registration info + $registrationInfo = $task.RegistrationInfo + + # Add the task details to the array + $taskDetails += [PSCustomObject]@{ + Folder = $taskFolder + TaskName = $taskName + RunBy = $principalUserId + # Commented out because CreatedBy is not needed + # CreatedBy = $registrationInfo.Author + # Commented out because Triggers are not needed + # Triggers = $taskTriggers -join ', ' + } + } +} + +# Check if the taskDetails array is empty +if ($taskDetails.Count -eq 0) { + Write-Output "No rogue tasks found" +} else { + Write-Output "Rogue tasks found, please execute with a service user" + # Output the task details + $taskDetails | Format-Table -AutoSize + # Exit with status code 1 + exit 1 +} \ No newline at end of file diff --git a/scripts_staging/Checks/Trigger tasks on boot.ps1 b/scripts_staging/Checks/Trigger tasks on boot.ps1 new file mode 100644 index 00000000..a2b9f46f --- /dev/null +++ b/scripts_staging/Checks/Trigger tasks on boot.ps1 @@ -0,0 +1,160 @@ +<# +.SYNOPSIS + Exits with code 1 if automation should trigger (key does not exist); exits with 0 otherwise. + +.DESCRIPTION + This script uses a volatile registry key to determine whether it has already run in the current boot cycle. + If the key already exists (i.e., automation has triggered before in this boot), the script exits with code 0. + If the key does not exist (i.e., first run since boot), it creates the key and exits with code 66 to trigger on failure tasks. + + This approach was implemented as a workaround for TacticalRMM's lack of native "on-boot" task support. + It enables TRMM tasks to detect the key’s lack of existence and act accordingly by triggering automations on failure of the check. + + Informational exit code should be set to 66 on the check. + +.NOTES + Author: SAN + Date: 08.05.25 + #public + Can't have any exit code on error in this script by nature otherwise it would trigger stuff left right and center. + +.CHANGELOG + 08.05.25 SAN added check to avoid runing C when not needed to help with runtime and better outputs + 11.05.25 SAN optimised C code and cleaned exit codes + 12.05.25 SAN fix exit codes, optimised C again, added event-log support to help with errors + +#> + +$subKey = "SOFTWARE\\TacticalRMM\\BootTrigger" +[string]$msg = $null +$ExitCreated = 66 +$ExitError = 0 +$ExitOK = 0 + +# Check if the registry key exists +$regKeyExists = Test-Path "HKLM:\$subKey" + +if ($regKeyExists) { + # Key already exists, proceed with no action + Write-Output "OK: Already triggered this boot" + $host.SetShouldExit($ExitOK) + exit $ExitOK +} else { + # Key doesn't exist, load and execute C# code to create the volatile registry key + Add-Type -TypeDefinition @" +using System; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +public class VolatileRegistry +{ + [DllImport("advapi32.dll", CharSet = CharSet.Unicode)] + private static extern int RegCreateKeyEx( + UIntPtr hKey, + string lpSubKey, + int Reserved, + string lpClass, + uint dwOptions, + int samDesired, + IntPtr lpSecurityAttributes, + out SafeRegistryHandle phkResult, + out int lpdwDisposition + ); + + [DllImport("advapi32.dll")] + private static extern int RegCloseKey(SafeRegistryHandle hKey); + + public static readonly UIntPtr HKEY_LOCAL_MACHINE = (UIntPtr)0x80000002; + + [Flags] + public enum RegistryAccess : int + { + KEY_QUERY_VALUE = 0x0001, + KEY_SET_VALUE = 0x0002, + KEY_CREATE_SUB_KEY = 0x0004, + KEY_ENUMERATE_SUB_KEYS = 0x0008, + KEY_NOTIFY = 0x0010, + KEY_CREATE_LINK = 0x0020, + KEY_WOW64_64KEY = 0x0100, + KEY_ALL_ACCESS = 0xF003F + } + + public const uint REG_OPTION_VOLATILE = 0x00000001; + + public static bool CreateVolatileKey(string subKey, out string message) + { + message = string.Empty; + + if (string.IsNullOrWhiteSpace(subKey)) + { + message = "KO: Invalid subKey value."; + return false; + } + + SafeRegistryHandle hKey; + int disposition; + + int result = RegCreateKeyEx( + HKEY_LOCAL_MACHINE, + subKey, + 0, + null, + REG_OPTION_VOLATILE, + (int)(RegistryAccess.KEY_ALL_ACCESS | RegistryAccess.KEY_WOW64_64KEY), + IntPtr.Zero, + out hKey, + out disposition + ); + + if (result == 0) + { + using (hKey) + { + message = string.Format("OK: Registry key created (disposition: {0}).", disposition); + return true; + } + } + else + { + message = string.Format("KO: Failed to create registry key. Error code: {0}", result); + return false; + } + } +} +"@ + $EventLogName = "Application" + $EventSource = "VolatileRegistryScript" + + if (-not [System.Diagnostics.EventLog]::SourceExists($EventSource)) { + New-EventLog -LogName $EventLogName -Source $EventSource + } + + try { + # Run the C# code to create the volatile registry key + $created = [VolatileRegistry]::CreateVolatileKey($subKey, [ref]$msg) + + Write-Output $msg + + if ($created -and ($msg -match 'OK')) { + Write-EventLog -LogName $EventLogName -Source $EventSource -EventId $ExitCreated -EntryType Information -Message $msg + + # First run since boot, and the message says OK — trigger automation + $host.SetShouldExit($ExitCreated) + exit $ExitCreated + } else { + Write-EventLog -LogName $EventLogName -Source $EventSource -EventId 1002 -EntryType Error -Message "Registry creation failed: $msg" + Write-Error "Failed to create the key" + $host.SetShouldExit($ExitError) + exit $ExitError + } + } catch { + $errorMsg = "An unexpected error occurred: $_" + + Write-EventLog -LogName $EventLogName -Source $EventSource -EventId 1003 -EntryType Error -Message $errorMsg + + Write-Error $errorMsg + $host.SetShouldExit($ExitError) + exit $ExitError + } + +} \ No newline at end of file diff --git a/scripts_staging/Checks/Windows Reliabilty Score.ps1 b/scripts_staging/Checks/Windows Reliabilty Score.ps1 new file mode 100644 index 00000000..d3cd64fe --- /dev/null +++ b/scripts_staging/Checks/Windows Reliabilty Score.ps1 @@ -0,0 +1,65 @@ +<# + .SYNOPSIS + This script gathers the average Windows Reliability Score (WRS) and checks it against a specified threshold. + +.DESCRIPTION + The script retrieves the average Windows Reliability Score from the system and compares it to the specified threshold value. + If the WRS is below the threshold, it outputs a message indicating the system is unreliable and exits with code 1. + If the WRS is above or equal to the threshold, it outputs a message indicating the system reliability is fine and exits with code 0. + If the threshold is set to 0, the script will skip the reliability check and exit with code 5. + If the average WRS cannot be calculated or is not a valid number, the script will also exit with code 5. + +.PARAMETER Unreliable + Specifies the threshold value for the reliability score. If the average WRS is below this value, the script will report the system as unreliable. + +.NOTE + Author: SAN + Date: 01.01.24 + #public + +.EXAMPLE + -Unreliable 5 + +.CHANGELOG + 30/10/2024 SAN Changed output format + +.TODO + Move to env var + +#> + +param ( + [string] $Unreliable = "5" +) + +# Check if $Unreliable is set to 0 +if ($Unreliable -eq "0") { + Write-Output "Skipping reliability check as the threshold is set to 0." + $host.SetShouldExit(5) + exit 5 +} + +# Attempt to retrieve and calculate the average Windows Reliability Score +try { + $wrs = (Get-CimInstance Win32_ReliabilityStabilityMetrics | Measure-Object -Average -Property SystemStabilityIndex).Average + + # Check if the retrieved WRS is a valid number + if (-not $wrs -or $wrs -lt 0) { + Write-Output "Error: Unable to calculate a valid Windows Reliability Score." + $host.SetShouldExit(5) + exit 5 + } + + # Compare WRS with the specified threshold + if ($wrs -lt [double]$Unreliable) { + Write-Output "WRS is unreliable with $wrs it is under $Unreliable." + Exit 1 + } else { + Write-Output "WRS is fine with $wrs it is over $Unreliable." + Exit 0 + } +} catch { + Write-Output "Error: $($_.Exception.Message)" + $host.SetShouldExit(5) + exit 5 +} \ No newline at end of file diff --git a/scripts_staging/Checks/Windows Services.ps1 b/scripts_staging/Checks/Windows Services.ps1 new file mode 100644 index 00000000..83db3abf --- /dev/null +++ b/scripts_staging/Checks/Windows Services.ps1 @@ -0,0 +1,136 @@ +<# +.SYNOPSIS + Checks the status of services with automatic or delayed start and identifies those that are not running. + Excludes services from a predefined ignore list and any additional ones specified. + +.DESCRIPTION + This script evaluates services configured with an automatic or delayed start and identifies those that are not running. + It compares these against a list of ignored services, including any specified via the "IgnoredServices" env variable. + +.EXEMPLE + IgnoredServices=service1,service2,service3 + IgnoredServices=Windows update + enabledebugscript=true + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.TODO + Recheck the list of services for any that should be monitored (e.g., ShellHWDetection). + Add "IgnoredServices" env to "ignoredPatternSuffix" also + + +.CHANGELOG + 28.10.24 SAN Removed ignored output without the debug flag. + 28.10.24 SAN cleanup documentation. + 21.01.25 SAN Code cleanup + 27.03.25 SAN added kerberos local key to default + 31.03.25 SAN Added a new patern for ignroring user services (servicename_XXX) while keeping their system counterpart inculded + 17.07.25 SAN Added InventorySvc it is expected to randomly turn on and off +#> + + +# Define a generic list of service names to be ignored by default +$ignoredByDefault = @( + "Software Protection", + "Remote Registry", + "State Repository Service", + "Service Google Update", + "Clipboard User Service", + "Service Brave Update", + "Google Update Service", + "Windows Modules Installer", # Unsure if this one should be monitored + "Downloaded Maps Manager", + "Windows Biometric Service", + "RemoteRegistry", + "edgeupdate", + "brave", + "gupdate", + "MapsBroker", + "WbioSrvc", + "cbdhsvc", + "GoogleUpdater", + "sppsvc", + "SharePoint Migration Service", + "dbupdate", + "TrustedInstaller", # Frequently failing; unclear if actionable + "MSExchangeNotificationsBroker", + "tiledatamodelsvc", + "BITS", + "CDPSvc", + "AGSService", + "ShellHWDetection", # Frequently failing; unclear if actionable + "DropboxUpdater", + "LocalKDC", # https://learn.microsoft.com/en-us/answers/questions/2136070/windows-server-2025-kerberos-local-key-distributio + "InventorySvc" #https://learn.microsoft.com/en-us/answers/questions/2258983/inventory-and-compatibility-appraisal-service-in-m +) + +# Define a list of services to ignore that match the pattern "nameoftheservice_xxxx" +$ignoredPatternSuffix = @( + "CDPUserSvc", + "OneSyncSvc", + "WpnUserService" +) + +# Get any additional services to ignore from the environment variable +$addonsToIgnoredList = [Environment]::GetEnvironmentVariable('IgnoredServices') +if (-not [string]::IsNullOrEmpty($addonsToIgnoredList)) { + $additionalServices = $addonsToIgnoredList -split ',' + $ignoredByDefault += $additionalServices +} + +# Combine the regular ignored services and the ones with suffix _xxxx pattern +$ignoredPattern = ($ignoredByDefault | ForEach-Object { [regex]::Escape($_) }) -join '|' +$ignoredPatternSuffixRegex = ($ignoredPatternSuffix | ForEach-Object { [regex]::Escape($_) + '_\w+' }) -join '|' + +# Get services with Automatic start type or Automatic (Delayed Start) that are not running +$servicesToCheck = Get-Service | Where-Object { ($_.StartType -eq 'Automatic' -or $_.StartType -eq 'Automatic (Delayed Start)') -and $_.Status -ne 'Running' } + +# Initialize arrays to store services that need attention and services that were stopped but ignored +$servicesNeedingAttention = @() +$ignoredStoppedServices = @() + +# Check the status of each service and categorize based on ignore patterns +foreach ($service in $servicesToCheck) { + # Ignore services that match the defined patterns (both regular and suffixed with _xxxx) + if ($service.DisplayName -notmatch $ignoredPattern -and $service.ServiceName -notmatch $ignoredPattern -and $service.ServiceName -notmatch $ignoredPatternSuffixRegex) { + $servicesNeedingAttention += $service + } else { + $ignoredStoppedServices += $service + } +} + +# Check if debug mode is enabled via the environment variable +$enableDebugValue = [System.Environment]::GetEnvironmentVariable("enabledebugscript") +$debugEnabled = $enableDebugValue -ne $null -and [System.Boolean]::Parse($enableDebugValue) + +# Display debug message if enabled +if ($debugEnabled) { + Write-Host "Debug mode is enabled." +} + +# Display the results based on the service statuses +if ($servicesNeedingAttention.Count -eq 0) { + if (-not $debugEnabled) { + Write-Host "All required services are running." + } + + if ($ignoredStoppedServices.Count -ne 0 -and $debugEnabled) { + Write-Host "The following services were stopped but are ignored:" + foreach ($service in $ignoredStoppedServices) { + Write-Host "$($service.DisplayName) ($($service.ServiceName))" + } + } + + Exit 0 + +} else { + Write-Host "The following services need attention:" + foreach ($service in $servicesNeedingAttention) { + Write-Host "$($service.DisplayName) ($($service.ServiceName))" + } + + Exit 1 +} diff --git a/scripts_staging/Checks/Windows Update Health.ps1 b/scripts_staging/Checks/Windows Update Health.ps1 new file mode 100644 index 00000000..81f986a9 --- /dev/null +++ b/scripts_staging/Checks/Windows Update Health.ps1 @@ -0,0 +1,70 @@ +<# +.SYNOPSIS + This script checks for available Windows updates and alerts if any updates are older than a specified threshold. + +.DESCRIPTION + The script retrieves a list of available updates using the PSWindowsUpdate module. It then checks + whether any updates have a release date older than the specified threshold in days and provides an alert + if such updates are found. + +.NOTES + Author: SAN + Date: 25.03.2025 + #public + Dependencies: + PSWindowsUpdate module + CallPowerShell7 snippet to upgrade the script to pwsh + +.CHANGELOG + 25.03.2025 SAN Initial version of the script to check updates older than a specified threshold. + 28.03.2025 SAN added skip for windows 2012 & pwsh support + 02.04.2025 SAN fix os version check + +.TODO + Add filters to ignore updates in env + +#> + +$osVersion = [System.Environment]::OSVersion.Version + +# Check if the OS version is Windows Server 2012 (6.2) +if ($osVersion.Major -eq 6 -and $osVersion.Minor -eq 2) { + Write-Host "Not supported on Server 2012" + exit 15 +} + + +{{CallPowerShell7}} + +$ThresholdDays = $env:ThresholdDays +if (-not $ThresholdDays) { + $ThresholdDays = 90 +} + +$CurrentDate = Get-Date +$AgeLimit = $CurrentDate.AddDays(-$ThresholdDays) + +try { + $updates = Get-WindowsUpdate -ErrorAction Stop +} catch { + Write-Host "KO: An error occurred while fetching the updates: $_" + exit 1 +} + +if ($updates.Count -eq 0) { + Write-Host "OK: No updates found." +} else { + $updates | ForEach-Object { + Write-Host "$($_.LastDeploymentChangeTime) | KB: $($_.KBArticleIDs) | $($_.Title)" + } + + $OldUpdates = $updates | Where-Object { $_.LastDeploymentChangeTime -lt $AgeLimit } + + if ($OldUpdates) { + Write-Host "KO: The following updates are older than $ThresholdDays days:" + $OldUpdates | Select-Object Title, KBArticleIDs, LastDeploymentChangeTime | Format-Table -AutoSize + exit 1 + } else { + Write-Host "OK: All available updates are within the last $ThresholdDays days." + } +} diff --git a/scripts_staging/Checks/is RDP port ok.ps1 b/scripts_staging/Checks/is RDP port ok.ps1 new file mode 100644 index 00000000..85ac3819 --- /dev/null +++ b/scripts_staging/Checks/is RDP port ok.ps1 @@ -0,0 +1,44 @@ +<# +.SYNOPSIS + Checks if TCP port 3389 (RDP) is open on the local machine. + +.DESCRIPTION + This script attempts to determine if TCP port 3389 is open using the `Test-NetConnection` cmdlet. + If `Test-NetConnection` is not available, it falls back to using the `System.Net.Sockets.TcpClient` class to perform the check. + The script will output whether the port is open or not and exit with a status code of 1 if the port is closed. + +.NOTES + Author: SAN + Date: 26.09.2024 + #public + +.CHANGELOG + 12.12.24 SAN Changed outputs + 20.12.24 SAN Changed outputs + 02.04.25 SAN Fixed Warn output +#> + +$port = 3389 +$address = "localhost" + +# Try Test-NetConnection if available +if (Get-Command Test-NetConnection -ErrorAction SilentlyContinue) { + $tcpConnection = Test-NetConnection -ComputerName 127.0.0.1 -Port $port 2>$null -WarningAction SilentlyContinue + if ($tcpConnection.TcpTestSucceeded) { + Write-Output "OK: RDP is open." + } else { + Write-Output "KO: Port $port is not open RDP will not work." + exit 1 + } +} else { + # Fallback using TcpClient + try { + $tcpClient = New-Object System.Net.Sockets.TcpClient + $tcpClient.Connect($address, $port) + Write-Output "OK: RDP is open but TNC does not work." + $tcpClient.Close() + } catch { + Write-Output "KO: Port $port is not open and TNC does not work." + exit 1 + } +} \ No newline at end of file diff --git a/scripts_staging/Checks/is Remote TCP port open.ps1 b/scripts_staging/Checks/is Remote TCP port open.ps1 new file mode 100644 index 00000000..a218c420 --- /dev/null +++ b/scripts_staging/Checks/is Remote TCP port open.ps1 @@ -0,0 +1,59 @@ +<# +.SYNOPSIS + Checks if a TCP port is open on a remote machine based on the environment variables "TCP_HOST" and "TCP_PORT". + +.DESCRIPTION + This script checks if a TCP port on a remote host is open using `Test-NetConnection`. + If unavailable, it falls back to `System.Net.Sockets.TcpClient`. + + If the port is closed or invalid, the script exits with status 1. + +.EXAMPLE + TCP_HOST=example.com + TCP_PORT=443 + +.NOTES + Author: SAN + Date: 07.02.2025 + #public +#> + +# Get environment variables +$hostName = [System.Environment]::GetEnvironmentVariable("TCP_HOST") +$portStr = [System.Environment]::GetEnvironmentVariable("TCP_PORT") + +# Validate inputs +if (-not $hostName) { + Write-Output "Error: Environment variable 'TCP_HOST' is not set." + exit 1 +} + +$port = 0 +if (-not $portStr -or -not [int]::TryParse($portStr, [ref]$port) -or $port -lt 1) { + Write-Output "Error: Environment variable 'TCP_PORT' is not set or invalid." + exit 1 +} + +# Use Test-NetConnection if available +if (Get-Command Test-NetConnection -ErrorAction SilentlyContinue) { + $tcpConnection = Test-NetConnection -ComputerName $hostName -Port $port + if ($tcpConnection.TcpTestSucceeded) { + Write-Output "OK: Port $port on $hostName is open." + exit 0 + } else { + Write-Output "KO: Port $port on $hostName is not open." + exit 1 + } +} else { + # Fallback to TcpClient if Test-NetConnection is unavailable + try { + $tcpClient = New-Object System.Net.Sockets.TcpClient + $tcpClient.Connect($hostName, $port) + Write-Output "OK: Port $port on $hostName is open." + $tcpClient.Close() + exit 0 + } catch { + Write-Output "KO: Port $port on $hostName is not open." + exit 1 + } +} diff --git a/scripts_staging/Checks/is TCP port open.ps1 b/scripts_staging/Checks/is TCP port open.ps1 new file mode 100644 index 00000000..b46b3e31 --- /dev/null +++ b/scripts_staging/Checks/is TCP port open.ps1 @@ -0,0 +1,87 @@ +<# +.SYNOPSIS + Checks if a TCP port is open on the local machine based on the environment variable "TCP_PORT". + +.DESCRIPTION + This script checks if the TCP port defined by the environment variable "TCP_PORT" is open using the `Test-NetConnection` cmdlet. + If `Test-NetConnection` is not available, it falls back to using the `System.Net.Sockets.TcpClient` class to perform the check. + Additionally, it will display the executable and process information that is holding the port open. + If the application is linked to a service, the service name and status will be displayed. + The script will exit with a status code of 1 if the port is closed or if the environment variable is not set. + +.EXEMPLE + TCP_PORT=3435 + +.NOTES + Author: SAN + Date: 01.10.2024 + #public + +.CHANGELOG + +#> + +$portStr = [System.Environment]::GetEnvironmentVariable("TCP_PORT") + +# Initialize the port variable +$port = 0 + +# Check if the environment variable is set and valid +if (-not $portStr -or -not [int]::TryParse($portStr, [ref]$port) -or $port -lt 1) { + Write-Output "Error: Environment variable 'TCP_PORT' is not set or is invalid." + exit 1 +} + +$address = "localhost" + +Write-Output "Checking connectivity to $address on port $port..." + +# Try Test-NetConnection if available +if (Get-Command Test-NetConnection -ErrorAction SilentlyContinue) { + $tcpConnection = Test-NetConnection -ComputerName $address -Port $port + if ($tcpConnection.TcpTestSucceeded) { + Write-Output "Success: Port $port on $address is open." + } else { + Write-Output "Failure: Port $port on $address is not open." + Write-Output "Details: TCP connection test failed." + exit 1 + } +} else { + # Fallback using TcpClient + try { + $tcpClient = New-Object System.Net.Sockets.TcpClient + $tcpClient.Connect($address, $port) + Write-Output "Success: Port $port on $address is open." + $tcpClient.Close() + } catch { + Write-Output "Failure: Port $port on $address is not open." + Write-Output "Details: TCP connection test threw an exception." + exit 1 + } +} + +# Find the process holding the port open for incoming connections only +$netstatOutput = netstat -ano | Select-String ":$port\s" | ForEach-Object { $_.Line } | Where-Object { $_ -match 'LISTENING' -and $_ -match '0.0.0.0|127.0.0.1' } +if ($netstatOutput) { + $portPID = $netstatOutput -replace '^.*\s+(\d+)$', '$1' + $process = Get-Process -Id $portPID -ErrorAction SilentlyContinue + + if ($process) { + Write-Output "The port $port is being used by the process '$($process.ProcessName)' (PID: $portPID)." + Write-Output "Executable Path: $($process.Path)" + + # Check if the process is linked to a service + $service = Get-WmiObject Win32_Service | Where-Object { $_.ProcessId -eq $portPID } + if ($service) { + Write-Output "This process is linked to the service: '$($service.Name)'" + Write-Output "Service Display Name: $($service.DisplayName)" + Write-Output "Service Status: $($service.State)" + } else { + Write-Output "This process is not linked to any service." + } + } else { + Write-Output "Unable to retrieve the process details for PID $portPID." + } +} else { + Write-Output "No process is currently using port $port for incoming connections." +} \ No newline at end of file diff --git a/scripts_staging/Checks/is process running.ps1 b/scripts_staging/Checks/is process running.ps1 new file mode 100644 index 00000000..a8e4eea8 --- /dev/null +++ b/scripts_staging/Checks/is process running.ps1 @@ -0,0 +1,53 @@ +<# +.SYNOPSIS + Checks whether a list of processes are running. + +.DESCRIPTION + This script retrieves a list of process names from the environment variable 'checkprocesslist' and checks whether + each process is running on the local system. If any process from the list is not running, the script will output the + process name and exit with a status of 1. If all processes are running, it outputs a success message. + This script assumes that process names provided do not include file extensions (e.g., ".exe"). + +.EXEMPLE + checkprocesslist=Explorer,explorer2 + +.NOTES + Author: SAN + Date: 26.09.24 + #public + +#> + +# Get the list of processes from the environment variable "checkprocesslist" +$processList = $env:checkprocesslist + +# Ensure the environment variable is not empty +if (-not $processList) { + Write-Output "Environment variable 'checkprocesslist' is empty or not set." + exit 1 +} + +# Split the process list (assuming comma-separated values) +$processes = $processList -split ',' + +# Initialize a flag to track if any process is not running +$allProcessesRunning = $true + +# Check each process and output its status +foreach ($process in $processes) { + $processName = $process.Trim() + + if (Get-Process -Name $processName -ErrorAction SilentlyContinue) { + Write-Output "$processName is running." + } else { + Write-Output "$processName is NOT running." + $allProcessesRunning = $false + } +} + +# Exit with status 1 if any process is not running +if (-not $allProcessesRunning) { + exit 1 +} + +Write-Output "All processes are running." \ No newline at end of file diff --git a/scripts_staging/Collectors/Collect Licensing 1 General.ps1 b/scripts_staging/Collectors/Collect Licensing 1 General.ps1 new file mode 100644 index 00000000..88e4c351 --- /dev/null +++ b/scripts_staging/Collectors/Collect Licensing 1 General.ps1 @@ -0,0 +1,54 @@ +<# +.SYNOPSIS + Gathers system information for licensing reporting to Microsoft. + +.DESCRIPTION + This script collects and displays key system details required for licensing reports, + such as OS version, build number, edition, workgroup or domain nameand the number of CPU sockets. + It utilizes PowerShell cmdlets like `Get-CimInstance` and `Get-WmiObject` to retrieve system data. + +.NOTES + Author: SAN + Date: YYYY-MM-DD + #public + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.CHANGELOG + +.TODO + Optimize the calculation of CPU sockets for clarity and accuracy. + +#> + + +function Get-WindowsVersion { + $osInfo = Get-CimInstance Win32_OperatingSystem + $osVersion = $osInfo.Version + $osBuild = $osInfo.BuildNumber + $osEdition = $osInfo.Caption + + $hostname = $env:COMPUTERNAME + $workgroup = (Get-WmiObject Win32_ComputerSystem).Domain + $localIP = (Test-Connection -ComputerName $hostname -Count 1).IPV4Address.IPAddressToString + + $CPU = Get-WmiObject -Class Win32_Processor + $CPUs = 0 + $Sockets = 0 + + foreach ($Processor in $CPU) { + $CPUs++ + $Sockets += $Processor.NumberOfLogicalProcessors / $Processor.NumberOfCores + } + + #Write-Host "Hostname: $hostname" + Write-Host "OS: $osEdition" + Write-Host "Workgroup/Domain: $workgroup" + #Write-Host "Local IP Address: $localIP" + Write-Host "Sockets: $Sockets" +} + +Get-WindowsVersion \ No newline at end of file diff --git a/scripts_staging/Collectors/Collect Licensing 2 SQL.ps1 b/scripts_staging/Collectors/Collect Licensing 2 SQL.ps1 new file mode 100644 index 00000000..327c45e4 --- /dev/null +++ b/scripts_staging/Collectors/Collect Licensing 2 SQL.ps1 @@ -0,0 +1,92 @@ +<# +.SYNOPSIS + Collects Microsoft SQL Server instance details for licensing and capacity reporting. + +.DESCRIPTION + This script identifies running Microsoft SQL Server instances on the local machine, + retrieves their edition, and provides detailed hardware and configuration information. + It includes data such as the number of CPUs, cores, and SQL Server capacity limits based on the edition. + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.CHANGELOG + +.TODO + +#> + + +function Get-MSSQLVersion { + $SQLInstances = Get-Service -Name MSSQL* | Where-Object { $_.Status -eq "Running" -and $_.Name -notlike 'MSSQLFDLauncher*' -and $_.Name -notlike 'MSSQLLaunchpad*' -and $_.Name -notlike '*WID*' -and $_.Name -notlike 'MSSQLServerOLAPService*' } | Select-Object -Property @{label='InstanceName';expression={$_.Name -replace '^.*\$'}} + + if ($SQLInstances.Count -eq 0) { + Write-Host "MS SQL not found" + return + } + + foreach ($SQLInstance in $SQLInstances.InstanceName) { + $ServerName = $env:COMPUTERNAME + + # Get Default SQL Server instance's Edition + if ($SQLInstance -like 'MSSQLSERVER') { + $SQLName = $ServerName + } else { + $SQLName = "$ServerName\$SQLInstance" + } + + $sqlconn = New-Object System.Data.SqlClient.SqlConnection("server=$SQLName;Trusted_Connection=true") + $query = "SELECT SERVERPROPERTY('Edition') AS Edition, SERVERPROPERTY('MachineName') AS MachineName, SERVERPROPERTY('IsClustered') AS [Clustered];" + + $sqlconn.Open() + $sqlcmd = New-Object System.Data.SqlClient.SqlCommand ($query, $sqlconn) + $sqlcmd.CommandTimeout = 0 + $dr = $sqlcmd.ExecuteReader() + + while ($dr.Read()) { + $SQLEdition = $dr.GetValue(0) + $MachineName = $dr.GetValue(1) + $IsClustered = $dr.GetValue(2) + } + + $dr.Close() + $sqlconn.Close() + + # Get processors information + $CPU = Get-WmiObject -Class Win32_Processor + + # Get Computer model information + $OS_Info = Get-WmiObject -Class Win32_ComputerSystem + + # Reset number of cores and use count for the CPUs counting + $CPUs = 0 + $Cores = 0 + + foreach ($Processor in $CPU) { + $CPUs++ + # Count the total number of cores + $Cores += $Processor.NumberOfCores + } + + Write-Host "ServerName: $ServerName" + Write-Host "Model: $($OS_Info.Model)" + Write-Host "InstanceName: $SQLInstance" + Write-Host "DataSource: $($sqlconn.DataSource)" + Write-Host "Edition: $SQLEdition" + Write-Host "SocketNumber: $CPUs" + Write-Host "TotalCores: $Cores" + $CoresPerSocketCPUsRatio = $Cores / $CPUs + Write-Host "Cores per Socket/CPUs Ratio: $CoresPerSocketCPUsRatio" + $ResumeCapacityLimits = + if ($SQLEdition -like "Developer*") { "Max SQL Server compute capacity: OS max" } + elseif ($SQLEdition -like "Express*") { "Max SQL Server compute capacity: 1 sockets or 4 cores" } + elseif ($SQLEdition -like "Standard*") { "Max SQL Server compute capacity: Lesser of 4 sockets or 24 cores" } + elseif ($SQLEdition -like "Web*") { "Max SQL Server compute capacity Lesser of 4 sockets or 16 cores" } + else { "SQL edition not detected" } + Write-Host "ResumeCapacityLimits: $ResumeCapacityLimits" + } +} + +Get-MSSQLVersion \ No newline at end of file diff --git a/scripts_staging/Collectors/Collect Licensing 3 Exchange.ps1 b/scripts_staging/Collectors/Collect Licensing 3 Exchange.ps1 new file mode 100644 index 00000000..81d9dddd --- /dev/null +++ b/scripts_staging/Collectors/Collect Licensing 3 Exchange.ps1 @@ -0,0 +1,38 @@ +<# +.SYNOPSIS + Retrieves the number of Exchange mailboxes for licensing compliance reporting. + +.DESCRIPTION + This script uses the Exchange Management Shell to determine the number of mailboxes + associated with a specific Exchange Server CAL (Client Access License), + such as the "Exchange Server 2016 Standard CAL." It ensures the Exchange snap-in is loaded and + captures the mailbox count for licensing purposes. + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.TODO + Extend support to handle multiple CAL types dynamically. + +#> + +function Get-ExchangeMailboxCount { + # Launch the Exchange Management Shell + Add-PSSnapin Microsoft.Exchange.Management.PowerShell.SnapIn -ErrorAction SilentlyContinue + + # Check if the Exchange snap-in is available + if (Get-PSSnapin -Registered | Where-Object { $_.Name -eq 'Microsoft.Exchange.Management.PowerShell.SnapIn' }) { + try { + # Run the command directly in the Exchange Management Shell and capture the count + $mailboxCount = (Get-ExchangeServerAccessLicenseUser -LicenseName "Exchange Server 2016 Standard CAL" | Measure-Object).Count + "Number of Exchange Mailboxes: $mailboxCount" + } catch { + "Error running command: $_" + } + } else { + "" + } +} +Get-ExchangeMailboxCount \ No newline at end of file diff --git a/scripts_staging/Collectors/Collect Licensing 4 RDS.ps1 b/scripts_staging/Collectors/Collect Licensing 4 RDS.ps1 new file mode 100644 index 00000000..d5fe61b8 --- /dev/null +++ b/scripts_staging/Collectors/Collect Licensing 4 RDS.ps1 @@ -0,0 +1,35 @@ +<# +.SYNOPSIS + Checks if the Remote Desktop Services role is installed and retrieves RDS license key pack details. + +.DESCRIPTION + This script verifies whether the Remote Desktop Services role is installed on the local machine. + If installed, it retrieves information about RDS license key packs, including details such as product version, + license type, total licenses, available licenses, and issued licenses. + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.TODO + Extend reporting to include CAL types and expiration details. +#> + + +try { + # Check if the Remote Desktop Services role is installed + $rdsRoleInstalled = Get-Service -Name TermServLicensing -ErrorAction Stop + # If the service is not installed, display a message and return + if ($rdsRoleInstalled -eq $null -or $rdsRoleInstalled.Installed -eq $false) { + #"TermServLicensing service is not installed." + return + } + # Get information about RDS license key packs + Get-WmiObject Win32_TSLicenseKeyPack | + Where-Object { $_.ProductVersion -like "*Windows Server*" } | + Select-Object PSComputerName, KeyPackId, ProductVersion, TypeAndModel, TotalLicenses, AvailableLicenses, IssuedLicenses +} catch { + # If an error occurs, display the error message + #"Error: $_" +} \ No newline at end of file diff --git a/scripts_staging/Collectors/Collect Licensing 5 Office.ps1 b/scripts_staging/Collectors/Collect Licensing 5 Office.ps1 new file mode 100644 index 00000000..f86cd0c5 --- /dev/null +++ b/scripts_staging/Collectors/Collect Licensing 5 Office.ps1 @@ -0,0 +1,19 @@ +<# +.SYNOPSIS + Retrieves licensing information for installed Microsoft Office products. + +.DESCRIPTION + This script uses the `Get-CimInstance` cmdlet to query the `SoftwareLicensingProduct` class for + details about installed Microsoft Office products with active licenses. + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.CHANGELOG + + +#> + +Get-CimInstance -ClassName SoftwareLicensingProduct | where {$_.name -like "*office*" -and $_.LicenseStatus -gt 0 }| select Name,description,LicenseStatus,ProductKeyChannel,PartialProductKey \ No newline at end of file diff --git a/scripts_staging/Collectors/OS Install Date.ps1 b/scripts_staging/Collectors/OS Install Date.ps1 new file mode 100644 index 00000000..309efa17 --- /dev/null +++ b/scripts_staging/Collectors/OS Install Date.ps1 @@ -0,0 +1,23 @@ +<# +.SYNOPSIS + Retrieves and formats the installation date of the operating system. + +.DESCRIPTION + This script fetches the installation date of the current Windows operating system and + formats it into a "dd/MM/yyyy" format, then outputs the formatted date to the console. + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.CHANGELOG + + +#> + + +$osInfo = Get-WmiObject Win32_OperatingSystem +$installDate = $osInfo.ConvertToDateTime($osInfo.InstallDate) +$formattedDate = $installDate.ToString("dd/MM/yyyy") +Write-Host "$formattedDate" \ No newline at end of file diff --git a/scripts_staging/Collectors/Retrieve all IIS bindings.ps1 b/scripts_staging/Collectors/Retrieve all IIS bindings.ps1 new file mode 100644 index 00000000..ea3be3bd --- /dev/null +++ b/scripts_staging/Collectors/Retrieve all IIS bindings.ps1 @@ -0,0 +1,64 @@ +<# +.SYNOPSIS + This script retrieves IIS bindings, extracts and sorts unique domain names from the bindings. + +.DESCRIPTION + The script imports the WebAdministration module, retrieves all IIS bindings, + and extracts unique domain names from the binding information. + The script excludes wildcard bindings and invalid domain names. + It then outputs the sorted list of unique domain names, with optional debugging information if the debug flag is set. + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.CHANGELOG + + +.TODO + set debug flag in env + more gracefully handle execution on non-iis devices +#> + + +# Set the debug flag +$debug = 0 + +# Import the WebAdministration module +Import-Module WebAdministration + +# Retrieve all IIS bindings +$bindings = Get-WebBinding + +# Output the initial bindings for debugging +if ($debug -eq 1) { + Write-Output "Initial Bindings:" + $bindings | ForEach-Object { Write-Output $_ } +} + +# Create a hash table to store unique domain names +$uniqueDomains = @{} + +# Process each binding +foreach ($binding in $bindings) { + $bindingInformation = $binding.bindingInformation + $hostname = $bindingInformation -replace ".*:(.*?)(:\d+)?$", '$1' # Extract only the domain part + + # Only add if the hostname is not empty, not a wildcard, and is a valid domain name + if ($hostname -ne "" -and $hostname -ne "*" -and $hostname -match '^(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$') { + $uniqueDomains[$hostname] = $null + } +} + +# Output the unique domain names for debugging +if ($debug -eq 1) { + Write-Output "Unique Domain Names:" + $uniqueDomains.Keys | ForEach-Object { Write-Output $_ } +} + +# Sort unique domain names alphabetically +$sortedDomains = $uniqueDomains.Keys | Sort-Object + +# Output the sorted unique domain names +$sortedDomains | ForEach-Object { Write-Output $_ } \ No newline at end of file diff --git a/scripts_staging/Collectors/SU update list.ps1 b/scripts_staging/Collectors/SU update list.ps1 new file mode 100644 index 00000000..a433e311 --- /dev/null +++ b/scripts_staging/Collectors/SU update list.ps1 @@ -0,0 +1,48 @@ +<# +.SYNOPSIS + Checks for outdated Chocolatey packages and lists them. + +.DESCRIPTION + This script verifies that Chocolatey is installed on the system. + If installed, it retrieves and displays a list of packages that have available updates. + If no packages are outdated, it confirms that all packages are up to date. + If Chocolatey is not installed or an error occurs, it exits with an error message. + + +.NOTES + Author: SAN + Date: 01.01.2024 + #public + +.CHANGELOG + 19.10.25 SAN Code cleanup and add output if up-to-date + +#> + +# Check if Chocolatey is installed +if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { + Write-Host "Chocolatey is not installed. Please install Chocolatey to use this script." + exit 1 +} + +# Get a list of upgradable packages +$upgradablePackages = choco outdated 2>$null + +# Check the output and display results +if ($upgradablePackages -match "Chocolatey has determined 0 package") { + Write-Host "All up-to-date" + exit 0 +} +elseif ($upgradablePackages) { + Write-Host "Upgradable packages:`n" + $upgradablePackages -split "`r?`n" | Select-Object -Skip 1 | ForEach-Object { + if ($_ -and ($_ -notmatch "Chocolatey has determined")) { + Write-Host $_ + } + } + exit 0 +} +else { + Write-Host "Error: Unable to determine upgradable packages." + exit 1 +} diff --git a/scripts_staging/Collectors/get Domains or Workgroup name.ps1 b/scripts_staging/Collectors/get Domains or Workgroup name.ps1 new file mode 100644 index 00000000..80a8bc4d --- /dev/null +++ b/scripts_staging/Collectors/get Domains or Workgroup name.ps1 @@ -0,0 +1,28 @@ +<# +.SYNOPSIS + Retrieves and displays the domain or workgroup name of the computer. + +.DESCRIPTION + This script checks if the computer is part of a domain or a workgroup. + If the computer is part of a domain, it outputs the domain name. + Otherwise, it outputs the workgroup name. + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.CHANGELOG + + +#> + +# Check if the computer is a member of a domain or workgroup +$computerInfo = Get-WmiObject Win32_ComputerSystem + +if ($computerInfo.PartOfDomain -eq $true) { + Write-Host "D: $($computerInfo.Domain)" +} else { + $workgroupName = $computerInfo.Workgroup + Write-Host "W: $workgroupName" +} \ No newline at end of file diff --git a/scripts_staging/Fixes/Bluescreen report.ps1 b/scripts_staging/Fixes/Bluescreen report.ps1 new file mode 100644 index 00000000..8d2fcbab --- /dev/null +++ b/scripts_staging/Fixes/Bluescreen report.ps1 @@ -0,0 +1,140 @@ +<# +.SYNOPSIS + This script automates the process of installing Bluescreen Viewer, running it to generate a crash log, and uploading Minidump files to a Nextcloud WebDAV server. + +.DESCRIPTION + The script installs Bluescreen Viewer using Chocolatey, runs it to generate a crash log, and displays the log in the terminal. + It then checks the local Minidump folder for dump files, uploads them to a specified Nextcloud WebDAV URL, and renames them with a "_sent" suffix after a successful upload. + +.EXEMPLE + NEXTCLOUD_WEBDAV_URL=https://nextcloud.XYZ.AB/public.php/webdav/ + NEXTCLOUD_TOKEN=SHARETOKEN + SITE_NAME={{site.name}} + CLIENT_NAME={{client.name}} + +.NOTES + Author: SAN + Date: 02.12.24 + Dependencies: Chocolatey, Nextcloud public share + #PUBLIC + +.CHANGELOG + 18.12.24 SAN Added site & client name to the uploaded file, added boot time to the report, moved dmp check + 08.01.25 SAN Remove error code in case of missing folder it is causing issues in case of false positive on failure runs + +#> +# Step 1: Retrieve Nextcloud WebDAV URL, Token, Client Name, and Site Name from environment variables +$nextcloudWebdavUrl = [System.Environment]::GetEnvironmentVariable("NEXTCLOUD_WEBDAV_URL") +$webdavUser = [System.Environment]::GetEnvironmentVariable("NEXTCLOUD_TOKEN") +$clientName = [System.Environment]::GetEnvironmentVariable("CLIENT_NAME") +$siteName = [System.Environment]::GetEnvironmentVariable("SITE_NAME") + +# Exit the script if the Nextcloud WebDAV URL or token is not provided +if (-not $nextcloudWebdavUrl -or -not $webdavUser) { + Write-Host "Error: Nextcloud WebDAV URL or token is not provided in environment variables." + exit 1 +} + +# Ensure WebDAV URL ends with a slash +if (-not $nextcloudWebdavUrl.EndsWith("/")) { + $nextcloudWebdavUrl += "/" +} + +# Variables (defined at the top for easy configuration) +$minidumpPath = "C:\Windows\Minidump" +$hostname = (Get-WmiObject -Class Win32_ComputerSystem).Name + +# Check if the Minidump directory exists and contains any .dmp +if (-not (Test-Path $minidumpPath)) { + Write-Error "Minidump folder not found!" + exit +} elseif (-not (Get-ChildItem -Path $minidumpPath -Filter "*.dmp")) { + Write-Error "No dump files found in Minidump folder!" + exit +} + +# Sanitize Client Name and Site Name to keep only a-z, 0-9, and spaces, then replace spaces with dashes +$sanitizePattern = "[^a-zA-Z0-9 ]" +$clientName = ($clientName -replace $sanitizePattern, "").Replace(" ", "-") +$siteName = ($siteName -replace $sanitizePattern, "").Replace(" ", "-") +$hostname = $hostname.Replace(" ", "-") # Ensure hostname has no spaces + +# Bluescreen Viewer Installation Variables +$bluescreenViewerPath = "C:\Program Files (x86)\NirSoft\BlueScreenView\BlueScreenView.exe" +$bluescreenLogFile = "$env:temp\bluescreen_log.txt" # Path to save Bluescreen log file + +# Force TLS 1.2 for secure connection when uploading to WebDAV +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 + +# Step 2: Install Bluescreen Viewer using Chocolatey (silent installation) +Write-Host "Installing Bluescreen Viewer..." +choco install bluescreenview -y --no-progress | Out-Null +if ($LASTEXITCODE -ne 0) { + Write-Host "Installation failed. Continuing with script." +} + +# Step 3: Run Bluescreen Viewer to generate the crash log and save it to a text file +Write-Host "Running Bluescreen Viewer to generate crash log..." +if (Test-Path $bluescreenViewerPath) { + Start-Process $bluescreenViewerPath -ArgumentList "/stext $bluescreenLogFile" -Wait + if ($LASTEXITCODE -ne 0) { + Write-Host "Failed to run Bluescreen Viewer. Continuing with script." + } +} else { + Write-Host "Bluescreen Viewer executable not found. Skipping crash log generation." +} + +# Step 4: Output the content of the crash log file to the terminal +if (Test-Path $bluescreenLogFile) { + $bootTime = (Get-CimInstance Win32_OperatingSystem).LastBootUpTime | Get-Date -Format 'dd-MM-yyyy HH:mm:ss' + Write-Host "Last 3 boot event:" + try { + $bootEvents = Get-WinEvent -LogName System -FilterXPath "*[System[EventID=6005]]" -ErrorAction Stop + $bootEvents | Select-Object -ExpandProperty TimeCreated | Sort-Object -Descending | Select-Object -First 3 + } catch { + Write-Host "An error occurred: $_" + } + Write-Host "Displaying crash logs..." + Get-Content $bluescreenLogFile +} else { + Write-Host "The crash log file does not exist." +} + +# Step 5: Get all files in the Minidump directory, excluding those already marked as "_sent" +$files = Get-ChildItem -Path $minidumpPath -Filter "*.dmp" | Where-Object { $_.Name -notlike "*_sent*" } + +# Step 6: Loop through each Minidump file and upload to Nextcloud WebDAV +foreach ($file in $files) { + $newFileName = "$clientName`_$siteName`_$hostname`_$($file.Name)" + $uploadUrl = $nextcloudWebdavUrl + $newFileName + + # Step 7: Prepare the authorization header for WebDAV (no password) + $headers = @{ + "Authorization" = "Basic $([Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${webdavUser}:")))" + "X-Requested-With" = "XMLHttpRequest" + } + + # Step 8: Upload the file to Nextcloud WebDAV + try { + Write-Host "Uploading $($file.Name) to $uploadUrl..." + $response = Invoke-WebRequest -Uri $uploadUrl -Method Put -InFile $file.FullName -Headers $headers -UseBasicParsing + if ($response.StatusCode -eq 201 -or $response.StatusCode -eq 204) { + Write-Host "Successfully uploaded $newFileName" + + # Step 9: Rename the file by appending "_sent" after successful upload + $fileBaseName = [System.IO.Path]::GetFileNameWithoutExtension($file.Name) + $fileExtension = [System.IO.Path]::GetExtension($file.Name) + $newSentName = "$fileBaseName`_sent$fileExtension" + + # Step 10: Rename the file to indicate it has been successfully sent + Rename-Item -Path $file.FullName -NewName $newSentName + Write-Host "Renamed $($file.Name) to $newSentName" + } else { + Write-Host "Unexpected response from server: $($response.StatusCode)" + exit 1 + } + } catch { + Write-Host "Failed to upload $($file.Name): $_" + exit 1 + } +} diff --git a/scripts_staging/Fixes/Ensure all services with startup type Automatic are running.ps1 b/scripts_staging/Fixes/Ensure all services with startup type Automatic are running.ps1 new file mode 100644 index 00000000..2b2920ad --- /dev/null +++ b/scripts_staging/Fixes/Ensure all services with startup type Automatic are running.ps1 @@ -0,0 +1,56 @@ +<# +.SYNOPSIS + This script retrieves Windows services that are set to start automatically (including delayed start) but are not currently running, + and optionally starts those services based on the value of an environment variable. + +.DESCRIPTION + The script checks for the environment variable "START_SERVICES". If this variable is set to "true", the script will attempt to start + all services that are configured to start automatically (including delayed start) but are not currently running. It displays the + list of such services in a formatted table for the user. If the environment variable is not set to "true", the script will only + display the list of services without starting them. + +.PARAMETER None + "START_SERVICES" environment variable to determine whether to start the services. + +.EXEMPLE + START_SERVICES=true + +.NOTES + Author:Dave Long + Date: 2021-05-12 + #public + +.Changelog + 02.12.24 SAN Full code refactorisation + +#> + +# Check for an environment variable (e.g., "START_SERVICES") to determine if services should be started +$StartServices = $false +if ($env:START_SERVICES -eq "true") { + $StartServices = $true + Write-Output "Start Services enabled" +} + +# Retrieve services that are set to start automatically (including delayed) but are not currently running +$servicesToStart = Get-Service | Where-Object { + $_.StartType -in @("Automatic", "AutomaticDelayedStart") -and + $_.Status -ne "Running" +} + +# Display the services in a formatted table +$servicesToStart | Format-Table -AutoSize + +# Start the services if the environment variable is set to true +if ($StartServices) { + foreach ($service in $servicesToStart) { + try { + Start-Service -Name $service.Name -ErrorAction Stop + Write-Output "Started service: $($service.Name)" + } catch { + Write-Warning "Failed to start service: $($service.Name). Error: $_" + } + } +} else { + Write-Output "Services will not be started. Set environment variable 'START_SERVICES' to 'true' to enable this." +} diff --git a/scripts_staging/Fixes/Fix broken ESET installation.ps1 b/scripts_staging/Fixes/Fix broken ESET installation.ps1 new file mode 100644 index 00000000..fc98a687 --- /dev/null +++ b/scripts_staging/Fixes/Fix broken ESET installation.ps1 @@ -0,0 +1,81 @@ +<# +.SYNOPSIS + This PowerShell script is designed to fix a broken installation of ESET Security that generaly happens after an update. + +.DESCRIPTION + The script performs the following actions: + 1. Deletes the ESET Security directory in Program Files. + 2. Deletes the ESET Security directory in ProgramData. + 3. Stops and deletes the ekrn service. + 4. Deletes the registry key for the ekrn service. + +.EXEMPLE + force_execution=true + +.NOTES + Author: SAN + Date: 19.08.24 + #public + +.CHANGELOG + + +#> + +# Check if the environment variable 'force_execution' is set to 'true' +if ($env:force_execution -eq 'true') { + Write-Host "Force execution is enabled. Skipping file existence check." +} else { + # Only check if the file exists if force_execution is not enabled + if (Test-Path "C:\Program Files\ESET\ESET Security\ermm.exe") { + Write-Host "Error: The file 'ermm.exe' exists at the specified path. This script may not work as expected." + exit 0 + } else { + Write-Host "The file 'ermm.exe' does not exist. The script can proceed." + } +} + + +Write-Host "Fixing broken installation of ESET Security..." + +# Define paths and service name +$ESET_PROG_FILES_DIR = "C:\Program Files\ESET\ESET Security" +$ESET_PROG_DATA_DIR = "C:\ProgramData\ESET\ESET Security" +$serviceName = "ekrn" +$registryPath = "HKLM:\SYSTEM\CurrentControlSet\Services\ekrn" + +# Delete the ESET Security directory in Program Files +if (Test-Path $ESET_PROG_FILES_DIR) { + Write-Host "Deleting directory: $ESET_PROG_FILES_DIR" + Remove-Item -Recurse -Force $ESET_PROG_FILES_DIR +} else { + Write-Host "Directory not found: $ESET_PROG_FILES_DIR" +} + +# Delete the ESET Security directory in ProgramData +if (Test-Path $ESET_PROG_DATA_DIR) { + Write-Host "Deleting directory: $ESET_PROG_DATA_DIR" + Remove-Item -Recurse -Force $ESET_PROG_DATA_DIR +} else { + Write-Host "Directory not found: $ESET_PROG_DATA_DIR" +} + +# Stop and delete the ekrn service +if (Get-Service -Name $serviceName -ErrorAction SilentlyContinue) { + Write-Host "Stopping service: $serviceName" + Stop-Service -Name $serviceName -Force + Write-Host "Deleting service: $serviceName" + Remove-Service -Name $serviceName -Force +} else { + Write-Host "Service not found: $serviceName" +} + +# Delete the registry key +if (Test-Path $registryPath) { + Write-Host "Deleting registry key: $registryPath" + Remove-Item -Path $registryPath -Recurse -Force +} else { + Write-Host "Registry key not found: $registryPath" +} + +Write-Host "Operation completed." \ No newline at end of file diff --git a/scripts_staging/Fixes/RDS Fix taskbar.ps1 b/scripts_staging/Fixes/RDS Fix taskbar.ps1 new file mode 100644 index 00000000..1c416f69 --- /dev/null +++ b/scripts_staging/Fixes/RDS Fix taskbar.ps1 @@ -0,0 +1,39 @@ +<# +.SYNOPSIS + Fixes taskbar issues on RDS servers by resetting and reconfiguring firewall rules in the Windows Registry. + +.DESCRIPTION + This script addresses taskbar issues on Remote Desktop Services (RDS) servers. + It removes and recreates specific firewall-related registry keys and sets the `DeleteUserAppContainersOnLogoff` configuration. + A manual reboot is required after running the script. + +.NOTES + Author: SAN + Date: ??? + #public + +.CHANGELOG + +.TODO + Implement a reboot flag + +#> + + +# Remove existing AppIso FirewallRules +Remove-Item "HKLM:\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\RestrictedServices\AppIso\FirewallRules" -Force + +# Create new AppIso FirewallRules key +New-Item "HKLM:\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\RestrictedServices\AppIso\FirewallRules" -Force + +# Remove existing FirewallRules +Remove-Item "HKLM:\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\FirewallRules" -Force + +# Create new FirewallRules key +New-Item "HKLM:\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\FirewallRules" -Force + +# Set DWORD DeleteUserAppContainersOnLogoff to 1 +Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy" -Name "DeleteUserAppContainersOnLogoff" -Value 1 -Type DWord + + +Write-Host "Registery fixed, Please reboot the device manualy" \ No newline at end of file diff --git a/scripts_staging/Fixes/Resync time NTP.ps1 b/scripts_staging/Fixes/Resync time NTP.ps1 new file mode 100644 index 00000000..ec708f8b --- /dev/null +++ b/scripts_staging/Fixes/Resync time NTP.ps1 @@ -0,0 +1,44 @@ +<# +.SYNOPSIS + Restarts the Windows Time Service, resyncs system time, and queries the current time source. + +.DESCRIPTION + This script ensures that the Windows Time Service (w32time) is restarted, the system clock is resynced with its configured time source, + and the current time source is queried. Useful for troubleshooting time synchronization issues on a Windows system. + +.NOTES + Author: SAN + Date: 15.11.24 + #public + +.CHANGELOG + 15.11.24 v2.0 SAN Cleanup of the code & added header + +#> + +Write-Host "Restarting time service..." +try { + Restart-Service w32time -ErrorAction Stop + Write-Host "Time service restarted successfully." +} catch { + Write-Host "Failed to restart time service: $_" -ForegroundColor Red + exit 1 +} + +Write-Host "Waiting for 10 seconds..." +Start-Sleep -Seconds 10 + +Write-Host "Resyncing system time..." +try { + w32tm /resync + Write-Host "System time resynced successfully." +} catch { + Write-Host "Failed to resync system time." -ForegroundColor Red +} + +Write-Host "Querying time source..." +try { + w32tm /query /source +} catch { + Write-Host "Failed to query time source." -ForegroundColor Red +} diff --git a/scripts_staging/Lab/Demo powershell visibility window.ps1 b/scripts_staging/Lab/Demo powershell visibility window.ps1 new file mode 100644 index 00000000..7e2d17d8 --- /dev/null +++ b/scripts_staging/Lab/Demo powershell visibility window.ps1 @@ -0,0 +1,51 @@ +<# +.SYNOPSIS + Demo script to controls the visibility state of the PowerShell console window. + +.DESCRIPTION + This script defines and uses a Win32 class to access native Windows API functions + for showing, hiding, or minimizing the PowerShell console window. It uses + GetConsoleWindow and ShowWindow from kernel32.dll and user32.dll, respectively. + +.NOTES + Author: SAN + Date:02.05.25 + #public + +.EXAMPLE + # Minimize the PowerShell console window + [Win32]::ShowWindow($consoleHandle, $SW_MINIMIZE) + + # Hide the PowerShell console window + [Win32]::ShowWindow($consoleHandle, $SW_HIDE) + + # Restore the PowerShell console window + [Win32]::ShowWindow($consoleHandle, $SW_RESTORE) + +#> + +Add-Type -TypeDefinition @" +using System; +using System.Runtime.InteropServices; + +public static class Win32 { + [DllImport("user32.dll")] + public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [DllImport("kernel32.dll")] + public static extern IntPtr GetConsoleWindow(); +} +"@ -PassThru + +# Constants for ShowWindow API +$SW_HIDE = 0 +$SW_SHOWNORMAL = 1 +$SW_MINIMIZE = 6 +$SW_SHOWMINNOACTIVE = 7 +$SW_RESTORE = 9 + +# Get handle to the current PowerShell console window +$consoleHandle = [Win32]::GetConsoleWindow() + +# Modify the window state here: +[Win32]::ShowWindow($consoleHandle, $SW_MINIMIZE) \ No newline at end of file diff --git a/scripts_staging/Lab/Fake CheckRandom Alert 2.py b/scripts_staging/Lab/Fake CheckRandom Alert 2.py new file mode 100644 index 00000000..b16920a3 --- /dev/null +++ b/scripts_staging/Lab/Fake CheckRandom Alert 2.py @@ -0,0 +1,26 @@ +#!/usr/bin/python3 +#public +import random +import sys + +def main(): + # Randomly choose an exit code with 50% probability for 0 + exit_code = random.choices([0, 1, 2, 3], weights=[0.5, 0.1667, 0.1667, 0.1667])[0] + + # Print the exit code and status message + if exit_code == 0: + print(f"Exit Code: {exit_code} - Resolved") + else: + print(f"Exit Code: {exit_code} - Failed") + + # Print some Lorem Ipsum text + print("Lorem ipsum dolor sit amet, consectetur adipiscing elit.") + print("Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.") + print("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.") + print("Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.") + + # Exit with the chosen code + sys.exit(exit_code) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts_staging/Lab/Fake CheckRandom Alert.py b/scripts_staging/Lab/Fake CheckRandom Alert.py new file mode 100644 index 00000000..aac987c6 --- /dev/null +++ b/scripts_staging/Lab/Fake CheckRandom Alert.py @@ -0,0 +1,145 @@ +#!/usr/bin/python3 +import random +import sys +import io +import json +#public + +# Ensure the standard output is set to UTF-8 encoding +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + +# Utility function to handle safe printing of Unicode +def safe_print(s): + try: + print(s) + except UnicodeEncodeError: + # Replace unprintable characters with replacement character + print(s.encode('utf-8', 'replace').decode('utf-8')) + +# Define problematic strings and edge cases +unicode_tests = [ + # Unicode and Special Cases + "Unicode test: \U0001D11E\u266B", # Musical symbol and note + "Contains special chars: !@#$%^&*()_+|}{:\"?><,./;'[]\\=-`~", + "Embedded newlines\nshould be handled", + "Embedded quotes \"double\" and 'single' quotes", + "JSON breaking chars: {}[]:,", + "Non-printable characters: \x1B \x07 \x00", + "Backslashes \\ and forward slashes /", + "Tab characters\t should be included", + "Carriage return\r characters", + "Mixed escape sequences \n \r \t \b \f", + "Control characters: " + "".join(chr(i) for i in range(32)), + "Combining characters: a\u0301 e\u0301 i\u0301 o\u0301 u\u0301", # á é í ó ú + "Right-to-left text: \u202Ethis text is rtl", + "Null character: \x00 in the middle", + "Special Unicode: \uFFFF \uFFFE", + "Mathematical symbols: ∑∏∫∬∭", + "Currency symbols: ¢£¤¥", + "Different spaces: \u2000 \u2001 \u2002 \u2003", + "Zero-width spaces: \u200B\u200C\u200D\uFEFF", + "Emoji sequence: 🧩🧪🧫", + "Surrogate pairs: \U0001F600\U0001F606", # 😀😆 + "Large code points: \U0002A7D4 \U0001F6D0", # Mathematical Operators, Shield + "Math symbols: ∆√∛∞", + "Text direction: \u061C\u202D\u202C", + "High surrogate: \uD83D\uDE00", # 😀 + "Low surrogate: \uDE00", # Low surrogate to pair with high surrogate + "Complex sequences: \uD83C\uDF1F\uD83C\uDF1F\uD83C\uDF1F", # Multiple Sun Symbols + "Combining diacritics: \u0041\u030A\u0042\u0308\u0043\u0327", # Å B̈ Ç + "Zero-width joiners: \u200D\u200D\u200D", # Zero-width joiner repetition + "Double-byte characters: \u4F60\u597D\uFF0C\u4E16\u754C", # Chinese characters + + # Escaping Characters + "Escaping quotes: \"\" \"'\" \\'", + # "Escaping backslashes: \\\\ \\\\ \\\\\\\\\\\", + "Escaping newlines: Line1\\nLine2", + "Escaping carriage returns: Line1\\rLine2", + + # Extremely Large Strings + "Extremely large string: " + "x" * 10000, + "Extremely large multiline string:\n" + "\n".join(["This is a test line."] * 500), + + # Additional Special Characters + "Rare symbols: ⧫ ⍟ ⎈ ⍾ ⏚", + "Technical symbols: ⌘ ⌥ ⌫ ⌦ ⎋", + "Mathematical operators: ⊥ ⊗ ⊙ ⊚ ⊻", + + # More Languages and Scripts + "Armenian: բարև աշխարհ", # Hello, World + "Bengali: হ্যালো বিশ্ব", # Hello, World + "Georgian: გამარჯობა მსოფლიო", # Hello, World + "Gujarati: નમસ્તે વિશ્વ", # Hello, World + "Hmong: Nyob zoo ntiaj teb", # Hello, World + "Javanese: Halo Dunia", # Hello, World + "Kannada: ನಮಸ್ಕಾರ ಜಗತ್ತಿಗೆ", # Hello, World + "Lao: ສະບາຍດີໂລກ", # Hello, World + "Malayalam: ഹലോ ലോകം", # Hello, World + "Myanmar: မင်္ဂလာပါ ကမ္ဘာလောက", # Hello, World + "Nepali: नमस्कार संसार", # Hello, World + "Sinhala: හෙලෝ ලෝකය", # Hello, World + "Tamil: வணக்கம் உலகம்", # Hello, World + "Telugu: హలో వరల్డ్", # Hello, World + "Tibetan: བཀྲིས་གནང་བརྗེད་", # Hello, World + "Uzbek: Salom Dunyo", # Hello, World + + # Extreme Unicode Edge Cases + "Extremely high Unicode: \U0010FFFF", # Highest code point in Unicode + "Extremely low Unicode: \u0001", # Lowest code point in Unicode + "Middle of Unicode range: \U00010000", # Supplementary Planes + + # Text Layout and Formatting + "Bidirectional text: \u05D0\u05D1\u05D2\u200F\u202E\u05D3\u05D4\u05D5", # Hebrew text with RTL overrides + "Zalgo text: H̵e̸l̷l̶o̴ W̵o̸r̷l̶d̴", # Zalgo effect + "Invisible characters: \u200B\u200C\u200D\uFEFF", # Invisible characters in between + + # Complex Combining Sequences + "Multiple combining diacritics: a\u0300\u0301\u0302\u0303\u0304\u0305", # Combining diacritics over a base character + "Overlapping combining characters: a\u0336\u0336\u0336\u0336", # Multiple strikethroughs + + # Additional Complex Cases + "Surrogates edge cases: \uD83D\uDE00\uD83D\uDE01\uD83D\uDE02\uD83D\uDE03", # Multiple emojis + "Mirrored text: \u0623\u0646\u0633\u0627\u0646", # Arabic script (human) + "Vowel diacritics: a\u0316\u0317\u0318\u0319\u031A", # Various vowel diacritics + "Overlap text: ᎣᏢᏯᏪᏮ", # Cherokee text + "Long text with mixed scripts: 你好, こんにちは, 안녕하세요, Hello!", # Multiple scripts + "Emoji with skin tones: 👋🏻👋🏼👋🏽👋🏾👋🏿", # Wave emoji with skin tones + "Complex formatting: \u2063\u2064\u2065\u2066", # Invisible and non-visible formatting characters + "Extremely high code point combined with low: \u0001\U0010FFFF", # Low and high code points combined + "Languages with various punctuation: ÀÀÀÀ àààà ¡Hola! ¿Cómo estás?", # Punctuation and accents + + # SQL Injection and Special Characters + "Basic SQL Injection: ' OR '1'='1", + "SQL Injection with comment: '; DROP TABLE users;--", + "SQL Injection with nested query: ' UNION SELECT null, username, password FROM users--", + "SQL Injection with hex encoding: 0x27 UNION SELECT null, username, password FROM users--", + "SQL Injection with multiple queries: ' ; SELECT * FROM users;--", + "SQL Injection with special characters: ' OR 1=1; --", + "SQL Injection with Unicode: ' OR 1=1 -- 𝒜𝒷𝒸", + "SQL Injection with long payload: " + "a" * 10000, + + # PostgreSQL Specific Cases + "PostgreSQL large string: " + "a" * 10000, + "PostgreSQL special chars: \u00A9 \u00AE \u20AC", + "PostgreSQL JSON injection: {\"key\": \"value\", \"test\": 1}", + "PostgreSQL JSON special chars: {\"key\": \"value\\nwith\\nnewlines\", \"test\": 1}", + "PostgreSQL complex JSON: {\"key\": {\"subkey\": [1, 2, 3], \"otherkey\": true}}", + "PostgreSQL JSON with Unicode: {\"key\": \"value\", \"emoji\": \"\U0001F600\"}", + "PostgreSQL JSON with SQL Injection: {\"key\": \"value' OR '1'='1\"}", +] + +# Randomly set the exit code to 1 or 2 +exit_code = random.choice([1, 2]) + +# Print out the Unicode test strings +for test in unicode_tests: + safe_print(test) + +# Output the exit code to a JSON file +output = {"exit_code": exit_code} + +with open("output.json", "w", encoding='utf-8') as f: + json.dump(output, f, ensure_ascii=False) + +# Exit with the chosen code +sys.exit(exit_code) \ No newline at end of file diff --git a/scripts_staging/Lab/IP block lists for specified countries.ps1 b/scripts_staging/Lab/IP block lists for specified countries.ps1 new file mode 100644 index 00000000..5dc6307c --- /dev/null +++ b/scripts_staging/Lab/IP block lists for specified countries.ps1 @@ -0,0 +1,212 @@ +<# +.SYNOPSIS + This script downloads and processes IP block lists for specified countries + from ipdeny.com and creates corresponding inbound and/or outbound firewall + rules on the local machine using PowerShell cmdlets. + +.DESCRIPTION + The script allows users to automate the creation of firewall rules that block + IP ranges from specific countries or from a provided input file. It can delete + existing firewall rules matching the specified rule name and recreate them with + updated block lists. + + This script can be used to block IPs from countries with high levels of unwanted + traffic or suspected malicious activity. + + Sample of problematic Countries (often associated with cyberattacks, fraud, or high-risk traffic): + - CN (China) + - RU (Russia) + - IN (India) + - TR (Turkey) + - BR (Brazil) + - UA (Ukraine) + - NG (Nigeria) + - KR (South Korea) + - PH (Philippines) + - IR (Iran) + +.PARAMETER Countries + A comma-separated list of two-letter country codes (e.g., "ru,cn") to download + IP block lists for each specified country. + +.PARAMETER InputFile + Path to an input file containing IP ranges to block. Each line should contain + a valid IP range. + +.PARAMETER RuleName + Name for the firewall rule. If not provided, the base name of the input file + or zone file is used. + +.PARAMETER ProfileType + The firewall profile to apply the rules to. Default: "any". Options: + - Domain + - Private + - Public + - Any + +.PARAMETER InterfaceType + The type of network interface for the rule. Default: "any". Options: + - Wired + - Wireless + - Any + +.PARAMETER Direction + Direction of traffic to block. Default: "Inbound". Options: + - Inbound + - Outbound + - Both + +.PARAMETER DeleteOnly + If set, deletes all firewall rules matching "*xx.zone*". + +.EXAMPLE + -Countries "ru,cn" + Downloads and processes IP block lists for Russia and China and creates corresponding inbound firewall rules + + -InputFile "C:\path\to\my-blocklist.txt" -RuleName "CustomBlock" -Direction Both + Processes an input file containing IP ranges and creates both inbound and outbound rules. + + # Remove all rules with "*xx.zone*" + -DeleteOnly + +.NOTE + V1 Author: Jason Fossen (http://www.sans.org/windows-security/) 20.Mar.2012 + V2 Author: Vinahost release (https://cloudcraft.info) 15.Aug.2017 + V3 Author: SAN 28.01.25 + #public + +.CHANGELOG + 28.01.25 New feature to set direction will default to inbound only to reduce the load on cpu, added feature to add countries in bulk, fixed deleteonly to remove all rules created, upgrade to PowerShell cmdlets for fw rules + +.TODO + add the postfix to rule name in every case to make sure DeleteOnly can catch them all + +#> + +param ( + [string] $Countries, + [string] $InputFile, + [string] $RuleName, + [string] $ProfileType = "Any", + [string] $InterfaceType = "Any", + [ValidateSet("Inbound", "Outbound", "Both")] + [string] $Direction = "Inbound", + [switch] $DeleteOnly +) + +# Function to delete existing firewall rules +function RemoveFirewallRules { + param ([string]$Pattern) + + $RulesToDelete = Get-NetFirewallRule | Where-Object { $_.Name -like $Pattern } + if ($RulesToDelete) { + Write-Host "`nDeleting rules matching '$Pattern'..." + $RulesToDelete | Remove-NetFirewallRule -Confirm:$false + Write-Host "`nRules deleted successfully." + } else { + Write-Host "`nNo matching rules found." + } +} + +# If DeleteOnly is set, remove all rules matching *xx.zone* +if ($DeleteOnly) { + RemoveFirewallRules -Pattern "*??.zone*" + exit +} + +# Function to process input file and create firewall rules +function ProcessFile { + param ( + [string]$InputFile, + [string]$RuleName, + [string]$ProfileType, + [string]$InterfaceType, + [string]$Direction + ) + + $file = Get-Item $InputFile -ErrorAction SilentlyContinue + if (-not $file) { + Write-Host "`nFile $InputFile not found, quitting..." + exit + } + + # Set default rule name if not provided + if (-not $RuleName) { $RuleName = $file.BaseName } + + # Remove existing firewall rules for this specific rule name + RemoveFirewallRules -Pattern "$RuleName-#*" + + # Load IP ranges from file + $Ranges = Get-Content $file | Where-Object { ($_ -match '^[0-9a-fA-F]{1,4}[\.\:]') -and ($_ -match '\d') } + if (-not $Ranges) { + Write-Host "`nNo valid IP addresses found in $InputFile, quitting..." + exit + } + + $LineCount = $Ranges.Count + Write-Host "`nLoaded $LineCount IP ranges from $InputFile..." + + # Define batch size for rules + $MaxRangesPerRule = 200 + $RuleIndex = 1 + $StartIndex = 0 + + # Process and create rules in batches + while ($StartIndex -lt $LineCount) { + $EndIndex = [Math]::Min($StartIndex + $MaxRangesPerRule, $LineCount) + $IPBatch = $Ranges[$StartIndex..($EndIndex - 1)] + $RuleSuffix = $RuleIndex.ToString("000") + + # Create rules based on direction + if ($Direction -eq "Inbound" -or $Direction -eq "Both") { + Write-Host "`nCreating inbound rule: $RuleName-#$RuleSuffix..." + New-NetFirewallRule -Name "$RuleName-#$RuleSuffix" -DisplayName "$RuleName-#$RuleSuffix" -Direction Inbound -Action Block -RemoteAddress $IPBatch -Profile $ProfileType -InterfaceType $InterfaceType + } + + if ($Direction -eq "Outbound" -or $Direction -eq "Both") { + Write-Host "`nCreating outbound rule: $RuleName-#$RuleSuffix..." + New-NetFirewallRule -Name "$RuleName-#$RuleSuffix" -DisplayName "$RuleName-#$RuleSuffix" -Direction Outbound -Action Block -RemoteAddress $IPBatch -Profile $ProfileType -InterfaceType $InterfaceType + } + + $StartIndex += $MaxRangesPerRule + $RuleIndex++ + } + + Write-Host "`nFirewall rules created successfully!" +} + +# Validate input parameters +if (-not $Countries -and -not $InputFile) { + Write-Host "Please specify at least one country or provide an input file." + exit +} + +# Split the list of countries if provided +$CountryList = if ($Countries) { $Countries.Split(',') } else { @() } + +if ($CountryList.Count -gt 0) { + foreach ($Zone in $CountryList) { + if ($Zone.Length -ne 2) { + Write-Host "`nInvalid zone specified for '$Zone', skipping..." + continue + } + + $Zone = $Zone.ToLower() + $InputFile = "$Zone.zone.txt" + + Write-Host "`nDownloading IP block list for zone: $Zone..." + try { + Invoke-WebRequest -Uri "http://www.ipdeny.com/ipblocks/data/countries/$Zone.zone" -OutFile $InputFile -UseBasicParsing + } catch { + Write-Host "`nFailed to download IP block list for $Zone, skipping..." + continue + } + + # Process the downloaded input file + ProcessFile -InputFile $InputFile -RuleName $Zone.zone -ProfileType $ProfileType -InterfaceType $InterfaceType -Direction $Direction + } +} else { + if ($InputFile) { + ProcessFile -InputFile $InputFile -RuleName $RuleName -ProfileType $ProfileType -InterfaceType $InterfaceType -Direction $Direction + } +} \ No newline at end of file diff --git a/scripts_staging/Lab/RustDesk Get ID.ps1 b/scripts_staging/Lab/RustDesk Get ID.ps1 new file mode 100644 index 00000000..a844160d --- /dev/null +++ b/scripts_staging/Lab/RustDesk Get ID.ps1 @@ -0,0 +1,10 @@ +#public +#grab public id of restdesk to set a custom field + +#V1 +$ErrorActionPreference= 'silentlycontinue' + +cd $env:ProgramFiles\RustDesk\ +.\RustDesk.exe --get-id | out-host + +exit diff --git a/scripts_staging/Lab/RustDesk install.ps1 b/scripts_staging/Lab/RustDesk install.ps1 new file mode 100644 index 00000000..bc349dad --- /dev/null +++ b/scripts_staging/Lab/RustDesk install.ps1 @@ -0,0 +1,72 @@ +<# + +#public +#alternative experimental rustdesk installer/configuration + +exemple var: +rustdeskkey={{global.rustdeskkey}} +rendezvousServer=192.x.x.x +customRendezvousServer=192.x.x.x + +#> +$ErrorActionPreference = 'SilentlyContinue' + +$ServiceName = 'Rustdesk' +$UserProfileConfigPath = "C:\Users\$username\AppData\Roaming\RustDesk\config\RustDesk2.toml" +$LocalServiceConfigPath = "C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config\RustDesk2.toml" + +# Configuration content +$rendezvousServer = $env:rendezvousServer +$customRendezvousServer = $env:customRendezvousServer +$key = $env:rustdeskkey + +# Hardcoded values +$natType = 2 +$serial = 0 +# Optional Values +# $relayServer = 'IPADDRESS' +# $apiServer = 'https://IPADDRESS' + +# Install Rustdesk using Chocolatey (latest version) +choco install rustdesk -y + +# Check and start Rustdesk service if necessary +$arrService = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue + +if ($arrService -eq $null) { + Start-Sleep -Seconds 20 +} + +while ($arrService.Status -ne 'Running') { + Start-Service $ServiceName + Start-Sleep -Seconds 5 + $arrService.Refresh() +} +net stop $ServiceName + +# Get current username +$username = ((Get-WMIObject -ClassName Win32_ComputerSystem).Username).Split('\')[1] + +# Update RustDesk configuration for the user and local service +$RustDeskConfigContent = @" +rendezvous_server = '$rendezvousServer' +nat_type = $natType +serial = $serial + +[options] +custom-rendezvous-server = '$customRendezvousServer' +key = '$key' +# relay-server = 'IPADDRESS' # Optional +# api-server = 'https://IPADDRESS' # Optional +"@ + +Remove-Item $UserProfileConfigPath -ErrorAction SilentlyContinue +New-Item -Path $UserProfileConfigPath -ItemType File -Force | Out-Null +Set-Content -Path $UserProfileConfigPath -Value $RustDeskConfigContent + +Remove-Item $LocalServiceConfigPath -ErrorAction SilentlyContinue +New-Item -Path $LocalServiceConfigPath -ItemType File -Force | Out-Null +Set-Content -Path $LocalServiceConfigPath -Value $RustDeskConfigContent + +# Start the Rustdesk service +net start $ServiceName \ No newline at end of file diff --git a/scripts_staging/Lab/RustDesk password set.ps1 b/scripts_staging/Lab/RustDesk password set.ps1 new file mode 100644 index 00000000..414919fc --- /dev/null +++ b/scripts_staging/Lab/RustDesk password set.ps1 @@ -0,0 +1,31 @@ +#public +#experimental password changer for rustdesk will use the content of a var for the source of the PW +#RDPWD={{agent.Local password}} + +$ErrorActionPreference = 'SilentlyContinue' + +$confirmation_file = "C:\program files\RustDesk\rdrunonce.txt" + +# Stop the RustDesk service if it is running +net stop rustdesk > $null +$ProcessActive = Get-Process rustdesk -ErrorAction SilentlyContinue +if ($ProcessActive -ne $null) { + Stop-Process -ProcessName rustdesk -Force +} + +# Use the password from the RDPWD environment variable +$rustdesk_pw = $env:RDPWD +if (-not $rustdesk_pw) { + Write-Error "The RDPWD environment variable is not set." + exit 1 +} + +# Start RustDesk with the provided password +Start-Process "$env:ProgramFiles\RustDesk\RustDesk.exe" "--password $rustdesk_pw" -Wait +Write-Output $rustdesk_pw + +# Restart the RustDesk service +net start rustdesk > $null + +# Create the confirmation file +New-Item $confirmation_file > $null \ No newline at end of file diff --git a/scripts_staging/Lab/Send mail test.ps1 b/scripts_staging/Lab/Send mail test.ps1 new file mode 100644 index 00000000..774b56db --- /dev/null +++ b/scripts_staging/Lab/Send mail test.ps1 @@ -0,0 +1,73 @@ +<# +.SYNOPSIS + Sends an email using SMTP commands over a TCP connection. + +.DESCRIPTION + This script sends an email using minimal SMTP commands via a TCP connection. + It retrieves configuration details (SMTP server, port, sender, recipient, subject, and body) + from environment variables. + +.EXEMPLE + SMTP_SERVER=XXXXX.mail.protection.outlook.com + SMTP_PORT=25 + EMAIL_FROM=whatever@domain1.asdf + EMAIL_TO=whatever@domain2.asdf + EMAIL_SUBJECT=Test Email via TCP + EMAIL_BODY=This is a test email sent using SMTP commands over TCP. + +.NOTE + Author: SAN + Date: 03.12.24 + #public + +.CHANGELOG + SAN 12.12.24 Fixed HELO to extract domain from "to" +#> + + +$smtpServer = $env:SMTP_SERVER +$smtpPort = [int]$env:SMTP_PORT +$from = $env:EMAIL_FROM +$to = $env:EMAIL_TO +$subject = $env:EMAIL_SUBJECT +$body = $env:EMAIL_BODY + +$domain = ($to -split '@')[1] + +$tcpClient = New-Object System.Net.Sockets.TcpClient($smtpServer, $smtpPort) +$stream = $tcpClient.GetStream() +$writer = New-Object System.IO.StreamWriter($stream) +$reader = New-Object System.IO.StreamReader($stream) + +function Send-SMTPCommand { + param ([string]$command) + if ($command) { + $writer.WriteLine($command) + $writer.Flush() + } + $response = $reader.ReadLine() + Write-Host "SERVER RESPONSE: $response" + return $response +} + +Send-SMTPCommand "" +Send-SMTPCommand "HELO $domain" +Send-SMTPCommand "MAIL FROM:<$from>" +Send-SMTPCommand "RCPT TO:<$to>" +Send-SMTPCommand "DATA" +Send-SMTPCommand @" +From: $from +To: $to +Subject: $subject + +$body +. +"@ +Send-SMTPCommand "QUIT" + +$writer.Close() +$reader.Close() +$stream.Close() +$tcpClient.Close() + +Write-Host "Email sent!" diff --git a/scripts_staging/Lab/TRMM Variable exemples.ps1 b/scripts_staging/Lab/TRMM Variable exemples.ps1 new file mode 100644 index 00000000..c0451628 --- /dev/null +++ b/scripts_staging/Lab/TRMM Variable exemples.ps1 @@ -0,0 +1,119 @@ +<# +.SYNOPSIS + Outputs Tactical RMM pre-made variables with prefixes for exemple. + + Documentation for script variables: https://docs.tacticalrmm.com/script_variables/ + + Documentation for custom fields: https://docs.tacticalrmm.com/functions/custom_fields/ + + Documentation for global keystore/custom fields: https://docs.tacticalrmm.com/functions/keystore/ + +.EXEMPLE + Example input in Environment vars: + version={{agent.version}} + operating_system={{agent.operating_system}} + plat={{agent.plat}} + hostname={{agent.hostname}} + local_ips={{agent.local_ips}} + public_ip={{agent.public_ip}} + agent_id={{agent.agent_id}} + last_seen={{agent.last_seen}} + total_ram={{agent.total_ram}} + boot_time={{agent.boot_time}} + logged_in_username={{agent.logged_in_username}} + last_logged_in_user={{agent.last_logged_in_user}} + monitoring_type={{agent.monitoring_type}} + description={{agent.description}} + mesh_node_id={{agent.mesh_node_id}} + overdue_email_alert={{agent.overdue_email_alert}} + overdue_text_alert={{agent.overdue_text_alert}} + overdue_dashboard_alert={{agent.overdue_dashboard_alert}} + offline_time={{agent.offline_time}} + overdue_time={{agent.overdue_time}} + check_interval={{agent.check_interval}} + needs_reboot={{agent.needs_reboot}} + choco_installed={{agent.choco_installed}} + patches_last_installed={{agent.patches_last_installed}} + timezone={{agent.timezone}} + maintenance_mode={{agent.maintenance_mode}} + block_policy_inheritance={{agent.block_policy_inheritance}} + alert_template={{agent.alert_template}} + site={{agent.site}} + + client_name={{client.name}} + + site_name={{site.name}} + site_client={{site.client}} + + Custom: + agent.custom={{agent.custom}} + site.custom={{site.custom}} + client.custom={{client.custom}} + global.custom={{global.custom}} + +.NOTE + Author: SAN + Date: 06.01.25 + #public + +#> + +# Block 1: Agent pre-made variables +Write-Output "===== Agent Information =====" +Write-Output "agent.version: $env:version" +Write-Output "agent.operating_system: $env:operating_system" +Write-Output "agent.plat: $env:plat" +Write-Output "agent.hostname: $env:hostname" +Write-Output "agent.local_ips: $env:local_ips" +Write-Output "agent.public_ip: $env:public_ip" +Write-Output "agent.agent_id: $env:agent_id" +Write-Output "agent.last_seen: $env:last_seen" +Write-Output "agent.total_ram: $env:total_ram" +Write-Output "agent.boot_time: $env:boot_time" +Write-Output "agent.logged_in_username: $env:logged_in_username" +Write-Output "agent.last_logged_in_user: $env:last_logged_in_user" +Write-Output "agent.monitoring_type: $env:monitoring_type" +Write-Output "agent.description: $env:description" +Write-Output "agent.mesh_node_id: $env:mesh_node_id" +Write-Output "agent.overdue_email_alert: $env:overdue_email_alert" +Write-Output "agent.overdue_text_alert: $env:overdue_text_alert" +Write-Output "agent.overdue_dashboard_alert: $env:overdue_dashboard_alert" +Write-Output "agent.offline_time: $env:offline_time" +Write-Output "agent.overdue_time: $env:overdue_time" +Write-Output "agent.check_interval: $env:check_interval" +Write-Output "agent.needs_reboot: $env:needs_reboot" +Write-Output "agent.choco_installed: $env:choco_installed" +Write-Output "agent.patches_last_installed: $env:patches_last_installed" +Write-Output "agent.timezone: $env:timezone" +Write-Output "agent.maintenance_mode: $env:maintenance_mode" +Write-Output "agent.block_policy_inheritance: $env:block_policy_inheritance" +Write-Output "agent.alert_template: $env:alert_template" +Write-Output "agent.site: $env:site" +Write-Output "" + +# Block 2: Client pre-made variables +Write-Output "===== Client Information =====" +Write-Output "client.name: $env:client_name" +Write-Output "" + +# Block 3: Site pre-made variables +Write-Output "===== Site Information =====" +Write-Output "site.name: $env:site_name" +Write-Output "site.client: $env:site_client" +Write-Output "" + +# Block 4: Agent Custom fields +Write-Output "===== Agent Custom fields =====" +Write-Output "" + +# Block 5: Site Custom fields +Write-Output "===== Site Custom fields =====" +Write-Output "" + +# Block 6: Client Custom fields +Write-Output "===== Client Custom fields =====" +Write-Output "" + +# Block 7: Global Custom fields +Write-Output "===== Global Custom fields =====" +Write-Output "" \ No newline at end of file diff --git "a/scripts_staging/Lab/\342\230\242\357\270\217 Generate Blue screen of death.ps1" "b/scripts_staging/Lab/\342\230\242\357\270\217 Generate Blue screen of death.ps1" new file mode 100644 index 00000000..898364ca --- /dev/null +++ "b/scripts_staging/Lab/\342\230\242\357\270\217 Generate Blue screen of death.ps1" @@ -0,0 +1,78 @@ +<# +.SYNOPSIS + Invoke-Death triggers a Blue Screen of Death (BSOD) on a Windows machine + by invoking a hard error using native Windows functions. + +.DESCRIPTION + This PowerShell script contains embedded C# code that uses interop calls to the `ntdll.dll` library. It: + 1. Adjusts privileges to enable `SeShutdownPrivilege`. + 2. Invokes the `NtRaiseHardError` function to trigger a critical system error, leading to a BSOD. + + This script is intended for testing or research purposes in controlled environments only. + 🕷️ With great power comes great responsibility. Use it wisely. + +.NOTES + Author: SAN + Date: 19.12.24 + Original concept by peewpw (https://github.com/peewpw/Invoke-BSOD). + Adapted for circumventing AV detection and more controled execution. + #public + +.CHANGELOG + + +#> + + +$eventSource = "InvokeDeathScript" +$eventLog = "Application" + +# Check if the event source exists; if not, create it +if (-not [System.Diagnostics.EventLog]::SourceExists($eventSource)) { + [System.Diagnostics.EventLog]::CreateEventSource($eventSource, $eventLog) +} + +function Invoke-Death { + $source = @" +using System; +using System.Runtime.InteropServices; + +public static class CS { + [DllImport("ntdll.dll")] + public static extern uint RtlAdjustPrivilege(int Privilege, bool bEnablePrivilege, bool IsThreadPrivilege, out bool PreviousValue); + + [DllImport("ntdll.dll")] + public static extern uint NtRaiseHardError(uint ErrorStatus, uint NumberOfParameters, uint UnicodeStringParameterMask, IntPtr Parameters, uint ValidResponseOption, out uint Response); + + public static void InvokeDeath() { + bool previousValue; + uint response; + + RtlAdjustPrivilege(19, true, false, out previousValue); + + string errorMessage = "Oppenheimer special: Fatal system error occurred!"; + IntPtr errorMessagePtr = Marshal.StringToHGlobalUni(errorMessage); + + NtRaiseHardError(0xc0000420, 1, 0, errorMessagePtr, 6, out response); + + Marshal.FreeHGlobal(errorMessagePtr); + } +} + +"@ + + # Compile the C# code + $compilerParameters = New-Object System.CodeDom.Compiler.CompilerParameters + $compilerParameters.CompilerOptions = '/unsafe' + $compiledType = Add-Type -TypeDefinition $source -Language CSharp -PassThru -CompilerParameters $compilerParameters + + # Call the method + [CS]::InvokeDeath() +} + + + +Write-EventLog -LogName $eventLog -Source $eventSource -EventId 1 -EntryType Error -Message "Now I am become Death, the destroyer of worlds." + + +Invoke-Death diff --git a/scripts_staging/Tasks/Auto-logoff users.ps1 b/scripts_staging/Tasks/Auto-logoff users.ps1 new file mode 100644 index 00000000..c16413b2 --- /dev/null +++ b/scripts_staging/Tasks/Auto-logoff users.ps1 @@ -0,0 +1,156 @@ +<# +.SYNOPSIS + Logs off users who have been inactive for a specified duration. + +.DESCRIPTION + This script retrieves all active user sessions on the server and logs off users + who have been inactive for more than the specified duration (50 minutes by default). + It handles different session states and extracts session IDs properly for both active and disconnected sessions. + +.PARAMETER maxInactivityTime + The maximum period of inactivity in seconds before a user is logged off. Default is 3000 seconds (50 minutes). + +.EXEMPLE + -maxInactivityTime 3600 + +.NOTES + Author: SAN + Date: 12.06.24 + #public + +.TODO + Add user warning for the users + move var to env + +#> + + +param ( + [int]$maxInactivityTime = 3000 +) + +function Get-LastInputTime { + Add-Type @" + using System; + using System.Runtime.InteropServices; + public class IdleTime { + [DllImport("user32.dll")] + public static extern bool GetLastInputInfo(ref LASTINPUTINFO plii); + public struct LASTINPUTINFO { + public uint cbSize; + public uint dwTime; + } + public static uint GetIdleTime() { + LASTINPUTINFO lastInputInfo = new LASTINPUTINFO(); + lastInputInfo.cbSize = (uint)Marshal.SizeOf(lastInputInfo); + GetLastInputInfo(ref lastInputInfo); + return (uint)Environment.TickCount - lastInputInfo.dwTime; + } + public static DateTime GetLastInputTime() { + return DateTime.Now.AddMilliseconds(-(long)GetIdleTime()); + } + } +"@ + return [IdleTime]::GetLastInputTime() +} + +# Arrays to store user information +$foundUsers = @() +$keptConnectedUsers = @() +$disconnectedUsers = @() + +# Get all explorer.exe processes +$explorerProcesses = Get-Process -Name explorer -ErrorAction SilentlyContinue + +if ($explorerProcesses) { + Write-Host "Explorer.exe processes found: $($explorerProcesses.Count)" + + # Get current time + $currentTime = Get-Date + + foreach ($process in $explorerProcesses) { + try { + # Get the user session ID + $sessionId = $process.SessionId + + if ($sessionId -ge 0) { + Write-Host "Processing Session ID: $sessionId" + + # Use query user to get session information + $queryUserOutput = query user + Write-Host "Query User Output:`n$queryUserOutput" + + $sessionInfo = $queryUserOutput | Select-String -Pattern " $sessionId " -SimpleMatch + + if ($sessionInfo) { + $sessionInfoParts = $sessionInfo -split '\s+' + Write-Host "Session Info Parts: $sessionInfoParts" + + # Find the username and idle time in session info parts + $username = $null + $idleTime = $null + for ($i = 0; $i -lt $sessionInfoParts.Length; $i++) { + if ($sessionInfoParts[$i] -match '^\d+$' -and $sessionInfoParts[$i] -eq $sessionId.ToString()) { + $username = $sessionInfoParts[$i-1] + $idleTime = $sessionInfoParts[$i+2] + break + } + } + + if ($username -and $idleTime) { + # Debugging information + Write-Host "Username: $username, Idle Time String: $idleTime" + + # Attempt to parse idle time to TimeSpan + $idleTimeSpan = $null + if ($idleTime -match '^\d{1,2}:\d{2}$') { + # Handle formats like "1:30" or "12:45" + $idleTimeSpan = [TimeSpan]::Parse("00:$idleTime") + } elseif ($idleTime -match '^\d{1,2}:\d{2}:\d{2}$') { + # Handle formats like "1:30:00" or "12:45:00" + $idleTimeSpan = [TimeSpan]::Parse($idleTime) + } elseif ($idleTime -match '^\d+$') { + # Handle single digit idle time representing minutes + $idleTimeSpan = New-TimeSpan -Minutes $idleTime + } else { + Write-Host "Unable to parse idle time: $idleTime" + } + + if ($idleTimeSpan) { + Write-Host "Username: $username, Session ID: $sessionId, Idle Time: $idleTimeSpan" + + # Add username to found users list + $foundUsers += $username + + # Check if the user is idle for more than X + if ($idleTimeSpan.TotalSeconds -ge $maxInactivityTime) { + Write-Host "User $username (Session ID: $sessionId) has been idle for more than 4 hours. Logging off..." + $disconnectedUsers += $username + + # Log off the user session + logoff $sessionId + } else { + $keptConnectedUsers += $username + } + } + } else { + Write-Host "Unable to find username or idle time in session info parts." + } + } else { + Write-Host "No session info found for Session ID: $sessionId" + } + } else { + Write-Host "Invalid session ID: $sessionId" + } + } catch { + Write-Error "Failed to process session ID $($process.SessionId): $_" + } + } +} else { + Write-Host "No explorer.exe processes found." +} + +# Output the lists +Write-Host "Users Found: $($foundUsers -join ', ')" +Write-Host "Users Kept Connected: $($keptConnectedUsers -join ', ')" +Write-Host "Users Disconnected: $($disconnectedUsers -join ', ')" \ No newline at end of file diff --git a/scripts_staging/Tasks/Change user password.ps1 b/scripts_staging/Tasks/Change user password.ps1 new file mode 100644 index 00000000..c927c076 --- /dev/null +++ b/scripts_staging/Tasks/Change user password.ps1 @@ -0,0 +1,61 @@ +<# +.SYNOPSIS + This script changes the password for the user to a randomly generated password. + +.DESCRIPTION + The script defines a function to generate a random password and then sets the generated password for the specified user. + +.NOTES + Author: SAN + Date: 01.01.24 + Dependencies: + GeneratedPassphrase snippet + #public + +.CHANGELOG + 06.06.25 SAN added not allow to change the password on non primary DC it causes conflicts if run on multiple DC + +.TODO + move param to env +#> + +param( + [Parameter(Mandatory=$true)] + [string]$username +) + +# Check if the machine is not a Primary Domain Controller +# this script should not run on multiple DC as it would cause syncronisation issues so for the sake of simplicity it's only allowed to run on PDC +$domainRole = (Get-WmiObject Win32_ComputerSystem).DomainRole +$isDomainController = $domainRole -ge 4 # 4 = Backup DC, 5 = Primary DC +if ($isDomainController) { + try { + Write-Host "Domain Controller detected" + $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + $pdc = $domain.PdcRoleOwner.Name.Split('.')[0] + $localComputer = $env:COMPUTERNAME + + if ($pdc -ine $localComputer) { + Write-Host "Not the Primary DC" + exit 0 + } + Write-Host "Primary DC detected" + } catch { + Write-Host "Error determining PDC role. Aborting." + exit 1 + } +} + +# Snippet for passphrase +{{GeneratedPassphrase}} + +# Set the new password for the user +net user $username $GeneratedPassphrase + +# Check if the password change was successful +if ($LASTEXITCODE -eq 0) { + Write-Host "$GeneratedPassphrase" +} else { + Write-Host "Password change for $username failed." + exit 1 +} diff --git a/scripts_staging/Tasks/Import RD Gateway Cert From IIS.ps1 b/scripts_staging/Tasks/Import RD Gateway Cert From IIS.ps1 new file mode 100644 index 00000000..f4790336 --- /dev/null +++ b/scripts_staging/Tasks/Import RD Gateway Cert From IIS.ps1 @@ -0,0 +1,300 @@ +<# +.SYNOPSIS +Configures the RD Gateway SSL certificate and checks settings for the "win-acme" task. + +.DESCRIPTION +This script checks if the RD Gateway service and a task with "win-acme" in its name exist. +It also verifies and updates the "settings.json" file to ensure PrivateKeyExportable is set to true. +If all conditions are met, it imports a new SSL certificate to the RD Gateway. +Optionally, it can run the win-acme (wacs.exe) command to install a Let's Encrypt certificate. + +.PARAMETER settingsJsonPath +Specifies the path to the settings.json file. Default is "C:\tools\win-acme\settings.json". the default location of instalation of Win-Acme by chocolatey. + +.PARAMETER InstallLE +Specifies whether to install a Let's Encrypt certificate using win-acme. Default is false. + +.PARAMETER RDSURL +Specifies what url will be set in the bindings when installLE is called + +.PARAMETER ForceReplaceCertRDS +ignore the fail-safe checks and force the replacement of rds certs and restart the gateway + +.EXEMPLE +-settingsJsonPath "C:\tools\win-acme\settings.json" +-RDSURL {{agent.RDSURL}} +-ForceReplaceCertRDS +-InstallLE + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.CHANGELOG + 28/08/24 SAN Added a deletion of old cert when changes happens (this may not be possible with the TODO planned and would require a scraping of the idea) + 02/09/24 SAN Full re-write of the cert management to make it smart, it will not restart the service if no change is needed or fore re-write of the cert and also change logic for deleting old certs + 02/09/24 SAN Legacy Code cleanup + 02/09/24 SAN added -ForceReplaceCertRDS and a couple of fail-safe + 03/09/24 SAN added choco install to install section + 04/09/24 SAN corrected logic for deployement and force + 07/01/24 SAN changed old cert deletion logic + + +.TODO + find a way to call the script from the renew -script process of win-acme + for referance: C:\tools\win-acme\wacs.exe --source iis --verbose --siteid 1 --commonname $RDSURL --installation iis --installationsiteid 1 --script "C:\tools\win-acme\Scripts\ImportRDGateway.ps1" --scriptparameters '{CertThumbprint}' + change pathing based on folder for both .json and .exe + better way than calling iis 0 for the change ? probably possible if called from -script + replace win-acme with simpleacme and migration path +#> +param ( + [string]$settingsJsonPath = "C:\tools\win-acme\settings.json", + [switch]$InstallLE, + [string]$RDSURL, + [switch]$ForceReplaceCertRDS +) + +Function InstallLetEncryptCertificate { + choco install win-acme + $wacsCommand = "C:\tools\win-acme\wacs.exe --source iis --siteid 1 --commonname $RDSURL --installation iis --installationsiteid 1" + Write-Host "Executing command: $wacsCommand" + try { + Invoke-Expression $wacsCommand + } catch { + Write-Error "Failed to execute win-acme command. Error: $_" + exit 1 + } +} + +Function BindRDSURL { + if ($RDSURL) { + Write-Host "Binding RDSURL to HTTPS of the default IIS site..." + try { + New-WebBinding -Name "Default Web Site" -IPAddress "*" -Port 443 -HostHeader $RDSURL -Protocol "https" + Write-Host "RDSURL bound to HTTPS of the default IIS site." + } catch { + Write-Error "Failed to bind RDSURL. Error: $_" + } + } else { + Write-Warning "RDSURL is not provided. Skipping binding process." + } +} + +Function Get-RDGatewaySSLCertificateThumbprint { + param ( + [string]$Path = 'RDS:\GatewayServer\SSLCertificate\Thumbprint' + ) + + try { + $thumbprintValue = (Get-Item -Path $Path).CurrentValue + if ([string]::IsNullOrWhiteSpace($thumbprintValue)) { + return $null + } else { + return $thumbprintValue + } + } catch { + Write-Error "An error occurred while retrieving the SSL certificate thumbprint: $_" + return $null + } +} + +function Is-ValidThumbprint { + param ( + [string]$Thumbprint + ) + return $Thumbprint -and $Thumbprint.Length -eq 40 -and $Thumbprint -match '^[0-9A-Fa-f]+$' +} + +Function Remove-OldLECertificates { + + $stores = @( + "Cert:\LocalMachine\My", + "Cert:\LocalMachine\WebHosting", + "Cert:\LocalMachine\Remote Desktop" + ) + + $certsRemoved = $false + + foreach ($store in $stores) { + try { + # Get Let's Encrypt certificates by checking the Issuer or Subject + $leCerts = Get-ChildItem -Path $store -Recurse | Where-Object { + $_.Issuer -like "*Let's Encrypt*" + } + + if ($leCerts.Count -le 1) { + Write-Host "Less than two Let's Encrypt certificates found in $store. No removal required." + } else { + # Sort certificates by NotAfter date (ascending) and select the oldest one + $oldCert = $leCerts | Sort-Object -Property NotAfter | Select-Object -First 1 + + if ($oldCert) { + Remove-Item -Path $oldCert.PSPath -Confirm:$false + Write-Host "Removed oldest Let's Encrypt certificate with thumbprint $($oldCert.Thumbprint) from $store." + $certsRemoved = $true + } + } + } catch { + Write-Error "Failed to remove certificates from $store. Error: $_" + } + } + + return $certsRemoved +} + + +# Check if Get-RDUserSession is available, if not exit with code 0 +try { + $null = Get-RDUserSession -ErrorAction Stop +} +catch { + if ($_.Exception.Message -match "A Remote Desktop Services deployment does not exist") { + Write-Output "Remote Desktop Services deployment does not exist. Exiting." + exit 0 + } + else { + Write-Output "An unexpected error occurred while checking for RDS deployment." + Write-Output "Error: $($_.Exception.Message)" + exit 0 + } +} + +# Check if settings.json file exists +if (-not $InstallLE.IsPresent -and -not (Test-Path $settingsJsonPath)) { + Write-Host "settings.json not found. EXIT" + exit 1 +} + +# Install Let's Encrypt certificate if InstallLE is set to true +if ($InstallLE) { + if (-not $RDSURL) { + Write-Error "RDSURL is required when InstallLE is true. Exiting script." + exit 1 + } + + BindRDSURL + InstallLetEncryptCertificate +} + +# Check if PrivateKeyExportable is set to true in settings.json +$settingsJson = Get-Content -Path $settingsJsonPath -Raw | ConvertFrom-Json +$privateKeyExportable = $settingsJson.Store.CertificateStore.PrivateKeyExportable + +if (-not $privateKeyExportable) { + $settingsJson.Store.CertificateStore.PrivateKeyExportable = $true + try { + $settingsJson | ConvertTo-Json | Set-Content -Path $settingsJsonPath + Write-Host "PrivateKeyExportable set to true in settings.json" + } catch { + Write-Error "Failed to update settings.json. Error: $_" + exit 1 + } +} + +# Check if the RD Gateway service exists +$gatewayService = Get-Service -Name TSGateway -ErrorAction SilentlyContinue +# Check if a task with "win-acme" in its name exists +$winAcmeTask = Get-ScheduledTask -TaskName "*win-acme*" -ErrorAction SilentlyContinue + +if ($gatewayService -and $winAcmeTask) { + Import-Module RemoteDesktopServices + Import-Module WebAdministration + + # Retrieve thumbprints currents + $IISCertThumbprint = (Get-ChildItem IIS:SSLBindings)[0].Thumbprint + $RDSCertThumbprint = Get-RDGatewaySSLCertificateThumbprint + + if (-not $ForceReplaceCertRDS) { + if ($RDSCertThumbprint -eq $IISCertThumbprint) { + Write-Host "RDS: $RDSCertThumbprint" + Write-Host "IIS: $IISCertThumbprint" + Write-Host "The RD Gateway SSL certificate is already the same as IIS. No replacement needed." + exit 0 + } + Write-Host "RDS: $RDSCertThumbprint" + Write-Host "IIS: $IISCertThumbprint" + + # Validate IIS certificate thumbprint + if (Is-ValidThumbprint -Thumbprint $IISCertThumbprint) { + Write-Host "IIS certificate thumbprint $IISCertThumbprint is valid. Continuing." + } else { + Write-Error "Invalid IIS certificate thumbprint: $IISCertThumbprint. Exiting script." + exit 1 + } + } + + # Retrieve the certificate from the local machine store that matches the specified thumbprint + $CertInStore = Get-ChildItem -Path Cert:\LocalMachine -Recurse | Where-Object {$_.Thumbprint -eq $IISCertThumbprint} | Sort-Object -Descending | Select-Object -First 1 + + if ($CertInStore) { + try { + # Check if the certificate is not already in the 'LocalMachine\My' store + if ($CertInStore.PSPath -notlike "*LocalMachine\My\*") { + # The certificate is not in the 'My' store, so we will move it there + $SourceStoreScope = 'LocalMachine' + $SourceStorename = $CertInStore.PSParentPath.Split("\")[-1] + + Write-Host "Certificate found in '$SourceStorename' store. Moving it to 'LocalMachine\My'." + + # Open the source certificate store (Read-Only) + $SourceStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $SourceStorename, $SourceStoreScope + $SourceStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly) + + # Retrieve the certificate from the source store + $cert = $SourceStore.Certificates | Where-Object {$_.Thumbprint -eq $CertInStore.Thumbprint} + + # Define the destination store ('My') and open it (Read-Write) + $DestStoreScope = 'LocalMachine' + $DestStoreName = 'My' + $DestStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $DestStoreName, $DestStoreScope + $DestStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) + + # Add the certificate to the destination store + $DestStore.Add($cert) + Write-Host "Certificate successfully added to 'LocalMachine\My'." + + # Close both stores + $SourceStore.Close() + $DestStore.Close() + + # Update the $CertInStore variable to reference the newly moved certificate + $CertInStore = Get-ChildItem -Path Cert:\LocalMachine\My -Recurse | Where-Object {$_.Thumbprint -eq $IISCertThumbprint} | Sort-Object -Descending | Select-Object -First 1 + } else { + Write-Host "Certificate is already in the 'LocalMachine\My' store." + } + + # Set the certificate thumbprint in the RD Gateway listener + Set-Item -Path RDS:\GatewayServer\SSLCertificate\Thumbprint -Value $CertInStore.Thumbprint -ErrorAction Stop + Write-Host "RD Gateway listener thumbprint set to the new certificate." + + # Restart the Terminal Services Gateway service to apply the new certificate + Restart-Service TSGateway -Force -ErrorAction Stop + Write-Host "TSGateway service restarted successfully." + + # Call function to remove old certificates + $certsRemoved = Remove-OldLECertificates + + # Check if old certificates were removed + if (-not $certsRemoved) { + Write-Error "No old certificates $RDSCertThumbprint were removed. Exiting script." + exit 1 + } else { + Write-Host "Old certificates removed successfully." + } + + } catch { + # Handle any errors that occurred during the process + Write-Error "Failed to set certificate thumbprint or restart the service. Error: $_" + exit 1 + } + } else { + # Certificate with the specified thumbprint was not found in the certificate store + Write-Error "Certificate with thumbprint '$IISCertThumbprint' not found in the certificate store." + exit 1 + } +} elseif (-not $gatewayService) { + Write-Error "RD Gateway service not found." +} elseif (-not $winAcmeTask) { + Write-Error "Task with 'win-acme' not found." +} \ No newline at end of file diff --git a/scripts_staging/Tasks/Kill Switch Manager.ps1 b/scripts_staging/Tasks/Kill Switch Manager.ps1 new file mode 100644 index 00000000..1fe5e7ba --- /dev/null +++ b/scripts_staging/Tasks/Kill Switch Manager.ps1 @@ -0,0 +1,114 @@ +<# +.SYNOPSIS + A PowerShell script to implement a kill switch mechanism for Tactical RMM using scheduled tasks and DNS TXT records. + +.DESCRIPTION + This script sets up a kill switch by creating a scheduled task that runs hourly. + It checks DNS TXT records for specific flags (`stop=true` or `uninstall=true`) and executes corresponding actions like stopping services or uninstalling Tactical RMM. + The script is designed as a safeguard in case the RMM system behaves unexpectedly or goes rogue, allowing administrators to disable or uninstall it remotely and independently. + +.PARAMETER killswitchdomain + The domain used to resolve the DNS TXT records containing kill switch flags. + This can be specified through the environment variable `killswitchdomain`. + +.PARAMETER companyfolder + The folder path where the script file (`RMM_Kill_Switch.ps1`) will be saved. + This can be specified through the environment variable `companyfolder`. + +.EXAMPLE + killswitchdomain=kill.alltacticalagents.example.com + companyfolder=C:\CompanyFolder + companyfolder={{global.Company_folder_path}} + +.NOTES + Author: SAN + Date: 01.01.2024 + #public + +.CHANGELOG + 12.06.25 SAN Fixed var passtrough issue + +.TODO + Integrate this script into the deployment process. + Cleanup the code + split script content to snippet + add company name folder for task + hide script + hide error when first setup +#> + + + +# Retrieve the domain and path from environment variables +$domain = [System.Environment]::GetEnvironmentVariable('killswitchdomain') +$envVar = [System.Environment]::GetEnvironmentVariable('companyfolder') + +if (-not $domain) { + Write-Host "Environment variable 'killswitchdomain' not found." + exit 1 +} + +if (-not $envVar) { + Write-Host "Environment variable 'companyfolder' not found." + exit 1 +} + +$scriptPath = Join-Path -Path $envVar -ChildPath "RMM_Kill_Switch.ps1" +$taskName = "RMM_Kill_Switch" + +# Delete the existing task if it exists +Unregister-ScheduledTask -TaskName $taskName -Confirm:$false + +$scriptContent = @' +function ExecuteStopBranch { + Stop-Service -Name "tacticalrmm" -Force + Get-Process -Name "tacticalrmm" -ErrorAction SilentlyContinue | Stop-Process -Force + Stop-Service -Name "Mesh Agent" -Force + Get-Process -Name "MeshAgent" -ErrorAction SilentlyContinue | Stop-Process -Force +} + +function ExecuteUninstallBranch { + Start-Process -FilePath "C:\Program Files\TacticalAgent\unins000.exe" -ArgumentList "/VERYSILENT" -Wait +} + +$record = Resolve-DnsName -Name "__DOMAIN_PLACEHOLDER__" -Type "TXT" -ErrorAction SilentlyContinue +if ($record) { + $txtData = $record | Select-Object -ExpandProperty Strings + $foundStop = $txtData -match "stop=true" + $foundUninstall = $txtData -match "uninstall=true" + + if (-not $foundStop -and -not $foundUninstall) { + Write-Host "Neither 'stop=true' nor 'uninstall=true' found in the TXT record for __DOMAIN_PLACEHOLDER__." + } + elseif ($foundStop) { + ExecuteStopBranch + } + elseif ($foundUninstall) { + ExecuteUninstallBranch + } +} else { + Write-Host "TXT record for __DOMAIN_PLACEHOLDER__ not found." +} +'@ + +# Replace placeholder with actual domain due to powershell shenanigans +$scriptContent = $scriptContent -replace '__DOMAIN_PLACEHOLDER__', $domain + + +# Save the script content to the file +$scriptContent | Out-File -FilePath $scriptPath -Encoding UTF8 -Force +#Set-ItemProperty -Path $scriptPath -Name Attributes -Value ([System.IO.FileAttributes]::Hidden) + +# Create a scheduled task to run the script hourly and daily +$action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`"" + +# Specify hourly triggers for 24 hours with random minutes +$triggers = @() +for ($hour = 0; $hour -lt 24; $hour++) { + $randomMinutes = Get-Random -Minimum 0 -Maximum 59 + $triggerHourly = New-ScheduledTaskTrigger -At (Get-Date).AddHours($hour).AddMinutes($randomMinutes) -Daily + $triggers += $triggerHourly +} + +$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries +Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $triggers -Settings $settings -Description "Task to run the Tactical RMM Kill Switch script hourly and daily." -User "SYSTEM" \ No newline at end of file diff --git a/scripts_staging/Tasks/Start Eset scan V2.ps1 b/scripts_staging/Tasks/Start Eset scan V2.ps1 new file mode 100644 index 00000000..b28c15c4 --- /dev/null +++ b/scripts_staging/Tasks/Start Eset scan V2.ps1 @@ -0,0 +1,117 @@ +<# +.SYNOPSIS + Initiates a threat scan with ESET Endpoint Antivirus for every drive. + +.DESCRIPTION + RMM feature must be enabled on the endpoints under Tools -> ESET RMM. See https://help.eset.com/ees/10/en-US/how_activate_rmm.html + It will scan every disk with the agent and report on any finding + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.VERSION + Updated to scan through every drive and gather data +#> + +# Function to start a scan for a given drive +function Start-EsetScan { + param( + [string]$driveLetter + ) + $profile = "@In-depth scan" + $ermmPath = "C:\Program Files\ESET\ESET Security\ermm.exe" + & $ermmPath start scan --profile $profile --target $driveLetter +} + +# Function to get scan state +function Get-ScanState { + $scanInfoJson = & "C:\Program Files\ESET\ESET Security\eRmm.exe" get scan-info | ConvertFrom-Json + if ($scanInfoJson.result.'scan-info'.scans -eq $null) { + Write-Host "Error: No scans found in the output." + return $null + } else { + $latestScan = $scanInfoJson.result.'scan-info'.scans | Sort-Object -Property scan_id -Descending | Select-Object -First 1 + return $latestScan.state + } +} + +# Function to get scan information +function Get-ScanInfo { + $scanInfoJson = & "C:\Program Files\ESET\ESET Security\eRmm.exe" get scan-info | ConvertFrom-Json + if ($scanInfoJson.result.'scan-info'.scans -eq $null) { + Write-Host "Error: No scans found in the output." + return $null + } else { + $latestScan = $scanInfoJson.result.'scan-info'.scans | Sort-Object -Property scan_id -Descending | Select-Object -First 1 + $scanInfoJson.result.'scan-info'.scans = @($latestScan) + return $scanInfoJson + } +} + +# Get all drives +$drives = Get-PSDrive -PSProvider FileSystem + +foreach ($drive in $drives) { + # Ignore network drives + if ($drive.Provider.Name -eq "FileSystem") { + $driveLetter = $drive.Root + Write-Host "Initiating scan for drive $driveLetter" + Start-EsetScan -driveLetter $driveLetter + + $timeout = New-TimeSpan -Hours 3 + $sw = [diagnostics.stopwatch]::StartNew() + + $scanInProgress = $true + + while ($scanInProgress) { + if ($sw.elapsed -ge $timeout) { + Write-Host "Timeout: Script exceeded 3 hours for drive $driveLetter. Exiting." + break + } + + Start-Sleep -Seconds 60 + $scanState = Get-ScanState + + if ($scanState -eq "finished") { + $scanInProgress = $false + } elseif ($scanState -eq $null) { + Write-Host "Error: Scan state is null for drive $driveLetter. Exiting." + break + } + } + + $sw.Stop() + + Write-Host "Scan completed for drive $driveLetter. Final results:" + $finalResults = Get-ScanInfo + + if ($finalResults -eq $null) { + Write-Host "Error: No scan information is available for drive $driveLetter. Exiting." + break + } + + $scan = $finalResults.result.'scan-info'.scans[0] + $scanObject = [PSCustomObject]@{ + 'Drive' = $driveLetter + 'Scan ID' = $scan.scan_id + 'Timestamp' = $scan.timestamp + 'State' = $scan.state + 'Start Time' = $scan.start_time + 'Pause Time Remain' = $scan.pause_time_remain + 'Elapsed Time (ticks)' = $scan.elapsed_tickcount + 'Exit Code' = $scan.exit_code + 'Total Object Count' = $scan.total_object_count + 'Infected Object Count' = $scan.infected_object_count + 'Cleaned Object Count' = $scan.cleaned_object_count + 'PUA Object Count' = $scan.pua_object_count + 'Log Timestamp' = $scan.log_timestamp + 'Log Path' = $scan.log_path + 'Task Type' = $scan.task_type + 'Flags' = $scan.flags + } + + $scanObject | Format-List + } +} \ No newline at end of file diff --git a/scripts_staging/Tasks/Start Eset update.ps1 b/scripts_staging/Tasks/Start Eset update.ps1 new file mode 100644 index 00000000..c9a37f8c --- /dev/null +++ b/scripts_staging/Tasks/Start Eset update.ps1 @@ -0,0 +1,74 @@ +<# +.SYNOPSIS + This script attempts to execute the ESET Security update process, retrying if the process fails due to specific errors or no output being returned. + +.DESCRIPTION + The script runs the ESET Security update command using `ermm.exe`, capturing the output in a temporary file. + If the process exits with a non-zero code or produces invalid output, the script retries the operation up to a maximum retry count. + +.NOTES + Author: SAN + Date: 2024-12-11 + #public + +.CHANGELOG + +.TODO + +#> + +$retryCount = 10 +$retryDelaySeconds = 5 + +for ($i = 0; $i -lt $retryCount; $i++) { + try { + $outputFile = [System.IO.Path]::GetTempFileName() + $process = Start-Process -FilePath "C:\Program Files\ESET\ESET Security\ermm.exe" -ArgumentList "start update" -NoNewWindow -RedirectStandardOutput $outputFile -PassThru -Wait + + if ($process.ExitCode -ne 0) { + Write-Host "Error: The process exited with code $($process.ExitCode)." + exit 1 + } + + $output = Get-Content -Path $outputFile -Raw + + if ($null -eq $output) { + Write-Host "Error: No output received from the process." + exit 1 + } + + if ($output -notmatch '"error":null') { + Write-Host "Error: 'error':null not found in output." + Write-Host "Output: $output" + # Continue to retry + Write-Host "Retrying in $retryDelaySeconds seconds..." + Start-Sleep -Seconds $retryDelaySeconds + continue + } + + # If execution reaches here, the operation was successful + Write-Host "Update process completed successfully." + break + } catch { + $errorMessage = $_.Exception.Message + Write-Host "Attempt $($i+1): An error occurred: $errorMessage" + if ($errorMessage -match "Cannot process request because the process" -or $errorMessage -match "Impossible de traiter la demande, car le processus") { + Write-Host "Retrying in $retryDelaySeconds seconds..." + Start-Sleep -Seconds $retryDelaySeconds + continue + } + else { + exit 1 + } + } finally { + if (Test-Path $outputFile) { + Write-Host "Output: $output" + Remove-Item $outputFile -Force + } + } +} + +if ($i -eq $retryCount) { + Write-Host "Error: Maximum retry attempts reached. Update process failed." + exit 1 +} \ No newline at end of file diff --git a/scripts_staging/TasksUpdater/Updater P1 Template WU, SU, PS and Cleaner.ps1 b/scripts_staging/TasksUpdater/Updater P1 Template WU, SU, PS and Cleaner.ps1 new file mode 100644 index 00000000..ae7f9fbb --- /dev/null +++ b/scripts_staging/TasksUpdater/Updater P1 Template WU, SU, PS and Cleaner.ps1 @@ -0,0 +1,272 @@ +<# +.SYNOPSIS + Poor's man WSUS/SCCM part 1 + This PowerShell script is the first part of a multi-phase automation process designed to manage and schedule system maintenance tasks based on device attributes, primarily focusing on the hostname. + In this phase, the script outputs the device's category (e.g., DC, DB, APP) and odd/even status, as well as generates initial schedules for updates and cleanup tasks, which will be used in the subsequent parts of the process to refine and manage these tasks based on device-specific criteria. + +.DESCRIPTION + The script begins by checking the value of the environment variable `CurrentSchedules` to determine if the script should be executed or skipped. + If the variable contains the word "skip" or the environment variable `forcechange` is not set to "true", the script exits early. + If the script proceeds, it retrieves and outputs the hostname of the device and the current date. + + The device's category is determined by matching the hostname against predefined keywords for various roles such as Domain Controller (DC), Database Server (DB), Application Server (APP), Remote Desktop Server (RDS), and Exchange Server. + The script then calculates the sum of digits in the hostname to classify the device as "Odd" or "Even", which will influence the update schedule. + + Based on the device's category and odd/even classification, the script assigns specific weeks and days for key maintenance tasks: + * Windows updates + * Software updates + * PowerShell module updates + * Temporary file cleanup + + The script outputs these initial schedules, using the last digit of the hostname to determine the exact time for each task in a `HH:mm:ss` format. + These schedules will serve as a foundation for the next phase of automation. + +.EXEMPLE + CurrentSchedules={{agent.SchedulesTemplate}} + forcechange=false + +.NOTES + Author: SAN // MSA + Date: 01.01.24 + #public + +.CHANGELOG + 04.09.24 SAN Refactored to determine device categories and odd/even status for scheduling purposes. + +.TODO + add debug flag to env + rename env CurrentSchedules to env CurrentTemplate and CurrentSchedules to ExistingTemplate + + +#> + + +$Debug = $false + +# Check if forcechange is not "true" or if CurrentSchedules contains "skip" or "lock" +# lock and skip check is to avoid unforcene changes dues to an onboarding task overwriting important client information when a variable has been customised manualy +if ($Env:forcechange -ne "true" -or $Env:CurrentSchedules -match "skip|lock") { + # Check if CurrentSchedules exists and does not contain "Collected" + # "collected" is part of our default value for the field so it should be ignored when found and generate a new set of values. + + if ($Env:CurrentSchedules -ne $null -and $Env:CurrentSchedules -notmatch "Collected") { + # Cleanup of the variable in case empty lines have been added. + + # Split CurrentSchedules into lines, filter out lines containing "CurrentSchedules" + $filteredLines = $Env:CurrentSchedules -split "`n" | Where-Object { $_ -notmatch "CurrentSchedules" } + + # Join the lines, trim extra spaces, and remove consecutive line breaks + $cleanedOutput = ($filteredLines -join "`n").Trim() -replace "(\r?\n){2,}", "`n" + + # Output the cleaned string + Write-Host $cleanedOutput + + # Exit the script + exit 0 + } +} + + + + +# Get the hostname of the device +$hostname = [System.Net.Dns]::GetHostName() + +# Output the hostname +if ($Debug) { Write-Output "Hostname: $hostname" } + +# Get the current date +$currentDate = Get-Date + +# Output the current date and the name of the day with its occurrence in the month +$currentDay = $currentDate.DayOfWeek +$occurrenceInMonth = [math]::Ceiling($currentDate.Day / 7) +if ($Debug) { + Write-Output "Current Date: $($currentDate.ToString('MM/dd/yyyy'))" + Write-Output "Current Day: $($currentDay.ToString()) (Occurrence in Month: $occurrenceInMonth)" + Write-Output "-----------------------------------" +} + +# Define keywords for each category +$categories = @{ + "DC" = @("DC", "AD") + "DB" = @("SQL", "DB") + "APP" = @("IIS", "WEB", "APP") + "RDS" = @("RDS", "Broker") + "Exchange" = @("exch", "MBX") +} + +# Determine the device category based on keywords in the hostname +$deviceCategory = "Unidentified" +try { + $foundCategory = $false + foreach ($category in $categories.Keys) { + foreach ($keyword in $categories[$category]) { + if ($hostname -like "*$keyword*") { + $deviceCategory = $category + $foundCategory = $true + break + } + } + if ($foundCategory) { + break + } + } +} catch { + Write-Host "Error occurred while determining device category: $_" +} + +# Output the device category +if ($Debug) { Write-Output "Device Category: $deviceCategory" } + +# Function to calculate the sum of digits in a string +function Get-DigitSum($inputString) { + $sum = 0 + foreach ($char in $inputString.ToCharArray()) { + if ($char -match '\d') { + $sum += [int]$char + } + } + return $sum +} + +# Calculate the sum of digits in the hostname +$digitSum = Get-DigitSum $hostname + +if ($Debug) { Write-Output "Device sum of digits: $digitSum" } + +# Determine if the device is odd or even +if ($digitSum % 2 -eq 0) { + $oddEven = "Even" +} else { + $oddEven = "Odd" +} + +# Output if the device is odd or even +if ($Debug) { Write-Output "Device Sum: $oddEven" } + +Write-Output "$deviceCategory $oddEven" + +switch -Regex ($deviceCategory + $oddEven) { + "DCEven" { + $windowsUpdateDay = "3rd Tuesday" + $softwareUpdateDay = "1st Tuesday" + $tempFileCleanupDay = "4th Tuesday" + $powershellUpdateDay = "2nd Tuesday" + } + "DCOdd" { + $windowsUpdateDay = "2nd Tuesday" + $softwareUpdateDay = "4th Tuesday" + $tempFileCleanupDay = "3rd Tuesday" + $powershellUpdateDay = "1st Tuesday" + } + "DBEven" { + $windowsUpdateDay = "1st Wednesday" + $softwareUpdateDay = "3rd Wednesday" + $tempFileCleanupDay = "4th Wednesday" + $powershellUpdateDay = "2nd Wednesday" + } + "DBOdd" { + $windowsUpdateDay = "2nd Wednesday" + $softwareUpdateDay = "4th Wednesday" + $tempFileCleanupDay = "1st Wednesday" + $powershellUpdateDay = "3rd Wednesday" + } + "APPEven" { + $windowsUpdateDay = "3rd Thursday" + $softwareUpdateDay = "1st Thursday" + $tempFileCleanupDay = "4th Thursday" + $powershellUpdateDay = "2nd Thursday" + } + "APPOdd" { + $windowsUpdateDay = "2nd Thursday" + $softwareUpdateDay = "4th Thursday" + $tempFileCleanupDay = "1st Thursday" + $powershellUpdateDay = "3rd Thursday" + } + "RDSEven" { + $windowsUpdateDay = "4th Tuesday" + $softwareUpdateDay = "1st Tuesday" + $tempFileCleanupDay = "2nd Tuesday" + $powershellUpdateDay = "3rd Tuesday" + } + "RDSOdd" { + $windowsUpdateDay = "3rd Tuesday" + $softwareUpdateDay = "2nd Tuesday" + $tempFileCleanupDay = "4th Tuesday" + $powershellUpdateDay = "1st Tuesday" + } + "ExchangeEven" { + $windowsUpdateDay = "4th Wednesday" + $softwareUpdateDay = "2nd Wednesday" + $tempFileCleanupDay = "3rd Wednesday" + $powershellUpdateDay = "1st Wednesday" + } + "ExchangeOdd" { + $windowsUpdateDay = "3rd Wednesday" + $softwareUpdateDay = "1st Wednesday" + $tempFileCleanupDay = "4th Wednesday" + $powershellUpdateDay = "2nd Wednesday" + } + default { + $windowsUpdateDay = "4th Thursday" + $softwareUpdateDay = "2nd Thursday" + $tempFileCleanupDay = "3rd Thursday" + $powershellUpdateDay = "1st Thursday" + } +} + + +# Function to get scheduled time based on the last digit of hostname +function Get-ScheduledTime($lastDigit) { + switch ($lastDigit) { + {$_ -eq 0 -or $_ -eq 9} { + Get-Date -Hour 2 -Minute 30 -Second 0 + } + {$_ -eq 1 -or $_ -eq 2} { + Get-Date -Hour 3 -Minute 0 -Second 0 + } + {$_ -eq 3 -or $_ -eq 4} { + Get-Date -Hour 4 -Minute 30 -Second 0 + } + {$_ -eq 5 -or $_ -eq 6} { + Get-Date -Hour 3 -Minute 30 -Second 0 + } + {$_ -eq 7 -or $_ -eq 8} { + Get-Date -Hour 4 -Minute 00 -Second 0 + } + default { + Get-Date -Hour 2 -Minute 0 -Second 0 + } + } +} + +# Get the last digit of the hostname +$secondDigit = $null +foreach ($char in $hostname.ToCharArray()) { + if ($char -match '\d') { + if ($secondDigit -eq $null) { + $secondDigit = $char + } else { + $secondDigit += $char + break + } + } +} +$lastDigit = [int]$secondDigit + +# Get the scheduled time based on the last digit +$scheduledTime = Get-ScheduledTime $lastDigit + +# Output the time attributed +if ($Debug) { Write-Output "Time: $scheduledTime" } + +$dateTime = [datetime]$scheduledTime + +$timeOnly = $dateTime.ToString('HH:mm:ss') + + +Write-Host "WindowsUpdate: $windowsUpdateDay $timeOnly" +Write-Host "SoftwareUpdate: $softwareUpdateDay $timeOnly" +Write-Host "ModuleUpdate: $powershellUpdateDay $timeOnly" +Write-Host "tempFileCleanup: $tempFileCleanupDay $timeOnly" \ No newline at end of file diff --git a/scripts_staging/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 b/scripts_staging/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 new file mode 100644 index 00000000..fff4d4f5 --- /dev/null +++ b/scripts_staging/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 @@ -0,0 +1,114 @@ +<# +.SYNOPSIS + Poor's man WSUS/SCCM part 2 + This PowerShell script is the second phase of a multi-part automation process designed to generate precise task execution dates for system maintenance. + It processes task schedules from the first phase, generating MONTHLY dates that are valid only for that month. + + +.DESCRIPTION + The script processes the task schedules extracted from the environment variable `SchedulesTemplate`—which was generated in the first phase—and generates exact dates and times for tasks based on specified recurrence patterns (e.g., first Monday of the month). + A random time offset is applied to each task’s time for additional variability. + + The script checks each task in the schedule to determine whether it should be executed or skipped: + * Tasks marked with "SKIP" in the template will always be marked as "SKIPPED" in the output to prevent them from being processed further in the update cycle. + * Tasks with a recurrence pattern (e.g., 1st Monday 14:30:00) will be converted into specific dates for the current month, with a randomized time added to create variability. + + The final output is a set of task schedules valid only for the current month. + These schedules will be used for automation and execution in the subsequent phases of the process. + + This script is agnostic to the tasks names and allow to add as much as needed in the 1st part + +.EXAMPLE + SchedulesTemplate={{agent.SchedulesTemplate}} + +.NOTES + Author: SAN // MSA + Date: 06.08.24 + #public + +.CHANGELOG + 06.08.24 SAN Initial release for generating task dates based on monthly recurrence patterns. + 12.12.24 SAN changed var names to make it clear that the template is used rather than the old current values, fixed empty values in the env var + 17.12.24 SAN fixed cases where the date contained dashes + +.TODO + Add error handling for invalid schedule formats. + set date format in a global var and call it here to replace "MM/dd/yyyy" + +#> + +# Check if the environment variable "SchedulesTemplate" is available +if ($Env:SchedulesTemplate -eq $null -or $Env:SchedulesTemplate -match "Collected") { + Write-Output "Template found:" + Write-Output "$Env:SchedulesTemplate" + exit 1 +} + +# Split the environment variable "SchedulesTemplate" by newline and remove the first line +$rawSchedules = $Env:SchedulesTemplate -split "`n" +$rawSchedules = $rawSchedules[1..($rawSchedules.Length - 1)] + +# Function to get the date for the Nth occurrence of a specified day of the week in a given month and year +function Get-DateForNthOccurrence($year, $month, $nthOccurrence, $dayOfWeek) { + # Create a DateTime object for the first day of the specified month and year + $firstDayOfMonth = Get-Date -Year $year -Month $month -Day 1 + + # Find the first occurrence of the specified day of the week in the month + $firstOccurrenceDay = (1..7 | Where-Object { + ($firstDayOfMonth.AddDays($_ - 1).DayOfWeek.ToString() -eq $dayOfWeek) + })[0] + + # Calculate the date for the Nth occurrence of the day of the week + $occurrenceDate = $firstDayOfMonth.AddDays($firstOccurrenceDay - 1 + ($nthOccurrence - 1) * 7) + return $occurrenceDate +} + +# Get the current year and month for generating monthly dates +$currentYear = (Get-Date).Year +$currentMonth = (Get-Date).Month + +# Initialize an array to hold the updated schedules for this month +$updatedMonthlySchedules = @() + +# Set a random number of minutes to add variability to the times +$randomMinutesOffset = Get-Random -Maximum 30 + +# Process each raw schedule from the first phase +foreach ($schedule in $rawSchedules) { + + # Check if the schedule indicates a task to be skipped + if ($schedule -match "^\w+:SKIP$") { + $updatedMonthlySchedules += ($schedule -replace "SKIP", "SKIPPED") + } + # Check if the schedule matches a recurrence pattern (e.g., 1st Monday 14:30:00) + elseif ($schedule -match "(\d+)(st|nd|rd|th) (\w+) (\d{2}:\d{2}:\d{2})") { + $nthOccurrence = [int]$matches[1] # Extract the occurrence number (e.g., 1st, 2nd) + $dayOfWeek = $matches[3] # Extract the day of the week (e.g., Monday) + $time = $matches[4] # Extract the time (e.g., 14:30:00) + + # Get the date for the Nth occurrence of the day of the week + $taskDate = Get-DateForNthOccurrence -year $currentYear -month $currentMonth -nthOccurrence $nthOccurrence -dayOfWeek $dayOfWeek + + # Add random minutes to the specified time for variability + $timeWithOffset = [datetime]::ParseExact($time, "HH:mm:ss", $null) + $timeWithOffset = $timeWithOffset.AddMinutes($randomMinutesOffset) + $updatedTime = $timeWithOffset.ToString("HH:mm:ss") + + # Format the date as MM/dd/yyyy + $formattedTaskDate = $taskDate.ToString("MM/dd/yyyy").Replace('.', '/').Replace('-', '/') + + + # Replace the occurrence and day of the week in the schedule with the formatted date and time + $updatedSchedule = $schedule -replace "(\d+)(st|nd|rd|th) (\w+) \d{2}:\d{2}:\d{2}", "$formattedTaskDate $updatedTime" + + # Add the updated schedule to the array + $updatedMonthlySchedules += $updatedSchedule + + } else { + # If the schedule does not match any known pattern, add it as-is + $updatedMonthlySchedules += $schedule + } +} + +# Output the updated monthly schedules, formatted with newlines and proper spacing +$updatedMonthlySchedules -join "`n" -replace ': ', ':' | ForEach-Object { Write-Output $_ } \ No newline at end of file diff --git a/scripts_staging/TasksUpdater/Updater P3 Run Cleaner.ps1 b/scripts_staging/TasksUpdater/Updater P3 Run Cleaner.ps1 new file mode 100644 index 00000000..72ed00af --- /dev/null +++ b/scripts_staging/TasksUpdater/Updater P3 Run Cleaner.ps1 @@ -0,0 +1,57 @@ +<# +.SYNOPSIS + Poor's man WSUS/SCCM part 3 - Temporary File Cleanup + This PowerShell script is the third phase of a multi-part automation process, focused on cleaning temporary files and optimizing VHDX files. + It is designed to run daily and uses a parsed schedule to determine whether cleanup tasks should be executed on the current date. + +.DESCRIPTION + The script automates system cleanup tasks to maintain optimal performance and storage utilization: + * Runs Daily + * Utilizes the `Updater P3.5 Schedules parser` snippet to check if cleanup tasks are scheduled for the current date. + * Logs results using the `Logging` snippet. + * Runs the `Cleaner` snippet to delete temporary and unnecessary files. + * Executes the `VHDXCleaner` snippet to optimize VHDX files. + + + +.EXAMPLE + Schedules={{agent. Schedules}} + Company_folder_path={{global.Company_folder_path}} + VHDX_PATH={{agent.VHDXPath}} + +.NOTES + Author: SAN // MSA + Date: 06.08.2024 + Dependencies: + Logging snippet for logging + Updater P3.5 Schedules parser snippet for parsing the date + Cleaner & VHDXCleaner snippet to run the actual cleans + Ps7 snippnet + #public + + +.CHANGELOG + 28.11.24 SAN Incorporated VHDX cleaner. + 13.12.24 SAN Split logging from parser. + 06.08.25 SAN upgrade to ps7 + +#> + +{{CallPowerShell7}} + +# Name will be used for both the name of the log file and what line of the Schedules to parse +$PartName = "TempFileCleanup" + +# Call the parser snippet env Schedules will be passed +{{Updater P3.5 Schedules parser}} + +# Call the logging snippet env Company_folder_path will be passed +{{Logging}} + +Write-Output "Start Cleaner:" +{{Cleaner}} + +Write-Output "-----------------------------------------------------" +Write-Output "Start VHDX Cleaner:" + +{{VHDXCleaner}} \ No newline at end of file diff --git a/scripts_staging/TasksUpdater/Updater P3 Run PS.ps1 b/scripts_staging/TasksUpdater/Updater P3 Run PS.ps1 new file mode 100644 index 00000000..03936c11 --- /dev/null +++ b/scripts_staging/TasksUpdater/Updater P3 Run PS.ps1 @@ -0,0 +1,57 @@ +<# +.SYNOPSIS + Poor's man WSUS/SCCM part 3 - Module Updates + This PowerShell script is the third phase of a multi-part automation process, focusing on updating PowerShell modules installed from the PSGallery. + It is designed to run daily to ensure all modules are up to date and log the update process for tracking purposes. + +.DESCRIPTION + The script performs the following actions: + * Runs daily + * Uses the `Updater P3.5 Schedules parser` snippet to parse and check if module updates are scheduled for the current date. + * Logs update results using the `Logging` snippet. + + For each installed module, the script attempts to update it using the `Update-Module` cmdlet. It then logs the version information of all updated modules for tracking purposes. +.EXAMPLE + Schedules={{agent.Schedules}} + Company_folder_path={{global.Company_folder_path}} + +.NOTES + Author: SAN // MSA + Date: 06.08.24 + Dependencies: + Logging snippet for logging + Updater P3.5 Schedules parser snippet for parsing the date + CallPowerShell7 snippet to upgrade the script to pwsh + #public + +.CHANGELOG + 13.12.24 SAN Split logging from parser. + 03.06.25 SAN move PS7 call at the start +#> + +# Call the pwsh snippet +{{CallPowerShell7}} + +# Name will be used for both the name of the log file and what line of the Schedules to parse +$PartName = "ModuleUpdate" + +# Call the parser snippet env Schedules will be passed +{{Updater P3.5 Schedules parser}} + +# Call the logging snippet env Company_folder_path will be passed +{{Logging}} + +# Set TLS version to 1.2 +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +# Update installed modules from PSGallery +Get-InstalledModule | ForEach-Object { +Write-Host "Updating module: $($_.Name)" +Update-Module -Name $_.Name -Force +} + +# Display last updates information +$installedModules = Get-InstalledModule +foreach ($module in $installedModules) { + "Module: $($module.Name) - Version: $($module.Version)" +} \ No newline at end of file diff --git a/scripts_staging/TasksUpdater/Updater P3 Run SU.ps1 b/scripts_staging/TasksUpdater/Updater P3 Run SU.ps1 new file mode 100644 index 00000000..bb255d86 --- /dev/null +++ b/scripts_staging/TasksUpdater/Updater P3 Run SU.ps1 @@ -0,0 +1,175 @@ +<# +.SYNOPSIS + Poor's man WSUS/SCCM part 3 - Software Update + This PowerShell script is the third phase of a multi-part automation process. + It manages the daily software update process, identifies pending system reboots, and schedules a reboot if necessary after completing updates. + It is designed to run daily to ensure all modules are up to date and log the update process for tracking purposes. + +.DESCRIPTION + This script is designed to ensure systems are kept up to date with minimal disruption: + * Run daily + * Uses the `Updater P3.5 Schedules parser` snippet to determine the current task's schedule. + * Logs all actions and outputs through the `Logging` snippet for troubleshooting and auditing. + * Leverages Chocolatey to identify outdated software packages and upgrades them. + * Automatically schedules a reboot if required, using the parsed time from the schedule. + +.EXAMPLE + Schedules={{agent.Schedules}} + Company_folder_path={{global.Company_folder_path}} + + trmm_sign_download_token={{global.trmm_sign_download_token}} + trmm_api_target={{global.RMM_API_URL}} + +.NOTES + Author: SAN // MSA + Date: 06.08.2024 + Dependencies: + Logging snippet for logging + Updater P3.5 Schedules parser snippet for parsing the date + CallPowerShell7 snippet to upgrade the script to pwsh + Update TRMM agent snipper for agent upgrade + #public + +.CHANGELOG + 24.10.24 SAN Conditional reboot added and removed the reboot snippet; this part is going to be canned. + 28.10.24 SAN Added even flag for reboot. + 04.11.24 SAN More verbose output for the reboot to help troubleshoot. + 27.11.24 SAN More verbose output for the reboot and fixed some lack of logs from the Chocolatey commands. + 27.11.24 SAN Disabled file rename check due to issues. + 13.12.24 SAN Split logging from parser. + 06.03.25 SAN added TRMM agent updater. + 11.09.25 SAN disabled choco download progress output to shrink log size + +.TODO + Fix rename? +#> + + +# Name will be used for both the name of the log file and what line of the Schedules to parse +$PartName = "SoftwareUpdate" + +# Call the parser snippet env Schedules will be passed +{{Updater P3.5 Schedules parser}} + +# Call the logging snippet env Company_folder_path will be passed +{{Logging}} + +# Function to check if a reboot is pending and return reasons +function Get-PendingReboot { + $rebootRequired = $false + $reasons = @() # Array to store reasons for reboot + + # Check for Windows Update reboot required + $WUReboot = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -ErrorAction SilentlyContinue + if ($WUReboot) { + $reasons += "Windows Update requires a reboot." + $rebootRequired = $true + } + + # DISABLED DUE TO FALSE POSITIVE + # Check for pending file rename operations + # $PendingFileRenameOperations = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name "PendingFileRenameOperations" -ErrorAction SilentlyContinue + if ($PendingFileRenameOperations) { + $reasons += "Pending file rename operations require a reboot." + $rebootRequired = $true + } + + # Check if Component-Based Servicing (CBS) requires a reboot + $CBSReboot = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -ErrorAction SilentlyContinue + if ($CBSReboot) { + $reasons += "Component-Based Servicing requires a reboot." + $rebootRequired = $true + } + + # Check for pending computer rename + $ComputerRename = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ActiveComputerName" -ErrorAction SilentlyContinue + $PendingComputerRename = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName" -ErrorAction SilentlyContinue + if ($ComputerRename -and $PendingComputerRename -and ($ComputerRename.ComputerName -ne $PendingComputerRename.ComputerName)) { + $reasons += "Computer rename operation requires a reboot." + $rebootRequired = $true + } + + # Check if Windows Installer (MSI) requires a reboot + $PendingMSIReboot = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\InProgress" -ErrorAction SilentlyContinue + if ($PendingMSIReboot) { + $reasons += "Windows Installer (MSI) operation requires a reboot." + $rebootRequired = $true + } + + # Check if Group Policy client requires a reboot + $GPReboot = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\RebootRequired" -ErrorAction SilentlyContinue + if ($GPReboot) { + $reasons += "Group Policy changes require a reboot." + $rebootRequired = $true + } + + # Check for pending package installations + $PendingPackageInstalls = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Updates" -ErrorAction SilentlyContinue + if ($PendingPackageInstalls) { + $reasons += "Pending package installations require a reboot." + $rebootRequired = $true + } + + # Return an object with reboot status and reasons + return [PSCustomObject]@{ + RebootRequired = $rebootRequired + Reasons = $reasons + } +} + +# check if reboot is needed +$result = Get-PendingReboot +if ($result.RebootRequired) { + Write-Host "Reboot is pending BEFORE updates for the following reasons:" + $result.Reasons | ForEach-Object { Write-Host "- $_" } +} else { + Write-Host "No Reboot is pending BEFORE updates." +} + +# The following section is in place due to the fact that ps logging does not capture RAW output from choco please do not touch +# List outdated packages and capture output +$outdatedPackages = choco outdated | Out-String +# Upgrade all packages and capture output +$upgradeResult = choco upgrade all -y --no-progress| Out-String + +Write-Host "" +Write-Host "------------------------------------------------------------" +Write-Host "" +Write-Host "Chocolatey Outdated Packages before upgrade:" +Write-Host $outdatedPackages +Write-Host "------------------------------------------------------------" +Write-Host "Chocolatey Upgrade Result:" +Write-Host $upgradeResult +Write-Host "" +Write-Host "------------------------------------------------------------" +Write-Host "" +Write-Host "------------------------------------------------------------" +Write-Host "TRMM Agent update" +{{Update TRMM agent}} + + +# Check if a reboot is pending and reboot if necessary +$result = Get-PendingReboot +if ($result.RebootRequired) { + Write-Host "Reboot is pending AFTER update for the following reasons:" + $result.Reasons | ForEach-Object { Write-Host "- $_" } + + Write-Host "The system will reboot at $scheduledTime." + $timeDifference = New-TimeSpan -Start (Get-Date) -End $scheduledTime + $SetReboot = [int]$timeDifference.TotalSeconds + + # Schedule the system reboot + Write-Host "shutdown.exe /r /f /t $SetReboot /c Reboot done by RMM task, required after packages updates /d p:4:1" + shutdown.exe /r /f /t $SetReboot /c "Reboot done by RMM task, required after packages updates" /d p:4:1 + + # Output a warning message + $minutes = [math]::Floor($SetReboot / 60) # Rounding + $Message = "The system will reboot in $minutes minutes. Please save your work." + Write-Host $Message + msg * $Message + exit 0 + +} else { + Write-Host "No reboot is pending. Exiting gracefully" + exit 0 +} \ No newline at end of file diff --git a/scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 b/scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 new file mode 100644 index 00000000..8885d014 --- /dev/null +++ b/scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 @@ -0,0 +1,97 @@ +<# +.SYNOPSIS + Poor's man WSUS/SCCM part 3 - Windows Update + This PowerShell script is the third phase of a multi-part automation process for managing system maintenance tasks. + It checks and executes scheduled tasks for Windows updates, using the dates and times generated in the second phase. + This script ensures that the updates are installed at the specified time and date and reboots the system if required. + It is designed to run daily but will only execute the windows updates on the parsed day otherwise will simply display the last log. + It also manages a blacklist of KBs to prevent their installation, with validation of the provided KBs to ensure correct format. + +.DESCRIPTION + The script processes tasks by: + * Runs Daily + * Parsing schedules using the `Updater P3.5 Schedules parser` snippet to determine the next applicable date and time for updates. + * Logging actions and results using the `Logging` snippet. + * Ensuring compatibility with PowerShell 7 through the `CallPowerShell7` snippet. + * Prevents installation of blacklisted KBs by hiding them using `Hide-WindowsUpdate`. + + The script validates the availability of the `PSWindowsUpdate` module, installing it if necessary. + +.EXAMPLE + Schedules={{agent.Schedules}} + Company_folder_path={{global.Company_folder_path}} + BLACKLISTED_KBS=KB1234567,KB1234567,KB1234567 + +.NOTES + Author: SAN // MSA + Date: 13.12.2024 + Dependencies: + Logging snippet for logging + Updater P3.5 Schedules parser snippet for parsing the date + CallPowerShell7 snippet to upgrade the script to pwsh + #public + +.CHANGELOG + 04.10.24 SAN Removed last output; the data is non-sense. + 13.12.24 SAN Split logging from parser. + 30.01.25 SAN Changed output for troubleshooting + 14.04.25 SAN Added validation for KB format and warnings for invalid KBs. + 03.06.25 SAN move PS7 call at the start +#> + +# Call the pwsh snippet +{{CallPowerShell7}} + +# Name will be used for both the name of the log file and what line of the Schedules to parse +$PartName = "WindowsUpdate" + +# Call the parser snippet env Schedules will be passed +{{Updater P3.5 Schedules parser}} + +# Call the logging snippet env Company_folder_path will be passed +{{Logging}} + +# Set TLS version to 1.2 +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + + +# Check if PSWindowsUpdate module is available +if (Get-Module -ListAvailable -Name PSWindowsUpdate) { + #Write-Output "PSWindowsUpdate is already installed" +} else { + # If module is not available, install it + Write-Output "Installing PSWindowsUpdate module..." + Install-Module -Name PSWindowsUpdate -Force + + # Check if there was an error during installation and attempt to install NuGet package provider if necessary + if ($?) { + Write-Output "PSWindowsUpdate module installed successfully." + } else { + Write-Output "Error occurred during PSWindowsUpdate module installation. Attempting to install NuGet package provider..." + Install-PackageProvider -Name NuGet -Force + + # Re-attempt to install PSWindowsUpdate module + Write-Output "Re-running PSWindowsUpdate module installation..." + Install-Module -Name PSWindowsUpdate -Force + } +} + +# Hide KB to avoid installations +$kbList = @() +if ($env:BLACKLISTED_KBS) { $kbList += $env:BLACKLISTED_KBS -split ',' | ForEach-Object { $_.Trim() } } + +$kbList = $kbList | ForEach-Object { + if ($_ -match '^KB\d{7}$') { $_ } + else { Write-Warning "Invalid KB format: '$_'"; $null } +} | Select-Object -Unique + +foreach ($kb in $kbList) { + Write-Host "Hiding $kb..." + Hide-WindowsUpdate -KBArticleID $kb -Verbose +} + + +# Run Windows update with PSWindowsUpdate and rebooting at time found in parser +Write-Host "Running windows updates:" +Write-Host "Get-WindowsUpdate -Verbose -Install -AcceptAll -AutoReboot -ScheduleReboot $scheduledTime" +Get-WindowsUpdate -Verbose -Install -AcceptAll -AutoReboot -ScheduleReboot $scheduledTime diff --git a/scripts_staging/Tools/Activate windows with KMS.ps1 b/scripts_staging/Tools/Activate windows with KMS.ps1 new file mode 100644 index 00000000..8048f82b --- /dev/null +++ b/scripts_staging/Tools/Activate windows with KMS.ps1 @@ -0,0 +1,128 @@ +<# +.SYNOPSIS + Script to activate Windows using a KMS server, with support for specifying the server and port via an environment variable. + +.DESCRIPTION + This script checks for the presence of the `kms_server` environment variable. If found, it sets the KMS server and initiates the Windows activation process using the specified server and port. If the `kms_server` is not set, the script prompts the user to set it. + +.EXAMPLE + kms_server=kms.example.com:1688 + kms_server=host:port + +.NOTES + Author: SAN + Date: 14.11.24 + #public + +.CHANGELOG + 11.12.24 SAN Code Cleanup + 16.07.25 SAN added network check + check if activation is successful + +.TODO + Convert the script to use the PowerShell module as the future of vbs is uncertain. + see code bellow for prototype + +#> +# Check if the 'kms_server' environment variable exists +if (-not $env:kms_server) { + Write-Host "The 'kms_server' environment variable is not set." + exit 1 +} + +# Parse kms_server into host and optional port (defaults to 1688 if not provided) +$rawKmsServer = $env:kms_server +$kmsParts = $rawKmsServer -split ":", 2 +$kmsHost = $kmsParts[0] +$kmsPort = if ($kmsParts.Count -eq 2 -and $kmsParts[1]) { $kmsParts[1] } else { "1688" } + +Write-Host "Parsed KMS server: Host = $kmsHost, Port = $kmsPort" + +# Test TCP connectivity to the KMS server +Write-Host "Testing network connectivity to $kmsHost on port $kmsPort..." +try { + $testResult = Test-NetConnection -ComputerName $kmsHost -Port ([int]$kmsPort) -WarningAction SilentlyContinue + if (-not $testResult.TcpTestSucceeded) { + Write-Host "Network test failed. Cannot reach $kmsHost on port $kmsPort." + exit 1 + } + Write-Host "Network test passed. KMS server is reachable." +} catch { + Write-Host "An error occurred during network test: $_" + exit 1 +} + +# Set KMS server +Write-Host "Setting KMS server to: $rawKmsServer..." +try { + Start-Process -FilePath "cscript.exe" -ArgumentList "$env:SystemRoot\System32\slmgr.vbs /skms $rawKmsServer" -NoNewWindow -Wait -ErrorAction Stop + Write-Host "Successfully set KMS server." +} catch { + Write-Host "Failed to set KMS server. Error: $_" + exit 1 +} + +# Activate Windows +Write-Host "Activating Windows..." +try { + Start-Process -FilePath "cscript.exe" -ArgumentList "$env:SystemRoot\System32\slmgr.vbs /ato" -NoNewWindow -Wait -ErrorAction Stop + Write-Host "Activation command sent." +} catch { + Write-Host "Windows activation command failed. Error: $_" + exit 1 +} + +# Check Windows activation status +Write-Host "Checking Windows activation status..." +try { + $licenseStatus = (Get-CimInstance -Query "SELECT LicenseStatus FROM SoftwareLicensingProduct WHERE PartialProductKey IS NOT NULL AND LicenseStatus = 1").LicenseStatus + if ($licenseStatus -eq 1) { + Write-Host "Windows is activated." + exit 0 + } else { + Write-Host "Windows is NOT activated." + exit 1 + } +} catch { + Write-Host "Failed to check activation status. Error: $_" + exit 1 +} + + + + +<# + +# Check if the environment variable 'kms_server' exists +if ($env:kms_server) { + # Extract the KMS server address and port + $kmsServerInfo = $env:kms_server + $kmsServerParts = $kmsServerInfo -split ':' + + if ($kmsServerParts.Length -eq 2) { + $kmsServer = $kmsServerParts[0] + $kmsPort = $kmsServerParts[1] + Write-Host "Found 'kms_server' environment variable: $kmsServer:$kmsPort" + + # Install the slmgr-ps module if it's not already installed + if (-not (Get-Module -ListAvailable -Name slmgr-ps)) { + Write-Host "Installing slmgr-ps module..." + Install-Module -Name slmgr-ps -Force -AllowClobber + } + + # Import the module + Import-Module -Name slmgr-ps -Force + + # Activate Windows using the KMS server and port extracted + Write-Host "Activating Windows with KMS server: $kmsServer and port: $kmsPort" + Start-WindowsActivation -KMSServerFQDN $kmsServer -KMSServerPort $kmsPort + + Write-Host "Windows activation process initiated." + } else { + Write-Host "Invalid 'kms_server' format. It should be in the form 'server:port'." + } +} else { + Write-Host "The 'kms_server' environment variable is not set." + Write-Host "Please set the 'kms_server' variable before running the script." +} + +#> \ No newline at end of file diff --git a/scripts_staging/Tools/Cleanup temp files.ps1 b/scripts_staging/Tools/Cleanup temp files.ps1 new file mode 100644 index 00000000..7afbd3b8 --- /dev/null +++ b/scripts_staging/Tools/Cleanup temp files.ps1 @@ -0,0 +1,35 @@ +<# +.SYNOPSIS + Automate cleaning up the C:\ drive with low disk space warning. + +.DESCRIPTION + Cleans the C: drive's Windows Temporary files, Windows SoftwareDistribution folder, + the local users Temporary folder, IIS logs(if applicable) and empties the recycle bin. + By default this script leaves files that are newer than 30 days old however this variable can be edited. + This script will typically clean up anywhere from 1GB up to 15GB of space from a C: drive. + + +.NOTES + Author: SAN + Date: 01.01.24 + #public + Dependencies: + Cleaner Snippet + +.EXEMPLE + DaysToDelete=25 + +.CHANGELOG + 25.10.24 SAN Changed to 25 day of IIS logs + 19.11.24 SAN Added adobe updates folder to cleanup + 19.11.24 SAN removed colors + 19.11.24 SAN added cleanup of search index + 17.12.24 SAN Full code refactoring, set a single value for file expiration + +.TODO + Integrate bleachbit this would help avoid having to update this script too often. + +#> + + +{{Cleaner}} \ No newline at end of file diff --git a/scripts_staging/Tools/DNS Cache inspector.ps1 b/scripts_staging/Tools/DNS Cache inspector.ps1 new file mode 100644 index 00000000..33bd44e8 --- /dev/null +++ b/scripts_staging/Tools/DNS Cache inspector.ps1 @@ -0,0 +1,104 @@ +<# +.SYNOPSIS +This script inspects and displays the Windows DNS client cache entries using either +`Get-DnsClientCache` (preferred) or by parsing the output of `ipconfig /displaydns` (fallback). + +.DESCRIPTION +The script queries the local DNS client cache to show cached domain entries, record types, +record data, and TTL values. If the PowerShell cmdlet `Get-DnsClientCache` is unavailable, +the script falls back to parsing the `ipconfig /displaydns` output. + +The script supports filtering cache entries based on a target string provided +through the environment variable `DNS_TARGET`. +If no environment variable is set, it defaults to `*` (all entries). + +.EXAMPLE + DNS_TARGET=*.microsoft.com + +.NOTE + Author: SAN + Date: 01.10.25 + #Public + +.CHANGELOG + +#> + +# Get filter target from environment variable +$Filter = $env:DNS_TARGET +if ([string]::IsNullOrWhiteSpace($Filter)) { + $Filter = '*' +} + +Write-Host '' +Write-Host '--- Windows DNS Cache Inspector ---' +Write-Host 'Target filter:' $Filter +Write-Host '' + +# Try Get-DnsClientCache first +try { + $results = Get-DnsClientCache -ErrorAction Stop | Where-Object { + ($_.Name -like $Filter) -or ($_.RecordData -like $Filter) -or ($_.RecordType -like $Filter) + } | Select-Object Name, Entry, RecordType, RecordData, + @{Name='TTL';Expression={$_.TimeToLive}}, + Section, Status +} catch { + $results = @() +} + +# Fallback to ipconfig /displaydns +if (-not $results -or $results.Count -eq 0) { + Write-Host 'No results from Get-DnsClientCache, falling back to ipconfig parsing...' + + $raw = ipconfig /displaydns 2>&1 + $blocks = -split ($raw -join "`n"), "`n`r?`n" + + $results = @() + foreach ($b in $blocks) { + $lines = $b -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } + if ($lines.Count -eq 0) { continue } + + $entry = [PSCustomObject]@{ + Name = $null + RecordType = $null + RecordData = $null + TTL = $null + CacheEntryType = $null + Section = $null + } + + foreach ($line in $lines) { + if ($line -match 'Record Name\s*:\s*(.+)$') { $entry.Name = $matches[1].Trim() } + elseif ($line -match 'Record Type\s*:\s*(.+)$') { $entry.RecordType = $matches[1].Trim() } + elseif ($line -match 'Time To Live\s*:\s*(\d+)') { $entry.TTL = [int]$matches[1] } + elseif ($line -match 'Data:\s*(.+)$') { $entry.RecordData = $matches[1].Trim() } + elseif ($line -match 'A\s+Record\s*:\s*(.+)$') { $entry.RecordData = $matches[1].Trim() } + elseif ($line -match 'Cache Entry Type\s*:\s*(.+)$') { $entry.CacheEntryType = $matches[1].Trim() } + elseif ($line -match 'Section\s*:\s*(.+)$') { $entry.Section = $matches[1].Trim() } + } + + if ($entry.Name) { $results += $entry } + } + + if ($Filter -ne '*') { + $results = $results | Where-Object { + ($_.Name -like $Filter) -or ($_.RecordData -like $Filter) -or ($_.RecordType -like $Filter) + } + } +} + +# Output +$results = $results | Sort-Object Name +Write-Host '' +Write-Host 'Entries found:' $results.Count +Write-Host '' + +if ($results.Count -gt 0) { + $results | Format-Table -AutoSize + Write-Host '' + Write-Host 'Match found — exiting with code 1.' + exit 1 +} else { + Write-Host 'No DNS cache entries found.' + exit 0 +} diff --git a/scripts_staging/Tools/Deploy diagnostic toolkit.ps1 b/scripts_staging/Tools/Deploy diagnostic toolkit.ps1 new file mode 100644 index 00000000..428a1ddb --- /dev/null +++ b/scripts_staging/Tools/Deploy diagnostic toolkit.ps1 @@ -0,0 +1,41 @@ +<# +.SYNOPSIS + Installs or uninstalls Sysinternals and nirlauncher using Chocolatey. + +.DESCRIPTION + This script installs or uninstalls the Sysinternals and nirlauncher packages via Chocolatey. . + If the environment variable "uninstall" is set to "1", it will uninstall both packages instead of installing. + +.EXAMPLE + uninstall=1 + +.NOTES + Author: SAN + Date: 26.06.25 + #public + +.CHANGELOG + +#> + + +if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { + Write-Error "Chocolatey is not installed or not in PATH." + exit 1 +} + +$uninstall = $env:uninstall + +if ($uninstall -eq "1") { + Write-Host "Start uninstall" + choco uninstall sysinternals -y + choco uninstall nirlauncher -y + choco uninstall powertoys -y +} else { + Write-Host "Start install" + choco install sysinternals -y --ignore-checksums --no-progress --force + choco install nirlauncher -y --package-parameters="/Sysinternals" --no-progress --force + choco install powertoys -y --no-progress --force + + Write-Host "Launcher available at C:\tools\NirLauncher" +} \ No newline at end of file diff --git a/scripts_staging/Tools/Expand partitiondrivedisk size.ps1 b/scripts_staging/Tools/Expand partitiondrivedisk size.ps1 new file mode 100644 index 00000000..14f42a99 --- /dev/null +++ b/scripts_staging/Tools/Expand partitiondrivedisk size.ps1 @@ -0,0 +1,154 @@ +<# +.SYNOPSIS + This script expands the partitions on a disk to use the maximum available space. + It can expand all partitions with assigned drive letters or target a specific partition + based on the drive letter provided via the `-ForceLetter` parameter. + +.DESCRIPTION + The script scans all partitions on the system that have a drive letter assigned. For each partition, + it checks if there is available space that can be used to expand the partition to its maximum possible size. + If the `-ForceLetter` parameter is provided, the script will only attempt to expand the partition + corresponding to that drive letter. + + Before expanding any partition, the script checks if the disk contains a recovery partition. + If a recovery partition is found, the script skips expanding any partitions on that disk to prevent + potential issues with system recovery. + +.PARAMETER ForceLetter + Optional. Specifies the drive letter of the partition to expand. If this parameter is provided, + only the specified partition will be processed. If the drive letter is invalid or does not exist, + an error message will be displayed. + +.NOTE + Author: SAN + Date: 19.08.24 + #public + + +#> + + +param ( + [string]$ForceLetter +) + +# Function to check for the presence of a recovery partition on a disk +function Check-RecoveryPartition { + param ( + [int]$DiskNumber + ) + + # Create a diskpart script to list partitions on the specified disk + $diskpartScriptContent = "select disk $DiskNumber `n list partition" + + # Write the diskpart script to a temporary file + $tempFile = [System.IO.Path]::GetTempFileName() + [System.IO.File]::WriteAllText($tempFile, $diskpartScriptContent) + + # Run the diskpart script and capture the output + $diskpartOutput = & diskpart /s $tempFile + + # Convert the output to an array of lines + $lines = $diskpartOutput -split "`n" + + # Check if the output contains a recovery partition + $recoveryLine = $lines | Where-Object { $_ -match "Recovery" } + + # Cleanup temporary file + Remove-Item $tempFile -ErrorAction SilentlyContinue + + return $recoveryLine -match "Recovery" +} + +# Function to expand a partition to its maximum available size +function Expand-Partition { + param ( + [string]$DriveLetter, + [int]$DiskNumber, + [int]$PartitionNumber + ) + + # Check if the disk contains a recovery partition + if (Check-RecoveryPartition -DiskNumber $DiskNumber) { + Write-Output "Recovery partition found on Disk $DiskNumber. Skipping expansion for partition $DriveLetter." + Write-Output "----" + return + } + + # Get the partition with the specified drive letter + $partition = Get-Partition | Where-Object { $_.DriveLetter -eq $DriveLetter -and $_.DiskNumber -eq $DiskNumber -and $_.PartitionNumber -eq $PartitionNumber } + + if ($partition) { + # Get the current size of the partition + $currentSize = $partition.Size + + # Get the maximum size available for the partition + $size = Get-PartitionSupportedSize -DiskNumber $DiskNumber -PartitionNumber $PartitionNumber + + # Calculate the new size and difference + $newSize = $size.SizeMax + $sizeDifference = $newSize - $currentSize + + Write-Output "Partition $($partition.DriveLetter):" + Write-Output " Disk Number: $DiskNumber" + Write-Output " Partition Number: $PartitionNumber" + Write-Output " Current Size: $([math]::round($currentSize / 1GB, 2)) GB" + Write-Output " Maximum Size: $([math]::round($newSize / 1GB, 2)) GB" + Write-Output " Size Difference: $([math]::round($sizeDifference / 1GB, 2)) GB" + + if ($currentSize -lt $newSize) { + try { + Write-Output " Expanding partition..." + Resize-Partition -DriveLetter $DriveLetter -Size $newSize + Write-Output " Expansion successful." + } catch { + Write-Output " Error expanding partition: $_" + } + } else { + Write-Output " Partition is already at its maximum size." + } + + Write-Output "----" + } else { + Write-Output " Partition with drive letter $DriveLetter not found." + } +} + +# Function to expand all partitions with drive letters +function Expand-AllPartitions { + # Get all drives and their partitions + $partitions = Get-Partition + + foreach ($partition in $partitions) { + # Retrieve drive letter + $driveLetter = $partition.DriveLetter + + # Skip partitions without a drive letter + if (-not $driveLetter) { + continue + } + + # Retrieve disk number and partition number + $diskNumber = $partition.DiskNumber + $partitionNumber = $partition.PartitionNumber + + # Call Expand-Partition for each drive letter + Expand-Partition -DriveLetter $driveLetter -DiskNumber $diskNumber -PartitionNumber $partitionNumber + } +} + +# Determine which partitions to expand +if ($ForceLetter) { + # Get the partition with the specified drive letter + $partition = Get-Partition | Where-Object { $_.DriveLetter -eq $ForceLetter } + + if ($partition) { + # Call Expand-Partition for the specified drive letter + Expand-Partition -DriveLetter $ForceLetter -DiskNumber $partition.DiskNumber -PartitionNumber $partition.PartitionNumber + } else { + Write-Output "Drive letter $ForceLetter not found." + } +} else { + # Expand all partitions with drive letters + Expand-AllPartitions +} \ No newline at end of file diff --git a/scripts_staging/Tools/Force Azureo365 AD sync.ps1 b/scripts_staging/Tools/Force Azureo365 AD sync.ps1 new file mode 100644 index 00000000..c79c43ef --- /dev/null +++ b/scripts_staging/Tools/Force Azureo365 AD sync.ps1 @@ -0,0 +1,35 @@ +<# +.SYNOPSIS + Initiates an Azure AD synchronization cycle. + +.DESCRIPTION + This script checks if the ADSync module is loaded, and if not, imports it. + It then triggers a delta synchronization cycle using the `Start-ADSyncSyncCycle` command. + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.CHANGELOG + 12.12.24 Simple polish + +#> + +# Check if the ADSync module is already imported, if not, import it +if (-not (Get-Module -Name 'ADSync' -ErrorAction SilentlyContinue)) { + Write-Host "Importing the Azure AD Sync module..." + Import-Module ADSync +} + +try { + Write-Host "Starting Azure AD Delta Synchronization..." + Start-ADSyncSyncCycle -PolicyType Delta + Write-Host "Azure AD sync initiated successfully!" + Write-Host "Please check the Azure AD Connect Health for status." + +} +catch { + Write-Host "An error occurred while initiating the Azure AD sync: $_" + Write-Host "Please check the Azure AD Connect logs for more details." +} diff --git a/scripts_staging/Tools/Get last shutdown info.ps1 b/scripts_staging/Tools/Get last shutdown info.ps1 new file mode 100644 index 00000000..f511564d --- /dev/null +++ b/scripts_staging/Tools/Get last shutdown info.ps1 @@ -0,0 +1,159 @@ +<# +.SYNOPSIS + Retrieves and logs system uptime and shutdown event information. + +.DESCRIPTION + This script retrieves the system's last boot time and calculates the uptime in days, hours, minutes, and seconds. + It queries the Windows Event Log for the most recent shutdown-related event, + extracts detailed shutdown metadata (including reason, process, type, and user), and optionally logs the data + to a CSV file if the 'sendtolog' environment variable is set to '1'. + + +.EXEMPLE + sendtolog=1 + Company_folder_path={{global.Company_folder_path}} + Company_folder_path=c:\folder + +.NOTES + Author: SAN + Created: 03.10.24 + Last Updated: 08.05.25 + #public + +.CHANGELOG + SAN 12.12.24 Code cleanup + SAN 08.05.25 added detailed event property logging, added 6008 and cleanup output + +#> + + +# Get system boot time and uptime +$lastBootTime = (Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime +$uptime = (Get-Date) - $lastBootTime +$formattedBootTime = $lastBootTime.ToString("yyyy-MM-dd HH:mm:ss") + +# Try to retrieve shutdown events +try { + $shutdownEvents = Get-WinEvent -LogName System -ErrorAction SilentlyContinue + $filteredEvents = $shutdownEvents | Where-Object { $_.Id -eq 1074 -or $_.Id -eq 6008 } + $shutdownEvent = $filteredEvents | Select-Object -First 1 +} catch { + Write-Output "Error fetching shutdown events: $_" + return +} + +# Output boot info +Write-Output "===========================" +Write-Output "Last Reboot Information" +Write-Output "===========================" +Write-Output "Last Boot Time : $formattedBootTime" +Write-Output "Uptime : $($uptime.Days)d $($uptime.Hours)h $($uptime.Minutes)m $($uptime.Seconds)s" + +if ($shutdownEvent) { + $eventTime = $shutdownEvent.TimeCreated + $eventId = $shutdownEvent.Id + $provider = $shutdownEvent.ProviderName + $msg = $shutdownEvent.Message -replace '\r\n',' ' + + Write-Output "Event Log Time : $eventTime" + Write-Output "Event ID : $eventId" + Write-Output "Event Source : $provider" + Write-Output "Event Message : $msg" + + # Initialize variables for extended shutdown details + $exe = ""; $machine = ""; $reason = ""; $code = ""; $type = ""; $info = ""; $user = "" + + if ($eventId -eq 1074) { + $exe = $shutdownEvent.Properties[0].Value + $machine = $shutdownEvent.Properties[1].Value + $reason = $shutdownEvent.Properties[2].Value + $code = $shutdownEvent.Properties[3].Value + $type = $shutdownEvent.Properties[4].Value + $info = $shutdownEvent.Properties[5].Value + $user = $shutdownEvent.Properties[6].Value + + Write-Output "===========================" + Write-Output "All Event Properties" + Write-Output "===========================" + Write-Output "Initiating Process/Executable : $exe" + Write-Output "Initiating Machine : $machine" + Write-Output "Shutdown Reason : $reason" + Write-Output "Shutdown Code : $code" + Write-Output "Shutdown Type : $type" + Write-Output "Additional Info : $info" + Write-Output "User Account : $user" + } + + # Check environment variable for log saving + if ($env:sendtolog -eq "1") { + $logFolder = $env:Company_folder_path + if (-not $logFolder) { + Write-Output "Error: Environment variable 'Company_folder_path' is not set." + return + } + if (-not (Test-Path $logFolder)) { + Write-Output "Error: The folder path '$logFolder' does not exist." + return + } + + $csvPath = Join-Path $logFolder "logs/PowerCycleLog.csv" + + $logEntry = [PSCustomObject]@{ + Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + LastBootTime = $formattedBootTime + Uptime = "$($uptime.Days)d $($uptime.Hours)h $($uptime.Minutes)m $($uptime.Seconds)s" + EventLogTime = $eventTime + EventID = $eventId + EventSource = $provider + EventMessage = $msg + Executable = $exe + Machine = $machine + Reason = $reason + Code = $code + Type = $type + Info = $info + User = $user + } + + $appendLog = $true + + if (Test-Path $csvPath) { + $fileSizeMB = (Get-Item $csvPath).Length / 1MB + $maxSizeMB = 10 + + if ($fileSizeMB -gt $maxSizeMB) { + $timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss" + $backupPath = Join-Path $logFolder "RebootLog_$timestamp.csv" + Rename-Item -Path $csvPath -NewName $backupPath + Write-Output "Log file exceeded $maxSizeMB MB. Backed up to $backupPath." + } + + $lastEntry = Import-Csv -Path $csvPath | Select-Object -Last 1 + if ($lastEntry) { + $propsToCompare = @("LastBootTime", "Uptime", "EventLogTime", "EventID", "EventSource", "EventMessage", "Executable", "Machine", "Reason", "Code", "Type", "Info", "User") + $isSame = $true + foreach ($prop in $propsToCompare) { + if ($logEntry.$prop -ne $lastEntry.$prop) { + $isSame = $false + break + } + } + if ($isSame) { + $appendLog = $false + Write-Output "`nLog entry already exists. Skipping append." + } + } + } + + if ($appendLog) { + if (-not (Test-Path $csvPath)) { + $logEntry | Export-Csv -Path $csvPath -NoTypeInformation + } else { + $logEntry | Export-Csv -Path $csvPath -Append -NoTypeInformation + } + Write-Output "`nNew entry logged to: $csvPath" + } + } +} else { + Write-Output "No shutdown or restart event (ID 1074/6008) found in the System log." +} diff --git a/scripts_staging/Tools/Get logon events.ps1 b/scripts_staging/Tools/Get logon events.ps1 new file mode 100644 index 00000000..e8f3cdc5 --- /dev/null +++ b/scripts_staging/Tools/Get logon events.ps1 @@ -0,0 +1,40 @@ +<# +.SYNOPSIS + Retrieves successful user logon events in the last 24 hours, + filtering for interactive logons and excluding system accounts. + +.DESCRIPTION + This script queries the Security event log for event ID 4624, which corresponds to successful user logons. + It filters the results to include only logon events from the last 24 hours and focuses on interactive logons + (LogonType 2). The script excludes events where the username is "NT AUTHORITY\SYSTEM". + +.NOTES + Author: SAN + Date: 19.09.24 + #public + +.CHANGELOG + + +.TODO + Add error handling for event log retrieval. + Add support for additional logon types or custom filters if required. +#> + +Get-WinEvent -FilterHashtable @{ + LogName = 'Security' + Id = 4624 + StartTime = (Get-Date).AddHours(-24) +} | +ForEach-Object { + $Event = [xml]$_.ToXml() + [pscustomobject]@{ + TimeCreated = $_.TimeCreated + Username = $Event.Event.EventData.Data[5].'#text' + LogonType = $Event.Event.EventData.Data[8].'#text' + IPAddress = $Event.Event.EventData.Data[18].'#text' + } +} | +Where-Object { + $_.Username -ne "NT AUTHORITY\SYSTEM" -and $_.LogonType -eq "2" +} \ No newline at end of file diff --git a/scripts_staging/Tools/IP block lists for specified countries.ps1 b/scripts_staging/Tools/IP block lists for specified countries.ps1 new file mode 100644 index 00000000..5dc6307c --- /dev/null +++ b/scripts_staging/Tools/IP block lists for specified countries.ps1 @@ -0,0 +1,212 @@ +<# +.SYNOPSIS + This script downloads and processes IP block lists for specified countries + from ipdeny.com and creates corresponding inbound and/or outbound firewall + rules on the local machine using PowerShell cmdlets. + +.DESCRIPTION + The script allows users to automate the creation of firewall rules that block + IP ranges from specific countries or from a provided input file. It can delete + existing firewall rules matching the specified rule name and recreate them with + updated block lists. + + This script can be used to block IPs from countries with high levels of unwanted + traffic or suspected malicious activity. + + Sample of problematic Countries (often associated with cyberattacks, fraud, or high-risk traffic): + - CN (China) + - RU (Russia) + - IN (India) + - TR (Turkey) + - BR (Brazil) + - UA (Ukraine) + - NG (Nigeria) + - KR (South Korea) + - PH (Philippines) + - IR (Iran) + +.PARAMETER Countries + A comma-separated list of two-letter country codes (e.g., "ru,cn") to download + IP block lists for each specified country. + +.PARAMETER InputFile + Path to an input file containing IP ranges to block. Each line should contain + a valid IP range. + +.PARAMETER RuleName + Name for the firewall rule. If not provided, the base name of the input file + or zone file is used. + +.PARAMETER ProfileType + The firewall profile to apply the rules to. Default: "any". Options: + - Domain + - Private + - Public + - Any + +.PARAMETER InterfaceType + The type of network interface for the rule. Default: "any". Options: + - Wired + - Wireless + - Any + +.PARAMETER Direction + Direction of traffic to block. Default: "Inbound". Options: + - Inbound + - Outbound + - Both + +.PARAMETER DeleteOnly + If set, deletes all firewall rules matching "*xx.zone*". + +.EXAMPLE + -Countries "ru,cn" + Downloads and processes IP block lists for Russia and China and creates corresponding inbound firewall rules + + -InputFile "C:\path\to\my-blocklist.txt" -RuleName "CustomBlock" -Direction Both + Processes an input file containing IP ranges and creates both inbound and outbound rules. + + # Remove all rules with "*xx.zone*" + -DeleteOnly + +.NOTE + V1 Author: Jason Fossen (http://www.sans.org/windows-security/) 20.Mar.2012 + V2 Author: Vinahost release (https://cloudcraft.info) 15.Aug.2017 + V3 Author: SAN 28.01.25 + #public + +.CHANGELOG + 28.01.25 New feature to set direction will default to inbound only to reduce the load on cpu, added feature to add countries in bulk, fixed deleteonly to remove all rules created, upgrade to PowerShell cmdlets for fw rules + +.TODO + add the postfix to rule name in every case to make sure DeleteOnly can catch them all + +#> + +param ( + [string] $Countries, + [string] $InputFile, + [string] $RuleName, + [string] $ProfileType = "Any", + [string] $InterfaceType = "Any", + [ValidateSet("Inbound", "Outbound", "Both")] + [string] $Direction = "Inbound", + [switch] $DeleteOnly +) + +# Function to delete existing firewall rules +function RemoveFirewallRules { + param ([string]$Pattern) + + $RulesToDelete = Get-NetFirewallRule | Where-Object { $_.Name -like $Pattern } + if ($RulesToDelete) { + Write-Host "`nDeleting rules matching '$Pattern'..." + $RulesToDelete | Remove-NetFirewallRule -Confirm:$false + Write-Host "`nRules deleted successfully." + } else { + Write-Host "`nNo matching rules found." + } +} + +# If DeleteOnly is set, remove all rules matching *xx.zone* +if ($DeleteOnly) { + RemoveFirewallRules -Pattern "*??.zone*" + exit +} + +# Function to process input file and create firewall rules +function ProcessFile { + param ( + [string]$InputFile, + [string]$RuleName, + [string]$ProfileType, + [string]$InterfaceType, + [string]$Direction + ) + + $file = Get-Item $InputFile -ErrorAction SilentlyContinue + if (-not $file) { + Write-Host "`nFile $InputFile not found, quitting..." + exit + } + + # Set default rule name if not provided + if (-not $RuleName) { $RuleName = $file.BaseName } + + # Remove existing firewall rules for this specific rule name + RemoveFirewallRules -Pattern "$RuleName-#*" + + # Load IP ranges from file + $Ranges = Get-Content $file | Where-Object { ($_ -match '^[0-9a-fA-F]{1,4}[\.\:]') -and ($_ -match '\d') } + if (-not $Ranges) { + Write-Host "`nNo valid IP addresses found in $InputFile, quitting..." + exit + } + + $LineCount = $Ranges.Count + Write-Host "`nLoaded $LineCount IP ranges from $InputFile..." + + # Define batch size for rules + $MaxRangesPerRule = 200 + $RuleIndex = 1 + $StartIndex = 0 + + # Process and create rules in batches + while ($StartIndex -lt $LineCount) { + $EndIndex = [Math]::Min($StartIndex + $MaxRangesPerRule, $LineCount) + $IPBatch = $Ranges[$StartIndex..($EndIndex - 1)] + $RuleSuffix = $RuleIndex.ToString("000") + + # Create rules based on direction + if ($Direction -eq "Inbound" -or $Direction -eq "Both") { + Write-Host "`nCreating inbound rule: $RuleName-#$RuleSuffix..." + New-NetFirewallRule -Name "$RuleName-#$RuleSuffix" -DisplayName "$RuleName-#$RuleSuffix" -Direction Inbound -Action Block -RemoteAddress $IPBatch -Profile $ProfileType -InterfaceType $InterfaceType + } + + if ($Direction -eq "Outbound" -or $Direction -eq "Both") { + Write-Host "`nCreating outbound rule: $RuleName-#$RuleSuffix..." + New-NetFirewallRule -Name "$RuleName-#$RuleSuffix" -DisplayName "$RuleName-#$RuleSuffix" -Direction Outbound -Action Block -RemoteAddress $IPBatch -Profile $ProfileType -InterfaceType $InterfaceType + } + + $StartIndex += $MaxRangesPerRule + $RuleIndex++ + } + + Write-Host "`nFirewall rules created successfully!" +} + +# Validate input parameters +if (-not $Countries -and -not $InputFile) { + Write-Host "Please specify at least one country or provide an input file." + exit +} + +# Split the list of countries if provided +$CountryList = if ($Countries) { $Countries.Split(',') } else { @() } + +if ($CountryList.Count -gt 0) { + foreach ($Zone in $CountryList) { + if ($Zone.Length -ne 2) { + Write-Host "`nInvalid zone specified for '$Zone', skipping..." + continue + } + + $Zone = $Zone.ToLower() + $InputFile = "$Zone.zone.txt" + + Write-Host "`nDownloading IP block list for zone: $Zone..." + try { + Invoke-WebRequest -Uri "http://www.ipdeny.com/ipblocks/data/countries/$Zone.zone" -OutFile $InputFile -UseBasicParsing + } catch { + Write-Host "`nFailed to download IP block list for $Zone, skipping..." + continue + } + + # Process the downloaded input file + ProcessFile -InputFile $InputFile -RuleName $Zone.zone -ProfileType $ProfileType -InterfaceType $InterfaceType -Direction $Direction + } +} else { + if ($InputFile) { + ProcessFile -InputFile $InputFile -RuleName $RuleName -ProfileType $ProfileType -InterfaceType $InterfaceType -Direction $Direction + } +} \ No newline at end of file diff --git a/scripts_staging/Tools/Measures TCP Latency.ps1 b/scripts_staging/Tools/Measures TCP Latency.ps1 new file mode 100644 index 00000000..840dd6ff --- /dev/null +++ b/scripts_staging/Tools/Measures TCP Latency.ps1 @@ -0,0 +1,165 @@ +<# +.SYNOPSIS + Measures TCP connection latency to a specified host and port with optional detailed output. + +.DESCRIPTION + This function performs multiple TCP connection attempts to a target host and port. + It includes an initial "warm-up" attempt to avoid DNS resolution or cold TCP stack, then measures latency + for the specified number of connection attempts. + This tool is intended for cases where ICMP is not available. + +.PARAMETER TargetHost + The hostname or IP address of the target to test. + +.PARAMETER Port + The TCP port to connect to on the target host. Default is 80. + +.PARAMETER Count + The number of test attempts to perform (excluding the first warm-up). Default is 5. + +.PARAMETER Timeout + The maximum time (in milliseconds) to wait for each connection attempt. Default is 3000 ms. + +.PARAMETER Silent + If set, disables output to the console and instead returns a list of latencies. + +.PARAMETER OutputMode + Optional output format. Can be 'None', 'Json', or 'Csv'. + +.EXAMPLE + -TargetHost "example.com" -Port 443 -Count 5 + -TargetHost "192.168.1.1" -Port 22 -Count 3 -Silent -OutputMode Json + +.NOTES + Author: SAN + Date: 15.04.25 + #public + +#> + +param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$TargetHost, + + [ValidateRange(1, 65535)] + [int]$Port = 80, + + [ValidateRange(1, 1000)] + [int]$Count = 5, + + [ValidateRange(1, 60000)] + [int]$Timeout = 3000, + + [switch]$Silent, + + [ValidateSet("None", "Json", "Csv")] + [string]$OutputMode = "None" +) + +function Test-TcpLatency { + param ( + [string]$TargetHost, + [int]$Port, + [int]$Count, + [int]$Timeout, + [switch]$Silent, + [string]$OutputMode + ) + + $latencies = @() + $successes = 0 + $failures = 0 + + for ($i = 0; $i -le $Count; $i++) { + $tcpClient = [System.Net.Sockets.TcpClient]::new() + $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() + $resultText = "" + + try { + $asyncResult = $tcpClient.BeginConnect($TargetHost, $Port, $null, $null) + $waitHandle = $asyncResult.AsyncWaitHandle + + if ($waitHandle.WaitOne($Timeout, $false)) { + $tcpClient.EndConnect($asyncResult) + $stopwatch.Stop() + $latency = [math]::Round($stopwatch.Elapsed.TotalMilliseconds, 3) + + if ($i -gt 0) { + $latencies += $latency + $successes++ + $resultText = "Attempt $i : Connected to $TargetHost : $Port in ${latency}ms" + if (-not $Silent) { + Write-Host $resultText + } + } else { + if (-not $Silent) { + Write-Host "Warm-up ignored (${latency}ms)" + } + } + } else { + $stopwatch.Stop() + if ($i -gt 0) { + $failures++ + $resultText = "Attempt $i : Timeout after $Timeout ms" + if (-not $Silent) { + Write-Host $resultText + } + } else { + if (-not $Silent) { + Write-Host "Warm-up attempt timed out (ignored)" + } + } + } + } catch { + if ($i -gt 0) { + $failures++ + $resultText = "Attempt $i : Connection error: $_" + if (-not $Silent) { + Write-Host $resultText + } + } else { + if (-not $Silent) { + Write-Host "Warm-up attempt failed (ignored): $_" + } + } + } finally { + $tcpClient.Close() + $waitHandle.Close() + } + + Start-Sleep -Seconds 1 + } + + if (-not $Silent) { + Write-Host "`nSummary for $TargetHost : $Port" + Write-Host (" Successful attempts: {0,3}" -f $successes) + Write-Host (" Failed attempts: {0,3}" -f $failures) + if ($latencies.Count -gt 0) { + $avg = [math]::Round(($latencies | Measure-Object -Average).Average, 3) + $min = ($latencies | Measure-Object -Minimum).Minimum + $max = ($latencies | Measure-Object -Maximum).Maximum + Write-Host (" Avg latency: {0,3} ms" -f $avg) + Write-Host (" Min latency: {0,3} ms" -f $min) + Write-Host (" Max latency: {0,3} ms" -f $max) + } else { + Write-Host " No successful connections to calculate latency." + } + } + + switch ($OutputMode) { + "Json" { $latencies | ConvertTo-Json -Depth 1 } + "Csv" { + $latencies | ForEach-Object { + [PSCustomObject]@{ Latency = $_ } + } | ConvertTo-Csv -NoTypeInformation + } + default { + if ($Silent) { + return $latencies + } + } + } +} + +Test-TcpLatency -TargetHost $TargetHost -Port $Port -Count $Count -Timeout $Timeout -Silent:$Silent -OutputMode $OutputMode diff --git a/scripts_staging/Tools/Reset permission of target folder.ps1 b/scripts_staging/Tools/Reset permission of target folder.ps1 new file mode 100644 index 00000000..a5171013 --- /dev/null +++ b/scripts_staging/Tools/Reset permission of target folder.ps1 @@ -0,0 +1,100 @@ +<# +.SYNOPSIS + This script resets folder and file permissions by applying inherited permissions from the target folder and checks for permission mismatches across subfolders and files. + +.DESCRIPTION + The script retrieves the target folder path from an environment variable and ensures the folder exists. + It then gets the ACL (Access Control List) of the target folder and applies the inherited permissions to all subfolders and files within the target folder. + It also compares the ACLs of child items with their parent folder to identify permission mismatches. The script includes two main functions: + - `Set-InheritedPermissions`: Resets permissions and inheritance on a folder or file. + - `Compare-Acls`: Compares the ACLs of a child item with its parent to identify mismatched permissions. + +.EXAMPLE + TARGETFOLDER=C:\TargetFolder + + +.NOTES + Author: SAN + Date: ??? + #public + +.CHANGELOG + + +#> + + + +# Get the target folder from environment variable +$TargetFolder = $env:TARGETFOLDER + +if (-not (Test-Path -Path $TargetFolder)) { + Write-Output "The specified path does not exist: $TargetFolder" + exit +} + +# Get the ACL of the target folder +$targetAcl = Get-Acl -Path $TargetFolder + +# Function to reset permissions and inheritance +function Set-InheritedPermissions { + param ( + [string]$Path + ) + + try { + # Reset ACLs to match the target folder + $acl = Get-Acl -Path $Path + $acl.SetAccessRuleProtection($false, $true) # Enable inheritance, remove explicit permissions + Set-Acl -Path $Path -AclObject $targetAcl + + # Get the owner of the target folder + $owner = $targetAcl.Owner + # Set owner to be the same as the target folder + $acl.SetOwner([System.Security.Principal.NTAccount]$owner) + Set-Acl -Path $Path -AclObject $acl + + Write-Output "Reset permissions and ownership for: $Path" + } catch { + Write-Output "Failed to process permissions for: $Path" + } +} + +# Function to compare ACLs of child items with parent folder +function Compare-Acls { + param ( + [string]$Path + ) + + try { + $parentAcl = Get-Acl -Path (Get-Item -Path $Path).Parent.FullName + $itemAcl = Get-Acl -Path $Path + + # Compare ACLs + if ($itemAcl -ne $parentAcl) { + Write-Output "Permission mismatch for: $Path" + } + } catch { + Write-Output "Unreadable ACL or permission issue with: $Path" + } +} + +Write-Output "Processing folder: $TargetFolder" + +# Set permissions for the target folder itself +Set-InheritedPermissions -Path $TargetFolder + +# Process all subfolders and files to reset permissions +$items = Get-ChildItem -Path $TargetFolder -Recurse +foreach ($item in $items) { + Set-InheritedPermissions -Path $item.FullName +} + +Write-Output "`nScanning for files with unreadable or mismatched permissions..." + +# Scan all subfolders and files to check for permission issues +foreach ($item in $items) { + Compare-Acls -Path $item.FullName +} + +Write-Output "Permission scan completed." \ No newline at end of file diff --git a/scripts_staging/Tools/SSL Certificate manager.ps1 b/scripts_staging/Tools/SSL Certificate manager.ps1 new file mode 100644 index 00000000..7379f28b --- /dev/null +++ b/scripts_staging/Tools/SSL Certificate manager.ps1 @@ -0,0 +1,283 @@ +<# +.SYNOPSIS + This script manages SSL certificates for IIS, RDS Gateway, and common Windows certificate stores. It identifies, lists, and optionally deletes certificates based on thumbprints. + +.DESCRIPTION + The script performs the following tasks: + - Imports necessary modules (WebAdministration for IIS, RemoteDesktopServices for RDS). + - Lists SSL certificates bound to IIS sites. + - Retrieves the SSL certificate thumbprint for an RDS Gateway (if applicable). + - Lists all self-signed certificates across common certificate stores. + - Identifies and lists certificates that are not already listed by other functions. + - Deletes certificates using an environment variable (DeleteThumbprint) if set. + +.EXEMPLE + DeleteThumbprint=DFDF45DF45F8DFD92QA + +.NOTES + Author:SAN + Date: 15.08.24 + #public + +.TODO + Add exchange section + Add CA to the reports. + +#> + + +# Import the necessary modules +$importIIS = $false +$importRDS = $false + +try { + Import-Module WebAdministration -ErrorAction Stop + $importIIS = $true +} catch { + Write-Host "Failed to import WebAdministration module. IIS-related functions will not run." +} + +try { + Import-Module RemoteDesktopServices -ErrorAction Stop + $importRDS = $true +} catch { + Write-Host "Failed to import RemoteDesktopServices module. RDS-related functions will not run." +} + +# List of thumbprints already found +$global:listedThumbprints = @() + +# Function to add thumbprints to the list +function Add-ToListedThumbprints { + param ( + [string]$Thumbprint + ) + if ($Thumbprint -notin $global:listedThumbprints) { + $global:listedThumbprints += $Thumbprint + } +} + +# Function to get certificate details from all known stores +function Get-CertificateDetails { + param ( + [string]$Thumbprint + ) + + $stores = Get-AllCertificateStores + + foreach ($store in $stores) { + try { + $certs = Get-ChildItem -Path $store -ErrorAction SilentlyContinue + $cert = $certs | Where-Object { $_.Thumbprint -eq $Thumbprint } + if ($cert) { + return @{ + Certificate = $cert + StorePath = $store + } + } + } catch { + Write-Host "Failed to access certificate store: $store" + } + } + + return $null +} + +# Function to get certificate details given its thumbprint +function Get-CertificateDetailsByThumbprint { + param ( + [string]$Thumbprint + ) + + $result = Get-CertificateDetails -Thumbprint $Thumbprint + if ($result) { + return [PSCustomObject]@{ + "Thumbprint" = $result.Certificate.Thumbprint + "Subject" = $result.Certificate.Subject + "ExpirationDate" = $result.Certificate.NotAfter + } + } else { + Write-Host "Certificate with Thumbprint $Thumbprint not found in any store." + return $null + } +} + +# Function to list IIS SSL certificate thumbprints +function List-IIS-SSL-Thumbprints { + $results = @() + $sites = Get-Website + + foreach ($site in $sites) { + foreach ($binding in $site.Bindings.Collection) { + if ($binding.Protocol -eq "https") { + $sslCertHash = $binding.CertificateHash + $thumbprint = -join ($sslCertHash | ForEach-Object { "{0:X2}" -f $_ }) + + $certDetails = Get-CertificateDetailsByThumbprint -Thumbprint $thumbprint + + # Add to listed thumbprints + Add-ToListedThumbprints -Thumbprint $thumbprint + + $results += [PSCustomObject]@{ + "Thumbprint" = $thumbprint + "Subject" = $certDetails.Subject + "Expiration Date"= $certDetails.ExpirationDate + "IIS Site" = $site.Name + "Binding Info" = $binding.BindingInformation + } + } + } + } + + $results | Format-Table -AutoSize +} + +# Function to get RDS Gateway SSL certificate thumbprint +function Get-RDGatewaySSLCertificateThumbprint { + param ( + [string]$Path = 'RDS:\GatewayServer\SSLCertificate\Thumbprint' + ) + + try { + $thumbprintValue = (Get-Item -Path $Path).CurrentValue + + if ([string]::IsNullOrWhiteSpace($thumbprintValue)) { + Write-Host "The SSL certificate thumbprint set for RD Gateway is empty or not set." + } else { + $certDetails = Get-CertificateDetailsByThumbprint -Thumbprint $thumbprintValue + + # Add to listed thumbprints + Add-ToListedThumbprints -Thumbprint $thumbprintValue + + [PSCustomObject]@{ + "Thumbprint" = $thumbprintValue + "Subject" = $certDetails.Subject + "Expiration Date" = $certDetails.ExpirationDate + } | Format-Table -AutoSize + } + } + catch { + Write-Host "An error occurred while retrieving the SSL certificate thumbprint or the RDS Gateway role is not installed." + } +} + +# Function to get all common certificate stores +function Get-AllCertificateStores { + return @( + "Cert:\LocalMachine\My", + "Cert:\LocalMachine\WebHosting", # Web hosting store, if applicable + "Cert:\LocalMachine\RDS\GatewayServer", # RDS Gateway Server, if applicable + "Cert:\LocalMachine\RDS\ConnectionBroker", # RDS Connection Broker, if applicable + "Cert:\LocalMachine\Remote Desktop" + ) +} + +# Function to list certificates that are self-signed +function List-SelfSignedCertificates { + $results = @() + Write-Host "Listing Self-Signed Certificates:" + + $stores = Get-AllCertificateStores + + foreach ($store in $stores) { + try { + $certs = Get-ChildItem -Path $store -ErrorAction SilentlyContinue + foreach ($cert in $certs) { + if ($cert.Issuer -eq $cert.Subject) { # Self-signed certificates + $thumbprint = $cert.Thumbprint + + # Add to listed thumbprints + Add-ToListedThumbprints -Thumbprint $thumbprint + + $results += [PSCustomObject]@{ + "Thumbprint" = $thumbprint + "Subject" = $cert.Subject + "Expiration Date"= $cert.NotAfter + "Store Location" = $store + } + } + } + } catch { + Write-Host "Failed to access certificate store: $store" + } + } + + $results | Format-Table -AutoSize +} + +# Function to list certificates that are not listed by other functions +function List-UnlistedCertificates { + $results = @() + Write-Host "Listing Unlisted Certificates:" + + $stores = Get-AllCertificateStores + + foreach ($store in $stores) { + try { + $certs = Get-ChildItem -Path $store -ErrorAction SilentlyContinue + foreach ($cert in $certs) { + $thumbprint = $cert.Thumbprint + + if ($thumbprint -notin $global:listedThumbprints) { + $results += [PSCustomObject]@{ + "Thumbprint" = $thumbprint + "Store Location" = $store + "Subject" = $cert.Subject + "Expiration Date"= $cert.NotAfter + } + } + } + } catch { + Write-Host "Failed to access certificate store: $store" + } + } + + $results | Format-Table -AutoSize +} + +# Function to delete certificates by thumbprint based on an environment variable +function Delete-CertificateByThumbprint { + $thumbprintToDelete = $env:DeleteThumbprint + + if ([string]::IsNullOrWhiteSpace($thumbprintToDelete)) { + Write-Host "Environment variable 'DeleteThumbprint' is not set or empty." + return + } + + Write-Host "Attempting to delete certificates with Thumbprint: $thumbprintToDelete" + $stores = Get-AllCertificateStores + + foreach ($store in $stores) { + try { + $certs = Get-ChildItem -Path $store -ErrorAction SilentlyContinue + foreach ($cert in $certs) { + if ($cert.Thumbprint -eq $thumbprintToDelete) { + Write-Host "Deleting certificate with Thumbprint: $thumbprintToDelete from $store" + Remove-Item -Path $cert.PSPath -Force + } + } + } catch { + Write-Host "Failed to delete certificate from store: $store" + } + } +} + +# Main script execution +if ($importIIS) { + Write-Host "Listing IIS SSL Thumbprints:" + List-IIS-SSL-Thumbprints +} + +if ($importRDS) { + Write-Host "Getting RDS Gateway SSL Certificate Thumbprint:" + Get-RDGatewaySSLCertificateThumbprint +} + +# List self-signed certificates +List-SelfSignedCertificates + +# List certificates that were not listed by other functions +List-UnlistedCertificates + +# Attempt to delete certificates based on the DeleteThumbprint environment variable +Delete-CertificateByThumbprint \ No newline at end of file diff --git a/scripts_staging/Tools/Troubleshoot windows update.ps1 b/scripts_staging/Tools/Troubleshoot windows update.ps1 new file mode 100644 index 00000000..db9009fc --- /dev/null +++ b/scripts_staging/Tools/Troubleshoot windows update.ps1 @@ -0,0 +1,169 @@ +<# +.SYNOPSIS + This script troubleshoots common issues related to fetching Windows updates, including checking local configuration for WSUS server settings. + +.DESCRIPTION + The script checks network connectivity, DNS resolution, the status of key services, the PSWindowsUpdate module, + Windows Update logs, and other important settings that could be preventing the retrieval of Windows updates. It also checks + whether a WSUS server is configured locally. + +.NOTES + Author: SAN + Date: 25.03.2025 + #public + Dependencies: + PSWindowsUpdate module + +.CHANGELOG + SAN 25.03.2025 initial release +#> + +function Test-NetworkConnectivity { + $url = "www.microsoft.com" + Write-Host "Checking network connectivity to $url..." + $pingResult = Test-Connection -ComputerName $url -Count 1 -Quiet + if (-not $pingResult) { + Write-Host "KO: No network connectivity to $url. Please check your internet connection." + return $false + } + Write-Host "OK: Network connectivity to $url is successful." + return $true +} + +function Test-WindowsUpdateService { + Write-Host "Checking Windows Update service status..." + $service = Get-Service wuauserv + if ($service.Status -ne 'Running') { + Write-Host "KO: The Windows Update service (wuauserv) is not running. Attempting to start it..." + try { + Start-Service wuauserv + Write-Host "OK: Windows Update service started successfully." + } catch { + Write-Host "KO: Failed to start Windows Update service: $_" + return $false + } + } else { + Write-Host "OK: Windows Update service is running." + } + return $true +} + +function Test-PSWindowsUpdateModule { + Write-Host "Checking PSWindowsUpdate module installation..." + if (Get-Module -ListAvailable -Name PSWindowsUpdate) { + Write-Host "OK: PSWindowsUpdate module is installed." + return $true + } else { + Write-Host "KO: PSWindowsUpdate module is not installed. Please install it using 'Install-Module PSWindowsUpdate'." + return $false + } +} + +function Test-DNSResolution { + Write-Host "Checking DNS resolution for update servers..." + try { + $dnsCheck = Resolve-DnsName "download.windowsupdate.com" + Write-Host "OK: DNS resolution for Windows Update servers is working." + return $true + } catch { + Write-Host "KO: DNS resolution failed for Windows Update servers. Please check your DNS settings." + return $false + } +} + +function Check-WindowsUpdateAgentVersion { + Write-Host "Checking Windows Update Agent version..." + try { + $wuaAgentVersion = (Get-Command "C:\Windows\System32\wuauclt.exe").FileVersionInfo.FileVersion + Write-Host "OK: Windows Update Agent version is $wuaAgentVersion." + return $true + } catch { + Write-Host "KO: Could not retrieve Windows Update Agent version. Please ensure the file exists." + return $false + } +} + +function Check-PendingReboot { + Write-Host "Checking for pending reboot..." + $rebootPending = Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" + if ($rebootPending) { + Write-Host "KO: There is a pending reboot. Please restart the machine and try again." + return $false + } + Write-Host "OK: No pending reboot." + return $true +} + +function Check-WindowsUpdateLogs { + Write-Host "Checking Windows Update logs for errors..." + $logPath = "C:\Windows\WindowsUpdate.log" + if (Test-Path $logPath) { + $logContent = Get-Content $logPath -Tail 50 + if ($logContent -match "error|failed") { + Write-Host "KO: Found errors in the Windows Update log:" + $logContent | Select-String "error|failed" | Format-Table -AutoSize + } else { + Write-Host "OK: No errors found in the recent Windows Update logs." + } + } else { + Write-Host "KO: Windows Update log file not found at $logPath." + return $false + } + return $true +} + +function Check-WindowsUpdateEventLogs { + Write-Host "Checking for Windows Update related errors in the Event Log..." + $events = Get-WinEvent -LogName "System" | Where-Object { $_.Message -match "update|windowsupdate" } | Select-Object -First 5 + if ($events) { + Write-Host "KO: Found the following Windows Update related event(s):" + $events | Format-Table -Property TimeCreated, Message -AutoSize + } else { + Write-Host "OK: No Windows Update related events found in the Event Log." + } +} + +function Check-WSUSServerConfiguration { + Write-Host "Checking if WSUS server is configured..." + $wsusServer = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate" -Name WUServer -ErrorAction SilentlyContinue + if ($wsusServer) { + Write-Host "INFO: WSUS server configured with address: $($wsusServer.WUServer)." + } else { + Write-Host "OK: No WSUS server is configured." + } +} + +Write-Host "Starting troubleshooting script..." + +if (-not (Test-NetworkConnectivity)) { + exit 1 +} + +if (-not (Test-WindowsUpdateService)) { + exit 1 +} + +if (-not (Test-PSWindowsUpdateModule)) { + exit 1 +} + +if (-not (Test-DNSResolution)) { + exit 1 +} + +if (-not (Check-WindowsUpdateAgentVersion)) { + exit 1 +} + +if (-not (Check-PendingReboot)) { + exit 1 +} + +if (-not (Check-WindowsUpdateLogs)) { + exit 1 +} + +Check-WSUSServerConfiguration +Check-WindowsUpdateEventLogs + +Write-Host "All checks completed. If any issues were detected, follow the suggested actions." diff --git a/scripts_staging/Tools/Windows update force install new updates.ps1 b/scripts_staging/Tools/Windows update force install new updates.ps1 new file mode 100644 index 00000000..0e7cd0a4 --- /dev/null +++ b/scripts_staging/Tools/Windows update force install new updates.ps1 @@ -0,0 +1,63 @@ +<# +.SYNOPSIS + This script checks for available Windows updates and installs them using the PSWindowsUpdate module. + +.DESCRIPTION + This PowerShell script is designed to automate the process of checking for and installing Windows updates. + It first ensures that the PSWindowsUpdate module is installed and then proceeds to check for available updates. + If updates are found, it initiates the update process, installs all available updates, and reboots if necessary. + Finally, it retrieves and displays the date of the last successful installation of Windows updates. + +.NOTES + Author: SAN + Date: 02.04.24 + Dependency: PowerShell 7 snippet, PSWindowsUpdate module + #public + +.CHANGELOG + 02.07.25 SAN optimisation + + +#> + + + + +{{CallPowerShell7}} + +# Set TLS version to 1.2 +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +# Check if PSWindowsUpdate module is available +if (Get-Module -ListAvailable -Name PSWindowsUpdate) { + Write-Output "PSWindowsUpdate is already installed" +} else { + # If module is not available, install it + Write-Output "Installing PSWindowsUpdate module..." + Install-Module -Name PSWindowsUpdate -Force + + # Check if there was an error during installation and attempt to install NuGet package provider if necessary + if ($?) { + Write-Output "PSWindowsUpdate module installed successfully." + } else { + Write-Output "Error occurred during PSWindowsUpdate module installation. Attempting to install NuGet package provider..." + Install-PackageProvider -Name NuGet -Force + + # Re-attempt to install PSWindowsUpdate module + Write-Output "Re-running PSWindowsUpdate module installation..." + Install-Module -Name PSWindowsUpdate -Force + } +} + +# Check updates +Write-Host "Check for available updates:" +Get-WindowsUpdate + +# Start update process +Write-Host "Updating Windows" +Get-WindowsUpdate -Verbose -Install -AcceptAll -AutoReboot + +Write-Host "Output of the last updates results:" +$results = Get-WULastResults +$lastInstallationSuccessDate = $results | Select-Object -ExpandProperty LastInstallationSuccessDate +$lastInstallationSuccessDate \ No newline at end of file diff --git a/scripts_staging/Tools/azure AD Connect Certificate Cleanup.ps1 b/scripts_staging/Tools/azure AD Connect Certificate Cleanup.ps1 new file mode 100644 index 00000000..8274c640 --- /dev/null +++ b/scripts_staging/Tools/azure AD Connect Certificate Cleanup.ps1 @@ -0,0 +1,79 @@ +<# +.TITLE + AD Connect Certificate Cleanup + +.DESCRIPTION + This script deletes all certificates issued by the Microsoft PolicyKeyService Certificate Authority + except for the one with the latest expiration date, and then restarts the AD Connect service (ADSync). + They are safe to delete. + +.NOTE + Author: SAN + Date: 19.11.24 + Usefull Links: + https://learn.microsoft.com/en-us/answers/questions/314565/adfs-multiple-certificates-from-microsoft-policyke + https://learn.microsoft.com/en-us/answers/questions/846864/why-generates-a-lot-of-certificate-in-my-azure-ad + #public + +.CHANGELOG + + +#> + +# Define the AD Connect service name +$serviceName = "ADSync" + +# Check if the AD Connect service is running +$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + +if ($service -eq $null) { + Write-Host "AD Connect service is not installed on this machine." + exit +} + +if ($service.Status -ne 'Running') { + Write-Host "AD Connect service is not running. Aborting script." + exit +} + +Write-Host "AD Connect service is running. Proceeding with certificate cleanup." + +# Get all certificates from the Microsoft PolicyKeyService Certificate Authority +$certificates = Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object { $_.Issuer -like "*Microsoft PolicyKeyService Certificate Authority*" } + +if (-not $certificates) { + Write-Host "No certificates found for Microsoft PolicyKeyService Certificate Authority." + exit +} + +# Sort certificates by expiry date, descending +$sortedCertificates = $certificates | Sort-Object -Property NotAfter -Descending + +# Ensure sortedCertificates is not empty +if ($sortedCertificates.Count -eq 0) { + Write-Host "No certificates available after sorting. Aborting script." + exit +} + +# The certificate with the biggest expiry date (first one after sorting) +$latestCert = $sortedCertificates[0] + +# Remove all certificates except the one with the biggest expiry date +foreach ($cert in $sortedCertificates) { + if ($cert.Thumbprint -ne $latestCert.Thumbprint) { + Write-Host "Deleting certificate with thumbprint: $($cert.Thumbprint)" + try { + Remove-Item -Path $cert.PSPath -Force + } catch { + Write-Host "Error deleting certificate: $($_.Exception.Message)" + } + } +} + +Write-Host "Deletion complete. Only the certificate with the biggest expiry date remains." + +# Restart the AD Connect service +Write-Host "Restarting the AD Connect service..." +Restart-Service -Name $serviceName -Force + +Write-Host "AD Connect service has been restarted." diff --git a/scripts_staging/Win_Clear_cookies.ps1 b/scripts_staging/Win_Clear_cookies.ps1 new file mode 100644 index 00000000..20169750 --- /dev/null +++ b/scripts_staging/Win_Clear_cookies.ps1 @@ -0,0 +1,58 @@ +<# +.SYNOPSIS +This script deletes cookies from common web browsers (Chrome, Firefox, Edge) on Windows systems. It targets the default locations where these browsers store their cookies. This operation is irreversible; ensure that any important data is backed up before running this script. + +.DESCRIPTION +The script iterates over the predefined paths for Chrome, Firefox, and Edge cookie storage, removing the cookies stored by these browsers. For Firefox, which may have multiple profiles, the script locates and clears cookies for each profile found. + +.PARAMETERS +None. + +When deploying this script via Tactical RMM, ensure it is executed as a user to correctly locate and access the browser profiles. + +#> + +# Function to delete cookies for a specific browser +function Clear-Cookies { + param ( + [string]$browserName, + [string[]]$paths + ) + + foreach ($path in $paths) { + if (Test-Path $path) { + Write-Output "Deleting cookies for $browserName from $path" + Remove-Item -Path $path -Recurse -Force -ErrorAction SilentlyContinue + } else { + Write-Output "$browserName cookies not found at $path" + } + } +} + +# Specify user profile paths (you may need to adjust these paths) +$userProfile = [Environment]::GetFolderPath('UserProfile') +$localAppData = [Environment]::GetFolderPath('LocalApplicationData') + +# Paths where browsers typically store cookies +$chromeCookiePaths = @("$localAppData\Google\Chrome\User Data\Default\Cookies") +$edgeCookiePaths = @("$localAppData\Microsoft\Edge\User Data\Default\Cookies") +$firefoxProfilesPath = "$userProfile\AppData\Roaming\Mozilla\Firefox\Profiles" + +# Clear cookies for Chrome +Clear-Cookies -browserName "Chrome" -paths $chromeCookiePaths + +# Clear cookies for Edge +Clear-Cookies -browserName "Edge" -paths $edgeCookiePaths + +# Clear cookies for Firefox (handles multiple profiles) +if (Test-Path $firefoxProfilesPath) { + $firefoxProfiles = Get-ChildItem -Path $firefoxProfilesPath -Directory + foreach ($profile in $firefoxProfiles) { + $cookiesPath = Join-Path -Path $profile.FullName -ChildPath "cookies.sqlite" + Clear-Cookies -browserName "Firefox" -paths @($cookiesPath) + } +} else { + Write-Output "Firefox profiles not found at $firefoxProfilesPath" +} + +Write-Output "Cookie deletion process completed." diff --git a/scripts_staging/Win_Defender_Enable_Exclusions.ps1 b/scripts_staging/Win_Defender_Enable_Exclusions.ps1 new file mode 100644 index 00000000..cf102c57 --- /dev/null +++ b/scripts_staging/Win_Defender_Enable_Exclusions.ps1 @@ -0,0 +1,112 @@ +# If you run the Defender Enable script it will enabled Controlled Folders. This is a starter list to minimize user pain saving files from programs giving errors +# Updated 9/11/2024 + +## Exclusions for Controlled Folder Access +#Microsoft Office +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\Microsoft Office\Office15\EXCEL.EXE" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\Microsoft Office\Office15\outlook.EXE" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\Microsoft Office\Office15\powerpoint.EXE" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\Microsoft Office\Office15\winword.EXE" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\Microsoft Office\root\Office16\EXCEL.EXE" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\Microsoft Office\root\Office16\outlook.EXE" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\Microsoft Office\root\Office16\powerpoint.EXE" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\Microsoft Office\root\Office16\winword.EXE" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Microsoft Office 15\root\office15\excel.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Microsoft Office 15\root\office15\outlook.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Microsoft Office 15\root\office15\powerpnt.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Microsoft Office 15\root\office15\powerpoint.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Microsoft Office 15\root\office15\winword.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Microsoft Office\root\Office15\EXCEL.EXE" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Microsoft Office\root\Office15\outlook.EXE" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Microsoft Office\root\Office15\powerpoint.EXE" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Microsoft Office\root\Office15\winword.EXE" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Microsoft Office\root\Office16\OUTLOOK.EXE" + +#Autodesk +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Autodesk\AutoCAD 2016\acad.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Autodesk\AutoCAD 2020\acad.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Autodesk\AutoCAD 2021\acad.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Autodesk\AutoCAD 2022\acad.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Autodesk\AutoCAD LT 2015\acadlt.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Autodesk\AutoCAD LT 2016\acadlt.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Autodesk\AutoCAD LT 2017\acadlt.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Autodesk\AutoCAD LT 2018\acadlt.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Autodesk\AutoCAD LT 2019\acadlt.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Autodesk\AutoCAD LT 2020\acadlt.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Autodesk\AutoCAD LT 2022\acadlt.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Autodesk\DWG TrueView 2021 - English\dwgviewr.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Autodesk\Revit 2019\Revit.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Autodesk\Revit 2020\Revit.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Autodesk\Revit 2022\Revit.exe" + +#Adobe +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\Adobe\Acrobat DC\Acrobat\Acrobat.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\Adobe\Reader 11.0\Reader\AcroRd32.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Adobe\Acrobat DC\Acrobat\Acrobat.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Adobe\Adobe Photoshop 2021\Photoshop.exe" + +#Others +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\ABRI\ILR2\UKSCL\ilr2.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\PROGRA~1\Nitro\PRO11~1\NitroPDF" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Bullzip\PDF Printer\gui.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\CCleaner\CCleaner64.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\LimitState\RING4.0\bin\ring64.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Nitro\Pro 11\NitroPDF.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\PeaZip\peazip.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\ShareX\ShareX.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\TacticalAgent\meshagent.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\TacticalAgent\tacticalrmm.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\WindowsApps\Microsoft.Office.Desktop.Excel_16051.14326.20348.0_x86__8wekyb3d8bbwe\Office16\excel.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Windows\SysWOW64\icacls.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Windows\System32\RuntimeBroker.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Windows\System32\SearchProtocolHost.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\CADS\VelVenti\Cads.VelVenti.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\CyberLink\PowerDVD12\Kernel\DMS\CLMSServerPDVD12.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\Draycir\Credit Hound\Credit Hound.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\Draycir\Spindle Document Distribution\PDF to Spindle\PDFtoSpindle.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\DYMO\DYMO Label Software\DLS.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\EvolutionM Client\client\wowclient.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\Sage\Accounts\SBDDesktop.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\Steelcalc .NET\uninstallSteelcalc.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\Tekla\Structural\Fastrak\PFR.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\Tekla\Structural\Fastrak\tcd.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\Tekla\Structural\Tedds\Tedds.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files (x86)\Thesaurus Software\BrightPay UK 2021-22\brightpay.exe" +Add-MpPreference -ControlledFolderAccessAllowedApplications "C:\Program Files\Bullzip\PDF Printer\gui.exe" + +##Exclusions for Processes +#Microsoft +Add-MpPreference -ExclusionPath "C:\Program Files (x86)\Microsoft Office\Office16\POWERPNT.EXE" +Add-MpPreference -ExclusionPath "C:\Program Files (x86)\Microsoft Office\root\Office16\EXCEL.EXE" +Add-MpPreference -ExclusionPath "C:\Program Files (x86)\Microsoft Office\root\Office16\OUTLOOK.EXE" +Add-MpPreference -ExclusionPath "C:\Program Files (x86)\Microsoft Office\root\Office16\WINWORD.EXE" +Add-MpPreference -ExclusionPath "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" +Add-MpPreference -ExclusionPath "C:\Program Files\Microsoft Office 15\root\office15\MSPUB.EXE" +Add-MpPreference -ExclusionPath "C:\Program Files\Microsoft Office\root\Office16\MSPUB.EXE" +Add-MpPreference -ExclusionPath "C:\Program Files\Microsoft Office\root\Office16\OUTLOOK.EXE" + +#Adobe +Add-MpPreference -ExclusionPath "C:\Program Files (x86)\Adobe\Acrobat DC\Acrobat\Acrobat.exe" +Add-MpPreference -ExclusionPath "C:\Program Files (x86)\Common Files\Adobe\Adobe Desktop Common\ADS\Adobe Desktop Service.exe" +Add-MpPreference -ExclusionPath "C:\Program Files (x86)\Common Files\Adobe\AdobeGCClient\AGCInvokerUtility.exe" +Add-MpPreference -ExclusionPath "C:\Program Files (x86)\Common Files\Adobe\AdobeGCClient\AGSService.exe" +Add-MpPreference -ExclusionPath "C:\Program Files (x86)\Common Files\Adobe\AdobeGCClient\AdobeGCClient.exe" +Add-MpPreference -ExclusionPath "C:\Program Files\Common Files\Adobe\Creative Cloud Libraries\libs\node.exe" + +#Autodesk +Add-MpPreference -ExclusionPath "C:\Program Files\Autodesk\AutoCAD 2023\acad.exe" + +#Others +Add-MpPreference -ExclusionPath "C:\ABRI\ILR2\UKSCL\ilr2.exe" +Add-MpPreference -ExclusionPath "C:\Program Files (x86)\CADS\VelVenti\Cads.VelVenti.exe" +Add-MpPreference -ExclusionPath "C:\Program Files (x86)\Intuit\QuickBooks 2013\AutoBackupEXE.exe" +Add-MpPreference -ExclusionPath "C:\Program Files (x86)\Nullifire Product Calculator\Nullifire.exe" +Add-MpPreference -ExclusionPath "C:\Program Files\LimitState\RING4.0\bin\ring64.exe" +Add-MpPreference -ExclusionPath "C:\Program Files\PDF Architect 6\architect.exe" +Add-MpPreference -ExclusionPath "C:\Program Files\PeaZip\peazip.exe" +Add-MpPreference -ExclusionPath "C:\Program Files\PeaZip\peazip.exe\%userprofile%\Documents" +Add-MpPreference -ExclusionPath "C:\Program Files\RStudio\bin\rsession-utf8.exe" +Add-MpPreference -ExclusionPath "C:\Program Files\Rclone\rclone.exe" + +Write-Output "Program Exclusions added to defender" \ No newline at end of file diff --git a/scripts_staging/Win_HPE-SSACLI_Install.ps1 b/scripts_staging/Win_HPE-SSACLI_Install.ps1 new file mode 100644 index 00000000..4e3ef0ef --- /dev/null +++ b/scripts_staging/Win_HPE-SSACLI_Install.ps1 @@ -0,0 +1,63 @@ +<# +.SYNOPSIS + Install HPE SSACLI + +.DESCRIPTION + Downloads and installs HPE SSACLI + +.PARAMETER Force (Optional) + [Boolean] - Default:$False - Force a reinstall or downgrade + +.OUTPUTS + Exit Code: 0 = Pass, 1 = Informational, 2 = Warning, 3 = Error + +.EXAMPLE + Win_HPE-SSACLI_Install.ps1 + #No Parameters, defaults apply + +.NOTES + v1.0 12/5/2023 ConvexSERV + Currently targetting version 4.21.7.0 of the HPE SSACLI (x64) +#> + +param ( + [Boolean] $ForceInstall #Force a reinstall or downgrade +) + +#Handle -ForceIntall Parameter +if (-not($ForceInstall)){ + $ArgumentList = "/s" +} +else { + $ArgumentList = "/s /f" +} + +try{ + Write-Host "Info - Downloading Installer..." + Invoke-WebRequest -Uri "https://downloads.hpe.com/pub/softlib2/software1/sc-windows/p955544928/v183348/cp044527.exe" -UseBasicParsing -OutFile "c:\ProgramData\TacticalRMM\temp\cp044527.exe" +} +catch { + $AlertText = "Alert - HPE_SSACLI Download Failed." + Write-Host $AlertText + $AlertLevel = 3 + $Host.SetShouldExit($AlertLevel) + Exit +} + +if (Test-Path "c:\ProgramData\TacticalRMM\temp\cp044527.exe") { + + Write-Host "Info - File Downloaded. Will attempt to install..." + + try { + Write-Host "Installing..." + Start-Process -NoNewWindow -FilePath "c:\ProgramData\TacticalRMM\temp\cp044527.exe" -ArgumentList '/s' -Wait + Write-Host "Install completed. Check refresh installed software to verify." + } + catch { + $AlertText = "Alert - HPE_SSACLI Install Failed." + Write-Host $AlertText + $AlertLevel = 3 + $Host.SetShouldExit($AlertLevel) + Exit + } +} diff --git a/scripts_staging/Win_HPE-SSA_Status.py b/scripts_staging/Win_HPE-SSA_Status.py new file mode 100644 index 00000000..5a2f1996 --- /dev/null +++ b/scripts_staging/Win_HPE-SSA_Status.py @@ -0,0 +1,746 @@ +#!/usr/bin/python3 +# +#.SYNOPSIS +# HPE SmartArray Status +# +#.DESCRIPTION +# Checks the status of RAID array(s) on HPE servers with Smart Array controllers - Requires SSACLI +# +#.OUTPUTS +# Exit Code: 0 = Pass, 1 = Informational, 2 = Warning, 3 = Error +# +#.EXAMPLE +# HPESmartArrayStatus() +# +#.NOTES +# v1.0 12/5/2023 ConvexSERV +# Requires SSACLI to be installed on the server. Will return an error if it's not installed. +# + +import platform +import subprocess +import sys +import os + +#=========================================# +# Declarations # +#=========================================# + +#Declare SSA Command +SSACmd = "" + +#Declare Data Structure for Controller Config/Status +hpssa_config = { + "controllers": { + #'model': "", # - Controller Model + #'slot': "", # - Controller Slot # (Address) [key] + #'embedded': "", # - Controller is Embedded (bool) + #'sn': "", # - Controller Serial Number + #'cages': { #Cages can be renamed? Ex. "Gen8 ServBP 3x6 at Port 2I, Box 1, OK", Typical "Internal Drive Cage at Port 1I, Box 1, OK" + #'internal': "", # - Controller Cage is internal (bool) + #'port': "", # - Controller Cage Port [key]+ + #'box': "", # - Controller Box Port [key]+ + #'status': "", # - Controller Cage Status + #} + # 'arrays': { # # - Arrays contain logical drives and physical drives - (3 leading spaces) + # 'name': "", # - Array Name [key] + # 'media': "", # - Array Media - Ex. "Solid State SATA", "SATA", "SAS", "Solid State SAS", "NVME?" + # 'unused_space': "", # - Free Space (not configured) + # 'unused_space_unit': '', # - Free Space Unit (MB,GB,TB,PB) + # 'logical_drives': { # - (6 leading spaces) + # 'number': "", # - Logical Drive Number [key] + # 'capacity_num': "", # - Logical Drive Capacity (just the number (decimal)) + # 'capacity_unit': "", # - Logical Drive Unit of Capacity (KB, MB, GB, TB, PB) + # 'raid_level': "", # - RAID Level (0 = Stripe, 1 = Mirror, 5 = Parity Stripe, 6 = Double Parity, 1+0 = Stripe of Mirrors) + # 'status': "", # - Logical Drive Status + # } + # 'physical_drives': { # - (6 leading spaces) + # 'address': "", # - Drive Port:Box:Bay [key] + # 'port': "", # - Drive Port + # 'box': "", # - Drive Box + # 'bay': "", # - Drive Bay + # 'type': "", # - Drive Type + # 'capacity_num': "", # - Physical Drive Capacity (just the number (decimal)) + # 'capacity_unit': "", # - Physical Drive Unit of Capacity (KB, MB, GB, TB, PB) + # 'status': "", # - Physical Drive Status + # 'spare': "" # - Physical Drive Spare Status (Is drive assigned as a spare?) + # } + # } + # 'enclosures': { # - (3 leading spaces) + # 'name': "", # - Enclosure Name - Ex. "SEP" - Not sure what it is, capture anyway + # 'vendor_id': "", # - Enclosure Vendor ID + # 'model': "", # - Enclosure Model + # 'device_num': "", # - Enclosure Device Number? - Not sure what it is, capture anyway + # 'port': "", # - Enclosure Port [key]+ + # 'box': "", # - Enclosure Box [key]+ + # 'wwid': "", # - Enclosure WWID - Serial? + # } + # 'expanders': { # - (3 leading spaces) + # 'device_num': "", # - Expander Device Number? - Not sure what it is, capture anyway + # 'port': "", # - Enclosure Port [key]+ + # 'box': "", # - Enclosure Box [key]+ + # 'wwid': "", # - Enclosure WWID - Serial? + # } + # 'devices': { # - (3 leading spaces) + # 'name': "", # - Device Name? - Ex. "SEP" - Not sure what it is, capture anyway + # 'vendor_id': "", # - Device Vendor ID + # 'model': "", # - Device Model + # 'device_num': "", # - Device Number? - Not sure what it is, capture anyway + # 'wwid': "", - # - Device WWID - Serial? [key] + # } + #'status': { + #"ctrl": "", # - Controller Status + #"cache": "", # - Cache Status + #"batt": "" # - Battery/Capacitor Status + #} + } +} + +#=========================================# +# Capture HPE SSA Config - SSAConfigAll[] # +#=========================================# + +#Detect OS, Locate the HPSSA Command +platform = platform.system() + +if platform == 'Linux': + if os.path.exists('/usr/local/bin/hpssacli'): + SSACmd = '/usr/local/bin/hpssacli' + +else: + #'Windows': # Path variables Use "r" (raw string) before the path to correct for backslashes in the path + if os.path.exists(r"C:\Tools\Vendors\HPE\ssacli.exe"): + SSACmd = r"C:\Tools\Vendors\HPE\ssacli.exe" + elif os.path.exists(r"C:\Program Files\Smart Storage Administrator\ssacli\bin\ssacli.exe"): + SSACmd = r"C:\Program Files\Smart Storage Administrator\ssacli\bin\ssacli.exe" + elif os.path.exists(r"C:\Program Files (x86)\Smart Storage Administrator\ssacli\bin\ssacli.exe"): + SSACmd = r"C:\Program Files (x86)\Smart Storage Administrator\ssacli\bin\ssacli.exe" + elif os.path.exists(r"C:\Program Files (x86)\hp\hpssacli\bin\hpssacli.exe"): + SSACmd = r"C:\Program Files (x86)\hp\hpssacli\bin\hpssacli.exe" + elif os.path.exists(r"C:\Program Files\hp\hpssacli\bin\hpssacli.exe"): + SSACmd = r"C:\Program Files\hp\hpssacli\bin\hpssacli.exe" + elif os.path.exists(r"C:\Tools\Vendors\HPE\hpssacli.exe"): + SSACmd = r"C:\Tools\Vendors\HPE\hpssacli.exe" + +#Exit and Return Error Status if HPSSA Command is not found - Commented out while we use test files +if SSACmd == "": + sys.stdout.write('HPASSA Command was not found in any of the configured paths.') + sys.exit(3) + +#Capture Output of HPSSA Config and store it - Commented out while we use test files +try: + SSAConfigAll = [] + with subprocess.Popen([SSACmd, "ctrl", "all", "show", "config"], stdout=subprocess.PIPE, + bufsize=1, universal_newlines=True) as SSACmdOutput: + for line in SSACmdOutput.stdout: + SSAConfigAll.append(line) +except subprocess.CalledProcessError as e: + sys.stdout.write(f'Command {e.cmd} failed with error {e.returncode}') + sys.exit(3) + +#=======================================# +# Parse HPE SSA Config - SSAConfigAll[] # +#=======================================# + +# Blank Line Counter +bl_count = 0 +for config_line in SSAConfigAll: + + #Remove NewLine Characters + config_line = config_line.replace("\n", "") + + #sys.stdout.write(config_line) + if config_line == "" or config_line == '\n' or config_line == " ": + bl_count = bl_count + 1 + else: + + # Split line by spaces to check for items on the config line + config_line_split = config_line.split(" ") + + if config_line[0:11] == "Smart Array" or config_line[0:2] == "HP": # New Controller + + # Initialize Dictionary for Controller + current_controller = { + 'model': "", + 'slot': "", + 'sn': "", + 'embedded': False, + 'cages': {}, + 'arrays': {}, + 'enclosures': {}, + 'expanders': {}, + 'devices': {}, + 'status': {} + } + + if config_line[0:11] == "Smart Array": + + # Check for Model + current_controller["model"] = config_line_split[2] + # Check for Slot + current_controller["slot"] = config_line_split[5] + current_controller_slot = config_line_split[5] + # Check for '(Embedded)' + if len(config_line_split) > 11: + if config_line_split[6] == '(Embedded)': + current_controller["embedded"] = True + else: + config_line_embedded = False + #Trim out the Serial Number + sn_item = config_line_split[len(config_line_split)-1] + current_controller["sn"] = sn_item[0:len(sn_item) - 1] + + elif config_line[0:2] == "HP": + + # Check for Model + current_controller["model"] = config_line_split[1] + # Check for Slot + current_controller["slot"] = config_line_split[4] + current_controller_slot = config_line_split[4] + + # Check for '(Embedded)' + if config_line_split[1][-1:0] == 'i': + current_controller["embedded"] = True + else: + config_line_embedded = False + + #Trim out the Serial Number + sn_item = config_line_split[len(config_line_split) - 1] + if sn_item != '()': + current_controller["sn"] = sn_item[0:len(sn_item) - 1] + + # Add Current Controller to hpssa_config["controllers"] + hpssa_config["controllers"].update({current_controller_slot: current_controller}) + + # Reset the Blank Line Counter + bl_count = 0 + + #Check for a Port Name + elif config_line_split[3] == 'Port' and config_line_split[4] == 'Name:': + #Port Names seem to pop up inconsistently in the config + #Ignore for now + bl_count = bl_count # Do something to appease the compiler + + else: # Anything but a Controller or blank line... + + #Check for a Cage - Cages have the string 'at Port'. + # Search backwards to avoid issues with spaces in the Cage Name + if config_line_split[len(config_line_split)-6] == 'at' and \ + config_line_split[len(config_line_split)-5] == 'Port': + + # Initialize Dictionary for Cage + current_cage = { + 'internal': "", # - Controller Cage is internal (bool) + 'port': "", # - Controller Cage Port [key]+ + 'box': "", # - Controller Box Port [key]+ + 'status': "", # - Controller Cage Status + } + #Check for internal Cage + if config_line_split[3] == 'External': + current_cage["internal"] = False + else: + current_cage["internal"] = True + + #Check for Cage Status + if config_line_split[-1][0:-1] == 'OK': + + #Set Status + current_cage["status"] = config_line_split[len(config_line_split)-1] + + #Set Port and Box + current_cage["port"] = config_line_split[len(config_line_split)-4] + current_cage["box"] = config_line_split[len(config_line_split)-2] + + else: #Cage Error Status + + #Set Status + current_cage["status"] = config_line_split[len(config_line_split)-1] + + #Set Port and Box - # ToDo - Spaces in Error status may change offsets + current_cage["port"] = config_line_split[len(config_line_split)-2] + current_cage["box"] = config_line_split[len(config_line_split)-4] + + # Add Current Cage to hpssa_config["controllers"][current_controller["slot"]]["cages"] + hpssa_config["controllers"][current_controller_slot]["cages"].update( \ + {current_cage["port"]+current_cage["box"]: current_cage}) + + #Check for an Array - Arrays usually start with ' Array' or ' array' + # However, it may be possible to rename an array. + # If an array name can have spaces, it will disrupt this logic. + # Arrays always seeem to have 'Unused Space' at positions (len -5, len -4) + + # Search backwards to avoid issues with spaces in the Array Name + if config_line_split[3] == 'Array' or config_line_split[3] == 'array' or \ + config_line_split[len(config_line_split) - 5] == 'Unused' and \ + config_line_split[len(config_line_split) - 4] == 'Space:': + + # Initialize Dictionary for Array + current_array = { + 'name': "", # - Array Name [key] + 'media': "", # - Array Media - Ex. "Solid State SATA", "SATA", "SAS", "Solid State SAS", "NVME?" + 'unused_space': "", # - Free Space (not configured) + 'unused_space_unit': '', # - Free Space Unit (MB,GB,TB,PB) + 'logical_drives': {}, # - (6 leading spaces) + 'physical_drives': {} # - (6 leading spaces) + } + + #Get Array Name - Array Name is everything (trim spaces) before the first '(' + open_paren = config_line.find('(') + current_array["name"] = config_line[3:open_paren][0:-1] + + #Get Array Media Type - Media Type is everything from the '(' to the first ',' + first_comma = config_line.find(',') + current_array["media"] = config_line[open_paren + 1:first_comma] + + #Get Unused Space and unit + current_array["unused_space"] = config_line_split[len(config_line_split) - 3] + current_array["unused_space_unit"] = config_line_split[len(config_line_split) - 1][0:2] + + # Add Current Array to hpssa_config["controllers"][current_controller["slot"]]["arrays"] + hpssa_config["controllers"][current_controller["slot"]]["arrays"].update( \ + {current_array["name"]: current_array}) + + #Unassigned disks are also structured like an array, use the same data structure (Special Case). + if config_line_split[3] == 'Unassigned': + + # Initialize Dictionary for (Unassigned) Array + current_array = { + 'name': "Unassigned", # - Array Name [key] + 'media': "Unassigned", # - Unassigned + 'unused_space': "0", # - Free Space (not configured) + 'unused_space_unit': 'MB', # - Free Space Unit (MB,GB,TB,PB) + 'logical_drives': {}, # - (6 leading spaces) + 'physical_drives': {} # - (6 leading spaces) + } + + # Add Current Array to hpssa_config["controllers"][current_controller["slot"]]["arrays"] + hpssa_config["controllers"][current_controller["slot"]]["arrays"].update( \ + {current_array["name"]: current_array}) + + + if len(config_line_split) > 6: + # Get Logical Drives + if config_line_split[6] == 'logicaldrive': + + # Initialize Dictionary for Logical Drive + current_ld = { # - (6 leading spaces) + 'number': "", # - Logical Drive Number [key] + 'capacity_num': "", # - Logical Drive Capacity (just the number (decimal)) + 'capacity_unit': "", # - Logical Drive Unit of Capacity (KB, MB, GB, TB, PB) + 'raid_level': "", # - RAID Level (0 = Stripe, 1 = Mirror, 5 = Parity Stripe, 6 = Double Parity, 1+0 = Stripe of Mirrors) + 'status': "", # - Logical Drive Status + } + + # Get Logical Drive Number + current_ld["number"] = config_line_split[7] + + # Get Logical Drive Capacity and Unit + current_ld["capacity_num"] = config_line_split[8][1:] + current_ld["capacity_unit"] = config_line_split[9][0:2] + + # Get Logical Drive RAID Level + current_ld["raid_level"] = config_line_split[11][0:-1] + + # Get Logical Drive Status + current_ld["status"] = config_line_split[12][0:-1] + + # Add Current Logical Drive to hpssa_config["controllers"][current_controller["slot"]]["arrays"][current_array_name]["logical_drives"] + hpssa_config["controllers"][current_controller["slot"]]["arrays"][current_array["name"]] \ + ["logical_drives"].update({current_ld["number"]: current_ld}) + + #Get Physical Drives + if config_line_split[6] == 'physicaldrive': + + # Initialize Dictionary for Physical Drive + current_pd = { # - (6 leading spaces) + 'address': "", # - Drive Port:Box:Bay [key] + 'port': "", # - Drive Port + 'box': "", # - Drive Box + 'bay': "", # - Drive Bay + 'type': "", # - Drive Type + 'capacity_num': "", # - Physical Drive Capacity (just the number (decimal)) + 'capacity_unit': "", # - Physical Drive Unit of Capacity (KB, MB, GB, TB, PB) + 'status': "", # - Physical Drive Status + 'spare': "" # - Physical Drive Spare Status (Is drive assigned as a spare?) + } + + # Get Physical Drive Address (Port:Box:Bay) + current_pd["address"] = config_line_split[7] + address_split = config_line_split[7].split(":") + current_pd["port"] = address_split[0] + current_pd["box"] = address_split[1] + current_pd["bay"] = address_split[2] + + # Get Physical Drive Type (Differences in SSA versions) + open_paren = config_line.find('(') + close_paren = config_line.find(')') + drive_split = config_line[open_paren + 1:close_paren].split(",") + current_pd["type"] = drive_split[1][1:] + + # Get Physical Drive Capacity and Unit + capacity_split = drive_split[2].split(" ") + current_pd["capacity_num"] = capacity_split[1] + current_pd["capacity_unit"] = capacity_split[2] + + # Get Physical Drive Status + current_pd["status"] = drive_split[3][1:] + + # Get Physical Drive Spare Status + if len(config_line_split) > 17: + if config_line_split[17][0:-1] == 'spare': + current_pd["spare"] = True + else: + current_pd["spare"] = False + else: + current_pd["spare"] = False + + # Add Current Physical Drive to hpssa_config["controllers"][current_controller["slot"]]["arrays"][current_array_name]["physical_drives"] + hpssa_config["controllers"][current_controller["slot"]]["arrays"][current_array["name"]]\ + ["physical_drives"].update({current_pd["address"]: current_pd}) + + #Get Devices - Enclosure + if config_line_split[3] == 'Enclosure': + + # Initialize Dictionary for Enclosure + current_enclosure = { # - (3 leading spaces) + 'name': "", # - Enclosure Name - Ex. "SEP" - Not sure what it is, capture anyway + 'vendor_id': "", # - Enclosure Vendor ID + 'model': "", # - Enclosure Model + 'device_num': "", # - Enclosure Device Number? - Not sure what it is, capture anyway + 'port': "", # - Enclosure Port [key]+ + 'box': "", # - Enclosure Box [key]+ + 'wwid': "" # - Enclosure WWID - Serial? + } + + # Get Enclosure Name - Enclosure Name is everything (trim spaces) before the first '(' + open_paren = config_line.find('(') + current_enclosure["name"] = config_line[3:open_paren] + + # Get Enclosure Vendor ID and Model - Contained between the first '(' and ')' + close_paren = config_line.find(')') + vendor_model_split = config_line[open_paren + 1:close_paren].split(",") + current_enclosure["vendor_id"] = vendor_model_split[0][10:] + current_enclosure["model"] = vendor_model_split[1][7:] + + # Get Enclosure Device Number - Contained after the first ')' + current_enclosure["device_num"] = config_line[close_paren + 2:close_paren + 5] + + # Get Enclosure WWID, Port and Box - Contained between the second '(' and ')' + open_paren = config_line.rfind('(') + close_paren = config_line.rfind(')') + wwid_port_box_split = config_line[open_paren + 1:close_paren].split(",") + current_enclosure["port"] = wwid_port_box_split[1].split(":")[1][1:] + current_enclosure["box"] = wwid_port_box_split[2].split(":")[1][1:] + current_enclosure["wwid"] = wwid_port_box_split[0].split(":")[1][1:] + + # Add Current Enclosure to hpssa_config["controllers"][current_controller["slot"]]["enclosures"] + hpssa_config["controllers"][current_controller["slot"]]["enclosures"]\ + .update({current_enclosure["port"] + ':' + current_enclosure["box"]: current_enclosure}) + + # Get Devices - Expander + if config_line_split[3] == 'Expander': + + # Initialize Dictionary for Expander + current_expander = { # - (3 leading spaces) + 'device_num': "", # - Expander Device Number? - Not sure what it is, capture anyway + 'port': "", # - Enclosure Port [key]+ + 'box': "", # - Enclosure Box [key]+ + 'wwid': "" # - Enclosure WWID - Serial? + } + + # Get Expander Device Number - Expander Device Number is between 'Expander; and the first '(' + current_expander["device_num"] = config_line[12:15] + + # Get Enclosure WWID, Port and Box - Contained between the '(' and ')' + open_paren = config_line.find('(') + close_paren = config_line.find(')') + wwid_port_box_split = config_line[open_paren + 1:close_paren].split(",") + current_expander["port"] = wwid_port_box_split[1].split(":")[1][1:] + current_expander["box"] = wwid_port_box_split[2].split(":")[1][1:] + current_expander["wwid"] = wwid_port_box_split[0].split(":")[1][1:] + + # Add Current Expander to hpssa_config["controllers"][current_controller["slot"]]["expanders"] + hpssa_config["controllers"][current_controller["slot"]]["expanders"] \ + .update({current_expander["port"] + ":" + current_expander["box"]: current_expander}) + + # Get Devices - SEP (Backplane?) + if config_line_split[3] == 'SEP': + + # Initialize Dictionary for SEP (Backplane?) + current_device = { # - (3 leading spaces) + 'name': "", # - Device Name? - Ex. "SEP" - Not sure what it is, capture anyway + 'vendor_id': "", # - Device Vendor ID + 'model': "", # - Device Model + 'device_num': "", # - Device Number? - Not sure what it is, capture anyway + 'wwid': "" # - Device WWID - Serial? [key] + } + + # Get Device Name - Enclosure Name is everything (trim spaces) before the first '(' + open_paren = config_line.find('(') + current_device["name"] = config_line[3:open_paren] + + # Get Device Vendor ID and Model - Contained between the first '(' and ')' + close_paren = config_line.find(')') + vendor_model_split = config_line[open_paren + 1:close_paren].split(",") + current_device["vendor_id"] = vendor_model_split[0][10:] + current_device["model"] = vendor_model_split[1][7:] + + # Get Device Device Number - Contained after the first ')' + current_device["device_num"] = config_line[close_paren + 2:close_paren + 5] + + # Get Device WWID - Contained between the second '(' and ')' + open_paren = config_line.rfind('(') + close_paren = config_line.rfind(')') + wwid_split = config_line[open_paren + 1:close_paren].split(":") + current_device["wwid"] = wwid_split[1][1:] + + # Add Current Device to hpssa_config["controllers"][current_controller["slot"]]["devices"] + hpssa_config["controllers"][current_controller["slot"]]["devices"] \ + .update({current_device["wwid"]: current_device}) + +#=========================================# +# Capture HPE SSA Status - SSAStatusAll[] # +#=========================================# + +# Capture Output of HPSSA Status and store it - Commented out while we use test files +try: + SSAStatus = [] + with subprocess.Popen([SSACmd, "ctrl", "all", "show", "status"], stdout=subprocess.PIPE, + bufsize=1, universal_newlines=True) as SSACmdOutput: + for line in SSACmdOutput.stdout: + SSAStatus.append(line) +except subprocess.CalledProcessError as e: + sys.stdout.write(f'Command {e.cmd} failed with error {e.returncode}') + sys.exit(3) + +#=======================================# +# Parse HPE SSA Status - SSAStatusAll[] # +#=======================================# + +#Blank Line Counter +bl_count = 0 +for status_line in SSAStatus: + + #Remove NewLine Characters + status_line = status_line.replace("\n", "") + + #sys.stdout.write(status_line) + if status_line == "": + bl_count = bl_count + 1 + else: + if status_line[0:11] == "Smart Array" or status_line[0:2] == "HP": + + if status_line[0:11] == "Smart Array": + + status_line_split = status_line.split(" ") + #Check for Model + status_model = status_line_split[2] + #Check for Slot + status_slot = status_line_split[5] + #Check for '(Embedded)' + if len(status_line_split) > 6: + if status_line_split[6][0:-1] == '(Embedded)': + status_ctrl_embedded = "e" + else: + status_ctrl_embedded = "" + + elif status_line[0:2] == "HP": + + status_line_split = status_line.split(" ") + #Check for Model + status_model = status_line_split[1] + #Check for Slot + status_slot = status_line_split[4] + + # Check for '(Embedded)' + if status_line_split[1][-1:0] == 'i': + status_ctrl_embedded = "e" + else: + status_ctrl_embedded = "" + + #Initialize Dictionary for Controller Status Dictionary + hpssa_config["controllers"][status_slot]["status"] = { + "ctrl": "", + "cache": "", + "batt": "" + } + + #Reset the Blank Line Counter + bl_count = 0 + + else: + status_line_split = status_line.split(":") + + #Detect and set Controller Status Item + if status_line_split[0].strip(" ") == 'Controller Status': + hpssa_config["controllers"][status_slot]["status"]["ctrl"] = status_line_split[1].strip(" ") + + elif status_line_split[0].strip(" ") == 'Cache Status': + hpssa_config["controllers"][status_slot]["status"]["cache"] = status_line_split[1].strip(" ") + + elif status_line_split[0].strip(" ") == 'Battery/Capacitor Status': + hpssa_config["controllers"][status_slot]["status"]["batt"] = status_line_split[1].strip(" ") + +#===========================================# +# Run Checks against HPE SSA Data Structure # +#===========================================# + +#Initialize Return Code to 0 - Pass +return_code = 0 + +#Walk Through the config's controllers dictionary... +for ctrl in hpssa_config["controllers"]: + + #Check if the controller is Embedded + if hpssa_config["controllers"][ctrl]["embedded"]: + embedded_line = "(Embedded) " + else: + embedded_line = "" + + # Check Each Controller's overall status (Fail if not 'OK') + if hpssa_config["controllers"][ctrl]["status"]["ctrl"] != 'OK': #Controller Critical State + + return_code = 3 + state_line = "Critical" + else: + state_line = "Normal" + + ctrl_line = embedded_line + "Controller " + hpssa_config["controllers"][ctrl]["model"] + " in slot " + \ + hpssa_config["controllers"][ctrl]["slot"] + " (SN:" + \ + hpssa_config["controllers"][ctrl]["sn"] + ") is in a " + state_line + " State (" + \ + hpssa_config["controllers"][ctrl]["status"]["ctrl"] + ")" + if return_code > 0: + print(ctrl_line) + + #Check Cache Status (Warn if not 'OK' or 'Not Configured' or Empty) + if hpssa_config["controllers"][ctrl]["status"]["cache"] != 'OK' and\ + hpssa_config["controllers"][ctrl]["status"]["cache"] != 'Not Configured' and\ + hpssa_config["controllers"][ctrl]["status"]["cache"] != '': #Cache Degraded State + + state_line = "Degraded" + if return_code < 2: + return_code = 2 + else: + state_line = "Normal" + + ctrl_line = embedded_line + "Controller " + hpssa_config["controllers"][ctrl]["model"] + " in slot " + \ + hpssa_config["controllers"][ctrl]["slot"] + " (SN:" + \ + hpssa_config["controllers"][ctrl]["sn"] + ") Cache is in a " + state_line + " State (" + \ + hpssa_config["controllers"][ctrl]["status"]["cache"] + ")" + if return_code > 0: + print(ctrl_line) + + #Check Battery/Capacitor Status (Warn if not 'OK' or Empty) + if hpssa_config["controllers"][ctrl]["status"]["batt"] != 'OK' and \ + hpssa_config["controllers"][ctrl]["status"]["batt"] != '' : #Cache Battery Degraded State + + state_line = "Degraded" + if return_code < 2: + return_code = 2 + else: + state_line = "Normal" + + ctrl_line = embedded_line + "Controller "+ hpssa_config["controllers"][ctrl]["model"] + " in slot " + \ + hpssa_config["controllers"][ctrl]["slot"] + " (SN:" + \ + hpssa_config["controllers"][ctrl]["sn"] + ") Cache Battery/Capacitor is in a " + state_line + " State (" + \ + hpssa_config["controllers"][ctrl]["status"]["batt"] + ")" + if return_code > 0: + print(ctrl_line) + + #Check Cage Status (Fail if not 'OK') + for cage in hpssa_config["controllers"][ctrl]["cages"]: + + if hpssa_config["controllers"][ctrl]["cages"][cage]["status"] != 'OK': # Cage is in a Critical State + + return_code = 3 + cage_code = 3 + state_line = "Critical" + else: + state_line = "Normal" + cage_code = 0 + + ctrl_line = embedded_line + "Controller " + hpssa_config["controllers"][ctrl]["model"] + " in slot " + \ + hpssa_config["controllers"][ctrl]["slot"] + " (SN:" + \ + hpssa_config["controllers"][ctrl]["sn"] + ") - Cage at Port:" + \ + hpssa_config["controllers"][ctrl]["cages"][cage]["port"] + \ + ", Box:" + hpssa_config["controllers"][ctrl]["cages"][cage]["box"] + \ + " is in a " + state_line + " State (" + \ + hpssa_config["controllers"][ctrl]["cages"][cage]["status"] + ")" + + if cage_code > 0: + print(ctrl_line) + + #Walk Arrays for Logical and Physical Drives + for array in hpssa_config["controllers"][ctrl]["arrays"]: + + #Check Logical Drive Status (Fail if not 'OK', Warn if Rebuilding) + for ld in hpssa_config["controllers"][ctrl]["arrays"][array]["logical_drives"]: + + # Logical Drive is in a Normal State + if hpssa_config["controllers"][ctrl]["arrays"][array]["logical_drives"][ld]["status"] == 'OK': + + state_line = "Normal" + drive_code = 0 + + # Logical Drive is in a Recovering State + elif hpssa_config["controllers"][ctrl]["arrays"][array]["logical_drives"][ld]["status"] == 'Recovering': + + state_line = "Recovering" + if return_code < 1: + return_code = 1 + drive_code = 1 + + # Logical Drive is in a Critical State + else: + + return_code = 3 + drive_code = 3 + state_line = "Critical" + + ctrl_line = embedded_line + "Controller " + hpssa_config["controllers"][ctrl]["model"] + " in slot " + \ + hpssa_config["controllers"][ctrl]["slot"] + " (SN:" + \ + hpssa_config["controllers"][ctrl]["sn"] + ") - Logical Drive :" + \ + hpssa_config["controllers"][ctrl]["arrays"][array]["logical_drives"][ld]["number"] + \ + " is in a " + state_line + " State (" + \ + hpssa_config["controllers"][ctrl]["arrays"][array]["logical_drives"][ld]["status"] + ")" + if drive_code > 0: + print(ctrl_line) + + #Check Physical Drive Status (Fail if not 'OK' - Info if not 'OK' and part of 'Unassisgned') + for pd in hpssa_config["controllers"][ctrl]["arrays"][array]["physical_drives"]: + + # Physical Drive is in a Normal State + if hpssa_config["controllers"][ctrl]["arrays"][array]["physical_drives"][pd]["status"] == 'OK': + + state_line = "Normal" + drive_code = 0 + + # Physical Drive is in a Rebuilding State + elif hpssa_config["controllers"][ctrl]["arrays"][array]["physical_drives"][pd]["status"] == 'Rebuilding': + + state_line = "Recovering" + if return_code < 1: + return_code = 1 + drive_code = 1 + + # Physical Drive is in a Critical State + else: + + return_code = 3 + drive_code = 3 + state_line = "Critical" + + ctrl_line = embedded_line + "Controller " + hpssa_config["controllers"][ctrl]["model"] + " in slot " + \ + hpssa_config["controllers"][ctrl]["slot"] + " (SN:" + \ + hpssa_config["controllers"][ctrl]["sn"] + ") - Physical Drive at Port:" + \ + hpssa_config["controllers"][ctrl]["arrays"][array]["physical_drives"][pd]["port"] + ", Box:" +\ + hpssa_config["controllers"][ctrl]["arrays"][array]["physical_drives"][pd]["box"] + ", Bay:" +\ + hpssa_config["controllers"][ctrl]["arrays"][array]["physical_drives"][pd]["bay"] + \ + " is in a " + state_line + " State (" + \ + hpssa_config["controllers"][ctrl]["arrays"][array]["physical_drives"][pd]["status"] + ")" + if drive_code > 0: + print(ctrl_line) + +if return_code == 0: + print("All Controller Checks Passed.") +sys.exit(return_code) diff --git a/scripts_staging/Win_Logon_FailsCheck.ps1 b/scripts_staging/Win_Logon_FailsCheck.ps1 new file mode 100644 index 00000000..d9776fc0 --- /dev/null +++ b/scripts_staging/Win_Logon_FailsCheck.ps1 @@ -0,0 +1,59 @@ +# Modified based off of the work of Discord user silverswordtheitguy. Thanks! + +Write-Output "Starting" + +# Define a function to log login and logout events as a table +function Log-LoginLogoutEvent { + param ( + [string]$UserName, + [string]$EventType, + [string]$LogonType, + [string]$WorkstationName, + [string]$SourceNetworkAddress + ) + $LogMessage = New-Object PSObject -Property @{ + 'Timestamp' = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + 'Username' = $UserName + 'EventType' = $EventType + 'LogonType' = $LogonType + 'WorkstationName' = $WorkstationName + 'SourceNetworkAddress' = $SourceNetworkAddress + } + Write-Output $LogMessage +} + +# Calculate the start time for the last 24 hours +$StartTime = (Get-Date).AddDays(-1) + +# Initialize an ArrayList for logged events +$LoggedEvents = New-Object System.Collections.ArrayList + +# Retrieve failed logon events within the last 24 hours +$FailedLogonEvents = Get-WinEvent -FilterHashtable @{LogName='Security'; ID=4625; StartTime=$StartTime} -ErrorAction SilentlyContinue + +foreach ($Event in $FailedLogonEvents) { + $EventId = $Event.Id + $UserName = $Event.Properties[5].Value + $LogonType = $Event.Properties[10].Value + $WorkstationName = $Event.Properties[13].Value + $SourceNetworkAddress = $Event.Properties[19].Value + + $EventType = "Failed Logon" + + # Check if the username is not "SYSTEM" before logging + if ($UserName -ne "SYSTEM") { + $null = $LoggedEvents.Add((Log-LoginLogoutEvent -UserName $UserName -EventType $EventType -LogonType $LogonType -WorkstationName $WorkstationName -SourceNetworkAddress $SourceNetworkAddress)) + } +} + +# Format the output as a table with five columns +$LoggedEvents | Format-Table -Property Timestamp, Username, EventType, LogonType, SourceNetworkAddress, WorkstationName -AutoSize + +Write-Output "Finished" + +# Output an exit code based on whether any failed logons were found +if ($LoggedEvents.Count -gt 0) { + exit 1 +} else { + exit 0 +} diff --git a/scripts_staging/Win_NetworkScanner.py b/scripts_staging/Win_NetworkScanner.py new file mode 100644 index 00000000..8413e867 --- /dev/null +++ b/scripts_staging/Win_NetworkScanner.py @@ -0,0 +1,170 @@ +#!/usr/bin/python3 + +""" +This script performs a network scan on a given target or subnet. +It checks if the target hosts are alive, and if ports 80 (HTTP) and 443 (HTTPS) are open, and optionally performs reverse DNS lookups if specified. + +Params: +--hostname Perform reverse DNS lookup +--mac Include MAC address in output + +v1.1 2/2024 silversword411 +v1.4 added open port checker +v1.5 5/2/2024 silversword411 integrated reverse DNS lookup into the ping function with 1-second timeout +v1.6 5/31/2024 silversword411 align output to columns and ports low to high +v1.7 2/18/2025 silversword411 fix columns with long host names and added response time +v1.8 5/21/2025 silversword411 added MAC address lookup with --mac option + +TODO: Make subnet get automatically detected +TODO: run on linux as well +""" + +import socket +import threading +import subprocess +import ipaddress +import re +from collections import defaultdict +import argparse + + +def get_host_ip(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s.connect(("10.255.255.255", 1)) + IP = s.getsockname()[0] + except Exception: + IP = "127.0.0.1" + finally: + s.close() + return IP + + +def ping_ip(ip, alive_hosts, do_reverse_dns): + try: + output = subprocess.check_output( + ["ping", "-n", "1", "-w", "1000", ip], + stderr=subprocess.STDOUT, + universal_newlines=True, + ) + if "Reply from" in output: + alive_ip = ipaddress.ip_address(ip) + response_time = re.search(r"time[=<]\s*(\d+)ms", output) + response_time = int(response_time.group(1)) if response_time else -1 + + hostname = "NA" + if do_reverse_dns: + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(1) + hostname = socket.gethostbyaddr(ip)[0] + except socket.error: + hostname = "unknown" + finally: + s.close() + + alive_hosts.append( + (alive_ip, hostname, response_time, "") + ) # Placeholder for MAC + except Exception: + pass + + +def get_mac_address(ip): + try: + output = subprocess.check_output(["arp", "-a", ip], universal_newlines=True) + match = re.search(r"(\w{2}-\w{2}-\w{2}-\w{2}-\w{2}-\w{2})", output) + return match.group(1) if match else "N/A" + except Exception: + return "N/A" + + +def check_ports(ip, port, open_ports): + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.settimeout(1) + if s.connect_ex((ip, port)) == 0: + open_ports[ip].append(port) + except Exception: + pass + + +def parse_arguments(): + parser = argparse.ArgumentParser( + description="Scan network subnet for alive hosts, open ports, and optionally perform reverse DNS or get MAC address." + ) + parser.add_argument( + "--hostname", help="Perform reverse DNS lookup", action="store_true" + ) + parser.add_argument( + "--mac", help="Include MAC address in output", action="store_true" + ) + return parser.parse_args() + + +def main(): + args = parse_arguments() + host_ip = get_host_ip() + print(f"Detected Host IP: {host_ip}") + + subnet = ipaddress.ip_network(f"{host_ip}/24", strict=False) + alive_hosts = [] + open_ports = defaultdict(list) + + threads = [] + for ip in subnet.hosts(): + t = threading.Thread(target=ping_ip, args=(str(ip), alive_hosts, args.hostname)) + t.start() + threads.append(t) + + for t in threads: + t.join() + + if args.mac: + for i, (ip, hostname, response_time, _) in enumerate(alive_hosts): + mac = get_mac_address(str(ip)) + alive_hosts[i] = (ip, hostname, response_time, mac) + + alive_hosts.sort(key=lambda x: x[0]) + + port_check_threads = [] + for host, _, _, _ in alive_hosts: + for port in [22, 23, 25, 80, 443, 2525, 8443, 10443, 10000, 20000]: + t = threading.Thread(target=check_ports, args=(str(host), port, open_ports)) + t.start() + port_check_threads.append(t) + + for t in port_check_threads: + t.join() + + max_hostname_length = max( + (len(hostname) for _, hostname, _, _ in alive_hosts), default=8 + ) + ip_column_width = 16 + hostname_column_width = max(max_hostname_length, 12) + 2 + response_time_column_width = 8 + mac_column_width = 20 if args.mac else 0 + ports_column_width = 50 + + header = f"{'IP':<{ip_column_width}}{'(ms)':<{response_time_column_width}}{'Hostname':<{hostname_column_width}}" + if args.mac: + header += f"{'MAC Address':<{mac_column_width}}" + header += f"{'Open Ports':<{ports_column_width}}" + print(header) + print("-" * len(header)) + + for host, hostname, response_time, mac in alive_hosts: + ports = sorted(open_ports[str(host)]) + ports_str = ", ".join(map(str, ports)) + response_time_str = f"{response_time} ms" if response_time >= 0 else "N/A" + line = f"{str(host):<{ip_column_width}}{response_time_str:<{response_time_column_width}}{hostname:<{hostname_column_width}}" + if args.mac: + line += f"{mac:<{mac_column_width}}" + line += f"{ports_str:<{ports_column_width}}" + print(line) + + print(f"\nTotal count of alive hosts: {len(alive_hosts)}") + + +if __name__ == "__main__": + main() diff --git a/scripts_staging/Win_Network_DisableEnable.ps1 b/scripts_staging/Win_Network_DisableEnable.ps1 new file mode 100644 index 00000000..f8ae36bb --- /dev/null +++ b/scripts_staging/Win_Network_DisableEnable.ps1 @@ -0,0 +1,50 @@ +<# +.SYNOPSIS + Toggle Network Interface Card (NIC) Status + This script alternates between enabling and disabling the specified NIC. + +.DESCRIPTION + This PowerShell script will toggle the status of the specified Network Interface Card (NIC). If you disable the active NIC you may have a script timeout because you can't get the return data back + +.PARAMETER NICName + The name of the Network Interface Card (NIC) to toggle. + +.EXEMPLE + -NICName 'Embedded LOM 1 Port 2' + +.NOTES + v1.0 2/11/2024 Orbitturner + +#> + +param ( + [string]$NICName +) + +# Function to get a list of available NICs with information +function Get-NICList { + Get-NetAdapter | Select-Object Name, Status, InterfaceDescription +} + +# Check if NICName is provided +if (-not $NICName) { + Write-Output "NICName parameter is required. Available NICs:" + Get-NICList + Exit 1 +} + +$up = "Up" +$disabled = "Disabled" + +# Check the current status of the specified NIC +$lanStatus = Get-NetAdapter | Select-Object Name, Status | Where-Object { $_.Status -match $up -and $_.Name -match $NICName } + +# Toggle the NIC status based on the current state +if ($lanStatus) { + Write-Output ("Disabling $NICName") + Disable-NetAdapter -Name $NICName -Confirm:$false +} +else { + Write-Output ("Enabling $NICName") + Enable-NetAdapter -Name $NICName -Confirm:$false +} diff --git a/scripts_staging/Win_Network_Speed_Test.ps1 b/scripts_staging/Win_Network_Speed_Test.ps1 new file mode 100644 index 00000000..4fb5a529 --- /dev/null +++ b/scripts_staging/Win_Network_Speed_Test.ps1 @@ -0,0 +1,81 @@ +<# + .SYNOPSIS + This will download and run iperf to check network speeds, you need one machine on the network as a server and another as a client. + .PARAMETER Mode + The only mode parameter is server, set by using -mode server. Obviously this will only work in-LAN and server mode will be killed after script timeout. + .PARAMETER IP + Set IP but using -IP IPADDRESS. Not to be used with server mode + .PARAMETER Seconds + Client tests default to 3 seconds unless you want to run the tests longer. + .EXAMPLE + Server mode + -mode server + .EXAMPLE + Client mode + -IP 192.168.11.18 + .EXAMPLE + -IP 192.168.11.18 -Seconds 10 + .NOTES + 3/30/2022 v1 dinger1986 initial release + 9/20/2023 v2 silversword411 adding -Seconds param. Updated to recommended folders. Needs testing to verify doesn't break in production scripts then replace official script + + #> + + param ( + [string] $IP, + [int] $Seconds, + [string] $Mode +) + +# Check if $Seconds is not specified or 0 and set default value +if (-not $Seconds) { + $Seconds = 3 +} + +If (!(test-path $env:programdata\TacticalRMM\temp\)) { + New-Item -ItemType Directory -Force -Path $env:programdata\TacticalRMM\temp\ +} +If (!(test-path $env:programdata\TacticalRMM\toolbox\)) { + New-Item -ItemType Directory -Force -Path $env:programdata\TacticalRMM\toolbox\ +} +If (!(test-path $env:programdata\TacticalRMM\toolbox\iperf3)) { + New-Item -ItemType Directory -Force -Path $env:programdata\TacticalRMM\toolbox\iperf3\ +} + +Set-Location $env:programdata\TacticalRMM\temp\ + +If (!(test-path "$env:programdata\TacticalRMM\toolbox\iperf3\iperf3.exe")) { + Write-Output "iperf3.exe doesn't exist, downloading and extracting" +Invoke-WebRequest https://iperf.fr/download/windows/iperf-3.1.3-win64.zip -Outfile iperf3.zip + +# Expand and move files to toolbox +expand-archive iperf3.zip +Set-Location $env:programdata\TacticalRMM\temp\iperf3\iperf-3.1.3-win64\ +Move-Item .\cygwin1.dll $env:programdata\TacticalRMM\toolbox\iperf3\ +Move-Item .\iperf3.exe $env:programdata\TacticalRMM\toolbox\iperf3\ + +# Cleanup +Set-Location $env:programdata\TacticalRMM\toolbox\ +Remove-Item -LiteralPath "$env:programdata\TacticalRMM\temp\iperf3.zip" -Force -Recurse +Remove-Item -LiteralPath "$env:programdata\TacticalRMM\temp\iperf3\" -Force -Recurse +} + +if ($Mode -eq "server") { + Write-Output "Starting iPerf3 Server" + netsh advfirewall firewall add rule name="iPerf3" dir=in action=allow program="$env:programdata\TacticalRMM\toolbox\iperf3\iperf3.exe" enable=yes + & '$env:programdata\TacticalRMM\toolbox\iperf3\iperf3.exe' -s + Start-Sleep -Seconds 20 + taskkill /IM "iPerf3.exe" /F + exit +} + +else { + Write-Output "################# TCP Upload #################" + & 'C:\ProgramData\TacticalRMM\toolbox\iperf3\iperf3.exe' -c $IP -p 9200 -t $Seconds -bidir + Write-Output "################# UDP Upload #################" + & 'C:\ProgramData\TacticalRMM\toolbox\iperf3\iperf3.exe' -c $IP -p 9200 -u -b 0 -t $Seconds -bidir + Write-Output "################# TCP Download ##################" + & 'C:\ProgramData\TacticalRMM\toolbox\iperf3\iperf3.exe' -c $IP -p 9200 -R -t $Seconds -bidir + Write-Output "################# UDP Download #################" + & 'C:\ProgramData\TacticalRMM\toolbox\iperf3\iperf3.exe' -c $IP -p 9200 -R -u -b 0 -t $Seconds -bidir +} diff --git a/scripts_wip/Win_WipeviaMDM.ps1 b/scripts_staging/Win_ResetviaMDM.ps1 similarity index 71% rename from scripts_wip/Win_WipeviaMDM.ps1 rename to scripts_staging/Win_ResetviaMDM.ps1 index 0f33953f..21314fc4 100644 --- a/scripts_wip/Win_WipeviaMDM.ps1 +++ b/scripts_staging/Win_ResetviaMDM.ps1 @@ -1,4 +1,13 @@ -#Uses MDM features of windows to perform a Windows Reset clearing all data +<# +.SYNOPSIS + Trigger a remote wipe via MDM. + +.DESCRIPTION + Invokes the 'doWipeMethod' in Windows equivalent to the Reset function in the Settings app. + +.NOTES + v1.0 7/2024 bbrendon Initial version +#> $namespaceName = "root\cimv2\mdm\dmmap" $className = "MDM_RemoteWipe" @@ -10,12 +19,10 @@ $params = New-Object Microsoft.Management.Infrastructure.CimMethodParametersColl $param = [Microsoft.Management.Infrastructure.CimMethodParameter]::Create("param", "", "String", "In") $params.Add($param) -try -{ +try { $instance = Get-CimInstance -Namespace $namespaceName -ClassName $className -Filter "ParentID='./Vendor/MSFT' and InstanceID='RemoteWipe'" $session.InvokeMethod($namespaceName, $instance, $methodName, $params) } -catch [Exception] -{ +catch [Exception] { write-host $_ | out-string -} \ No newline at end of file +} diff --git a/scripts_staging/Win_Services_CheckforProblems.ps1 b/scripts_staging/Win_Services_CheckforProblems.ps1 new file mode 100644 index 00000000..1f196d14 --- /dev/null +++ b/scripts_staging/Win_Services_CheckforProblems.ps1 @@ -0,0 +1,115 @@ +<# +.SYNOPSIS + Checks the status of services and makes sure all the damn required services are started, including any others you throw in through + an environment variable. + +.DESCRIPTION + This script checks for services with automatic or delayed start that are just sitting there not running. It compares those with a list + of ignored services, including any additional ones you set in the "IgnoredServices" environment variable. No need for separate checks, + this script will tell you which ones need attention so you can get your shit together and fix it. + +.NOTES + Author: SAN + +.TODO + Recheck the list of services for any that should be monitored like ShellHWDetection + cleanup and streamline the output with a debug flag + +.CHANGELOG 28.10.24 SAN Removed ignored output without flag + +#> + + +# Define a list of partial display names to be ignored in the check +$ignoredPartialDisplayNames = @( + "Software Protection", + "Remote Registry", + "State Repository Service", + "Service Google Update", + "Clipboard User Service", + "Service Brave Update", + "Google Update Service", + "Windows Modules Installer", #not sure about this one if we should monitor it or not + "Downloaded Maps Manager", + "Windows Biometric Service", + "RemoteRegistry", + "edgeupdate", + "brave", + "gupdate", + "MapsBroker", + "WbioSrvc", + "cbdhsvc", + "GoogleUpdater", + "sppsvc", + "SharePoint Migration Service", + "dbupdate", + "TrustedInstaller", #this one is strange it was failing on a lot of devices but no idea if we should fix it. + "MSExchangeNotificationsBroker", + "tiledatamodelsvc", + "BITS", + "CDPSvc", + "AGSService", + "ShellHWDetection" #this one is strange it was failing on a lot of devices but no idea if we should fix it. + +) + +# Check if "IgnoredServices" environment variable exists and add those services to the ignore list +$envIgnoredServices = [Environment]::GetEnvironmentVariable('IgnoredServices') +if (-not [string]::IsNullOrEmpty($envIgnoredServices)) { + $additionalIgnoredServices = $envIgnoredServices -split ',' + $ignoredPartialDisplayNames += $additionalIgnoredServices +} + +# Convert ignored partial display names to a regular expression pattern +$ignoredPattern = ($ignoredPartialDisplayNames | ForEach-Object { [regex]::Escape($_) }) -join '|' + +# Get services with automatic start type or Automatic (Delayed Start) that are not running +$servicesToCheck = Get-Service | Where-Object { ($_.StartType -eq 'Automatic' -or $_.StartType -eq 'Automatic (Delayed Start)') -and $_.Status -ne 'Running' } + +# Initialize arrays to store services that need attention and services that were stopped but ignored +$servicesToStart = @() +$ignoredStoppedServices = @() + +# Check the status of each service +foreach ($service in $servicesToCheck) { + # Check if the display name or service name matches the ignored pattern + if ($service.DisplayName -notmatch $ignoredPattern -and $service.ServiceName -notmatch $ignoredPattern) { + # Add the service to the list of services to start + $servicesToStart += $service + } + else { + # Add the service to the list of ignored stopped services + $ignoredStoppedServices += $service + } +} + +# Check if enabledebug environment variable is set to true +$enableDebugValue = [System.Environment]::GetEnvironmentVariable("enabledebugscript") +$debugEnabled = $enableDebugValue -ne $null -and [System.Boolean]::Parse($enableDebugValue) + + +if ($debugEnabled) { + Write-Host "Debug enabled" +} +# Display the results +if ($servicesToStart.Count -eq 0) { + if (-not $debugEnabled) { + Write-Host "All required services are running." + } + + if ($ignoredStoppedServices.Count -ne 0 -and $debugEnabled) { + Write-Host "The following services were stopped but ignored:" + foreach ($service in $ignoredStoppedServices) { + Write-Host "$($service.DisplayName) ($($service.ServiceName))" + } + } + Exit 0 +} +else { + Write-Host "The following services need attention:" + foreach ($service in $servicesToStart) { + Write-Host "$($service.DisplayName) ($($service.ServiceName))" + } + Write-Host "Run the script 'Ensure all services with startup type Automatic are running' before trying to troubleshoot" + Exit 1 +} diff --git a/scripts_staging/Win_StorageCraftImageManager_Status.ps1 b/scripts_staging/Win_StorageCraftImageManager_Status.ps1 new file mode 100644 index 00000000..d30c0431 --- /dev/null +++ b/scripts_staging/Win_StorageCraftImageManager_Status.ps1 @@ -0,0 +1,1002 @@ +<# +.SYNOPSIS + Monitor Image Manager Status + +.DESCRIPTION + Checks to see of scheduled Image Manager jobs are completing or failing. + Reads Image Manager DB (takes a copy) to check for failing events. + Checks for IM folder activity. + Checks for IM Events in Application Log. + Returns Result. + +.PARAMETER SkipWarnFTP (Optional) + [Boolean] - Default:$False - Enable Warning Checks on FTP Queues + +.PARAMETER SkipWarnHSR (Optional) + [Boolean] - Default:$False - Enable Warning Checks on HSR Sent Logs + +.PARAMETER SkipFileSystemChecks (Optional) + [Boolean] - Default:$False - Enable File System Checks + +.PARAMETER SkipEventLogChecks (Optional) + [Boolean] - Default:$False - Enable Event Log Checks + +.PARAMETER FreeSpaceWarnThreshold (Optional) + [Int] - Default: 10 - Threshold for Percent Free space on a Watch Folder before raising a warning. + +.PARAMETER FreeSpaceAlertThreshold (Optional) + [Int] - Default: 5 - Threshold for Percent Free space on a Watch Folder before raising an Alert. + +.OUTPUTS + Exit Code: 0 = Pass, 1 = Informational, 2 = Warning, 3 = Error + +.EXAMPLE + Win_ShadowProtectIM_Status.ps1 + #No Parameters, defaults apply + +.NOTES + v1.4 12/8/2023 ConvexSERV + Requires Access 2010 Runtime. Script will attempt to download/install +#> + +param ( + [Boolean] $SkipWarnFTP, #Add the ability to disable FTP Warnings + [Boolean] $SkipWarnHSR, #Add the ability to disable HSR Warnings + [Boolean] $SkipFileSystemChecks, #Add the ability to disable File System Checks + [Boolean] $SkipFreeSpaceChecks, #Add the ability to disable Free Space Checks + [Boolean] $SkipEventLogChecks, #Add the ability to disable Event Log Checks + [Int] $FreeSpaceWarnThreshold, #Add the ability to disable Event Log Checks + [Int] $FreeSpaceAlertThreshold #Add the ability to disable Event Log Checks +) + +#Environmental Variables: +#$env:shareuser = "" +#$env:sharepass = "" +#$env:hsrshareuser = "" +#$env:hsrsharepass = "" + +#If ShareUser/SharePass environmental variable are set, but HSRShareUser/HSRSharePass variables are not, +#Set the HSRShareUser/HSRSharePass variables to match ShareUser/SharePass +if ((Test-Path env:shareuser) -and (-not(Test-Path env:hsrshareuser))){$env:hsrshareuser = $env:shareuser} +if ((Test-Path env:sharepass) -and (-not(Test-Path env:hsrsharepass))){$env:hsrsharepass = $env:sharepass} + +#Set Parameter Defaults +if (!$SkipWarnFTP) {$WarnFTP = $True} +if (!$SkipWarnHSR) {$WarnHSR = $True} +if (!$SkipFileSystemChecks) {$FileSystemChecks = $True} +if (!$SkipFreeSpaceChecks) {$FreeSpaceChecks = $True} +if (!$SkipEventLogChecks) {$EventLogChecks = $True} +if (!$FreeSpaceWarnThreshold) {$FreeSpaceWarnThreshold = 5} +if (!$FreeSpaceAlertThreshold) {$FreeSpaceAlertThreshold = 10} + +###------------------------------Declare Functions----------------------------------### +###---------------------------------------------------------------------------------### + +#Takes in an Index (from a hsr** or ftp** Table), Returns [String] containing the Target Path +Function Get-TargetPath ([Int] $Index) { #Returns String + + ForEach ($drTargetPath in $dtTargetPaths) { + + if ($drTargetPath["Index"] -eq $Index) { + Return $drTargetPath["Path"] + } + } +} + +#Takes in an Index (from a w*Files Table), Returns DataRow containing the Watch Path +Function Get-WatchPath ([Int] $Index) { #Returns DataRow + + ForEach ($drWatchPath in $dtWatchPaths) { + + if ($drWatchPath["Index"] -eq $Index) { + Return $drWatchPath + } + } + +} + +#Takes in a w*Sets Table and Index, Returns DataRow containing the Watch Set +Function Get-WatchSet ([System.Data.DataTable] $Table, [Int] $Index) { #Returns DataRow + + ForEach ($drWatchSet in $Table) { + + if ($drWatchSet["Index"] -eq $Index) { + Return $drWatchSet + } + } +} + +#Takes in the Index from the WatchPaths Table and returns [Boolean] $True if a w*Files Table Exists, otherwise returns $False. +Function Check-WatchPathTable ([Int] $Index) { #Returns Boolean + + $Result = $False + + ForEach ($drTableWP in $dtTableList){ + + [Int]$CurrentTableIndexWP = $drTableWP["name"] -replace "[^0-9]",'' #Extract the index number from the table name + $CurrentTableNameWP = $drTableWP["name"] + if (($CurrentTableIndexWP -eq $Index) -and ($CurrentTableNameWP.Substring(0,1) -eq "w")) { + + $Result = $True + Break + } + } + + Return $Result +} +###---------------------------------------------------------------------------------### + +#Initialize Variables +$NowTime = [DateTime]::Now +$AlertLevel = 0 #0 = Pass, 1 = Informational, 2 = Warning, 3 = Error +$AlertText = '' + +#Create c:\ProgramData\TacticalRMM\temp\ if it does't exist. +if (-not(test-path "c:\ProgramData\TacticalRMM\temp\")){ + mkdir "c:\ProgramData\TacticalRMM\temp\" +} + +####-------------------------------### +# Database Checks # +####-------------------------------### + +#Image Manager will have the DB open exclusively (At the MS Access Level, not the FileSystem Level). Copy the DB to Temp. +$IMDBPath = "c:\ProgramData\TacticalRMM\temp\Imagemanager.mdb" +try { + copy "C:\Program Files (x86)\StorageCraft\ImageManager\ImageManager.mdb" "c:\ProgramData\TacticalRMM\temp\Imagemanager.mdb" +} +catch +{ + $AlertText = "Alert - Failed to make a copy of the ImageManager Database. Please check if ImageManager is actually installed, and if another process has a lock on the .mdb file (Source or Dest)" + Write-Host $AlertText + $AlertLevel = 3 + $Host.SetShouldExit($AlertLevel) + Exit +} + +#Attempt to create an OLDDB Connection. Connection will fail if the Access RunTime is not installed +try{ + $conn = New-Object System.Data.OleDb.OleDbConnection("Provider=Microsoft.ACE.OLEDB.12.0;Data Source=$IMDBPath;Persist Security Info=False") + $conn.open() +} +catch +{ + Write-Host "Info - Access Runtime Not Installed. Will attempt to download and install..." + try{ + Invoke-WebRequest -Uri "https://download.microsoft.com/download/2/4/3/24375141-E08D-4803-AB0E-10F2E3A07AAA/AccessDatabaseEngine_X64.exe" -UseBasicParsing -OutFile "c:\ProgramData\TacticalRMM\temp\AccessDatabaseEngine_X64.exe" + } + catch { + $AlertText = "Alert - MS Access Runtime Download Failed." + Write-Host $AlertText + $AlertLevel = 3 + $Host.SetShouldExit($AlertLevel) + Exit + } + + if (Test-Path c:\ProgramData\TacticalRMM\temp\AccessDatabaseEngine_X64.exe) { + + Write-Host "Info - File Downloaded. Will attempt to install..." + + try { + Start-Process -NoNewWindow -FilePath "c:\ProgramData\TacticalRMM\temp\AccessDatabaseEngine_X64.exe" -ArgumentList '/q' -Wait + } + catch { + $AlertText = "Alert - MS Access Runtime Install Failed." + Write-Host $AlertText + $AlertLevel = 3 + $Host.SetShouldExit($AlertLevel) + Exit + } + + Write-Host "Info - MS Access Runtime Install Succeeded. Try to open the connection again..." + try { + $conn.close() + $conn = New-Object System.Data.OleDb.OleDbConnection("Provider=Microsoft.ACE.OLEDB.12.0;Data Source=$filename;Persist Security Info=False") + $conn.open() + } + catch { + $AlertText = "Alert - DB Connection failed after installing Access Runtime." + Write-Host $AlertText + $AlertLevel = 3 + $Host.SetShouldExit($AlertLevel) + Exit + } + } +} + +$cmd=$conn.CreateCommand() + +#Get All Tables in the DB +$cmd.CommandText="select MSysObjects.name from MSysObjects where MSysObjects.type In (1,4,6) and MSysObjects.name not like '~*' and MSysObjects.name not like 'MSys*' order by MSysObjects.name" +$rdr = $cmd.ExecuteReader() +$dtTableList = New-Object System.Data.Datatable +$dtTableList.Load($rdr) + +#Add an Ignore Column to the Table List Table +$NoOutput = $dtTableList.Columns.Add("Ignore", [Boolean]) + + +#Get the TargetPaths Table +$cmd.CommandText="select * from TargetPaths" +$rdr = $cmd.ExecuteReader() +$dtTargetPaths = New-Object System.Data.Datatable +$dtTargetPaths.Load($rdr) + +#Add an Ignore Column to the TargetPaths Table +$NoOutput = $dtTargetPaths.Columns.Add("Ignore", [Boolean]) + + +#Get the WatchPaths Table +$cmd.CommandText="select * from WatchPaths" +$rdr = $cmd.ExecuteReader() +$dtWatchPaths = New-Object System.Data.Datatable +$dtWatchPaths.Load($rdr) + +#Add an Ignore Column to the TargetPaths Table +$NoOutput = $dtWatchPaths.Columns.Add("Ignore", [Boolean]) + +#Walk through the tablelist and mark the tables we don't need +ForEach ($drTable in $dtTableList) { + + #Ignore System Tables + if ($drTable["name"] -like "MSys*") {$drTable["Ignore"] = $true} + + #Ignore HSR Queue and Remote Tables + elseif (($drTable["name"] -like "hsr*Queue") -or ($drTable["name"] -like "hsr*Remote")) {$drTable["Ignore"] = $true} + + #Ignore FTP Sent and Remote Tables + elseif (($drTable["name"] -like "ftp*Sent") -or ($drTable["name"] -like "ftp*Remote")) {$drTable["Ignore"] = $true} + + #Ignore Adv Tables + elseif ($drTable["name"] -like "adv*Verify") {$drTable["Ignore"] = $true} + + #Include w*Files,w*Sets,TargetPaths and WathcPaths Tables + elseif (($drTable["name"] -eq "TargetPaths") -or ($drTable["name"] -eq "WatchPaths")) {$drTable["Ignore"] = $false} + #excluded (($drTable["name"] -like "w*Files") -or ($drTable["name"] -like "w*Sets") -or ) + + else { + + $CurrentTableName = $drTable["name"] + + #Ignore ftp tables if they are empty + if ($CurrentTableName.SubString(0,3) -eq "ftp") { + + $cmd.CommandText="select count(*) from $CurrentTableName" + $CurrentTableCount = $cmd.ExecuteScalar() + if ($CurrentTableCount -eq 0) { + $drTable["Ignore"] = $true + } + else {$drTable["Ignore"] = $false} + + #Ignore all FTP TargetPaths, we don't need to check the filesystem for FTP + [Int]$CurrentTableIndex = $drTable["name"] -replace "[^0-9]",'' #Extract the index number from the table name + ForEach ($drTargetPath in $dtTargetPaths) { + if ($drTargetPath["Index"] -eq $CurrentTableIndex){ + $drTargetPath["Ignore"] = $True + } + + } + + } + + #Ignore remaining hsr if they are empty + if ($CurrentTableName.SubString(0,3) -eq "hsr") { + + $cmd.CommandText="select count(*) from $CurrentTableName" + $CurrentTableCount = $cmd.ExecuteScalar() + if ($CurrentTableCount -eq 0) { + $drTable["Ignore"] = $true + #Ignore HSR TargetPaths if the cooresponding table is empty + [Int]$CurrentTableIndex = $drTable["name"] -replace "[^0-9]",'' #Extract the index number from the table name + ForEach ($drTargetPath in $dtTargetPaths) { + if ($drTargetPath["Index"] -eq $CurrentTableIndex){ + $drTargetPath["Ignore"] = $True + } + + } + } + else {$drTable["Ignore"] = $false} + } + + #Ignore remaining w tables if they are empty + if ($CurrentTableName.SubString(0,1) -eq "w") { + + $cmd.CommandText="select count(*) from $CurrentTableName" + $CurrentTableCount = $cmd.ExecuteScalar() + if ($CurrentTableCount -eq 0) { + $drTable["Ignore"] = $true + #Mark the index in WatchPaths too + [Int]$CurrentTableIndex = $drTable["name"] -replace "[^0-9]",'' #Extract the index number from the table name + ForEach ($drWatchPath in $dtWatchPaths) { + if ($drWatchPath["Index"] -eq $CurrentTableIndex){ + $drWatchPath["Ignore"] = $True + } + } + } + else {$drTable["Ignore"] = $false} + } + } +} +#Also Ignore Watch Paths that contain "Archive", "RestoreTest", "Permanently Retain" +ForEach ($drWatchPath in $dtWatchPaths) { + if (($drWatchPath["Path"] -like "*Archive*") -or ($drWatchPath["Path"] -like "*RestoreTest*") -or ($drWatchPath["Path"] -like "*Permanently Retain*")){ + $drWatchPath["Ignore"] = $True + } + else { + $drWatchPath["Ignore"] = $False + } +} + +#Walk through the tablelist again to run our checks +ForEach ($drTable in $dtTableList) { + + [Int]$CurrentTableIndex = $drTable["name"] -replace "[^0-9]",'' #Extract the index number from the table name + $CurrentTableName = $drTable["name"] + + #Skip any table flagged to be ignored + if (-not ($drTable["Ignore"])) { + + #Check FTP Queue Tables - Warn on items in the Queue older than 4 days (If Enabled) + if (($drTable["name"] -like "ftp*Queue") -and $WarnFTP){ + + $cmd.CommandText="select Name, CreateTime, FileSize from $CurrentTableName" + $rdr = $cmd.ExecuteReader() + $dtFTPQueue = New-Object System.Data.Datatable + $dtFTPQueue.Load($rdr) + + #Walk the FTP Queue Table + ForEach ($drFTPQueue in $dtFTPQueue){ + If ($drFTPQueue["CreateTime"] -lt (Get-Date).Date.AddDays(-4)){ + + #Set AlertLevel to Warn if not already set the same or higher + If ($AlertLevel -le 1) { + $AlertLevel = 2 + } + + #Get the Target Path for the Current Table + $CurrentTargetPath = Get-TargetPath -Index $CurrentTableIndex + $curFileSize = [math]::round($drFTPQueue["FileSize"] /1Gb, 3) + $curFileName = $drFTPQueue["Name"] + $curFileSent = $drFTPQueue["Sent"] + + #Set AlertText if not already set + If ($AlertText -eq "") { + $AlertText = "FTP Item in Queue is older than 4 Days ( File: $CurrentTargetPath\$curFileName, Size: $curFileSize GB, Sent: $curFileSent)" + } + + #Write to StdOut Result + Write-Host "FTP Item in Queue is older than 4 Days ( File: $CurrentTargetPath\$curFileName, Size: $curFileSize GB, Sent: $curFileSent)" + } + } + } + + #Check HSR Sent Tables - Warn if the latest entry is older than 4 days + if (($drTable["name"] -like "hsr*Sent") -and $WarnHSR){ + + $cmd.CommandText="select top 1 Name, CreateTime, FileSize, Sent, BytesSent from $CurrentTableName order by Sent DESC" + $rdr = $cmd.ExecuteReader() + $dtHSRSent = New-Object System.Data.Datatable + $dtHSRSent.Load($rdr) + + #Walk the HSR Queue Table + ForEach ($drHSRSent in $dtHSRSent){ + If ($drHSRSent["Sent"] -lt (Get-Date).Date.AddDays(-4)){ + + #Get the Target Path for the Current Table + $CurrentTargetPath = Get-TargetPath -Index $CurrentTableIndex + $curFileSize = [math]::round($drHSRSent["FileSize"] /1Gb, 3) + $curFileName = $drHSRSent["Name"] + $curFileSent = $drHSRSent["Sent"] + + #Check if the Target Path actually exists. + #If it doesn't the entry is probably not valid because IM doesn't clean up the DB. + if (($CurrentTargetPath.substring(0,2) -eq "\\") -and (Test-Path env:hsrshareuser) -and (Test-Path env:hsrsharepass)){ + Try{ + #Target Path will be a complete path with a filename. Strip the path down to the hostname and share. + $SplitPath = $CurrentTargetPath.split('\',5) + $CurrentHostName = $SplitPath[2] + $CurrentShareName = $SplitPath[3] + $CurrentTestPath = "\\$CurrentHostName\$CurrentShareName" + $TargetPathSMB = New-SmbMapping -remotepath $CurrentTestPath -UserName $env:hsrshareuser -Password $env:hsrsharepass + $TargetPathExists = Test-Path "$CurrentTargetPath\$curFileName" + $TargetPathSMB.Dispose() + } + Catch { + $TargetPathExists = $False + } + } + else { + $TargetPathExists = Test-Path "$CurrentTargetPath\$curFileName" + } + + If ($TargetPathExists){ + + #Set AlertLevel to Warn if not already set the same or higher + If ($AlertLevel -le 1) { + $AlertLevel = 2 + } + + #Set AlertText if not already set + If ($AlertText -eq "") { + $AlertText = "Warning - HSR Item in has not been updated in 4 Days ( File: $CurrentTargetPath\$curFileName, Size: $curFileSize GB, Sent: $curFileSent)" + } + + #Write to StdOut Result + Write-Host "Warning - HSR Item in has not been updated in 4 Days ( File: $CurrentTargetPath\$curFileName, Size: $curFileSize GB, Sent: $curFileSent)" + } + } + } + } + + #Check W Files Tables - Alert if there are any entries older than 4 days, or if VerifyFailed is not empty, or if VerifyStatus != 1 + if ($drTable["name"] -like "w*Files"){ + + $cmd.CommandText="select Name, ImageType, FileSize, LastVerified, VerifyStatus, VerifyFailed from $CurrentTableName order by LastVerified DESC" + $rdr = $cmd.ExecuteReader() + $dtWFiles = New-Object System.Data.Datatable + $dtWFiles.Load($rdr) + + #Get the Watch Path for the Current Table + $drCurrentWatchPath = Get-WatchPath -Index $CurrentTableIndex + $CurrentWatchPath = $drCurrentWatchPath["Path"] + + #Check the first Row (Newest Entry) + if ($dtWFiles.Rows[0]["LastVerified"] -lt (Get-Date).Date.AddDays(-4)){ + + if (-not ($drCurrentWatchPath["Ignore"])) { + + #Format Variables for Output + $curFileSize = [math]::round($dtWFiles.Rows[0]["FileSize"] /1Gb, 3) + $curFileName = $dtWFiles.Rows[0]["Name"] + $curFileVerified = $dtWFiles.Rows[0]["LastVerified"] + + #Set AlertLevel to Alert if not already set the same or higher + If ($AlertLevel -le 2) { + $AlertLevel = 3 + } + + #Set AlertText + $AlertText = "Alert - File has not been verified in 4 Days ( File: $CurrentWatchPath\$curFileName, Size: $curFileSize GB, Verified: $curFileVerified)" + #Write to StdOut Result + Write-Host $AlertText + } + } + + #Walk the W Files Table, looking for verification failures + ForEach ($drWFiles in $dtWFiles){ + If (($drWFiles["VerifyStatus"] -ne 1 ) -and ($drWFiles["VerifyStatus"] -eq "" )){ + + #Set AlertLevel to Alert if not already set the same or higher + If ($AlertLevel -le 2) { + $AlertLevel = 3 + } + + #Format Variables for Output + $curFileSize = [math]::round($drWFiles["FileSize"] /1Gb, 3) + $curFileName = $drWFiles["Name"] + $curFileVerified = $drWFiles["LastVerified"] + + #Set AlertText if not already set + $AlertText = "Alert - File verification FAILED! ( File: $CurrentTargetPath\$curFileName, Size: $curFileSize GB, Verified: $curFileVerified)" + #Write to StdOut Result + Write-Host $AlertText + } + } + } + } +} +#Write Output +if ($AlertLevel -eq 0) { + Write-Output "Info - Database Checks Passed" +} + +####-------------------------------### +# Filesystem Checks # +####-------------------------------### + +#Perform FileSystem Checks (If Enabled) +if ($FileSystemChecks) { + + #Check Target Paths Table - Warn if file timestamps are older than 4 days + ForEach ($drTargetPath in $dtTargetPaths) { + + if (-not($drTargetPath["Ignore"])){ + + $CurrentTargetPath = $drTargetPath["Path"] + if (($CurrentTargetPath.substring(0,2) -eq "\\") -and (Test-Path env:hsrshareuser) -and (Test-Path env:hsrsharepass)){ + Try{ + $NoOutput = New-SmbMapping -remotepath $CurrentTargetPath -UserName $env:hsrshareuser -Password $env:hsrsharepass + $TargetPathExists = Test-Path($drTargetPath["Path"]) + } + Catch { + $TargetPathExists = $False + } + } + else { + $TargetPathExists = Test-Path($drTargetPath["Path"]) + } + + #Check that file Exists and that the file date is recent + if($TargetPathExists){ + $TargetFile = Get-ChildItem $drTargetPath["Path"] + if ($TargetFile.LastWriteTime -lt (Get-Date).Date.AddDays(-4)) { + + #Format Variables for Output + $curFileName = $drTargetPath["Path"] + $curFileLastWrite = $TargetFile.LastWriteTime + $curFileSize = [math]::round($TargetFile.Length /1Gb, 3) + + #Warn if HSR File TimeStamp is older than 4 days + #Set AlertLevel to Alert if not already set the same or higher + If ($AlertLevel -le 1) { + $AlertLevel = 2 + } + #Set AlertText + $AlertText = "Warning - HSR File timeStamp is older than 4 Days ( File: $CurrentTargetPath\$curFileName, Size: $curFileSize GB, Created: $curFileLastWrite)" + #Write to StdOut Result + Write-Host $AlertText + } + } + else + { + #Format Variables for Output + $curFileName = $drTargetPath["Path"] + + #Alert if HSR File Missing or Inaccessible + #Set AlertLevel to Alert if not already set the same or higher + If ($AlertLevel -le 2) { + $AlertLevel = 3 + } + #Set AlertText + $AlertText = "Alert - HSR file is Missing or Inaccessible ( File: $curFileName)" + + #Write to StdOut Result + Write-Host $AlertText + } + } + } + + #Check Watch Paths Table - Alert if file timestamps are older than 4 days, Warn if Base images are older than 1 Year, Alert if Base Images are older than 2 years. + ForEach ($drWatchPath in $dtWatchPaths) { + + if (-not($drWatchPath["Ignore"]) -and (Check-WatchPathTable($drWatchPath["Index"]))){ + + $CurrentWatchPath = $drWatchPath["Path"] + if (($CurrentWatchPath.substring(0,2) -eq "\\") -and (Test-Path env:shareuser) -and (Test-Path env:sharepass)){ + Try{ + $WatchPathSMB = New-SmbMapping -remotepath $CurrentWatchPath -UserName $env:shareuser -Password $env:sharepass + $WatchPathExists = Test-Path($drWatchPath["Path"]) + $CurrentWatchPathLocal = $False + } + Catch { + $WatchPathExists = $False + } + } + else { + $WatchPathExists = Test-Path($drWatchPath["Path"]) + $CurrentWatchPathLocal = $True + } + + #Check that file Exists and that the file date is recent + if($WatchPathExists){ + + #Get all *.Sp? files in the path, sort by Creation Time (Descending) + $WatchFiles = Get-ChildItem $drWatchPath["Path"] -Filter "*.sp?" | sort CreationTime -Descending + + if ($WatchFiles.Length -gt 0){ + + #Check the newest file to see if it's older than 4 days + if ($WatchFiles[0].LastWriteTime -lt (Get-Date).Date.AddDays(-4)) { + + #Format Variables for Output + $curFileName = $WatchFiles[0].FullName + $curFileLastWrite = $WatchFiles[0].LastWriteTime + $curFileSize = [math]::round($WatchFiles[0].Length /1Gb, 3) + + #Alert if IM File TimeStamp is older than 4 days + #Set AlertLevel to Alert if not already set the same or higher + If ($AlertLevel -le 1) { + $AlertLevel = 3 + } + #Set AlertText + $AlertText = "Alert - Last IM File written timeStamp is older than 4 Days ( File: $curFileName, Size: $curFileSize GB, Created: $curFileLastWrite)" + #Write to StdOut Result + Write-Host $AlertText + } + + #Get all *.Spf (Base) files in the path, sort by Creation Time (Descending) + $WatchFiles = Get-ChildItem $drWatchPath["Path"] -Filter "*.sp?" | sort CreationTime -Descending + + #Check the newest file to see if it's older than 1 Year (But Less than 2 Years) + if ($WatchFiles[0].LastWriteTime -lt (Get-Date).Date.AddDays(-365) -and $WatchFiles[0].LastWriteTime -ge (Get-Date).Date.AddDays(-731)) { + + #Format Variables for Output + $curFileName = $WatchFiles[0].FullName + $curFileLastWrite = $WatchFiles[0].LastWriteTime + $curFileSize = [math]::round($WatchFiles[0].Length /1Gb, 3) + + #Warn if SPX Base File TimeStamp is older than 1 Year + #Set AlertLevel to Alert if not already set the same or higher + If ($AlertLevel -le 1) { + $AlertLevel = 2 + } + #Set AlertText + $AlertText = "Warning - SPX Base is over 1 Yr. Old. ( File: $curFileName, Size: $curFileSize GB, Created: $curFileLastWrite)" + #Write to StdOut Result + Write-Host $AlertText + } + elseif ($WatchFiles[0].LastWriteTime -lt (Get-Date).Date.AddDays(-731)) { + + #Format Variables for Output + $curFileName = $WatchFiles[0].FullName + $curFileLastWrite = $WatchFiles[0].LastWriteTime + $curFileSize = [math]::round($WatchFiles[0].Length /1Gb, 3) + + #Warn if SPX Base File TimeStamp is older than 2 Years. + #Set AlertLevel to Alert if not already set the same or higher + If ($AlertLevel -le 1) { + $AlertLevel = 2 + } + #Set AlertText + $AlertText = "Alert - SPX Base is over 2 Yrs. Old. ( File: $curFileName, Size: $curFileSize GB, Created: $curFileLastWrite)" + #Write to StdOut Result + Write-Host $AlertText + } + } + + #Check Free Space on Watch Path + if ($FreeSpaceChecks){ + + if ($CurrentWatchPathLocal) { #Local Path + $CurrentWatchPathDriveLetter = $CurrentWatchPath.Substring(0,1) + $drive = get-psdrive $CurrentWatchPathDriveLetter + $free = $drive.Free + $used = $drive.Used + $total = $free + $used + } + else { #UNC Path + + #Target Path might be a complete path with a subfolder name. Strip the path down to the hostname and share. + $SplitPath = $CurrentWatchPath.split('\',5) + $CurrentHostName = $SplitPath[2] + $CurrentShareName = $SplitPath[3] + $CurrentTestPath = "\\$CurrentHostName\$CurrentShareName" + + $drive = (New-Object -com scripting.filesystemobject).getdrive("$CurrentTestPath") + $free = $drive.FreeSpace + $total = $drive.TotalSize + $used = ($total - $free) + + } + + #Clean up the total sizes + $totalGB = ($total / 1GB) + $totalPretty = [math]::Round($totalGB,2) + + $usedGB = ($used / 1GB) + $usedPretty = [math]::Round($usedGB,2) + + $usedPercent = ($used / $total)*100 + $usedPercentPretty = [math]::Round($usedPercent) + + $freePercent = ($free / $total)*100 + $freePercentPretty = [math]::Round($freePercent) + + $freeGB = ($free / 1GB) + $freePretty = [math]::Round($freeGB,2) + + #Warn on 10% or less disk space or less + if (($freePercentPretty -le 10) -and ($freePercentPretty -gt 5)){ + + #Set AlertLevel to Warn if not already set the same or higher + If ($AlertLevel -le 1) { + $AlertLevel = 2 + } + #Set AlertText + $AlertText = "Warn - IM Destination Free Space ($freePercentPretty%) is at a low level. (Threshold: $FreeSpaceWarnThreshold%, $freePretty GB free of $totalPretty GB)" + + #Write to StdOut Result + Write-Host $AlertText + + } + #Alert on 5% or less free disk space + elseif ($freePercentPretty -le 5){ + + #Set AlertLevel to Alert if not already set the same or higher + If ($AlertLevel -le 2) { + $AlertLevel = 3 + } + #Set AlertText + $AlertText = "Alert - IM Destination Free Space ($freePercentPretty%) is at a Critical Level.(Threshold: $FreeSpaceAlertThreshold%, $freePretty GB free of $totalPrettyGB )" + + #Write to StdOut Result + Write-Host $AlertText + + } + else{ + Write-Host "Info - IM Destination Free Space ($freePercentPretty%) is (OK) (Threshold: $FreeSpaceWarnThreshold%, $freePretty GB free of $totalPretty GB)" + } + } + } + else + { + #Format Variables for Output + $curFileName = $drWatchPath["Path"] + + #Alert if IM Destination Missing or Inaccessible + #Set AlertLevel to Alert if not already set the same or higher + If ($AlertLevel -le 2) { + $AlertLevel = 3 + } + #Set AlertText + $AlertText = "Alert - IM Destination is Missing or Inaccessible ( File: $curFileName)" + + #Write to StdOut Result + Write-Host $AlertText + } + } + } +} +#Write Output +if ($AlertLevel -eq 0) { + Write-Output "Info - File System Checks Passed" +} + +####-------------------------------### +# Event Log Checks # +####-------------------------------### + +#Perform Event Log Checks (If Enabled) +if ($EventLogChecks) { + + #ImageManager Event IDs: Windows Logs> Application{source "StorageCraft ImageManager"}]: + + #Information Codes are from 1100 to 1120: + $IM_Success = 1120 # Successful collapse occurred which created the file listed + + + $IM_FailedCollapse = 1121 #Failed collapse + $IM_Error = 1122 #Reserved + $IM_DataCorruption = 1123 #Data corruption (a file failed to verify) + $IM_IncompleteChain = 1124 #Incomplete chain (missing a file necessary to form a complete chain) + $IM_ProcessingError = 1125 #Processing error (error preparing for collapse / verify operations such as trying to sync files with database) + $IM_ReplicationError = 1126 #Replication error (failed replication will retry next time a file is verified) + $IM_HSRError = 1127 #HSR error + $IM_CriticalError = 1128 #Critical error (the service must be restarted due to an unrecoverable error) + $IM_Exception = 1129 #Exception (ImageManager will retry the failed operation later so the service does not need to be restarted) + #Note: In ImageManager release version 7.0.2 event IDs 1125 and above were shifted up by one digit so Event ID 1126 = Processing error, etc. These were shifted back to the original values in the next release. + + #Check Windows Event Log for ImageManager Events Occurring Today (After Midnight) + $Events = Get-EventLog -LogName Application -Source "StorageCraft ImageManager" -After (Get-Date).Date + + #If there are no Events... + If ($Events.Count -eq 0){ + + #Check Windows Event Log for ImageManager Events Occurring Today (After Midnight) + $MoreEvents = Get-EventLog -LogName Application -Source "StorageCraft ImageManager" -After (Get-Date).Date.AddDays(-4) + + #If there have been no events, look back another 4 days. + If ($MoreEvents.Count -eq 0){ + + #We haven't had any activity in over 4 days. Fail the Check. + $AlertText = "Alert - There has not been any activity in over 4 days. Please check that Image Manage and SPX are both running. (EventLog-NoEvents)" + $AlertLevel = 3 + + } Else { #There are Events in the 3-Day search... + + #Walk the 3 days of events, looking for errors. + ForEach ($Event in $MoreEvents) { + + If ($Event.InstanceID -gt 1120){ + + Switch ($Event.InstanceID) { + + #Error codes are from 1121 to 1129: + $IM_FailedCollapse { #1121 Failed collapse + #Event is an Error + $AlertText = "Alert: (EventLog 3-Day) ID:" + $Event.InstanceID + $Event.Message + Write-Output $AlertText + $AlertLevel = 3 + break + } + + $IM_Error { #1122 Reserved + #Event is an Error + $AlertText = "Alert: (EventLog 3-Day) ID:" + $Event.InstanceID + $Event.Message + Write-Output $AlertText + $AlertLevel = 3 + break + } + + $IM_DataCorruption { # 1123 Data corruption (a file failed to verify) + #Event is an Error + $AlertText = "Alert: (EventLog 3-Day) ID:" + $Event.InstanceID + $Event.Message + Write-Output $AlertText + $AlertLevel = 3 + break + } + + $IM_IncompleteChain { # 1124 Incomplete chain (missing a file necessary to form a complete chain) + #Event is an Error + $AlertText = "Alert:(EventLog 3-Day) ID:" + $Event.InstanceID + $Event.Message + Write-Output $AlertText + $AlertLevel = 3 + break + } + + $IM_ProcessingError { #1125 Processing error (error preparing for collapse / verify operations such as trying to sync files with database) + #Event is an Error + $AlertText = "Alert: (EventLog 3-Day) ID:" + $Event.InstanceID + $Event.Message + Write-Output $AlertText + $AlertLevel = 3 + break + } + + $IM_ReplicationError { #1126 Replication error (failed replication will retry next time a file is verified) + #Event is a Warning (Potentially Recoverable) + if ($AlertText = ""){ + $AlertText = "Warning: (EventLog 3-Day) ID:" + $Event.InstanceID + $Event.Message + } + if ($AlertLevel -le 2) { + $AlertLevel = 2 + } + Write-Output = "Warning: (EventLog 3-Day) ID:" + $Event.InstanceID + $Event.Message + break + } + + $IM_HSRError { #1127 HSR error + #Event is an Error + $AlertText = "Alert: (EventLog 3-Day) ID:" + $Event.InstanceID + $Event.Message + Write-Output $AlertText + $AlertLevel = 3 + break + } + + $IM_CriticalError { #1128 Critical error (the service must be restarted due to an unrecoverable error) + #Event is an Error + $AlertText = "Alert: (EventLog 3-Day) ID:" + $Event.InstanceID + $Event.Message + Write-Output $AlertText + $AlertLevel = 3 + break + } + + $IM_Exception { #Exception (ImageManager will retry the failed operation later so the service does not need to be restarted) + #Event is a Warning (Potentially Recoverable) + if ($AlertText = ""){ + $AlertText = "Warning: (EventLog 3-Day) ID:" + $Event.InstanceID + $Event.Message + } + if ($AlertLevel -le 2) { + $AlertLevel = 2 + } + Write-Output = "Warning: (EventLog 3-Day) ID:"$Event.InstanceID $Event.Message + break + } + } + } ElseIf ($Event.InstanceID -le 1120){ + #Event is Informational + #Write-Output = "Info: (EventLog 3-Day) "$Event.Message + } + } + } + } Else { #There are Events in the 1-Day search... + + #Walk the 1 day of events, looking for errors. + ForEach ($Event in $Events) { + + If ($Event.InstanceID -gt 1120){ + + Switch ($Event.InstanceID) { + + #Error codes are from 1121 to 1129: + $IM_FailedCollapse { #1121 Failed collapse + #Event is an Error + $AlertText = "Alert: (EventLog 1-Day) ID:" + $Event.InstanceID + $Event.Message + Write-Output $AlertText + $AlertLevel = 3 + break + } + + $IM_Error { #1122 Reserved + #Event is an Error + $AlertText = "Alert: (EventLog 1-Day) ID:" + $Event.InstanceID + $Event.Message + Write-Output $AlertText + $AlertLevel = 3 + break + } + + $IM_DataCorruption { # 1123 Data corruption (a file failed to verify) + #Event is an Error + $AlertText = "Alert: (EventLog 1-Day) ID:" + $Event.InstanceID + $Event.Message + Write-Output $AlertText + $AlertLevel = 3 + break + } + + $IM_IncompleteChain { # 1124 Incomplete chain (missing a file necessary to form a complete chain) + #Event is an Error + $AlertText = "Alert:(EventLog 1-Day) ID:" + $Event.InstanceID + $Event.Message + Write-Output $AlertText + $AlertLevel = 3 + break + } + + $IM_ProcessingError { #1125 Processing error (error preparing for collapse / verify operations such as trying to sync files with database) + #Event is an Error + $AlertText = "Alert: (EventLog 1-Day) ID:" + $Event.InstanceID + $Event.Message + Write-Output $AlertText + $AlertLevel = 3 + break + } + + $IM_ReplicationError { #1126 Replication error (failed replication will retry next time a file is verified) + #Event is a Warning (Potentially Recoverable) + if ($AlertText = ""){ + $AlertText = "Warning: (EventLog 3-Day) ID:" + $Event.InstanceID + $Event.Message + } + if ($AlertLevel -le 2) { + $AlertLevel = 2 + } + Write-Output = "Warning: (EventLog 3-Day) ID:" $Event.InstanceID $Event.Message + break + } + + $IM_HSRError { #1127 HSR error + #Event is an Error + $AlertText = "Alert: (EventLog 1-Day) ID:" + $Event.InstanceID + $Event.Message + Write-Output $AlertText + $AlertLevel = 3 + break + } + + $IM_CriticalError { #1128 Critical error (the service must be restarted due to an unrecoverable error) + #Event is an Error + $AlertText = "Alert: (EventLog 1-Day) ID:" + $Event.InstanceID + $Event.Message + Write-Output $AlertText + $AlertLevel = 3 + break + } + + $IM_Exception { #Exception (ImageManager will retry the failed operation later so the service does not need to be restarted) + #Event is a Warning (Potentially Recoverable) + if ($AlertText = ""){ + $AlertText = "Warning: (EventLog 3-Day) ID:" + $Event.InstanceID + $Event.Message + } + if ($AlertLevel -le 2) { + $AlertLevel = 2 + } + Write-Output = "Warning: (EventLog 3-Day) ID:" $Event.InstanceID $Event.Message + break + } + } + + } ElseIf ($Event.InstanceID -le 1120){ + + #Event is Informational + #Write-Output = "Info: (EventLog 1-Day) "$Event.Message + } + } + } +} +#Write Output +if ($AlertLevel -eq 0) { + Write-Output "Info - Event Log Checks Passed" +} + +#Close the DB Connection +$conn.Close() +#Give the DB Time to Close +Start-Sleep -Seconds 1 + +#Delete the Database Copy +try{ + Del "c:\ProgramData\TacticalRMM\temp\Imagemanager.mdb" +} +catch{ + Write-Output "Couldn't delete DB." +} +#Report back to the RMM +if ($AlertLevel -gt 0) { + Write-Output $AlertText +} +$Host.SetShouldExit($AlertLevel) +Exit diff --git a/scripts_staging/Win_StorageCraftSPX_Status.ps1 b/scripts_staging/Win_StorageCraftSPX_Status.ps1 new file mode 100644 index 00000000..556be640 --- /dev/null +++ b/scripts_staging/Win_StorageCraftSPX_Status.ps1 @@ -0,0 +1,161 @@ +<# +.SYNOPSIS + Monitor SPX Backups + +.DESCRIPTION + Checks to see of scheduled SPX backups are completing or failing. Returns Result. + +.PARAMETER FailThreshold + Number of failed backups to tolerate before raising an alert. + Defaults to 3. + +.PARAMETER PSSQLLiteIsInstalled + Lets the script know if it can skip the (expensive) check to see if PSSQLLite Is Installed + Defaults to $False + +.OUTPUTS + Exit Code: 0 = Pass, 1 = Informational, 2 = Warning, 3 = Error + +.EXAMPLE + Win_ShadowProtectSPX_BackupStatus.ps1 + #No Parameters, defaults apply + +.EXAMPLE + Win_ShadowProtectSPX_BackupStatus.ps1 (4, $True) + #Specify Parameters for Failure Threshold and PSSQLLiteIsInstalled + +.NOTES + v1.1 11/29/2023 ConvexSERV + Utilizes PSSQLLite Module to query the SPX database. +#> + +param ( + [Int] $FailThreshold, + [Boolean] $PSSQLLiteIsInstalled +) + +#Set Parameter Defaults +if (!$FailThreshold) {$FailThreshold = 3} +if (!$PSSQLLiteIsInstalled) {$PSSQLLiteIsInstalled = $False} + +#Initialize Variables +$NowTime = [DateTime]::Now +$AlertLevel = 0 #0 = Pass, 1 = Informational, 2 = Warning, 3 = Error +$AlertText = '' +$SPXDB = 'C:\ProgramData\StorageCraft\spx\spx.db3' + +If (Test-Path $SPXDB) { + + #Allow passing in $PSSQLLiteIsInstalled as a parameter to skip the check to see if it's installed. + if (-not($PSSQLLiteIsInstalled)){ + #This script requires the PSSQLite Module. Check is it's installed. + $InstalledModules = Get-Module -ListAvailable + foreach ($Module in $InstalledModules){ + if ($Module.Name -eq 'PSSQLite'){ + $PSSQLLiteIsInstalled = $True + break + } + } + + #If PSSQLite is not already installed, install it now. + if (-not($PSSQLLiteIsInstalled)) { + Install-PackageProvider NuGet -Force + Install-Module PSSQLite -Confirm:$False -Force + + #ToDo - Set a KeyPair to let the RMM know that PSSQLLite Is Installed and to skip the check next time. + #Do That Here + } + } + + #Catch the Exception if we can't import PSSQLite + Try {Import-Module PSSQLite} + Catch { + + #PSSQLite Module failed to load, try running the check without parameters + $AlertText = "PSSQLite Module failed to load, try running the check without parameters" + $AlertLevel = 3 + + #Report back to the RMM + Write-Output $AlertText + $Host.SetShouldExit($AlertLevel) + Exit + + } + + #Get the backup job entries from the SPX Database + $Qry = 'SELECT id, name, created, schedule_id, settings, paused, description, destination_id FROM job' + $Jobs = Invoke-SqliteQuery -DataSource $SPXDB -Query $Qry + + ForEach ($Job in $Jobs){ + + $JobCreated = [DateTime]::$Job.created + + #Get the backup job results from the SPX Database, in descending order (newest at the top) + $Job_ID = $Job.id + $Qry = "SELECT id, updated, ts, dt, result, summary_type, mode, snapshot_method, size, info, is_completed FROM job_event WHERE job_id = $Job_ID order by id desc" + $JobEvents = Invoke-SqliteQuery -DataSource $SPXDB -Query $Qry + + #Check to see if the last backup is more than 3 days old + $LastBackup = $JobEvents[0] + $LastBackupTS = $LastBackup.ts + If ($LastBackupTS -lt $NowTime.AddDays(-3)){ + + #We haven't had a backup in over 3 days. Fail the Check. + $AlertText = "Backups are not running. Last Backup attempt was $LastBackupTSs" + $AlertLevel = 3 + + } Else #Investigate Further... + { + + #Walk the Events, looking for failures + $FailCount = 0 + ForEach ($JobEvent in $JobEvents){ + + If ($JobEvent.is_completed -eq 1){ + + If ($FailCount -eq 0){ + + #Last Backup was successful. Pass the Check. + $LastBackupTS = $JobEvent.ts + $AlertText = "Last Backup completed at $LastBackupTS" + $AlertLevel = 0 + break + + } Else + { + #We have failures, but have not met the threshold. Pass with a Warning. + $LastBackupTS = $JobEvent.ts + $AlertText = "Last $FailCount Backup(s) Failed. Last successful Backup completed at $LastBackupTS (Threshold not met)" + $AlertLevel = 2 + } + + + } Else + { + #Increment the Fail Count. + $FailCount++ + } + + If ($FailCount -ge $FailThreshold){ + + #We have Backup failures amd have met the failure threshold + $AlertText = "Last $FailCount Backup(s) Failed. (Failure Threshold met)" + $AlertLevel = 3 + Break + } + } + } + } + +} Else{ + + #The SPX Database Doesn't Exist. Is SPX Even Installed? + $AlertText = "StorageCraft SPX Does not appear to be properly installed. (DB Not Found.)" + $AlertLevel = 3 + +} + +#Report back to the RMM +Write-Output $AlertText +$Host.SetShouldExit($AlertLevel) +Exit diff --git a/scripts_staging/Win_TRMM_ScheduledTasks_List.ps1 b/scripts_staging/Win_TRMM_ScheduledTasks_List.ps1 new file mode 100644 index 00000000..2a203924 --- /dev/null +++ b/scripts_staging/Win_TRMM_ScheduledTasks_List.ps1 @@ -0,0 +1,22 @@ +<# + +.NOTES + v1.2 8/2/2024 silversword411 adding is running column, fixed last run column +#> + +# Get the count of tasks starting with "Tac" +$taskCount = (Get-ScheduledTask | Where-Object { $_.TaskName -like "Tac*" }).Count + +# Output the total count +Write-Output "Total: $taskCount" + +# Get detailed information for tasks starting with "Tac" +Get-ScheduledTask | Where-Object { $_.TaskName -like "Tac*" } | ForEach-Object { + $taskInfo = Get-ScheduledTaskInfo -TaskName $_.TaskName + [PSCustomObject]@{ + TaskName = $_.TaskName + CreationDate = $_.Date + LastRunTime = $taskInfo.LastRunTime + IsRunning = if ($_.State -eq 'Running') { 'Yes' } else { 'No' } + } +} | Format-Table -AutoSize \ No newline at end of file diff --git a/scripts_staging/Win_TRMM_Troubleshooting_Agent.ps1 b/scripts_staging/Win_TRMM_Troubleshooting_Agent.ps1 new file mode 100644 index 00000000..d889e78b --- /dev/null +++ b/scripts_staging/Win_TRMM_Troubleshooting_Agent.ps1 @@ -0,0 +1,458 @@ +<# +.SYNOPSIS + Checks for all problems related to TRMM and Mesh Agent. + +.DESCRIPTION + This script checks for the presence of Mesh Agent service, folder, and executable file. If any of these components are missing, it returns an error code of 1. + +.PARAMETER debug + Switch parameter to enable debug output. + +.NOTES + Version: 1.0 Created 6/6/2023 by silversword411 + v1.2 5/15/2024 Adding default NIC info, TRMM registry data + v1.3 5/15/2024 Adding mesh server URL discovery, connection check to mesh and API, and checking for files and services + v1.4 5/15/2024 Rework and simplify. Write out logfile + v1.5 6/21/2024 Adding trmm agent to Check-Memorysize + v1.6 8/26/2024 checking mesh for CF proxy +#> + +param( + [String] $procname = "meshagent,tacticalrmm", + [Int] $warnwhenovermemsize = 100000000, + [switch]$debug +) + +if ($debug) { + $DebugPreference = "Continue" +} +else { + $DebugPreference = "SilentlyContinue" +} + +$logfile = "$(Get-Date -Format 'yyyy-MM-dd_HH-mm-ss')-trmmagenttroubleshooting.log" +Start-Transcript -Path $logfile -Append + +function Get-CloudflareIPRanges { + $ipv4Url = "https://www.cloudflare.com/ips-v4" + $ipv6Url = "https://www.cloudflare.com/ips-v6" + + try { + if ($Debug) { Write-Output "Downloading Cloudflare IPv4 ranges..." } + $ipv4Ranges = Invoke-WebRequest -Uri $ipv4Url -UseBasicParsing | Select-Object -ExpandProperty Content + + if ($Debug) { Write-Output "Downloading Cloudflare IPv6 ranges..." } + $ipv6Ranges = Invoke-WebRequest -Uri $ipv6Url -UseBasicParsing | Select-Object -ExpandProperty Content + + $global:CloudflareIPRanges = @() + $global:CloudflareIPRanges += $ipv4Ranges -split "`n" + $global:CloudflareIPRanges += $ipv6Ranges -split "`n" + + if ($Debug) { Write-Output "Cloudflare IP ranges downloaded successfully." } + } + catch { + Write-Output "Failed to download Cloudflare IP ranges. Please check your internet connection." + $global:CloudflareIPRanges = $null + } +} + +function ConvertTo-IPv4Integer { + param ([string]$ip) + + $ipBytes = [System.Net.IPAddress]::Parse($ip).GetAddressBytes() + [Array]::Reverse($ipBytes) # Convert to little-endian format + return [BitConverter]::ToUInt32($ipBytes, 0) +} + +function Test-IPv4InRange { + param ( + [string]$ip, + [string]$cidr + ) + + # Split the CIDR notation + $parts = $cidr -split '/' + $baseIP = $parts[0] + $subnetMask = [int]$parts[1] + + # Convert IP and base IP to 32-bit integers + $ipInt = ConvertTo-IPv4Integer -ip $ip + $baseIPInt = ConvertTo-IPv4Integer -ip $baseIP + + # Create the mask as a 32-bit unsigned integer + $mask = 0xFFFFFFFF -shl (32 - $subnetMask) + + # Compare the masked IP with the base IP + return (($ipInt -band $mask) -eq ($baseIPInt -band $mask)) +} + +function Test-CloudflareProxy { + if ($Debug) { Write-Output "Starting Cloudflare IP range retrieval..." } + Get-CloudflareIPRanges + + if ($Debug) { Write-Output "Resolving IP addresses for $global:MeshServerAddress..." } + + try { + $resolvedIPs = [System.Net.Dns]::GetHostAddresses($global:MeshServerAddress) + + if ($resolvedIPs.Count -eq 0) { + Write-Output "No IP addresses resolved for $global:MeshServerAddress." + return + } + else { + if ($Debug) { + Write-Output "Resolved IP addresses:" + foreach ($ip in $resolvedIPs) { + Write-Output " - $($ip.IPAddressToString)" + } + } + } + } + catch { + Write-Output "Failed to resolve IP addresses for $global:MeshServerAddress. Error: $_" + return + } + + $cloudflareDetected = $false + $matchedIP = $null + + foreach ($ip in $resolvedIPs) { + if ($ip.AddressFamily -eq "InterNetwork") { + # Only IPv4 + foreach ($range in $global:CloudflareIPRanges) { + if ($Debug) { Write-Output "Checking if IP $($ip.IPAddressToString) is in range $range..." } + if (Test-IPv4InRange -ip $ip.IPAddressToString -cidr $range) { + $cloudflareDetected = $true + $matchedIP = $ip.IPAddressToString + break + } + } + } + if ($cloudflareDetected) { break } + } + + if ($cloudflareDetected) { + if ($Debug) { + Write-Output "The IP address $matchedIP is within Cloudflare ranges." + } + else { + Write-Output "WARNING: $global:MeshServerAddress is using Cloudflare proxy IP $matchedIP." + } + } + else { + $notMatchedIP = $resolvedIPs | Where-Object { $_.AddressFamily -eq "InterNetwork" } | Select-Object -First 1 + if ($Debug) { + Write-Output "None of the resolved IPs are within Cloudflare ranges." + } + else { + Write-Output "The MeshServerAddress $global:MeshServerAddress is NOT using Cloudflare (IP $($notMatchedIP.IPAddressToString))." + } + } +} + +function Check-MemorySize { + if (!($procname)) { + Write-Output "No procname defined, and it is required. Exiting" + Stop-Transcript + Exit 1 + } + + if (!($warnwhenovermemsize)) { + Write-Output "No warnwhenovermemsize defined, and it is required. Exiting" + Stop-Transcript + Exit 1 + } + + Write-Debug "Warn when Memsize exceeds: $warnwhenovermemsize" + Write-Debug "#####" + + $procnameList = $procname -split ',' + + foreach ($proc in $procnameList) { + $proc = $proc.Trim() + Write-Debug "Checking process: $proc" + + $proc_pid = (get-process -Name $proc -ErrorAction SilentlyContinue).Id + + if ($null -eq $proc_pid) { + Write-Output "Process $proc not found." + continue + } + + $Processes = Get-WmiObject -Query "SELECT * FROM Win32_PerfFormattedData_PerfProc_Process WHERE IDProcess=$proc_pid" + + foreach ($Process in $Processes) { + $WS_MB = [math]::Round($Process.WorkingSetPrivate / 1MB, 2) + + if ($Process.WorkingSetPrivate -gt $warnwhenovermemsize) { + Write-Output "WARNING: $($WS_MB)MB: $($proc) has high memory usage" + Restart-service -name "Mesh Agent" + Stop-Transcript + Exit 1 + } + else { + Write-Output "$($WS_MB)MB: $($proc) is below the expected memory usage" + } + } + } +} + + +function Check-ForMeshComponents { + $serviceName = "Mesh Agent" + $ErrorCount = 0 + + if (!(Get-Service $serviceName -ErrorAction SilentlyContinue)) { + Write-Output "Mesh Agent Service Missing" + $ErrorCount += 1 + } + else { + Write-Output "Mesh Agent Service Found" + } + + if (!(Test-Path "c:\Program Files\Mesh Agent")) { + Write-Output "Mesh Agent Folder missing" + $ErrorCount += 1 + } + else { + Write-Output "Mesh Agent Folder exists" + } + + if (!(Test-Path "c:\Program Files\Mesh Agent\MeshAgent.exe")) { + Write-Output "Mesh Agent executable missing" + $ErrorCount += 1 + } + else { + Write-Output "Mesh Agent executable exists" + } + + if ($ErrorCount -ne 0) { + Stop-Transcript + exit 1 + } +} + +function Get-DefaultNetworkAdapter { + $networkConfigs = Get-NetIPConfiguration + $defaultRoutes = Get-NetRoute -DestinationPrefix '0.0.0.0/0' + + if ($defaultRoutes.Count -eq 0) { + Write-Output "No default route found." + return + } + + $defaultConfigs = @() + foreach ($route in $defaultRoutes) { + $config = $networkConfigs | Where-Object { $_.InterfaceIndex -eq $route.InterfaceIndex } + if ($config) { + $defaultConfigs += [PSCustomObject]@{ + InterfaceAlias = $config.InterfaceAlias + InterfaceMetric = $route.RouteMetric + $config.InterfaceMetric + IPv4Address = $config.IPv4Address.IPAddress + DefaultGateway = $route.NextHop + DnsServers = $config.DnsServer.ServerAddresses + } + } + } + + if ($defaultConfigs.Count -eq 0) { + Write-Output "No default network adapter found." + return + } + + $defaultConfig = $defaultConfigs | Sort-Object { $_.InterfaceMetric } | Select-Object -First 1 + + Write-Output "Default Network Adapter:" + Write-Output "Name : $($defaultConfig.InterfaceAlias)" + Write-Output "IP Address : $($defaultConfig.IPv4Address)" + Write-Output "Default Gateway : $($defaultConfig.DefaultGateway)" + Write-Output "DNS Servers : $($defaultConfig.DnsServers -join ', ')" +} + +function Get-TacticalRMMData { + $registryPath = "HKLM:\SOFTWARE\TacticalRMM" + $global:ApiURL = $null + + if (Test-Path $registryPath) { + $registryData = Get-ItemProperty -Path $registryPath + + foreach ($property in $registryData.PSObject.Properties) { + if ($property.Name -eq "AgentID" -or $property.Name -eq "Token") { + $truncatedValue = $property.Value.Substring(0, [Math]::Min(5, $property.Value.Length)) + "-snipped" + Write-Output "$($property.Name): $truncatedValue" + } + elseif ($property.Name -eq "ApiURL") { + $global:ApiURL = $property.Value + Write-Output "$($property.Name): $($property.Value)" + } + else { + Write-Output "$($property.Name): $($property.Value)" + } + } + } + else { + Write-Output "The registry key '$registryPath' does not exist." + } +} + +$global:MeshServerAddress = $null + +function Get-MeshServer { + param ( + [string]$filePath = "C:\Program Files\Mesh Agent\MeshAgent.msh" + ) + $global:MeshServerAddress = $null + + if (Test-Path $filePath) { + $content = Get-Content -Path $filePath + $meshServerLine = $content | Select-String -Pattern "MeshServer" + + if ($meshServerLine) { + $meshServer = $meshServerLine -replace "MeshServer=wss://", "" -replace ":.*", "" + $global:MeshServerAddress = $meshServer + } + else { + Write-Output "MeshServer not found in the file." + } + } + else { + Write-Output "File not found: $filePath" + } +} + +function Test-ServerConnections { + if ($global:MeshServerAddress) { + Write-Output "Pinging MeshServerAddress: $global:MeshServerAddress" + Test-Connection -ComputerName $global:MeshServerAddress -Count 2 | Format-Table -AutoSize + } + else { + Write-Output "MeshServerAddress is not set." + } + + if ($global:ApiURL) { + try { + if ($global:ApiURL -notmatch "^[a-zA-Z][a-zA-Z0-9+.-]*://") { + $global:ApiURL = "http://$global:ApiURL" + } + + $uri = [System.Uri]::new($global:ApiURL) + $hostname = $uri.Host + Write-Output "Pinging ApiURL: $hostname" + Test-Connection -ComputerName $hostname -Count 2 | Format-Table -AutoSize + } + catch { + Write-Output "Failed to parse ApiURL: $global:ApiURL" + Write-Output "Error: $_" + } + } + else { + Write-Output "ApiURL is not set." + } +} + +function Check-ServicesAndFiles { + param ( + [string]$MeshAgentPath = "C:\Program Files\Mesh Agent\MeshAgent.exe", + [string]$TacticalRmmPath = "C:\Program Files\TacticalAgent\tacticalrmm.exe", + [string]$MeshAgentService = "Mesh Agent", + [string]$TacticalRmmService = "tacticalrmm" + ) + + function Test-File { + param ( + [string]$FilePath + ) + return Test-Path -Path $FilePath + } + + function Test-Service { + param ( + [string]$ServiceName + ) + $service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue + if ($null -eq $service) { + Write-Output "PROBLEM: $ServiceName service does not exist." + return $false + } + elseif ($service.Status -ne 'Running') { + Write-Output "PROBLEM: $ServiceName service is not running. Attempting to start..." + Start-Service -Name $ServiceName + if ($?) { + Write-Output "OK: $ServiceName service started successfully." + return $true + } + else { + Write-Output "PROBLEM: Failed to start $ServiceName service." + return $false + } + } + else { + Write-Output "OK: $ServiceName service is running." + return $true + } + } + + if (Test-File -FilePath $MeshAgentPath) { + Write-Output "OK: MeshAgent.exe file exists." + } + else { + Write-Output "PROBLEM: MeshAgent.exe file does not exist." + } + + if (Test-File -FilePath $TacticalRmmPath) { + Write-Output "OK: tacticalrmm.exe file exists." + } + else { + Write-Output "PROBLEM: tacticalrmm.exe file does not exist." + } + + if (Test-Service -ServiceName $MeshAgentService) { + Write-Output "OK: $MeshAgentService service is verified." + } + else { + Write-Output "PROBLEM: $MeshAgentService service verification failed." + } + + if (Test-Service -ServiceName $TacticalRmmService) { + Write-Output "OK: $TacticalRmmService service is verified." + } + else { + Write-Output "PROBLEM: $TacticalRmmService service verification failed." + } +} + +Write-Output "******************** TRMM Registry Data ***********************" +Get-TacticalRMMData +Write-Output "" +Get-MeshServer + +Write-Output "" +Write-Output "********************** Usable Variables ***********************" +Write-Output "Global MeshServerAddress: $global:MeshServerAddress" +Write-Output "Global ApiURL: $global:ApiURL" +Write-Output "" + +Write-Output "**************** Check for files and services *****************" +Check-ServicesAndFiles +Write-Output "" + +Write-Output "************************ Default NIC *************************" +Get-DefaultNetworkAdapter +Write-Output "" + +Write-Output "************ Test Connectivity to Mesh and TRMM ***************" +Test-ServerConnections +Write-Output "" + +Write-Output "************ Checking if MeshServer is using Cloudflare *******" +Test-CloudflareProxy +Write-Output "" + +Write-Output "******************* Checking Mesh Agent ***********************" +Check-ForMeshComponents +Write-Output "" + +Write-Output "********************* Mesh Memory Size ************************" +Check-MemorySize + +Stop-Transcript \ No newline at end of file diff --git a/scripts_staging/Win_Template.ps1 b/scripts_staging/Win_Template.ps1 index 49652b3c..ef966657 100644 --- a/scripts_staging/Win_Template.ps1 +++ b/scripts_staging/Win_Template.ps1 @@ -233,7 +233,7 @@ if (Test-IsAdmin) { function Test-IsInteractiveShell { # https://stackoverflow.com/questions/9738535/powershell-test-for-noninteractive-mode # Test each Arg for match of abbreviated '-NonInteractive' command. - $NonInteractive = [Environment]::GetCommandLineArgs() | Where-Object{ $_ -like '-NonI*' } + $NonInteractive = [Environment]::GetCommandLineArgs() | Where-Object { $_ -like '-NonI*' } if ([Environment]::UserInteractive -and -not$NonInteractive) { # We are in an interactive shell. @@ -298,3 +298,18 @@ If ("SetRegistryValue" -Match "true") { # Set-RegistryValue -registryPath $RegistryPath -name "PersonalizationReportingEnabled" -value 0 #Set-RegistryValue } + +<# ================================================================================ #> +Function Foldercreate { + param ( + [Parameter(Mandatory = $false)] + [String[]]$Paths + ) + + foreach ($Path in $Paths) { + if (!(Test-Path $Path)) { + New-Item -ItemType Directory -Force -Path $Path + } + } +} +Foldercreate -Paths "$env:ProgramData\TacticalRMM\temp", "C:\Temp" \ No newline at end of file diff --git a/scripts_staging/Win_WindowsBackup_Monitor.ps1 b/scripts_staging/Win_WindowsBackup_Monitor.ps1 new file mode 100644 index 00000000..09f5dcd7 --- /dev/null +++ b/scripts_staging/Win_WindowsBackup_Monitor.ps1 @@ -0,0 +1,66 @@ +<# +.SYNOPSIS + Monitors and reports on the status of system backups. + +.DESCRIPTION + This script checks the status of the most recent successful backup and recent failed backups in the Windows Backup event log. It provides the date of the last successful backup and lists details of the last 20 failed backup events, including the date and error message. + +.OUTPUTS + Outputs the date of the last successful backup if available, otherwise notifies of no successful backups. Also, lists the last 20 failed backup events if any, otherwise returns an error message about no failed backups. + +.NOTES + v1.0 5/13/2024 silversword411 Initial version + +#> + + +# Define the log name and source +$logName = "Microsoft-Windows-Backup" +$successEventId = 14 +$exitCode = 0 # Default exit code + +# Retrieve the most recent successful backup event +$lastSuccessfulBackupEvent = Get-WinEvent -FilterHashtable @{LogName = $logName; ID = $successEventId } | Sort-Object TimeCreated -Descending | Select-Object -First 1 + +# Check if a successful backup event was found +if ($lastSuccessfulBackupEvent) { + Write-Output "Last successful backup date: $($lastSuccessfulBackupEvent.TimeCreated)" + + # Check if the last successful backup is older than 15 days + $currentDate = Get-Date + $timeDifference = $currentDate - $lastSuccessfulBackupEvent.TimeCreated + + if ($timeDifference.Days -gt 15) { + Write-Output "The last successful backup is older than 15 days." + $exitCode = 1 # Set exit code to 1 + } +} +else { + Write-Output "No successful backup events found." + $exitCode = 1 # Set exit code to 1 +} + + + +Write-Output "---------------------------------" + +# Define the log name and source +$logName = "Microsoft-Windows-Backup" +$failureEventId = 49 + +# Retrieve the 20 most recent failed backup events +$recentFailedBackupEvents = Get-WinEvent -FilterHashtable @{LogName = $logName; ID = $failureEventId } | Sort-Object TimeCreated -Descending | Select-Object -First 20 + +# Check if there are any failed backup events +if ($recentFailedBackupEvents) { + Write-Output "Last 20 failed backup events:" + foreach ($event in $recentFailedBackupEvents) { + Write-Output ("Date: " + $event.TimeCreated + ", Message: " + $event.Message) + } +} +else { + Write-Error "No failed backup events found." +} + +# Exit script with the determined exit code +exit $exitCode diff --git a/scripts_staging/linux_os_update.sh b/scripts_staging/linux_os_update.sh index fa38e8ae..d30eab66 100644 --- a/scripts_staging/linux_os_update.sh +++ b/scripts_staging/linux_os_update.sh @@ -1,18 +1,43 @@ #!/bin/bash -#Update script to run for most common linux distros +# Synopsis: This script automates the process of updating software packages across multiple Linux distributions. +# It checks for the available package manager (dnf, yum, apt, pacman, or zypper) and executes the appropriate commands to update the system. +# Users can optionally allow the script to automatically reboot the system after updates by passing the --autoreboot flag. +# +# Usage: +# Update with automatic reboot --autoreboot +# +# Note: The script is designed to be flexible, catering both to interactive use cases and automated workflows. -if [[ `which yum` ]]; then +AUTO_REBOOT=0 + +# Check for --autoreboot flag +for arg in "$@"; do + if [[ $arg == "--autoreboot" ]]; then + AUTO_REBOOT=1 + fi +done + +# Update system based on package manager availability +if command -v dnf &> /dev/null; then + dnf -y update +elif command -v yum &> /dev/null; then yum -y update -elif [[ `which apt` ]]; then - apt-get -y update - apt-get -y upgrade -elif [[ `which pacman` ]]; then +elif command -v apt &> /dev/null; then + apt-get -y update && apt-get -y upgrade +elif command -v pacman &> /dev/null; then pacman -Syu -elif [[ `which zypper` ]]; then +elif command -v zypper &> /dev/null; then zypper update else - echo "Unknown Platform" + echo "Package manager not detected. Please update your system manually." + exit 1 fi -sleep 10 && reboot & +# Handle auto-reboot +if [ $AUTO_REBOOT -eq 1 ]; then + echo "Rebooting in 10 seconds..." + sleep 10 && reboot & +else + echo "Updates done, please reboot" +fi diff --git a/scripts_staging/linux_os_update_check.sh b/scripts_staging/linux_os_update_check.sh new file mode 100644 index 00000000..dff073fe --- /dev/null +++ b/scripts_staging/linux_os_update_check.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# Synopsis: +# This script is designed to check for available package updates on Linux systems. It supports multiple package +# managers, including apt-get (used by Debian-based distributions like Ubuntu), dnf (used by Fedora), and yum +# (used by CentOS and RHEL). The script identifies which package manager is available on the system and uses it +# to check for updates. If updates are available, it lists them and exits with code 1. If updates cannot be checked, +# it exits with code 2. If there are no updates, it exits with code 0. + +# Exit Codes: +# 0 - Success: No updates are available. +# 1 - Error: Updates are available (this script treats the availability of updates as an actionable item, thus 'Error'). +# 2 - Warning: The script was unable to check for updates, possibly due to an unsupported package manager or other issue. + +# The script provides a straightforward way for administrators and scripts to check for software updates across a +# variety of Linux distributions using Tactical RMM, simplifying maintenance tasks and ensuring systems can be kept up to date with +# minimal manual intervention. + +#!/bin/bash + +# Function to check for updates using apt-get +check_apt_get() { + apt-get update > /dev/null + UPDATES=$(apt-get -s upgrade | awk '/^Inst/ { print $2 }') + if [ -n "$UPDATES" ]; then + echo "Updates available:" + echo "$UPDATES" + exit 1 + fi +} + +# Function to check for updates using dnf +check_dnf() { + UPDATES=$(dnf check-update | awk '{if (NR!=1) {print $1}}') + if [ -n "$UPDATES" ]; then + echo "Updates available:" + echo "$UPDATES" + exit 1 + fi +} + +# Function to check for updates using yum +check_yum() { + UPDATES=$(yum check-update | awk '{if (NR!=1 && !/Loaded plugins/) {print $1}}') + if [ -n "$UPDATES" ]; then + echo "Updates available:" + echo "$UPDATES" + exit 1 + fi +} + +# Determine which package manager is available and check for updates +if command -v apt-get &> /dev/null; then + check_apt_get +elif command -v dnf &> /dev/null; then + check_dnf +elif command -v yum &> /dev/null; then + check_yum +else + echo "Unable to determine package manager or check updates." + exit 2 +fi + +echo "No updates available." +exit 0 diff --git a/scripts_staging/snippets/CallPowerShell7.ps1 b/scripts_staging/snippets/CallPowerShell7.ps1 new file mode 100644 index 00000000..e578bff6 --- /dev/null +++ b/scripts_staging/snippets/CallPowerShell7.ps1 @@ -0,0 +1,59 @@ +<# +.SYNOPSIS + Script to ensure PowerShell 7+ is installed and set up properly. + +.DESCRIPTION + This script checks if PowerShell 7+ is installed. If not, it installs Chocolatey first, then uses Chocolatey to install PowerShell 7. + It also sets up the correct rendering for PowerShell 7. + +.NOTES + Author: SAN + #public + Date: 01.01.24 + +.CHANGELOG + 22.05.25 SAN Added UTF8 to fix encoding issue with russian & french chars + +#> + +# Check for required PowerShell version (7+) +if (!($PSVersionTable.PSVersion.Major -ge 7)) { + try { + # Check if Chocolatey is installed + if (!(Get-Command choco -ErrorAction SilentlyContinue)) { + Write-Output 'Chocolatey is not installed. Installing Chocolatey...' + # Install Chocolatey + Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) + # Refresh PATH + $env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'User') + if (!(Get-Command choco -ErrorAction SilentlyContinue)) { + Write-Output 'Chocolatey installation failed.' + exit 1 + } + } + # Check if PowerShell 7 is installed + if (!(Get-Command pwsh -ErrorAction SilentlyContinue)) { + Write-Output 'PowerShell 7 is not installed. Installing PowerShell 7...' + # Install PowerShell 7 using Chocolatey + choco install powershell-core --install-arguments='"DISABLE_TELEMETRY"'-'"ADD_FILE_CONTEXT_MENU_RUNPOWERSHELL=1"'-'"ADD_EXPLORER_CONTEXT_MENU_OPENPOWERSHELL=1"'-'"REGISTER_MANIFEST=1"' -y + if (!(Get-Command pwsh -ErrorAction SilentlyContinue)) { + Write-Output 'PowerShell 7 installation failed.' + exit 1 + } + } + # Refresh PATH + $env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'User') + # Restart script in PowerShell 7 + pwsh -File "`"$PSCommandPath`"" @PSBoundParameters + } + catch { + Write-Output 'Error occurred while installing PowerShell 7.' + throw $Error + exit 1 + } + finally { exit $LASTEXITCODE } +} + +#Set the correct rendering for pwsh +$PSStyle.OutputRendering = "plaintext" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 \ No newline at end of file diff --git a/scripts_staging/snippets/CallPowerShell7Lite.ps1 b/scripts_staging/snippets/CallPowerShell7Lite.ps1 new file mode 100644 index 00000000..83e62750 --- /dev/null +++ b/scripts_staging/snippets/CallPowerShell7Lite.ps1 @@ -0,0 +1,31 @@ +<# +.SYNOPSIS + Ensures the script is executed using PowerShell 7 or higher. + +.DESCRIPTION + This script verifies whether it is running in a PowerShell 7+ environment. + If not, and if PowerShell 7 (pwsh) is available on the system, it re-invokes itself using pwsh, passing along any parameters. + If pwsh is not found, the script outputs a message and exits with an error code. + Once running in PowerShell 7 or higher, it sets the output rendering mode to plaintext for consistent formatting. + +.NOTES + Author: SAN + Date: 29/04/2025 + #public + +.CHANGELOG + 22.05.25 SAN Added UTF8 to fix encoding issue with russian & french chars +#> + + +if (!($PSVersionTable.PSVersion.Major -ge 7)) { + if (Get-Command pwsh -ErrorAction SilentlyContinue) { + pwsh -File "`"$PSCommandPath`"" @PSBoundParameters + exit $LASTEXITCODE + } else { + Write-Output "ERROR: PowerShell 7 is not available. Exiting." + exit 1 + } +} +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$PSStyle.OutputRendering = "plaintext" diff --git a/scripts_staging/snippets/Cleaner.ps1 b/scripts_staging/snippets/Cleaner.ps1 new file mode 100644 index 00000000..f6e47e82 --- /dev/null +++ b/scripts_staging/snippets/Cleaner.ps1 @@ -0,0 +1,215 @@ +<# +.SYNOPSIS + Automate cleaning up the C:\ drive with low disk space warning. + +.DESCRIPTION + Cleans the C: drive's Windows Temporary files, Windows SoftwareDistribution folder, + the local users Temporary folder, IIS logs(if applicable) and empties the recycle bin. + By default this script leaves files that are newer than 30 days old however this variable can be edited. + This script will typically clean up anywhere from 1GB up to 15GB of space from a C: drive. + + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.EXEMPLE + DaysToDelete=25 + +.CHANGELOG + 25.10.24 SAN Changed to 25 day of IIS logs + 19.11.24 SAN Added adobe updates folder to cleanup + 19.11.24 SAN removed colors + 19.11.24 SAN added cleanup of search index + 17.12.24 SAN Full code refactoring, set a single value for file expiration + 14.01.25 SAN More verbose output for the deletion of items + 11.08.25 SAN Run cleanmgr in the background + 18.08.25 SAN fix disk info error, missing function, sccm condition and made it move verbose. + 10.09.25 SAN added output for to check if cleanmgr is running + +.TODO + Integrate bleachbit this would help avoid having to update this script too often. + add days to array to overide defaut day to delete in some folder +#> + +# Check environment variable and set default if not defined +$DaysToDelete = if ([string]::IsNullOrEmpty($env:DaysToDelete)) { 30 } else { [int]$env:DaysToDelete } + +Write-Host "Days to delete set to: $DaysToDelete" + + +$Starters = Get-Date + +# Function to retrieve and display disk space info +function Get-DiskInfo { + try { + $DiskInfo = Get-WmiObject Win32_LogicalDisk | Where-Object { $_.DriveType -eq 3 } # 3 = Local Disk + foreach ($disk in $DiskInfo) { + [PSCustomObject]@{ + DeviceID = $disk.DeviceID + VolumeName = $disk.VolumeName + FileSystem = $disk.FileSystem + FreeSpaceGB = [math]::Round($disk.FreeSpace / 1GB, 2) + SizeGB = [math]::Round($disk.Size / 1GB, 2) + } + } + } catch { + Write-Host "[ERROR] Failed to retrieve disk information. $_" + } +} + +function Remove-Items { + param( + [string]$Path, + [int]$Days + ) + if (Test-Path $Path) { + Get-ChildItem -Path $Path -Recurse -Force -ErrorAction SilentlyContinue | + Where-Object { -not $_.PSIsContainer -and $_.LastWriteTime -lt (Get-Date).AddDays(-$Days) } | + Remove-Item -Force -Recurse -ErrorAction SilentlyContinue + } +} + +# Function to add or update registry keys for Disk Cleanup +function Add-RegistryKeys-CleanMGR { + $baseKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches" + $valueName = "StateFlags0001" + $value = 2 + + # Get all subkeys except the one named "StateFlags0001" + $subKeys = Get-ChildItem -Path $baseKey -ErrorAction SilentlyContinue | Where-Object { $_.PSChildName -ne $valueName } + + foreach ($subKey in $subKeys) { + $keyPath = $subKey.PSPath + + # Add or update the StateFlags0001 property + New-ItemProperty -Path $keyPath -Name $valueName -Value $value -PropertyType DWORD -Force | Out-Null + } + Write-Host "StateFlags0001 DWORD value successfully created/updated for all subkeys under $baseKey." +} + +# Cleanup paths grouped by purpose +$PathsToClean = @{ + "SystemTemp" = "$env:windir\Temp\*" + "Minidump" = "$env:windir\minidump\*" + "Prefetch" = "$env:windir\Prefetch\*" + "MemoryDump" = "$env:windir\memory.dmp" + "RecycleBin" = "C:\$Recycle.Bin" + "AdobeARM" = "C:\ProgramData\Adobe\ARM" + "SoftwareDistribution" = "C:\Windows\SoftwareDistribution" + "CSBack" = "C:\csback" + "CBSLogs" = "C:\Windows\logs\CBS\*.log" + "IISLogs" = "C:\inetpub\logs\LogFiles" + "ConfigMsi" = "C:\Config.Msi" + "Intel" = "C:\Intel" + "PerfLogs" = "C:\PerfLogs" + "ErrorReporting" = "C:\ProgramData\Microsoft\Windows\WER" + "PreviousWindows" = "C:\Windows.old" +} + +# User-specific cleanup paths +$UserPathsToClean = @{ + "UserTemp" = "C:\Users\*\AppData\Local\Temp\*" + "ErrorReporting" = "C:\Users\*\AppData\Local\Microsoft\Windows\WER\*" + "TempInternetFiles" = "C:\Users\*\AppData\Local\Microsoft\Windows\Temporary Internet Files\*" + "IECache" = "C:\Users\*\AppData\Local\Microsoft\Windows\IECompatCache\*" + "IECompatUaCache" = "C:\Users\*\AppData\Local\Microsoft\Windows\IECompatUaCache\*" + "IEDownloadHistory" = "C:\Users\*\AppData\Local\Microsoft\Windows\IEDownloadHistory\*" + "INetCache" = "C:\Users\*\AppData\Local\Microsoft\Windows\INetCache\*" + "INetCookies" = "C:\Users\*\AppData\Local\Microsoft\Windows\INetCookies\*" + "TerminalServerCache" = "C:\Users\*\AppData\Local\Microsoft\Terminal Server Client\Cache\*" +} + +# Display disk space before cleanup +Write-Host "[INFO] Retrieving current disk percent free for comparison after script completion." +$Before = Get-DiskInfo | Format-Table -AutoSize | Out-String + +# Stop Windows Update service +Stop-Service -Name wuauserv -Force -ErrorAction SilentlyContinue -Verbose + +# Adjust SCCM cache size if configured +try { + $cache = Get-WmiObject -Namespace root\ccm\SoftMgmtAgent -Class CacheConfig -ErrorAction Stop + if ($cache) { + $cache.size = 1024 + $cache.Put() | Out-Null + Restart-Service ccmexec -ErrorAction SilentlyContinue + Write-Host "[INFO] SCCM cache size adjusted and ccmexec service restarted." + } +} catch { + Write-Host "[INFO] SCCM client not detected. Skipping cache configuration." +} + + +# Compaction of Windows.edb +$windowsEdbPath = "$env:ALLUSERSPROFILE\\Microsoft\\Search\\Data\\Applications\\Windows\\Windows.edb" +if (Test-Path $windowsEdbPath) { + Write-Host "Disabling Windows Search service..." + Set-Service -Name wsearch -StartupType Disabled + Stop-Service -Name wsearch -Force + Write-Host "Performing offline compaction of the Windows.edb file..." + Start-Process -FilePath "esentutl.exe" -ArgumentList "/d `"$windowsEdbPath`"" -NoNewWindow -Wait + Write-Host "Compaction completed." + Set-Service -Name wsearch -StartupType Automatic + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\wsearch" -Name DelayedAutostart -Value 1 + Start-Service -Name wsearch + Write-Host "Windows Search service restarted." +} else { + Write-Host "[WARNING] Windows.edb file not found, skipping compaction." +} + +# Empty recycle bin based on PowerShell version +if ($PSVersionTable.PSVersion.Major -le 4) { + $Recycler = (New-Object -ComObject Shell.Application).NameSpace(0xA) + $Recycler.Items() | ForEach-Object { + Remove-Item -Path $_.Path -Force -Recurse -Verbose + } + Write-Host "[DONE] The recycle bin has been cleaned successfully!" +} elseif ($PSVersionTable.PSVersion.Major -ge 5) { + Clear-RecycleBin -DriveLetter C: -Force -Verbose + Write-Host "[DONE] The recycle bin has been cleaned successfully!" +} + +# Perform cleanup for system paths +foreach ($Path in $PathsToClean.Values) { + Remove-Items -Path $Path -Days $DaysToDelete +} + +# Perform cleanup for user paths +foreach ($Path in $UserPathsToClean.Values) { + Remove-Items -Path $Path -Days $DaysToDelete +} + +# Add registry keys for Disk Cleanup +Add-RegistryKeys-CleanMGR +# Run Disk Cleanup with custom settings +$processStartInfo = New-Object System.Diagnostics.ProcessStartInfo +$processStartInfo.FileName = "cleanmgr.exe" +$processStartInfo.Arguments = "/sagerun:1" +$processStartInfo.UseShellExecute = $true # Allows the executable to run independently +$processStartInfo.CreateNoWindow = $true # Prevents a new window from being created +# Start the process (no wait) +[System.Diagnostics.Process]::Start($processStartInfo) | Out-Null +Start-Sleep -Seconds 5 +$process = Get-Process -Name "cleanmgr" -ErrorAction SilentlyContinue +if ($null -ne $process) { + Write-Output "[DONE] Disk Cleanup is running." +} else { + Write-Output "[ERR] Disk Cleanup is NOT running." +} + + +# Gather disk usage after cleanup +$After = Get-DiskInfo | Format-Table -AutoSize | Out-String + +# Restart Windows Update service +Start-Service -Name wuauserv -ErrorAction SilentlyContinue + +# Calculate and display elapsed time +$Enders = Get-Date +$ElapsedTime = ($Enders - $Starters).TotalSeconds +Write-Host "[DONE] Script finished" +Write-Host "[INFO] Elapsed Time: $ElapsedTime seconds" +Write-Host "[INFO] Before Cleanup: $Before" +Write-Host "[INFO] After Cleanup: $After" diff --git a/scripts_staging/snippets/GeneratedPassphrase.ps1 b/scripts_staging/snippets/GeneratedPassphrase.ps1 new file mode 100644 index 00000000..5838b85f --- /dev/null +++ b/scripts_staging/snippets/GeneratedPassphrase.ps1 @@ -0,0 +1,100 @@ + +<# +.SYNOPSIS + This function generate a random passphrase based on an integrated wordlist + +.DESCRIPTION + The script defines a function to generate a random passphrase based on eff wordlist + Snipet output called from function "GeneratedPassphrase" or preferably from the already pre-generated "$GeneratedPassphrase" variable. + +.NOTES + Author : SAN + https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases + Date: 01.01.2024 + #public + + + +.CHANGELOG + 01.05.2025 SAN Increased default password length + 01.05.2025 SAN added a lot of param and random symbol by default + +.TODO + +#> + + +function GeneratedPassphrase { + param ( + [int] [ValidateRange(1, 20)] + $NumWords = 3, # Number of words to generate in the passphrase. + + [int] [ValidateRange(3, 15)] + $MinWordLength = 5, # Minimum length of each word. Words shorter than this will be excluded. + + [int] [ValidateRange(3, 15)] + $MaxWordLength = 11, # Maximum length of each word. Words longer than this will be excluded. + + [bool] + $UseCapitalization = $true, # If $true, capitalizes the first letter of each word in the passphrase. + + [bool] + $IncludeNumber = $true, # If $true, appends a random number (from $MinNumber to $MaxNumber) to one of the words. + + [int] + $MinNumber = 10, # The minimum number to append to a word when $IncludeNumber is $true. + + [int] + $MaxNumber = 99, # The maximum number to append to a word when $IncludeNumber is $true. + + [string] + $Separator = "", # The character or string used to separate the words in the passphrase (e.g., "-", "_", etc.). + + [bool] + $RandomSeparator = $true # If $true, a random separator is chosen from a predefined list. If $Separator is set, this is ignored. + ) + +<# +# small list of words + $WordList = @( + "abacus", "abdomen", "abstract", "academy", "accelerate", "access", + "accident", "accuracy", "activate", "adventure", "aesthetic", "algorithm", + "balance", "banana", "battery", "benefit", "biology", "blanket", "breathe" + ) +#> + + # big list of words + $WordList = @( + "abacus","abdomen","abdominal","abide","abiding","ability","ablaze","able","abnormal","abrasion","abrasive","abreast","abridge","abroad","abruptly","absence","absentee","absently","absinthe","absolute","absolve","abstain","abstract","absurd","accent","acclaim","acclimate","accompany","account","accuracy","accurate","accustom","acetone","achiness","aching","acid","acorn","acquaint","acquire","acre","acrobat","acronym","acting","action","activate","activator","active","activism","activist","activity","actress","acts","acutely","acuteness","aeration","aerobics","aerosol","aerospace","afar","affair","affected","affecting","affection","affidavit","affiliate","affirm","affix","afflicted","affluent","afford","affront","aflame","afloat","aflutter","afoot","afraid","afterglow","afterlife","aftermath","aftermost","afternoon","aged","ageless","agency","agenda","agent","aggregate","aghast","agile","agility","aging","agnostic","agonize","agonizing","agony","agreeable","agreeably","agreed","agreeing","agreement","aground","ahead","ahoy","aide","aids","aim","ajar","alabaster","alarm","albatross","album","alfalfa","algebra","algorithm","alias","alibi","alienable","alienate","aliens","alike","alive","alkaline","alkalize","almanac","almighty","almost","aloe","aloft","aloha","alone","alongside","aloof","alphabet","alright","although","altitude","alto","aluminum","alumni","always","amaretto","amaze","amazingly","amber","ambiance","ambiguity","ambiguous","ambition","ambitious","ambulance","ambush","amendable","amendment","amends","amenity","amiable","amicably","amid","amigo","amino","amiss","ammonia","ammonium","amnesty","amniotic","among","amount","amperage","ample","amplifier","amplify","amply","amuck","amulet","amusable","amused","amusement","amuser","amusing","anaconda","anaerobic","anagram","anatomist","anatomy","anchor","anchovy","ancient","android","anemia","anemic","aneurism","anew","angelfish","angelic","anger","angled","angler","angles","angling","angrily","angriness","anguished","angular","animal","animate","animating","animation","animator","anime","animosity","ankle","annex","annotate","announcer","annoying","annually","annuity","anointer","another","answering","antacid","antarctic","anteater","antelope","antennae","anthem","anthill","anthology","antibody","antics","antidote","antihero","antiquely","antiques","antiquity","antirust","antitoxic","antitrust","antiviral","antivirus","antler","antonym","antsy","anvil","anybody","anyhow","anymore","anyone","anyplace","anything","anytime","anyway","anywhere","aorta","apache","apostle","appealing","appear","appease","appeasing","appendage","appendix","appetite","appetizer","applaud","applause","apple","appliance","applicant","applied","apply","appointee","appraisal","appraiser","apprehend","approach","approval","approve","apricot","april","apron","aptitude","aptly","aqua","aqueduct","arbitrary","arbitrate","ardently","area","arena","arguable","arguably","argue","arise","armadillo","armband","armchair","armed","armful","armhole","arming","armless","armoire","armored","armory","armrest","army","aroma","arose","around","arousal","arrange","array","arrest","arrival","arrive","arrogance","arrogant","arson","art","ascend","ascension","ascent","ascertain","ashamed","ashen","ashes","ashy","aside","askew","asleep","asparagus","aspect","aspirate","aspire","aspirin","astonish","astound","astride","astrology","astronaut","astronomy","astute","atlantic","atlas","atom","atonable","atop","atrium","atrocious","atrophy","attach","attain","attempt","attendant","attendee","attention","attentive","attest","attic","attire","attitude","attractor","attribute","atypical","auction","audacious","audacity","audible","audibly","audience","audio","audition","augmented","august","authentic","author","autism","autistic","autograph","automaker","automated","automatic","autopilot","available","avalanche","avatar","avenge","avenging","avenue","average","aversion","avert","aviation","aviator","avid","avoid","await","awaken","award","aware","awhile","awkward","awning","awoke","awry","axis","babble","babbling","babied","baboon","backache","backboard","backboned","backdrop","backed","backer","backfield","backfire","backhand","backing","backlands","backlash","backless","backlight","backlit","backlog","backpack","backpedal","backrest","backroom","backshift","backside","backslid","backspace","backspin","backstab","backstage","backtalk","backtrack","backup","backward","backwash","backwater","backyard","bacon","bacteria","bacterium","badass","badge","badland","badly","badness","baffle","baffling","bagel","bagful","baggage","bagged","baggie","bagginess","bagging","baggy","bagpipe","baguette","baked","bakery","bakeshop","baking","balance","balancing","balcony","balmy","balsamic","bamboo","banana","banish","banister","banjo","bankable","bankbook","banked","banker","banking","banknote","bankroll","banner","bannister","banshee","banter","barbecue","barbed","barbell","barber","barcode","barge","bargraph","barista","baritone","barley","barmaid","barman","barn","barometer","barrack","barracuda","barrel","barrette","barricade","barrier","barstool","bartender","barterer","bash","basically","basics","basil","basin","basis","basket","batboy","batch","bath","baton","bats","battalion","battered","battering","battery","batting","battle","bauble","bazooka","blabber","bladder","blade","blah","blame","blaming","blanching","blandness","blank","blaspheme","blasphemy","blast","blatancy","blatantly","blazer","blazing","bleach","bleak","bleep","blemish","blend","bless","blighted","blimp","bling","blinked","blinker","blinking","blinks","blip","blissful","blitz","blizzard","bloated","bloating","blob","blog","bloomers","blooming","blooper","blot","blouse","blubber","bluff","bluish","blunderer","blunt","blurb","blurred","blurry","blurt","blush","blustery","boaster","boastful","boasting","boat","bobbed","bobbing","bobble","bobcat","bobsled","bobtail","bodacious","body","bogged","boggle","bogus","boil","bok","bolster","bolt","bonanza","bonded","bonding","bondless","boned","bonehead","boneless","bonelike","boney","bonfire","bonnet","bonsai","bonus","bony","boogeyman","boogieman","book","boondocks","booted","booth","bootie","booting","bootlace","bootleg","boots","boozy","borax","boring","borough","borrower","borrowing","boss","botanical","botanist","botany","botch","both","bottle","bottling","bottom","bounce","bouncing","bouncy","bounding","boundless","bountiful","bovine","boxcar","boxer","boxing","boxlike","boxy","breach","breath","breeches","breeching","breeder","breeding","breeze","breezy","brethren","brewery","brewing","briar","bribe","brick","bride","bridged","brigade","bright","brilliant","brim","bring","brink","brisket","briskly","briskness","bristle","brittle","broadband","broadcast","broaden","broadly","broadness","broadside","broadways","broiler","broiling","broken","broker","bronchial","bronco","bronze","bronzing","brook","broom","brought","browbeat","brownnose","browse","browsing","bruising","brunch","brunette","brunt","brush","brussels","brute","brutishly","bubble","bubbling","bubbly","buccaneer","bucked","bucket","buckle","buckshot","buckskin","bucktooth","buckwheat","buddhism","buddhist","budding","buddy","budget","buffalo","buffed","buffer","buffing","buffoon","buggy","bulb","bulge","bulginess","bulgur","bulk","bulldog","bulldozer","bullfight","bullfrog","bullhorn","bullion","bullish","bullpen","bullring","bullseye","bullwhip","bully","bunch","bundle","bungee","bunion","bunkbed","bunkhouse","bunkmate","bunny","bunt","busboy","bush","busily","busload","bust","busybody","buzz","cabana","cabbage","cabbie","cabdriver","cable","caboose","cache","cackle","cacti","cactus","caddie","caddy","cadet","cadillac","cadmium","cage","cahoots","cake","calamari","calamity","calcium","calculate","calculus","caliber","calibrate","calm","caloric","calorie","calzone","camcorder","cameo","camera","camisole","camper","campfire","camping","campsite","campus","canal","canary","cancel","candied","candle","candy","cane","canine","canister","cannabis","canned","canning","cannon","cannot","canola","canon","canopener","canopy","canteen","canyon","capable","capably","capacity","cape","capillary","capital","capitol","capped","capricorn","capsize","capsule","caption","captivate","captive","captivity","capture","caramel","carat","caravan","carbon","cardboard","carded","cardiac","cardigan","cardinal","cardstock","carefully","caregiver","careless","caress","caretaker","cargo","caring","carless","carload","carmaker","carnage","carnation","carnival","carnivore","carol","carpenter","carpentry","carpool","carport","carried","carrot","carrousel","carry","cartel","cartload","carton","cartoon","cartridge","cartwheel","carve","carving","carwash","cascade","case","cash","casing","casino","casket","cassette","casually","casualty","catacomb","catalog","catalyst","catalyze","catapult","cataract","catatonic","catcall","catchable","catcher","catching","catchy","caterer","catering","catfight","catfish","cathedral","cathouse","catlike","catnap","catnip","catsup","cattail","cattishly","cattle","catty","catwalk","caucasian","caucus","causal","causation","cause","causing","cauterize","caution","cautious","cavalier","cavalry","caviar","cavity","cedar","celery","celestial","celibacy","celibate","celtic","cement","census","ceramics","ceremony","certainly","certainty","certified","certify","cesarean","cesspool","chafe","chaffing","chain","chair","chalice","challenge","chamber","chamomile","champion","chance","change","channel","chant","chaos","chaperone","chaplain","chapped","chaps","chapter","character","charbroil","charcoal","charger","charging","chariot","charity","charm","charred","charter","charting","chase","chasing","chaste","chastise","chastity","chatroom","chatter","chatting","chatty","cheating","cheddar","cheek","cheer","cheese","cheesy","chef","chemicals","chemist","chemo","cherisher","cherub","chess","chest","chevron","chevy","chewable","chewer","chewing","chewy","chief","chihuahua","childcare","childhood","childish","childless","childlike","chili","chill","chimp","chip","chirping","chirpy","chitchat","chivalry","chive","chloride","chlorine","choice","chokehold","choking","chomp","chooser","choosing","choosy","chop","chosen","chowder","chowtime","chrome","chubby","chuck","chug","chummy","chump","chunk","churn","chute","cider","cilantro","cinch","cinema","cinnamon","circle","circling","circular","circulate","circus","citable","citadel","citation","citizen","citric","citrus","city","civic","civil","clad","claim","clambake","clammy","clamor","clamp","clamshell","clang","clanking","clapped","clapper","clapping","clarify","clarinet","clarity","clash","clasp","class","clatter","clause","clavicle","claw","clay","clean","clear","cleat","cleaver","cleft","clench","clergyman","clerical","clerk","clever","clicker","client","climate","climatic","cling","clinic","clinking","clip","clique","cloak","clobber","clock","clone","cloning","closable","closure","clothes","clothing","cloud","clover","clubbed","clubbing","clubhouse","clump","clumsily","clumsy","clunky","clustered","clutch","clutter","coach","coagulant","coastal","coaster","coasting","coastland","coastline","coat","coauthor","cobalt","cobbler","cobweb","cocoa","coconut","cod","coeditor","coerce","coexist","coffee","cofounder","cognition","cognitive","cogwheel","coherence","coherent","cohesive","coil","coke","cola","cold","coleslaw","coliseum","collage","collapse","collar","collected","collector","collide","collie","collision","colonial","colonist","colonize","colony","colossal","colt","coma","come","comfort","comfy","comic","coming","comma","commence","commend","comment","commerce","commode","commodity","commodore","common","commotion","commute","commuting","compacted","compacter","compactly","compactor","companion","company","compare","compel","compile","comply","component","composed","composer","composite","compost","composure","compound","compress","comprised","computer","computing","comrade","concave","conceal","conceded","concept","concerned","concert","conch","concierge","concise","conclude","concrete","concur","condense","condiment","condition","condone","conducive","conductor","conduit","cone","confess","confetti","confidant","confident","confider","confiding","configure","confined","confining","confirm","conflict","conform","confound","confront","confused","confusing","confusion","congenial","congested","congrats","congress","conical","conjoined","conjure","conjuror","connected","connector","consensus","consent","console","consoling","consonant","constable","constant","constrain","constrict","construct","consult","consumer","consuming","contact","container","contempt","contend","contented","contently","contents","contest","context","contort","contour","contrite","control","contusion","convene","convent","copartner","cope","copied","copier","copilot","coping","copious","copper","copy","coral","cork","cornball","cornbread","corncob","cornea","corned","corner","cornfield","cornflake","cornhusk","cornmeal","cornstalk","corny","coronary","coroner","corporal","corporate","corral","correct","corridor","corrode","corroding","corrosive","corsage","corset","cortex","cosigner","cosmetics","cosmic","cosmos","cosponsor","cost","cottage","cotton","couch","cough","could","countable","countdown","counting","countless","country","county","courier","covenant","cover","coveted","coveting","coyness","cozily","coziness","cozy","crabbing","crabgrass","crablike","crabmeat","cradle","cradling","crafter","craftily","craftsman","craftwork","crafty","cramp","cranberry","crane","cranial","cranium","crank","crate","crave","craving","crawfish","crawlers","crawling","crayfish","crayon","crazed","crazily","craziness","crazy","creamed","creamer","creamlike","crease","creasing","creatable","create","creation","creative","creature","credible","credibly","credit","creed","creme","creole","crepe","crept","crescent","crested","cresting","crestless","crevice","crewless","crewman","crewmate","crib","cricket","cried","crier","crimp","crimson","cringe","cringing","crinkle","crinkly","crisped","crisping","crisply","crispness","crispy","criteria","critter","croak","crock","crook","croon","crop","cross","crouch","crouton","crowbar","crowd","crown","crucial","crudely","crudeness","cruelly","cruelness","cruelty","crumb","crummiest","crummy","crumpet","crumpled","cruncher","crunching","crunchy","crusader","crushable","crushed","crusher","crushing","crust","crux","crying","cryptic","crystal","cubbyhole","cube","cubical","cubicle","cucumber","cuddle","cuddly","cufflink","culinary","culminate","culpable","culprit","cultivate","cultural","culture","cupbearer","cupcake","cupid","cupped","cupping","curable","curator","curdle","cure","curfew","curing","curled","curler","curliness","curling","curly","curry","curse","cursive","cursor","curtain","curtly","curtsy","curvature","curve","curvy","cushy","cusp","cussed","custard","custodian","custody","customary","customer","customize","customs","cut","cycle","cyclic","cycling","cyclist","cylinder","cymbal","cytoplasm","cytoplast","dab","dad","daffodil","dagger","daily","daintily","dainty","dairy","daisy","dallying","dance","dancing","dandelion","dander","dandruff","dandy","danger","dangle","dangling","daredevil","dares","daringly","darkened","darkening","darkish","darkness","darkroom","darling","darn","dart","darwinism","dash","dastardly","data","datebook","dating","daughter","daunting","dawdler","dawn","daybed","daybreak","daycare","daydream","daylight","daylong","dayroom","daytime","dazzler","dazzling","deacon","deafening","deafness","dealer","dealing","dealmaker","dealt","dean","debatable","debate","debating","debit","debrief","debtless","debtor","debug","debunk","decade","decaf","decal","decathlon","decay","deceased","deceit","deceiver","deceiving","december","decency","decent","deception","deceptive","decibel","decidable","decimal","decimeter","decipher","deck","declared","decline","decode","decompose","decorated","decorator","decoy","decrease","decree","dedicate","dedicator","deduce","deduct","deed","deem","deepen","deeply","deepness","deface","defacing","defame","default","defeat","defection","defective","defendant","defender","defense","defensive","deferral","deferred","defiance","defiant","defile","defiling","define","definite","deflate","deflation","deflator","deflected","deflector","defog","deforest","defraud","defrost","deftly","defuse","defy","degraded","degrading","degrease","degree","dehydrate","deity","dejected","delay","delegate","delegator","delete","deletion","delicacy","delicate","delicious","delighted","delirious","delirium","deliverer","delivery","delouse","delta","deluge","delusion","deluxe","demanding","demeaning","demeanor","demise","democracy","democrat","demote","demotion","demystify","denatured","deniable","denial","denim","denote","dense","density","dental","dentist","denture","deny","deodorant","deodorize","departed","departure","depict","deplete","depletion","deplored","deploy","deport","depose","depraved","depravity","deprecate","depress","deprive","depth","deputize","deputy","derail","deranged","derby","derived","desecrate","deserve","deserving","designate","designed","designer","designing","deskbound","desktop","deskwork","desolate","despair","despise","despite","destiny","destitute","destruct","detached","detail","detection","detective","detector","detention","detergent","detest","detonate","detonator","detoxify","detract","deuce","devalue","deviancy","deviant","deviate","deviation","deviator","device","devious","devotedly","devotee","devotion","devourer","devouring","devoutly","dexterity","dexterous","diabetes","diabetic","diabolic","diagnoses","diagnosis","diagram","dial","diameter","diaper","diaphragm","diary","dice","dicing","dictate","dictation","dictator","difficult","diffused","diffuser","diffusion","diffusive","dig","dilation","diligence","diligent","dill","dilute","dime","diminish","dimly","dimmed","dimmer","dimness","dimple","diner","dingbat","dinghy","dinginess","dingo","dingy","dining","dinner","diocese","dioxide","diploma","dipped","dipper","dipping","directed","direction","directive","directly","directory","direness","dirtiness","disabled","disagree","disallow","disarm","disarray","disaster","disband","disbelief","disburse","discard","discern","discharge","disclose","discolor","discount","discourse","discover","discuss","disdain","disengage","disfigure","disgrace","dish","disinfect","disjoin","disk","dislike","disliking","dislocate","dislodge","disloyal","dismantle","dismay","dismiss","dismount","disobey","disorder","disown","disparate","disparity","dispatch","dispense","dispersal","dispersed","disperser","displace","display","displease","disposal","dispose","disprove","dispute","disregard","disrupt","dissuade","distance","distant","distaste","distill","distinct","distort","distract","distress","district","distrust","ditch","ditto","ditzy","dividable","divided","dividend","dividers","dividing","divinely","diving","divinity","divisible","divisibly","division","divisive","divorcee","dizziness","dizzy","doable","docile","dock","doctrine","document","dodge","dodgy","doily","doing","dole","dollar","dollhouse","dollop","dolly","dolphin","domain","domelike","domestic","dominion","dominoes","donated","donation","donator","donor","donut","doodle","doorbell","doorframe","doorknob","doorman","doormat","doornail","doorpost","doorstep","doorstop","doorway","doozy","dork","dormitory","dorsal","dosage","dose","dotted","doubling","douche","dove","down","dowry","doze","drab","dragging","dragonfly","dragonish","dragster","drainable","drainage","drained","drainer","drainpipe","dramatic","dramatize","drank","drapery","drastic","draw","dreaded","dreadful","dreadlock","dreamboat","dreamily","dreamland","dreamless","dreamlike","dreamt","dreamy","drearily","dreary","drench","dress","drew","dribble","dried","drier","drift","driller","drilling","drinkable","drinking","dripping","drippy","drivable","driven","driver","driveway","driving","drizzle","drizzly","drone","drool","droop","drop-down","dropbox","dropkick","droplet","dropout","dropper","drove","drown","drowsily","drudge","drum","dry","dubbed","dubiously","duchess","duckbill","ducking","duckling","ducktail","ducky","duct","dude","duffel","dugout","duh","duke","duller","dullness","duly","dumping","dumpling","dumpster","duo","dupe","duplex","duplicate","duplicity","durable","durably","duration","duress","during","dusk","dust","dutiful","duty","duvet","dwarf","dweeb","dwelled","dweller","dwelling","dwindle","dwindling","dynamic","dynamite","dynasty","dyslexia","dyslexic","each","eagle","earache","eardrum","earflap","earful","earlobe","early","earmark","earmuff","earphone","earpiece","earplugs","earring","earshot","earthen","earthlike","earthling","earthly","earthworm","earthy","earwig","easeful","easel","easiest","easily","easiness","easing","eastbound","eastcoast","easter","eastward","eatable","eaten","eatery","eating","eats","ebay","ebony","ebook","ecard","eccentric","echo","eclair","eclipse","ecologist","ecology","economic","economist","economy","ecosphere","ecosystem","edge","edginess","edging","edgy","edition","editor","educated","education","educator","eel","effective","effects","efficient","effort","eggbeater","egging","eggnog","eggplant","eggshell","egomaniac","egotism","egotistic","either","eject","elaborate","elastic","elated","elbow","eldercare","elderly","eldest","electable","election","elective","elephant","elevate","elevating","elevation","elevator","eleven","elf","eligible","eligibly","eliminate","elite","elitism","elixir","elk","ellipse","elliptic","elm","elongated","elope","eloquence","eloquent","elsewhere","elude","elusive","elves","email","embargo","embark","embassy","embattled","embellish","ember","embezzle","emblaze","emblem","embody","embolism","emboss","embroider","emcee","emerald","emergency","emission","emit","emote","emoticon","emotion","empathic","empathy","emperor","emphases","emphasis","emphasize","emphatic","empirical","employed","employee","employer","emporium","empower","emptier","emptiness","empty","emu","enable","enactment","enamel","enchanted","enchilada","encircle","enclose","enclosure","encode","encore","encounter","encourage","encroach","encrust","encrypt","endanger","endeared","endearing","ended","ending","endless","endnote","endocrine","endorphin","endorse","endowment","endpoint","endurable","endurance","enduring","energetic","energize","energy","enforced","enforcer","engaged","engaging","engine","engorge","engraved","engraver","engraving","engross","engulf","enhance","enigmatic","enjoyable","enjoyably","enjoyer","enjoying","enjoyment","enlarged","enlarging","enlighten","enlisted","enquirer","enrage","enrich","enroll","enslave","ensnare","ensure","entail","entangled","entering","entertain","enticing","entire","entitle","entity","entomb","entourage","entrap","entree","entrench","entrust","entryway","entwine","enunciate","envelope","enviable","enviably","envious","envision","envoy","envy","enzyme","epic","epidemic","epidermal","epidermis","epidural","epilepsy","epileptic","epilogue","epiphany","episode","equal","equate","equation","equator","equinox","equipment","equity","equivocal","eradicate","erasable","erased","eraser","erasure","ergonomic","errand","errant","erratic","error","erupt","escalate","escalator","escapable","escapade","escapist","escargot","eskimo","esophagus","espionage","espresso","esquire","essay","essence","essential","establish","estate","esteemed","estimate","estimator","estranged","estrogen","etching","eternal","eternity","ethanol","ether","ethically","ethics","euphemism","evacuate","evacuee","evade","evaluate","evaluator","evaporate","evasion","evasive","even","everglade","evergreen","everybody","everyday","everyone","evict","evidence","evident","evil","evoke","evolution","evolve","exact","exalted","example","excavate","excavator","exceeding","exception","excess","exchange","excitable","exciting","exclaim","exclude","excluding","exclusion","exclusive","excretion","excretory","excursion","excusable","excusably","excuse","exemplary","exemplify","exemption","exerciser","exert","exes","exfoliate","exhale","exhaust","exhume","exile","existing","exit","exodus","exonerate","exorcism","exorcist","expand","expanse","expansion","expansive","expectant","expedited","expediter","expel","expend","expenses","expensive","expert","expire","expiring","explain","expletive","explicit","explode","exploit","explore","exploring","exponent","exporter","exposable","expose","exposure","express","expulsion","exquisite","extended","extending","extent","extenuate","exterior","external","extinct","extortion","extradite","extras","extrovert","extrude","extruding","exuberant","fable","fabric","fabulous","facebook","facecloth","facedown","faceless","facelift","faceplate","faceted","facial","facility","facing","facsimile","faction","factoid","factor","factsheet","factual","faculty","fade","fading","failing","falcon","fall","FALSE","falsify","fame","familiar","family","famine","famished","fanatic","fancied","fanciness","fancy","fanfare","fang","fanning","fantasize","fantastic","fantasy","fascism","fastball","faster","fasting","fastness","faucet","favorable","favorably","favored","favoring","favorite","fax","feast","federal","fedora","feeble","feed","feel","feisty","feline","felt-tip","feminine","feminism","feminist","feminize","femur","fence","fencing","fender","ferment","fernlike","ferocious","ferocity","ferret","ferris","ferry","fervor","fester","festival","festive","festivity","fetal","fetch","fever","fiber","fiction","fiddle","fiddling","fidelity","fidgeting","fidgety","fifteen","fifth","fiftieth","fifty","figment","figure","figurine","filing","filled","filler","filling","film","filter","filth","filtrate","finale","finalist","finalize","finally","finance","financial","finch","fineness","finer","finicky","finished","finisher","finishing","finite","finless","finlike","fiscally","fit","five","flaccid","flagman","flagpole","flagship","flagstick","flagstone","flail","flakily","flaky","flame","flammable","flanked","flanking","flannels","flap","flaring","flashback","flashbulb","flashcard","flashily","flashing","flashy","flask","flatbed","flatfoot","flatly","flatness","flatten","flattered","flatterer","flattery","flattop","flatware","flatworm","flavored","flavorful","flavoring","flaxseed","fled","fleshed","fleshy","flick","flier","flight","flinch","fling","flint","flip","flirt","float","flock","flogging","flop","floral","florist","floss","flounder","flyable","flyaway","flyer","flying","flyover","flypaper","foam","foe","fog","foil","folic","folk","follicle","follow","fondling","fondly","fondness","fondue","font","food","fool","footage","football","footbath","footboard","footer","footgear","foothill","foothold","footing","footless","footman","footnote","footpad","footpath","footprint","footrest","footsie","footsore","footwear","footwork","fossil","foster","founder","founding","fountain","fox","foyer","fraction","fracture","fragile","fragility","fragment","fragrance","fragrant","frail","frame","framing","frantic","fraternal","frayed","fraying","frays","freckled","freckles","freebase","freebee","freebie","freedom","freefall","freehand","freeing","freeload","freely","freemason","freeness","freestyle","freeware","freeway","freewill","freezable","freezing","freight","french","frenzied","frenzy","frequency","frequent","fresh","fretful","fretted","friction","friday","fridge","fried","friend","frighten","frightful","frigidity","frigidly","frill","fringe","frisbee","frisk","fritter","frivolous","frolic","from","front","frostbite","frosted","frostily","frosting","frostlike","frosty","froth","frown","frozen","fructose","frugality","frugally","fruit","frustrate","frying","gab","gaffe","gag","gainfully","gaining","gains","gala","gallantly","galleria","gallery","galley","gallon","gallows","gallstone","galore","galvanize","gambling","game","gaming","gamma","gander","gangly","gangrene","gangway","gap","garage","garbage","garden","gargle","garland","garlic","garment","garnet","garnish","garter","gas","gatherer","gathering","gating","gauging","gauntlet","gauze","gave","gawk","gazing","gear","gecko","geek","geiger","gem","gender","generic","generous","genetics","genre","gentile","gentleman","gently","gents","geography","geologic","geologist","geology","geometric","geometry","geranium","gerbil","geriatric","germicide","germinate","germless","germproof","gestate","gestation","gesture","getaway","getting","getup","giant","gibberish","giblet","giddily","giddiness","giddy","gift","gigabyte","gigahertz","gigantic","giggle","giggling","giggly","gigolo","gilled","gills","gimmick","girdle","giveaway","given","giver","giving","gizmo","gizzard","glacial","glacier","glade","gladiator","gladly","glamorous","glamour","glance","glancing","glandular","glare","glaring","glass","glaucoma","glazing","gleaming","gleeful","glider","gliding","glimmer","glimpse","glisten","glitch","glitter","glitzy","gloater","gloating","gloomily","gloomy","glorified","glorifier","glorify","glorious","glory","gloss","glove","glowing","glowworm","glucose","glue","gluten","glutinous","glutton","gnarly","gnat","goal","goatskin","goes","goggles","going","goldfish","goldmine","goldsmith","golf","goliath","gonad","gondola","gone","gong","good","gooey","goofball","goofiness","goofy","google","goon","gopher","gore","gorged","gorgeous","gory","gosling","gossip","gothic","gotten","gout","gown","grab","graceful","graceless","gracious","gradation","graded","grader","gradient","grading","gradually","graduate","graffiti","grafted","grafting","grain","granddad","grandkid","grandly","grandma","grandpa","grandson","granite","granny","granola","grant","granular","grape","graph","grapple","grappling","grasp","grass","gratified","gratify","grating","gratitude","gratuity","gravel","graveness","graves","graveyard","gravitate","gravity","gravy","gray","grazing","greasily","greedily","greedless","greedy","green","greeter","greeting","grew","greyhound","grid","grief","grievance","grieving","grievous","grill","grimace","grimacing","grime","griminess","grimy","grinch","grinning","grip","gristle","grit","groggily","groggy","groin","groom","groove","grooving","groovy","grope","ground","grouped","grout","grove","grower","growing","growl","grub","grudge","grudging","grueling","gruffly","grumble","grumbling","grumbly","grumpily","grunge","grunt","guacamole","guidable","guidance","guide","guiding","guileless","guise","gulf","gullible","gully","gulp","gumball","gumdrop","gumminess","gumming","gummy","gurgle","gurgling","guru","gush","gusto","gusty","gutless","guts","gutter","guy","guzzler","gyration","habitable","habitant","habitat","habitual","hacked","hacker","hacking","hacksaw","had","haggler","haiku","half","halogen","halt","halved","halves","hamburger","hamlet","hammock","hamper","hamster","hamstring","handbag","handball","handbook","handbrake","handcart","handclap","handclasp","handcraft","handcuff","handed","handful","handgrip","handgun","handheld","handiness","handiwork","handlebar","handled","handler","handling","handmade","handoff","handpick","handprint","handrail","handsaw","handset","handsfree","handshake","handstand","handwash","handwork","handwoven","handwrite","handyman","hangnail","hangout","hangover","hangup","hankering","hankie","hanky","haphazard","happening","happier","happiest","happily","happiness","happy","harbor","hardcopy","hardcore","hardcover","harddisk","hardened","hardener","hardening","hardhat","hardhead","hardiness","hardly","hardness","hardship","hardware","hardwired","hardwood","hardy","harmful","harmless","harmonica","harmonics","harmonize","harmony","harness","harpist","harsh","harvest","hash","hassle","haste","hastily","hastiness","hasty","hatbox","hatchback","hatchery","hatchet","hatching","hatchling","hate","hatless","hatred","haunt","haven","hazard","hazelnut","hazily","haziness","hazing","hazy","headache","headband","headboard","headcount","headdress","headed","header","headfirst","headgear","heading","headlamp","headless","headlock","headphone","headpiece","headrest","headroom","headscarf","headset","headsman","headstand","headstone","headway","headwear","heap","heat","heave","heavily","heaviness","heaving","hedge","hedging","heftiness","hefty","helium","helmet","helper","helpful","helping","helpless","helpline","hemlock","hemstitch","hence","henchman","henna","herald","herbal","herbicide","herbs","heritage","hermit","heroics","heroism","herring","herself","hertz","hesitancy","hesitant","hesitate","hexagon","hexagram","hubcap","huddle","huddling","huff","hug","hula","hulk","hull","human","humble","humbling","humbly","humid","humiliate","humility","humming","hummus","humongous","humorist","humorless","humorous","humpback","humped","humvee","hunchback","hundredth","hunger","hungrily","hungry","hunk","hunter","hunting","huntress","huntsman","hurdle","hurled","hurler","hurling","hurray","hurricane","hurried","hurry","hurt","husband","hush","husked","huskiness","hut","hybrid","hydrant","hydrated","hydration","hydrogen","hydroxide","hyperlink","hypertext","hyphen","hypnoses","hypnosis","hypnotic","hypnotism","hypnotist","hypnotize","hypocrisy","hypocrite","ibuprofen","ice","iciness","icing","icky","icon","icy","idealism","idealist","idealize","ideally","idealness","identical","identify","identity","ideology","idiocy","idiom","idly","igloo","ignition","ignore","iguana","illicitly","illusion","illusive","image","imaginary","imagines","imaging","imbecile","imitate","imitation","immature","immerse","immersion","imminent","immobile","immodest","immorally","immortal","immovable","immovably","immunity","immunize","impaired","impale","impart","impatient","impeach","impeding","impending","imperfect","imperial","impish","implant","implement","implicate","implicit","implode","implosion","implosive","imply","impolite","important","importer","impose","imposing","impotence","impotency","impotent","impound","imprecise","imprint","imprison","impromptu","improper","improve","improving","improvise","imprudent","impulse","impulsive","impure","impurity","iodine","iodize","ion","ipad","iphone","ipod","irate","irk","iron","irregular","irrigate","irritable","irritably","irritant","irritate","islamic","islamist","isolated","isolating","isolation","isotope","issue","issuing","italicize","italics","item","itinerary","itunes","ivory","ivy","jab","jackal","jacket","jackknife","jackpot","jailbird","jailbreak","jailer","jailhouse","jalapeno","jam","janitor","january","jargon","jarring","jasmine","jaundice","jaunt","java","jawed","jawless","jawline","jaws","jaybird","jaywalker","jazz","jeep","jeeringly","jellied","jelly","jersey","jester","jet","jiffy","jigsaw","jimmy","jingle","jingling","jinx","jitters","jittery","job","jockey","jockstrap","jogger","jogging","john","joining","jokester","jokingly","jolliness","jolly","jolt","jot","jovial","joyfully","joylessly","joyous","joyride","joystick","jubilance","jubilant","judge","judgingly","judicial","judiciary","judo","juggle","juggling","jugular","juice","juiciness","juicy","jujitsu","jukebox","july","jumble","jumbo","jump","junction","juncture","june","junior","juniper","junkie","junkman","junkyard","jurist","juror","jury","justice","justifier","justify","justly","justness","juvenile","kabob","kangaroo","karaoke","karate","karma","kebab","keenly","keenness","keep","keg","kelp","kennel","kept","kerchief","kerosene","kettle","kick","kiln","kilobyte","kilogram","kilometer","kilowatt","kilt","kimono","kindle","kindling","kindly","kindness","kindred","kinetic","kinfolk","king","kinship","kinsman","kinswoman","kissable","kisser","kissing","kitchen","kite","kitten","kitty","kiwi","kleenex","knapsack","knee","knelt","knickers","knoll","koala","kooky","kosher","krypton","kudos","kung","labored","laborer","laboring","laborious","labrador","ladder","ladies","ladle","ladybug","ladylike","lagged","lagging","lagoon","lair","lake","lance","landed","landfall","landfill","landing","landlady","landless","landline","landlord","landmark","landmass","landmine","landowner","landscape","landside","landslide","language","lankiness","lanky","lantern","lapdog","lapel","lapped","lapping","laptop","lard","large","lark","lash","lasso","last","latch","late","lather","latitude","latrine","latter","latticed","launch","launder","laundry","laurel","lavender","lavish","laxative","lazily","laziness","lazy","lecturer","left","legacy","legal","legend","legged","leggings","legible","legibly","legislate","lego","legroom","legume","legwarmer","legwork","lemon","lend","length","lens","lent","leotard","lesser","letdown","lethargic","lethargy","letter","lettuce","level","leverage","levers","levitate","levitator","liability","liable","liberty","librarian","library","licking","licorice","lid","life","lifter","lifting","liftoff","ligament","likely","likeness","likewise","liking","lilac","lilly","lily","limb","limeade","limelight","limes","limit","limping","limpness","line","lingo","linguini","linguist","lining","linked","linoleum","linseed","lint","lion","lip","liquefy","liqueur","liquid","lisp","list","litigate","litigator","litmus","litter","little","livable","lived","lively","liver","livestock","lividly","living","lizard","lubricant","lubricate","lucid","luckily","luckiness","luckless","lucrative","ludicrous","lugged","lukewarm","lullaby","lumber","luminance","luminous","lumpiness","lumping","lumpish","lunacy","lunar","lunchbox","luncheon","lunchroom","lunchtime","lung","lurch","lure","luridness","lurk","lushly","lushness","luster","lustfully","lustily","lustiness","lustrous","lusty","luxurious","luxury","lying","lyrically","lyricism","lyricist","lyrics","macarena","macaroni","macaw","mace","machine","machinist","magazine","magenta","maggot","magical","magician","magma","magnesium","magnetic","magnetism","magnetize","magnifier","magnify","magnitude","magnolia","mahogany","maimed","majestic","majesty","majorette","majority","makeover","maker","makeshift","making","malformed","malt","mama","mammal","mammary","mammogram","manager","managing","manatee","mandarin","mandate","mandatory","mandolin","manger","mangle","mango","mangy","manhandle","manhole","manhood","manhunt","manicotti","manicure","manifesto","manila","mankind","manlike","manliness","manly","manmade","manned","mannish","manor","manpower","mantis","mantra","manual","many","map","marathon","marauding","marbled","marbles","marbling","march","mardi","margarine","margarita","margin","marigold","marina","marine","marital","maritime","marlin","marmalade","maroon","married","marrow","marry","marshland","marshy","marsupial","marvelous","marxism","mascot","masculine","mashed","mashing","massager","masses","massive","mastiff","matador","matchbook","matchbox","matcher","matching","matchless","material","maternal","maternity","math","mating","matriarch","matrimony","matrix","matron","matted","matter","maturely","maturing","maturity","mauve","maverick","maximize","maximum","maybe","mayday","mayflower","moaner","moaning","mobile","mobility","mobilize","mobster","mocha","mocker","mockup","modified","modify","modular","modulator","module","moisten","moistness","moisture","molar","molasses","mold","molecular","molecule","molehill","mollusk","mom","monastery","monday","monetary","monetize","moneybags","moneyless","moneywise","mongoose","mongrel","monitor","monkhood","monogamy","monogram","monologue","monopoly","monorail","monotone","monotype","monoxide","monsieur","monsoon","monstrous","monthly","monument","moocher","moodiness","moody","mooing","moonbeam","mooned","moonlight","moonlike","moonlit","moonrise","moonscape","moonshine","moonstone","moonwalk","mop","morale","morality","morally","morbidity","morbidly","morphine","morphing","morse","mortality","mortally","mortician","mortified","mortify","mortuary","mosaic","mossy","most","mothball","mothproof","motion","motivate","motivator","motive","motocross","motor","motto","mountable","mountain","mounted","mounting","mourner","mournful","mouse","mousiness","moustache","mousy","mouth","movable","move","movie","moving","mower","mowing","much","muck","mud","mug","mulberry","mulch","mule","mulled","mullets","multiple","multiply","multitask","multitude","mumble","mumbling","mumbo","mummified","mummify","mummy","mumps","munchkin","mundane","municipal","muppet","mural","murkiness","murky","murmuring","muscular","museum","mushily","mushiness","mushroom","mushy","music","musket","muskiness","musky","mustang","mustard","muster","mustiness","musty","mutable","mutate","mutation","mute","mutilated","mutilator","mutiny","mutt","mutual","muzzle","myself","myspace","mystified","mystify","myth","nacho","nag","nail","name","naming","nanny","nanometer","nape","napkin","napped","napping","nappy","narrow","nastily","nastiness","national","native","nativity","natural","nature","naturist","nautical","navigate","navigator","navy","nearby","nearest","nearly","nearness","neatly","neatness","nebula","nebulizer","nectar","negate","negation","negative","neglector","negligee","negligent","negotiate","nemeses","nemesis","neon","nephew","nerd","nervous","nervy","nest","net","neurology","neuron","neurosis","neurotic","neuter","neutron","never","next","nibble","nickname","nicotine","niece","nifty","nimble","nimbly","nineteen","ninetieth","ninja","nintendo","ninth","nuclear","nuclei","nucleus","nugget","nullify","number","numbing","numbly","numbness","numeral","numerate","numerator","numeric","numerous","nuptials","nursery","nursing","nurture","nutcase","nutlike","nutmeg","nutrient","nutshell","nuttiness","nutty","nuzzle","nylon","oaf","oak","oasis","oat","obedience","obedient","obituary","object","obligate","obliged","oblivion","oblivious","oblong","obnoxious","oboe","obscure","obscurity","observant","observer","observing","obsessed","obsession","obsessive","obsolete","obstacle","obstinate","obstruct","obtain","obtrusive","obtuse","obvious","occultist","occupancy","occupant","occupier","occupy","ocean","ocelot","octagon","octane","october","octopus","ogle","oil","oink","ointment","okay","old","olive","olympics","omega","omen","ominous","omission","omit","omnivore","onboard","oncoming","ongoing","onion","online","onlooker","only","onscreen","onset","onshore","onslaught","onstage","onto","onward","onyx","oops","ooze","oozy","opacity","opal","open","operable","operate","operating","operation","operative","operator","opium","opossum","opponent","oppose","opposing","opposite","oppressed","oppressor","opt","opulently","osmosis","other","otter","ouch","ought","ounce","outage","outback","outbid","outboard","outbound","outbreak","outburst","outcast","outclass","outcome","outdated","outdoors","outer","outfield","outfit","outflank","outgoing","outgrow","outhouse","outing","outlast","outlet","outline","outlook","outlying","outmatch","outmost","outnumber","outplayed","outpost","outpour","output","outrage","outrank","outreach","outright","outscore","outsell","outshine","outshoot","outsider","outskirts","outsmart","outsource","outspoken","outtakes","outthink","outward","outweigh","outwit","oval","ovary","oven","overact","overall","overarch","overbid","overbill","overbite","overblown","overboard","overbook","overbuilt","overcast","overcoat","overcome","overcook","overcrowd","overdraft","overdrawn","overdress","overdrive","overdue","overeager","overeater","overexert","overfed","overfeed","overfill","overflow","overfull","overgrown","overhand","overhang","overhaul","overhead","overhear","overheat","overhung","overjoyed","overkill","overlabor","overlaid","overlap","overlay","overload","overlook","overlord","overlying","overnight","overpass","overpay","overplant","overplay","overpower","overprice","overrate","overreach","overreact","override","overripe","overrule","overrun","overshoot","overshot","oversight","oversized","oversleep","oversold","overspend","overstate","overstay","overstep","overstock","overstuff","oversweet","overtake","overthrow","overtime","overtly","overtone","overture","overturn","overuse","overvalue","overview","overwrite","owl","oxford","oxidant","oxidation","oxidize","oxidizing","oxygen","oxymoron","oyster","ozone","paced","pacemaker","pacific","pacifier","pacifism","pacifist","pacify","padded","padding","paddle","paddling","padlock","pagan","pager","paging","pajamas","palace","palatable","palm","palpable","palpitate","paltry","pampered","pamperer","pampers","pamphlet","panama","pancake","pancreas","panda","pandemic","pang","panhandle","panic","panning","panorama","panoramic","panther","pantomime","pantry","pants","pantyhose","paparazzi","papaya","paper","paprika","papyrus","parabola","parachute","parade","paradox","paragraph","parakeet","paralegal","paralyses","paralysis","paralyze","paramedic","parameter","paramount","parasail","parasite","parasitic","parcel","parched","parchment","pardon","parish","parka","parking","parkway","parlor","parmesan","parole","parrot","parsley","parsnip","partake","parted","parting","partition","partly","partner","partridge","party","passable","passably","passage","passcode","passenger","passerby","passing","passion","passive","passivism","passover","passport","password","pasta","pasted","pastel","pastime","pastor","pastrami","pasture","pasty","patchwork","patchy","paternal","paternity","path","patience","patient","patio","patriarch","patriot","patrol","patronage","patronize","pauper","pavement","paver","pavestone","pavilion","paving","pawing","payable","payback","paycheck","payday","payee","payer","paying","payment","payphone","payroll","pebble","pebbly","pecan","pectin","peculiar","peddling","pediatric","pedicure","pedigree","pedometer","pegboard","pelican","pellet","pelt","pelvis","penalize","penalty","pencil","pendant","pending","penholder","penknife","pennant","penniless","penny","penpal","pension","pentagon","pentagram","pep","perceive","percent","perch","percolate","perennial","perfected","perfectly","perfume","periscope","perish","perjurer","perjury","perkiness","perky","perm","peroxide","perpetual","perplexed","persecute","persevere","persuaded","persuader","pesky","peso","pessimism","pessimist","pester","pesticide","petal","petite","petition","petri","petroleum","petted","petticoat","pettiness","petty","petunia","phantom","phobia","phoenix","phonebook","phoney","phonics","phoniness","phony","phosphate","photo","phrase","phrasing","placard","placate","placidly","plank","planner","plant","plasma","plaster","plastic","plated","platform","plating","platinum","platonic","platter","platypus","plausible","plausibly","playable","playback","player","playful","playgroup","playhouse","playing","playlist","playmaker","playmate","playoff","playpen","playroom","playset","plaything","playtime","plaza","pleading","pleat","pledge","plentiful","plenty","plethora","plexiglas","pliable","plod","plop","plot","plow","ploy","pluck","plug","plunder","plunging","plural","plus","plutonium","plywood","poach","pod","poem","poet","pogo","pointed","pointer","pointing","pointless","pointy","poise","poison","poker","poking","polar","police","policy","polio","polish","politely","polka","polo","polyester","polygon","polygraph","polymer","poncho","pond","pony","popcorn","pope","poplar","popper","poppy","popsicle","populace","popular","populate","porcupine","pork","porous","porridge","portable","portal","portfolio","porthole","portion","portly","portside","poser","posh","posing","possible","possibly","possum","postage","postal","postbox","postcard","posted","poster","posting","postnasal","posture","postwar","pouch","pounce","pouncing","pound","pouring","pout","powdered","powdering","powdery","power","powwow","pox","praising","prance","prancing","pranker","prankish","prankster","prayer","praying","preacher","preaching","preachy","preamble","precinct","precise","precision","precook","precut","predator","predefine","predict","preface","prefix","preflight","preformed","pregame","pregnancy","pregnant","preheated","prelaunch","prelaw","prelude","premiere","premises","premium","prenatal","preoccupy","preorder","prepaid","prepay","preplan","preppy","preschool","prescribe","preseason","preset","preshow","president","presoak","press","presume","presuming","preteen","pretended","pretender","pretense","pretext","pretty","pretzel","prevail","prevalent","prevent","preview","previous","prewar","prewashed","prideful","pried","primal","primarily","primary","primate","primer","primp","princess","print","prior","prism","prison","prissy","pristine","privacy","private","privatize","prize","proactive","probable","probably","probation","probe","probing","probiotic","problem","procedure","process","proclaim","procreate","procurer","prodigal","prodigy","produce","product","profane","profanity","professed","professor","profile","profound","profusely","progeny","prognosis","program","progress","projector","prologue","prolonged","promenade","prominent","promoter","promotion","prompter","promptly","prone","prong","pronounce","pronto","proofing","proofread","proofs","propeller","properly","property","proponent","proposal","propose","props","prorate","protector","protegee","proton","prototype","protozoan","protract","protrude","proud","provable","proved","proven","provided","provider","providing","province","proving","provoke","provoking","provolone","prowess","prowler","prowling","proximity","proxy","prozac","prude","prudishly","prune","pruning","pry","psychic","public","publisher","pucker","pueblo","pug","pull","pulmonary","pulp","pulsate","pulse","pulverize","puma","pumice","pummel","punch","punctual","punctuate","punctured","pungent","punisher","punk","pupil","puppet","puppy","purchase","pureblood","purebred","purely","pureness","purgatory","purge","purging","purifier","purify","purist","puritan","purity","purple","purplish","purposely","purr","purse","pursuable","pursuant","pursuit","purveyor","pushcart","pushchair","pusher","pushiness","pushing","pushover","pushpin","pushup","pushy","putdown","putt","puzzle","puzzling","pyramid","pyromania","python","quack","quadrant","quail","quaintly","quake","quaking","qualified","qualifier","qualify","quality","qualm","quantum","quarrel","quarry","quartered","quarterly","quarters","quartet","quench","query","quicken","quickly","quickness","quicksand","quickstep","quiet","quill","quilt","quintet","quintuple","quirk","quit","quiver","quizzical","quotable","quotation","quote","rabid","race","racing","racism","rack","racoon","radar","radial","radiance","radiantly","radiated","radiation","radiator","radio","radish","raffle","raft","rage","ragged","raging","ragweed","raider","railcar","railing","railroad","railway","raisin","rake","raking","rally","ramble","rambling","ramp","ramrod","ranch","rancidity","random","ranged","ranger","ranging","ranked","ranking","ransack","ranting","rants","rare","rarity","rascal","rash","rasping","ravage","raven","ravine","raving","ravioli","ravishing","reabsorb","reach","reacquire","reaction","reactive","reactor","reaffirm","ream","reanalyze","reappear","reapply","reappoint","reapprove","rearrange","rearview","reason","reassign","reassure","reattach","reawake","rebalance","rebate","rebel","rebirth","reboot","reborn","rebound","rebuff","rebuild","rebuilt","reburial","rebuttal","recall","recant","recapture","recast","recede","recent","recess","recharger","recipient","recital","recite","reckless","reclaim","recliner","reclining","recluse","reclusive","recognize","recoil","recollect","recolor","reconcile","reconfirm","reconvene","recopy","record","recount","recoup","recovery","recreate","rectal","rectangle","rectified","rectify","recycled","recycler","recycling","reemerge","reenact","reenter","reentry","reexamine","referable","referee","reference","refill","refinance","refined","refinery","refining","refinish","reflected","reflector","reflex","reflux","refocus","refold","reforest","reformat","reformed","reformer","reformist","refract","refrain","refreeze","refresh","refried","refueling","refund","refurbish","refurnish","refusal","refuse","refusing","refutable","refute","regain","regalia","regally","reggae","regime","region","register","registrar","registry","regress","regretful","regroup","regular","regulate","regulator","rehab","reheat","rehire","rehydrate","reimburse","reissue","reiterate","rejoice","rejoicing","rejoin","rekindle","relapse","relapsing","relatable","related","relation","relative","relax","relay","relearn","release","relenting","reliable","reliably","reliance","reliant","relic","relieve","relieving","relight","relish","relive","reload","relocate","relock","reluctant","rely","remake","remark","remarry","rematch","remedial","remedy","remember","reminder","remindful","remission","remix","remnant","remodeler","remold","remorse","remote","removable","removal","removed","remover","removing","rename","renderer","rendering","rendition","renegade","renewable","renewably","renewal","renewed","renounce","renovate","renovator","rentable","rental","rented","renter","reoccupy","reoccur","reopen","reorder","repackage","repacking","repaint","repair","repave","repaying","repayment","repeal","repeated","repeater","repent","rephrase","replace","replay","replica","reply","reporter","repose","repossess","repost","repressed","reprimand","reprint","reprise","reproach","reprocess","reproduce","reprogram","reps","reptile","reptilian","repugnant","repulsion","repulsive","repurpose","reputable","reputably","request","require","requisite","reroute","rerun","resale","resample","rescuer","reseal","research","reselect","reseller","resemble","resend","resent","reset","reshape","reshoot","reshuffle","residence","residency","resident","residual","residue","resigned","resilient","resistant","resisting","resize","resolute","resolved","resonant","resonate","resort","resource","respect","resubmit","result","resume","resupply","resurface","resurrect","retail","retainer","retaining","retake","retaliate","retention","rethink","retinal","retired","retiree","retiring","retold","retool","retorted","retouch","retrace","retract","retrain","retread","retreat","retrial","retrieval","retriever","retry","return","retying","retype","reunion","reunite","reusable","reuse","reveal","reveler","revenge","revenue","reverb","revered","reverence","reverend","reversal","reverse","reversing","reversion","revert","revisable","revise","revision","revisit","revivable","revival","reviver","reviving","revocable","revoke","revolt","revolver","revolving","reward","rewash","rewind","rewire","reword","rework","rewrap","rewrite","rhyme","ribbon","ribcage","rice","riches","richly","richness","rickety","ricotta","riddance","ridden","ride","riding","rifling","rift","rigging","rigid","rigor","rimless","rimmed","rind","rink","rinse","rinsing","riot","ripcord","ripeness","ripening","ripping","ripple","rippling","riptide","rise","rising","risk","risotto","ritalin","ritzy","rival","riverbank","riverbed","riverboat","riverside","riveter","riveting","roamer","roaming","roast","robbing","robe","robin","robotics","robust","rockband","rocker","rocket","rockfish","rockiness","rocking","rocklike","rockslide","rockstar","rocky","rogue","roman","romp","rope","roping","roster","rosy","rotten","rotting","rotunda","roulette","rounding","roundish","roundness","roundup","roundworm","routine","routing","rover","roving","royal","rubbed","rubber","rubbing","rubble","rubdown","ruby","ruckus","rudder","rug","ruined","rule","rumble","rumbling","rummage","rumor","runaround","rundown","runner","running","runny","runt","runway","rupture","rural","ruse","rush","rust","rut","sabbath","sabotage","sacrament","sacred","sacrifice","sadden","saddlebag","saddled","saddling","sadly","sadness","safari","safeguard","safehouse","safely","safeness","saffron","saga","sage","sagging","saggy","said","saint","sake","salad","salami","salaried","salary","saline","salon","saloon","salsa","salt","salutary","salute","salvage","salvaging","salvation","same","sample","sampling","sanction","sanctity","sanctuary","sandal","sandbag","sandbank","sandbar","sandblast","sandbox","sanded","sandfish","sanding","sandlot","sandpaper","sandpit","sandstone","sandstorm","sandworm","sandy","sanitary","sanitizer","sank","santa","sapling","sappiness","sappy","sarcasm","sarcastic","sardine","sash","sasquatch","sassy","satchel","satiable","satin","satirical","satisfied","satisfy","saturate","saturday","sauciness","saucy","sauna","savage","savanna","saved","savings","savior","savor","saxophone","say","scabbed","scabby","scalded","scalding","scale","scaling","scallion","scallop","scalping","scam","scandal","scanner","scanning","scant","scapegoat","scarce","scarcity","scarecrow","scared","scarf","scarily","scariness","scarring","scary","scavenger","scenic","schedule","schematic","scheme","scheming","schilling","schnapps","scholar","science","scientist","scion","scoff","scolding","scone","scoop","scooter","scope","scorch","scorebook","scorecard","scored","scoreless","scorer","scoring","scorn","scorpion","scotch","scoundrel","scoured","scouring","scouting","scouts","scowling","scrabble","scraggly","scrambled","scrambler","scrap","scratch","scrawny","screen","scribble","scribe","scribing","scrimmage","script","scroll","scrooge","scrounger","scrubbed","scrubber","scruffy","scrunch","scrutiny","scuba","scuff","sculptor","sculpture","scurvy","scuttle","secluded","secluding","seclusion","second","secrecy","secret","sectional","sector","secular","securely","security","sedan","sedate","sedation","sedative","sediment","seduce","seducing","segment","seismic","seizing","seldom","selected","selection","selective","selector","self","seltzer","semantic","semester","semicolon","semifinal","seminar","semisoft","semisweet","senate","senator","send","senior","senorita","sensation","sensitive","sensitize","sensually","sensuous","sepia","september","septic","septum","sequel","sequence","sequester","series","sermon","serotonin","serpent","serrated","serve","service","serving","sesame","sessions","setback","setting","settle","settling","setup","sevenfold","seventeen","seventh","seventy","severity","shabby","shack","shaded","shadily","shadiness","shading","shadow","shady","shaft","shakable","shakily","shakiness","shaking","shaky","shale","shallot","shallow","shame","shampoo","shamrock","shank","shanty","shape","shaping","share","sharpener","sharper","sharpie","sharply","sharpness","shawl","sheath","shed","sheep","sheet","shelf","shell","shelter","shelve","shelving","sherry","shield","shifter","shifting","shiftless","shifty","shimmer","shimmy","shindig","shine","shingle","shininess","shining","shiny","ship","shirt","shivering","shock","shone","shoplift","shopper","shopping","shoptalk","shore","shortage","shortcake","shortcut","shorten","shorter","shorthand","shortlist","shortly","shortness","shorts","shortwave","shorty","shout","shove","showbiz","showcase","showdown","shower","showgirl","showing","showman","shown","showoff","showpiece","showplace","showroom","showy","shrank","shrapnel","shredder","shredding","shrewdly","shriek","shrill","shrimp","shrine","shrink","shrivel","shrouded","shrubbery","shrubs","shrug","shrunk","shucking","shudder","shuffle","shuffling","shun","shush","shut","shy","siamese","siberian","sibling","siding","sierra","siesta","sift","sighing","silenced","silencer","silent","silica","silicon","silk","silliness","silly","silo","silt","silver","similarly","simile","simmering","simple","simplify","simply","sincere","sincerity","singer","singing","single","singular","sinister","sinless","sinner","sinuous","sip","siren","sister","sitcom","sitter","sitting","situated","situation","sixfold","sixteen","sixth","sixties","sixtieth","sixtyfold","sizable","sizably","size","sizing","sizzle","sizzling","skater","skating","skedaddle","skeletal","skeleton","skeptic","sketch","skewed","skewer","skid","skied","skier","skies","skiing","skilled","skillet","skillful","skimmed","skimmer","skimming","skimpily","skincare","skinhead","skinless","skinning","skinny","skintight","skipper","skipping","skirmish","skirt","skittle","skydiver","skylight","skyline","skype","skyrocket","skyward","slab","slacked","slacker","slacking","slackness","slacks","slain","slam","slander","slang","slapping","slapstick","slashed","slashing","slate","slather","slaw","sled","sleek","sleep","sleet","sleeve","slept","sliceable","sliced","slicer","slicing","slick","slider","slideshow","sliding","slighted","slighting","slightly","slimness","slimy","slinging","slingshot","slinky","slip","slit","sliver","slobbery","slogan","sloped","sloping","sloppily","sloppy","slot","slouching","slouchy","sludge","slug","slum","slurp","slush","sly","small","smartly","smartness","smasher","smashing","smashup","smell","smelting","smile","smilingly","smirk","smite","smith","smitten","smock","smog","smoked","smokeless","smokiness","smoking","smoky","smolder","smooth","smother","smudge","smudgy","smuggler","smuggling","smugly","smugness","snack","snagged","snaking","snap","snare","snarl","snazzy","sneak","sneer","sneeze","sneezing","snide","sniff","snippet","snipping","snitch","snooper","snooze","snore","snoring","snorkel","snort","snout","snowbird","snowboard","snowbound","snowcap","snowdrift","snowdrop","snowfall","snowfield","snowflake","snowiness","snowless","snowman","snowplow","snowshoe","snowstorm","snowsuit","snowy","snub","snuff","snuggle","snugly","snugness","speak","spearfish","spearhead","spearman","spearmint","species","specimen","specked","speckled","specks","spectacle","spectator","spectrum","speculate","speech","speed","spellbind","speller","spelling","spendable","spender","spending","spent","spew","sphere","spherical","sphinx","spider","spied","spiffy","spill","spilt","spinach","spinal","spindle","spinner","spinning","spinout","spinster","spiny","spiral","spirited","spiritism","spirits","spiritual","splashed","splashing","splashy","splatter","spleen","splendid","splendor","splice","splicing","splinter","splotchy","splurge","spoilage","spoiled","spoiler","spoiling","spoils","spoken","spokesman","sponge","spongy","sponsor","spoof","spookily","spooky","spool","spoon","spore","sporting","sports","sporty","spotless","spotlight","spotted","spotter","spotting","spotty","spousal","spouse","spout","sprain","sprang","sprawl","spray","spree","sprig","spring","sprinkled","sprinkler","sprint","sprite","sprout","spruce","sprung","spry","spud","spur","sputter","spyglass","squabble","squad","squall","squander","squash","squatted","squatter","squatting","squeak","squealer","squealing","squeamish","squeegee","squeeze","squeezing","squid","squiggle","squiggly","squint","squire","squirt","squishier","squishy","stability","stabilize","stable","stack","stadium","staff","stage","staging","stagnant","stagnate","stainable","stained","staining","stainless","stalemate","staleness","stalling","stallion","stamina","stammer","stamp","stand","stank","staple","stapling","starboard","starch","stardom","stardust","starfish","stargazer","staring","stark","starless","starlet","starlight","starlit","starring","starry","starship","starter","starting","startle","startling","startup","starved","starving","stash","state","static","statistic","statue","stature","status","statute","statutory","staunch","stays","steadfast","steadier","steadily","steadying","steam","steed","steep","steerable","steering","steersman","stegosaur","stellar","stem","stench","stencil","step","stereo","sterile","sterility","sterilize","sterling","sternness","sternum","stew","stick","stiffen","stiffly","stiffness","stifle","stifling","stillness","stilt","stimulant","stimulate","stimuli","stimulus","stinger","stingily","stinging","stingray","stingy","stinking","stinky","stipend","stipulate","stir","stitch","stock","stoic","stoke","stole","stomp","stonewall","stoneware","stonework","stoning","stony","stood","stooge","stool","stoop","stoplight","stoppable","stoppage","stopped","stopper","stopping","stopwatch","storable","storage","storeroom","storewide","storm","stout","stove","stowaway","stowing","straddle","straggler","strained","strainer","straining","strangely","stranger","strangle","strategic","strategy","stratus","straw","stray","streak","stream","street","strength","strenuous","strep","stress","stretch","strewn","stricken","strict","stride","strife","strike","striking","strive","striving","strobe","strode","stroller","strongbox","strongly","strongman","struck","structure","strudel","struggle","strum","strung","strut","stubbed","stubble","stubbly","stubborn","stucco","stuck","student","studied","studio","study","stuffed","stuffing","stuffy","stumble","stumbling","stump","stung","stunned","stunner","stunning","stunt","stupor","sturdily","sturdy","styling","stylishly","stylist","stylized","stylus","suave","subarctic","subatomic","subdivide","subdued","subduing","subfloor","subgroup","subheader","subject","sublease","sublet","sublevel","sublime","submarine","submerge","submersed","submitter","subpanel","subpar","subplot","subprime","subscribe","subscript","subsector","subside","subsiding","subsidize","subsidy","subsoil","subsonic","substance","subsystem","subtext","subtitle","subtly","subtotal","subtract","subtype","suburb","subway","subwoofer","subzero","succulent","such","suction","sudden","sudoku","suds","sufferer","suffering","suffice","suffix","suffocate","suffrage","sugar","suggest","suing","suitable","suitably","suitcase","suitor","sulfate","sulfide","sulfite","sulfur","sulk","sullen","sulphate","sulphuric","sultry","superbowl","superglue","superhero","superior","superjet","superman","supermom","supernova","supervise","supper","supplier","supply","support","supremacy","supreme","surcharge","surely","sureness","surface","surfacing","surfboard","surfer","surgery","surgical","surging","surname","surpass","surplus","surprise","surreal","surrender","surrogate","surround","survey","survival","survive","surviving","survivor","sushi","suspect","suspend","suspense","sustained","sustainer","swab","swaddling","swagger","swampland","swan","swapping","swarm","sway","swear","sweat","sweep","swell","swept","swerve","swifter","swiftly","swiftness","swimmable","swimmer","swimming","swimsuit","swimwear","swinger","swinging","swipe","swirl","switch","swivel","swizzle","swooned","swoop","swoosh","swore","sworn","swung","sycamore","sympathy","symphonic","symphony","symptom","synapse","syndrome","synergy","synopses","synopsis","synthesis","synthetic","syrup","system","t-shirt","tabasco","tabby","tableful","tables","tablet","tableware","tabloid","tackiness","tacking","tackle","tackling","tacky","taco","tactful","tactical","tactics","tactile","tactless","tadpole","taekwondo","tag","tainted","take","taking","talcum","talisman","tall","talon","tamale","tameness","tamer","tamper","tank","tanned","tannery","tanning","tantrum","tapeless","tapered","tapering","tapestry","tapioca","tapping","taps","tarantula","target","tarmac","tarnish","tarot","tartar","tartly","tartness","task","tassel","taste","tastiness","tasting","tasty","tattered","tattle","tattling","tattoo","taunt","tavern","thank","that","thaw","theater","theatrics","thee","theft","theme","theology","theorize","thermal","thermos","thesaurus","these","thesis","thespian","thicken","thicket","thickness","thieving","thievish","thigh","thimble","thing","think","thinly","thinner","thinness","thinning","thirstily","thirsting","thirsty","thirteen","thirty","thong","thorn","those","thousand","thrash","thread","threaten","threefold","thrift","thrill","thrive","thriving","throat","throbbing","throng","throttle","throwaway","throwback","thrower","throwing","thud","thumb","thumping","thursday","thus","thwarting","thyself","tiara","tibia","tidal","tidbit","tidiness","tidings","tidy","tiger","tighten","tightly","tightness","tightrope","tightwad","tigress","tile","tiling","till","tilt","timid","timing","timothy","tinderbox","tinfoil","tingle","tingling","tingly","tinker","tinkling","tinsel","tinsmith","tint","tinwork","tiny","tipoff","tipped","tipper","tipping","tiptoeing","tiptop","tiring","tissue","trace","tracing","track","traction","tractor","trade","trading","tradition","traffic","tragedy","trailing","trailside","train","traitor","trance","tranquil","transfer","transform","translate","transpire","transport","transpose","trapdoor","trapeze","trapezoid","trapped","trapper","trapping","traps","trash","travel","traverse","travesty","tray","treachery","treading","treadmill","treason","treat","treble","tree","trekker","tremble","trembling","tremor","trench","trend","trespass","triage","trial","triangle","tribesman","tribunal","tribune","tributary","tribute","triceps","trickery","trickily","tricking","trickle","trickster","tricky","tricolor","tricycle","trident","tried","trifle","trifocals","trillion","trilogy","trimester","trimmer","trimming","trimness","trinity","trio","tripod","tripping","triumph","trivial","trodden","trolling","trombone","trophy","tropical","tropics","trouble","troubling","trough","trousers","trout","trowel","truce","truck","truffle","trump","trunks","trustable","trustee","trustful","trusting","trustless","truth","try","tubby","tubeless","tubular","tucking","tuesday","tug","tuition","tulip","tumble","tumbling","tummy","turban","turbine","turbofan","turbojet","turbulent","turf","turkey","turmoil","turret","turtle","tusk","tutor","tutu","tux","tweak","tweed","tweet","tweezers","twelve","twentieth","twenty","twerp","twice","twiddle","twiddling","twig","twilight","twine","twins","twirl","twistable","twisted","twister","twisting","twisty","twitch","twitter","tycoon","tying","tyke","udder","ultimate","ultimatum","ultra","umbilical","umbrella","umpire","unabashed","unable","unadorned","unadvised","unafraid","unaired","unaligned","unaltered","unarmored","unashamed","unaudited","unawake","unaware","unbaked","unbalance","unbeaten","unbend","unbent","unbiased","unbitten","unblended","unblessed","unblock","unbolted","unbounded","unboxed","unbraided","unbridle","unbroken","unbuckled","unbundle","unburned","unbutton","uncanny","uncapped","uncaring","uncertain","unchain","unchanged","uncharted","uncheck","uncivil","unclad","unclaimed","unclamped","unclasp","uncle","unclip","uncloak","unclog","unclothed","uncoated","uncoiled","uncolored","uncombed","uncommon","uncooked","uncork","uncorrupt","uncounted","uncouple","uncouth","uncover","uncross","uncrown","uncrushed","uncured","uncurious","uncurled","uncut","undamaged","undated","undaunted","undead","undecided","undefined","underage","underarm","undercoat","undercook","undercut","underdog","underdone","underfed","underfeed","underfoot","undergo","undergrad","underhand","underline","underling","undermine","undermost","underpaid","underpass","underpay","underrate","undertake","undertone","undertook","undertow","underuse","underwear","underwent","underwire","undesired","undiluted","undivided","undocked","undoing","undone","undrafted","undress","undrilled","undusted","undying","unearned","unearth","unease","uneasily","uneasy","uneatable","uneaten","unedited","unelected","unending","unengaged","unenvied","unequal","unethical","uneven","unexpired","unexposed","unfailing","unfair","unfasten","unfazed","unfeeling","unfiled","unfilled","unfitted","unfitting","unfixable","unfixed","unflawed","unfocused","unfold","unfounded","unframed","unfreeze","unfrosted","unfrozen","unfunded","unglazed","ungloved","unglue","ungodly","ungraded","ungreased","unguarded","unguided","unhappily","unhappy","unharmed","unhealthy","unheard","unhearing","unheated","unhelpful","unhidden","unhinge","unhitched","unholy","unhook","unicorn","unicycle","unified","unifier","uniformed","uniformly","unify","unimpeded","uninjured","uninstall","uninsured","uninvited","union","uniquely","unisexual","unison","unissued","unit","universal","universe","unjustly","unkempt","unkind","unknotted","unknowing","unknown","unlaced","unlatch","unlawful","unleaded","unlearned","unleash","unless","unleveled","unlighted","unlikable","unlimited","unlined","unlinked","unlisted","unlit","unlivable","unloaded","unloader","unlocked","unlocking","unlovable","unloved","unlovely","unloving","unluckily","unlucky","unmade","unmanaged","unmanned","unmapped","unmarked","unmasked","unmasking","unmatched","unmindful","unmixable","unmixed","unmolded","unmoral","unmovable","unmoved","unmoving","unnamable","unnamed","unnatural","unneeded","unnerve","unnerving","unnoticed","unopened","unopposed","unpack","unpadded","unpaid","unpainted","unpaired","unpaved","unpeeled","unpicked","unpiloted","unpinned","unplanned","unplanted","unpleased","unpledged","unplowed","unplug","unpopular","unproven","unquote","unranked","unrated","unraveled","unreached","unread","unreal","unreeling","unrefined","unrelated","unrented","unrest","unretired","unrevised","unrigged","unripe","unrivaled","unroasted","unrobed","unroll","unruffled","unruly","unrushed","unsaddle","unsafe","unsaid","unsalted","unsaved","unsavory","unscathed","unscented","unscrew","unsealed","unseated","unsecured","unseeing","unseemly","unseen","unselect","unselfish","unsent","unsettled","unshackle","unshaken","unshaved","unshaven","unsheathe","unshipped","unsightly","unsigned","unskilled","unsliced","unsmooth","unsnap","unsocial","unsoiled","unsold","unsolved","unsorted","unspoiled","unspoken","unstable","unstaffed","unstamped","unsteady","unsterile","unstirred","unstitch","unstopped","unstuck","unstuffed","unstylish","unsubtle","unsubtly","unsuited","unsure","unsworn","untagged","untainted","untaken","untamed","untangled","untapped","untaxed","unthawed","unthread","untidy","untie","until","untimed","untimely","untitled","untoasted","untold","untouched","untracked","untrained","untreated","untried","untrimmed","untrue","untruth","unturned","untwist","untying","unusable","unused","unusual","unvalued","unvaried","unvarying","unveiled","unveiling","unvented","unviable","unvisited","unvocal","unwanted","unwarlike","unwary","unwashed","unwatched","unweave","unwed","unwelcome","unwell","unwieldy","unwilling","unwind","unwired","unwitting","unwomanly","unworldly","unworn","unworried","unworthy","unwound","unwoven","unwrapped","unwritten","unzip","upbeat","upchuck","upcoming","upcountry","update","upfront","upgrade","upheaval","upheld","uphill","uphold","uplifted","uplifting","upload","upon","upper","upright","uprising","upriver","uproar","uproot","upscale","upside","upstage","upstairs","upstart","upstate","upstream","upstroke","upswing","uptake","uptight","uptown","upturned","upward","upwind","uranium","urban","urchin","urethane","urgency","urgent","urging","urologist","urology","usable","usage","useable","used","uselessly","user","usher","usual","utensil","utility","utilize","utmost","utopia","utter","vacancy","vacant","vacate","vacation","vagabond","vagrancy","vagrantly","vaguely","vagueness","valiant","valid","valium","valley","valuables","value","vanilla","vanish","vanity","vanquish","vantage","vaporizer","variable","variably","varied","variety","various","varmint","varnish","varsity","varying","vascular","vaseline","vastly","vastness","veal","vegan","veggie","vehicular","velcro","velocity","velvet","vendetta","vending","vendor","veneering","vengeful","venomous","ventricle","venture","venue","venus","verbalize","verbally","verbose","verdict","verify","verse","version","versus","vertebrae","vertical","vertigo","very","vessel","vest","veteran","veto","vexingly","viability","viable","vibes","vice","vicinity","victory","video","viewable","viewer","viewing","viewless","viewpoint","vigorous","village","villain","vindicate","vineyard","vintage","violate","violation","violator","violet","violin","viper","viral","virtual","virtuous","virus","visa","viscosity","viscous","viselike","visible","visibly","vision","visiting","visitor","visor","vista","vitality","vitalize","vitally","vitamins","vivacious","vividly","vividness","vixen","vocalist","vocalize","vocally","vocation","voice","voicing","void","volatile","volley","voltage","volumes","voter","voting","voucher","vowed","vowel","voyage","wackiness","wad","wafer","waffle","waged","wager","wages","waggle","wagon","wake","waking","walk","walmart","walnut","walrus","waltz","wand","wannabe","wanted","wanting","wasabi","washable","washbasin","washboard","washbowl","washcloth","washday","washed","washer","washhouse","washing","washout","washroom","washstand","washtub","wasp","wasting","watch","water","waviness","waving","wavy","whacking","whacky","wham","wharf","wheat","whenever","whiff","whimsical","whinny","whiny","whisking","whoever","whole","whomever","whoopee","whooping","whoops","why","wick","widely","widen","widget","widow","width","wieldable","wielder","wife","wifi","wikipedia","wildcard","wildcat","wilder","wildfire","wildfowl","wildland","wildlife","wildly","wildness","willed","willfully","willing","willow","willpower","wilt","wimp","wince","wincing","wind","wing","winking","winner","winnings","winter","wipe","wired","wireless","wiring","wiry","wisdom","wise","wish","wisplike","wispy","wistful","wizard","wobble","wobbling","wobbly","wok","wolf","wolverine","womanhood","womankind","womanless","womanlike","womanly","womb","woof","wooing","wool","woozy","word","work","worried","worrier","worrisome","worry","worsening","worshiper","worst","wound","woven","wow","wrangle","wrath","wreath","wreckage","wrecker","wrecking","wrench","wriggle","wriggly","wrinkle","wrinkly","wrist","writing","written","wrongdoer","wronged","wrongful","wrongly","wrongness","wrought","xbox","xerox","yahoo","yam","yanking","yapping","yard","yarn","yeah","yearbook","yearling","yearly","yearning","yeast","yelling","yelp","yen","yesterday","yiddish","yield","yin","yippee","yo-yo","yodel","yoga","yogurt","yonder","yoyo","yummy","zap","zealous","zebra","zen","zeppelin","zero","zestfully","zesty","zigzagged","zipfile","zipping","zippy","zips","zit","zodiac","zombie","zone","zoning","zookeeper","zoologist","zoology","zoom" + ) + + + if ($RandomSeparator -or !$Separator) { + $Separators = @("-", "_", ".", "*", "#", "+", "@", "%", "=") + $Separator = Get-Random -InputObject $Separators + } + + $FilteredWords = $WordList | Where-Object { $_.Length -ge $MinWordLength -and $_.Length -le $MaxWordLength } + + if ($FilteredWords.Count -lt $NumWords) { + Write-Error "Not enough words match the length constraints." + return $null + } + + $PassphraseWords = Get-Random -InputObject ($FilteredWords | Get-Unique) -Count $NumWords + + if ($UseCapitalization) { + $PassphraseWords = $PassphraseWords | ForEach-Object { $_.Substring(0,1).ToUpper() + $_.Substring(1) } + } + + if ($IncludeNumber) { + $RandomIndex = Get-Random -Minimum 0 -Maximum $NumWords + $PassphraseWords[$RandomIndex] += (Get-Random -Minimum $MinNumber -Maximum $MaxNumber) + } + + $Passphrase = $PassphraseWords -join $Separator + + return $Passphrase +} + +$GeneratedPassphrase = GeneratedPassphrase diff --git a/scripts_staging/snippets/Logging.ps1 b/scripts_staging/snippets/Logging.ps1 new file mode 100644 index 00000000..1046a6d4 --- /dev/null +++ b/scripts_staging/snippets/Logging.ps1 @@ -0,0 +1,120 @@ +<# +.SYNOPSIS + This script performs logging and log rotation for a specified part name. + It checks for required environment variables, creates the log folder if necessary, + starts logging to a timestamped log file, and registers an event to stop logging and rotate logs upon script exit. + +.DESCRIPTION + The script first checks whether the `$PartName` variable is set. If it is not, the script terminates with a warning. + Then, it retrieves the base folder path from the environment variable `$env:Company_folder_path`. If this environment variable is not set, the script exits with a warning. + + The script attempts to create the log folder (if it doesn't already exist) at the path derived from `$env:Company_folder_path\logs`. + If the folder creation fails, it exits with an error. + + The script then generates a log file name based on the part name and a timestamp. A logging session is initiated. + + A log rotation function is defined, which removes log files older than a specified number of days based on the last write time. + + Finally, the script registers an event to stop the logging session and rotate the logs when the PowerShell session exits. + +.EXEMPLE + $PartName = Name (set in main script) + Company_folder_path={{global.Company_folder_path}} + +.NOTES + Author: SAN + Date: 28.11.24 + #public + +.CHANGELOG + +.TODO + +#> + +if (-not $PartName) { + Write-Warning "Variable 'PartName' is not set." + exit 1 +} + +# Retrieve the log folder base path from the environment variable +$Company_folder_path = $env:Company_folder_path +if (-not $Company_folder_path) { + Write-Warning "Environment variable 'Company_folder_path' is not set." + exit 1 +} + +# update the log folder path by appending '\logs' to the base folder +$logFolderPath = Join-Path -Path $Company_folder_path -ChildPath "logs" + +# Attempt to create the log folder if it doesn't exist +try { + if (-not (Test-Path $logFolderPath)) { + New-Item -Path $logFolderPath -ItemType Directory | Out-Null + Write-Host "Log folder created at: $logFolderPath" + } +} catch { + Write-Warning "Failed to create log folder at: $logFolderPath. Error: $($_.Exception.Message)" + exit 1 +} + +# Generate a timestamped log file name +$timestamp = (Get-Date).ToString("dd-MM-yy-HHmmss") +$logFilePath = Join-Path -Path $logFolderPath -ChildPath "$PartName-$timestamp.txt" + +# Function to rotate log files +function Rotate-LogFiles { + param( + [string]$LogFolder, + [string]$PartName, + [int]$DaysOld = 200 + ) + + try { + # Calculate the cutoff date for old files + $cutoffDate = (Get-Date).AddDays(-$DaysOld) + + # Retrieve log files matching the specified pattern + $logFiles = Get-ChildItem -Path $LogFolder -Filter "$PartName-*.txt" + + # Select files older than the cutoff date + $filesToRemove = $logFiles | Where-Object { $_.LastWriteTime -lt $cutoffDate } + + # Remove old log files + foreach ($file in $filesToRemove) { + try { + Remove-Item -Path $file.FullName -Force -Verbose + } catch { + Write-Warning "Failed to remove $($file.FullName): $($_.Exception.Message)" + exit 1 + } + } + } catch { + Write-Warning "Log rotation failed. Error: $($_.Exception.Message)" + exit 1 + } +} + +# Register event to stop logging and rotate logs on script exit +try { + Write-Host "Registering PowerShell.Exiting event to rotate logs" + Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action { + Write-Host "PowerShell.Exiting event triggered: Stopping transcript and rotating logs" + Stop-Transcript + Rotate-LogFiles -LogFolder $using:logFolderPath -PartName $using:PartName + } +} catch { + Write-Warning "Failed to register PowerShell.Exiting event. Error: $($_.Exception.Message)" + exit 1 +} + +# Start logging +try { + Write-Host "Starting logging to file: $logFilePath" + Start-Transcript -Path $logFilePath -Append + +} catch { + Write-Warning "Failed to start logging to file: $logFilePath. Error: $($_.Exception.Message)" + exit 1 +} + diff --git a/scripts_staging/snippets/Update TRMM agent.ps1 b/scripts_staging/snippets/Update TRMM agent.ps1 new file mode 100644 index 00000000..6b32487e --- /dev/null +++ b/scripts_staging/snippets/Update TRMM agent.ps1 @@ -0,0 +1,159 @@ +<# +.SYNOPSIS + Downloads and installs the latest or specified version of the Tactical RMM agent, with support for signed and unsigned downloads. + +.DESCRIPTION + This script retrieves the latest version of the Tactical RMM agent from GitHub or downloads a specified version based on the input environment variables. + It supports downloading a signed version using a provided token, or an unsigned version directly from GitHub. + If the specified version is set to "latest," the script fetches the most recent release information. + Before downloading, it checks the locally installed version from the software list and skips the download if it matches the desired version. + +.PARAMETER version + Specifies the version to download. If set to "latest," the script retrieves the latest version available on GitHub. + This should be specified through the environment variable `version`. + +.PARAMETER signedDownloadToken + The token used for authenticated signed downloads. This should be set in the environment variable `trmm_sign_download_token`. + If this token is provided, the script will download the signed version. + +.PARAMETER trmm_api_target + The API target required for signed downloads. This should be specified in the environment variable `trmm_api_target`. + This is only necessary if using a signed download. + +.EXEMPLE + trmm_sign_download_token={{global.trmm_sign_download_token}} + version=latest + version=2.7.0 + trmm_api_target=api.exemple.com + trmm_api_target={{global.RMM_API_URL}} + +.NOTES + Author: SAN + Date: 29.10.24 + #public + +.CHANGELOG + 29.10.24 SAN Initial script with signed and unsigned download support. + 21.12.24 SAN updated the script to not require "issigned" + 22.12.24 SAN default to latest when no version is set + +.TODO + Add a small (15 seconds) delay to the execution of the exe to ensure trmm is capable of properly capturing the output of the script before the agent kills the service + +#> +# Variables +$version = $env:version # Specify a version manually, or leave empty to get the latest version from GitHub +$signedDownloadToken = $env:trmm_sign_download_token # Token used for signed downloads only +$apiTarget = $env:trmm_api_target # Environment variable for the API target URL + +# Define GitHub API URL for the RMMAgent repository +$repoUrl = "https://api.github.com/repos/amidaware/rmmagent/releases/latest" + +# Function to get the currently installed version of the Tactical RMM agent from the software list +function Get-InstalledVersion { + $appName = "Tactical RMM Agent" # Adjust if the application's display name differs left this in case whitelabel changes the name of the app + $installedSoftware = Get-CimInstance -ClassName Win32_Product | Where-Object { $_.Name -like "*$appName*" } + + if ($installedSoftware) { + return $installedSoftware.Version + } else { + # Check the uninstall registry key for a more complete list + $uninstallKeys = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" + ) + + foreach ($key in $uninstallKeys) { + $installedSoftware = Get-ItemProperty $key | Where-Object { $_.DisplayName -like "*$appName*" } + if ($installedSoftware) { + return $installedSoftware.DisplayVersion + } + } + + return $null + } +} + +try { + # Set up headers for GitHub API request + $headers = @{ + "User-Agent" = "PowerShell Script" + } + + # If version is not set, default to "latest" + if (-not $version) { + $version = "latest" + } + if ($version -eq "latest") { + Write-Output "Fetching the latest version information of the TRMM agent from GitHub..." + $response = Invoke-RestMethod -Uri $repoUrl -Headers $headers -Method Get -ErrorAction Stop + $version = $response.tag_name.TrimStart('v') # Remove 'v' prefix if exists + Write-Output "Latest version found: $version" + } else { + Write-Output "Using specified version: $version" + } + + # Check if the installed version matches the desired version + $installedVersion = Get-InstalledVersion + if ($installedVersion) { + Write-Output "Installed version of 'Tactical RMM Agent': $installedVersion" + if ($installedVersion -eq $version) { + Write-Output "The installed version matches the desired version. No upgrade required." + exit 0 + } else { + Write-Output "The installed version ($installedVersion) does not match the desired version ($version). Proceeding with download." + } + } else { + Write-Output "'Tactical RMM Agent' is not installed on this system. Checking installed software..." + } + + # Define the temp directory for downloading + $tempDir = [System.IO.Path]::GetTempPath() + $outputFile = Join-Path -Path $tempDir -ChildPath "tacticalagent-v$version.exe" + + # Determine the download URL based on the presence of $signedDownloadToken + if ($signedDownloadToken) { + if (-not $apiTarget) { + Write-Output "Error: Missing API target for signed downloads. Exiting..." + exit 1 + } + # Download the signed agent using the token + $downloadUrl = "https://agents.tacticalrmm.com/api/v2/agents?version=$version&arch=amd64&token=$signedDownloadToken&plat=windows&api=$apiTarget" + } else { + # Download the unsigned agent directly from GitHub releases + $downloadUrl = "https://github.com/amidaware/rmmagent/releases/download/v$version/tacticalagent-v$version-windows-amd64.exe" + } + + Write-Output "Downloading from: $downloadUrl" + + # Download the agent file + try { + Invoke-WebRequest -Uri $downloadUrl -OutFile $outputFile -ErrorAction Stop + Write-Output "Download completed: $outputFile" + } catch { + Write-Output "Failed to download the agent. Error: $($_.Exception.Message)" + exit 1 + } + + # Run the downloaded file in a new context (using cmd) + $processStartInfo = New-Object System.Diagnostics.ProcessStartInfo + $processStartInfo.FileName = $outputFile + $processStartInfo.Arguments = "/VERYSILENT" + $processStartInfo.UseShellExecute = $true # Allows the executable to run independently + $processStartInfo.CreateNoWindow = $true # Prevents a new window from being created + + Write-Output "Starting installation..." + + # Start the process without attempting to cast the result + try { + [System.Diagnostics.Process]::Start($processStartInfo) + Write-Output "Installation started. The process is running in the background." + } catch { + Write-Output "Failed to start the installation process. Error: $($_.Exception.Message)" + exit 1 + } +} catch { + # Handle unexpected errors with output + Write-Output "An unexpected error occurred: $($_.Exception.Message)" + exit 1 +} diff --git a/scripts_staging/snippets/Updater P3.5 Schedules parser.ps1 b/scripts_staging/snippets/Updater P3.5 Schedules parser.ps1 new file mode 100644 index 00000000..d6a2db3d --- /dev/null +++ b/scripts_staging/snippets/Updater P3.5 Schedules parser.ps1 @@ -0,0 +1,200 @@ +<# +.SYNOPSIS +This PowerShell script checks whether a specific part's schedule is due for today based on a provided schedule string. +It retrieves and parses schedules, determines if the schedule matches the current date, and outputs relevant information. +If the schedule is set to "skip" or is not found, the script exits with appropriate messages. + +.DESCRIPTION +The script attempts to retrieve the scheduled time for the part using the Get-CheckSchedule function. If an error occurs during this process, it outputs an error message and exits with a status code of 1. + +After retrieving the schedule, it checks if the schedule is for today using the Is-ScheduleForToday function. Based on the result: +* If the schedule is for today, it outputs a message indicating that the schedule is due today and provides the scheduled update time. +* If the schedule is not for today, it informs the user of the number of days until the schedule and exits. + +This script is useful for validating and managing update schedules for specific parts, ensuring timely execution of scheduled tasks based on current dates. + +.NOTES + Author: MSA/SAN + Date: 12.08.2024 + #public + +.Changelog + SAN corrected some bugs and added logging function + added a lot of debug + Fixed log output on other days + 27.11.24 SAN Added more output + 27.11.24 SAN changed log-rotate logic + 13.12.24 SAN Moved logging to another script + +.TODO + Change date to dd/MM/YYYY in both this script and the P2 + +#> + +$Debug = 0 # Set to 1 to enable debug output, 0 to disable +$Schedules = $env:SCHEDULES + + +function Get-CheckSchedule { + param( + [string]$Schedules, + [string]$PartName + ) + + if ($Debug -eq 1) { + Write-Host "Debug: Received Schedules: $Schedules" + Write-Host "Debug: Received PartName: $PartName" + } + + # Normalize newline characters and split into lines + $scheduleLines = $Schedules -replace "`r`n", "`n" -replace "`r", "`n" -split "`n" + + if ($Debug -eq 1) { + Write-Host "Debug: Split schedule lines:" + $scheduleLines | ForEach-Object { Write-Host "Debug: $_" } + } + + foreach ($line in $scheduleLines) { + $line = $line.Trim() + + if ($Debug -eq 1) { + Write-Host "Debug: Processing line: '$line'" + } + + if ($line -match "^$($PartName):skip.*$") { + if ($Debug -eq 1) { + Write-Host "Debug: Skip pattern detected. Exiting function." + } + Write-Host "$PartName lines contains skip exiting as per requirement" + exit 0 + } + + elseif ($line -match "^$($PartName):(\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2})$") { + $updateTime = $matches[1] + if ($Debug -eq 1) { + Write-Host "Debug: Found date-time pattern: $updateTime" + } + + try { + $scheduledTime = [datetime]::ParseExact($updateTime, 'MM/dd/yyyy HH:mm:ss', $null) + if ($Debug -eq 1) { + Write-Host "Debug: Parsed scheduled time: $scheduledTime" + } + return $scheduledTime + } catch { + if ($Debug -eq 1) { + Write-Host "Debug: Error parsing date-time: $_" + } + Write-Host "Error parsing date-time." + exit 1 + } + } + } + + if ($Debug -eq 1) { + Write-Host "No $PartName schedule found." + Write-Host "Debug: No schedule found for $PartName." + } + Write-Host "No schedule found for $PartName." + exit 1 +} + +# Function to check if the schedule is for today +function Is-ScheduleForToday { + param( + [Parameter(Mandatory=$true)] + [datetime]$ScheduledTime + ) + + $today = Get-Date + $scheduleDate = $ScheduledTime.Date + $daysDifference = ($scheduleDate - $today.Date).Days + $isToday = $daysDifference -eq 0 + return $isToday, $daysDifference +} + +# Function that will get the last log entry when the parsed day is not current +function Get-LastLogEntry { + param( + [string]$LogFolder, + [string]$PartName + ) + + # Validate parameters + if (-not (Test-Path $LogFolder)) { + Write-Error "The specified log folder does not exist: $LogFolder" + return + } + + if ([string]::IsNullOrWhiteSpace($PartName)) { + Write-Error "The PartName parameter cannot be empty or null." + return + } + + # Get log files matching the pattern + $logFiles = Get-ChildItem -Path $LogFolder -Filter "$PartName-*.txt" -ErrorAction SilentlyContinue | Sort-Object -Property LastWriteTime -Descending + + if ($logFiles.Count -gt 0) { + $lastLogFile = $logFiles[0].FullName + + try { + # Read the full content of the file, preserving line breaks + $logContents = Get-Content -Path $lastLogFile -Raw -ErrorAction Stop + return $logContents + } catch { + Write-Error "Failed to read the log file: $lastLogFile. Error: $_" + return + } + } else { + return "No log files found." + } +} + +# Parse schedules and get Module Update schedule +$scheduledTime = Get-CheckSchedule -Schedules $Schedules -PartName $PartName + +if ($Debug -eq 1) { + Write-Host "Debug: Retrieved scheduled time: $scheduledTime" +} + +# Check the type of $scheduledTime +if ($scheduledTime -is [datetime]) { + if ($Debug -eq 1) { + Write-Host "Debug: Type of scheduledTime: $($scheduledTime.GetType())" + Write-Host "Debug: ScheduledTime is a valid DateTime object." + } +} else { + if ($Debug -eq 1) { + Write-Host "Debug: ScheduledTime is NOT a valid DateTime object. Type is: $($scheduledTime.GetType())" + } + Write-Host "ScheduledTime is not a valid DateTime object." + exit 1 +} + +# Check if the schedule is for today +$isToday, $daysDifference = Is-ScheduleForToday -ScheduledTime $scheduledTime + +if ($isToday) { + + # let's get the ball rolling + Write-Host "The $PartName schedule is for today. Scheduled update time: $scheduledTime. Start updates:" + +} else { + + # Not today just display the logs of the previous run. + Write-Host "The $PartName schedule is not for today. It is scheduled $daysDifference days from today." + + $Company_folder_path = $env:Company_folder_path + if (-not $Company_folder_path) { + Write-Warning "Environment variable 'Company_folder_path' is not set." + } + + # update the log folder path by appending '\logs' to the base folder + $logFolderPath = Join-Path -Path $Company_folder_path -ChildPath "logs" + + # Get the last log entry and display it + $lastLog = Get-LastLogEntry -LogFolder $logFolderPath -PartName $PartName + Write-Host "Last log entry:" + Write-Host $lastLog + exit 0 +} \ No newline at end of file diff --git a/scripts_staging/snippets/VHDXCleaner.ps1 b/scripts_staging/snippets/VHDXCleaner.ps1 new file mode 100644 index 00000000..36e0dd31 --- /dev/null +++ b/scripts_staging/snippets/VHDXCleaner.ps1 @@ -0,0 +1,192 @@ +<# +.SYNOPSIS + This script optimizes VHDX files by performing cleanup, defragmentation, and compaction, with options for targeted or random selection and download folder management. + +.DESCRIPTION + This PowerShell script optimizes VHDX files located in a specified directory. + It performs the following tasks: + - Cleanup operations to remove temporary files and optionally clean the Downloads folder. + - Defragments the disks to improve performance. + - Compacts the VHDX files to reduce their size. + + The script behavior can be controlled using environment variables: + - Specify the directory containing VHDX files. + - Optionally target a specific VHDX file or randomly process 50% of the files. + - Enable or disable cleanup of the Downloads folder. + +.EXEMPLE + VHDX_PATH + Specifies the path where the VHDX files are located. + RANDOM_PICKS + If set to "1", the script will randomly pick 50% of the VHDX files for optimization. Default is to process all files. + VHDX_TARGET + Specifies the name of a specific VHDX file to be optimized. If specified, only the targeted VHDX file will be optimized. + ENABLE_DOWNLOAD_CLEANUP + If set to "1", the script cleans up the Downloads folder in the mounted VHDX images. Default is to skip this step. + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.CHANGELOG + 29/08/24 SAN Swapped Write-Host to Write-Output + 19/09/24 SAN Added a disabled flag to avoid alerts + 23/10/24 SAN Prepared download cleanup + 28/11/24 SAN Updated script to use environment variables to prep transfers to snippet and added download cleanup toggle + 28/11/24 SAN added Temporary Internet Files + +.TODO + - Investigate "compact vdisk" errors related to non-read-only mode + - Finalize download cleanup implementation. + - Logoff only 1 user when using target + +#> + +# Read environment variables +$Path = $env:VHDX_PATH +$RandomPicks = $env:RANDOM_PICKS -eq "1" +$Target = $env:VHDX_TARGET +$EnableDownloadCleanup = $env:ENABLE_DOWNLOAD_CLEANUP -eq "1" + +# Check if Get-RDUserSession is available, if not exit with code 0 +try { + $null = Get-RDUserSession -ErrorAction Stop +} +catch { + if ($_.Exception.Message -match "A Remote Desktop Services deployment does not exist") { + Write-Output "Remote Desktop Services deployment does not exist. Exiting." + exit 0 + } + else { + Write-Output "An unexpected error occurred while checking for RDS deployment." + Write-Output "Error: $($_.Exception.Message)" + exit 0 + } +} + +# Check if the path contains the word "disabled" +if ($Path -like "*disabled*") { + Write-Output "Script disabled for this server" + exit 0 +} + +# Check if the specified path exists +if (-not (Test-Path $Path)) { + Write-Output "Specified path '$Path' does not exist or is invalid." + exit 1 +} + +# Close active user sessions +Write-Output "Closing active user sessions..." +Get-RDUserSession | ForEach-Object { + Write-Output "Logging off session ID $($_.UnifiedSessionId) on host $($_.HostServer)..." + Invoke-RDUserLogoff -HostServer $_.HostServer -UnifiedSessionID $_.UnifiedSessionId -Force +} + +# Define function to perform cleanup, defragmentation, and compaction +function Optimize-VHDX { + param ( + [string]$VHDXFilePath + ) + + Write-Output "Processing VHDX file: $VHDXFilePath" + + # Mount VHDX + Write-Output "Mounting VHDX file: $VHDXFilePath..." + Mount-DiskImage $VHDXFilePath -ErrorAction Stop + $mountedDisk = Get-DiskImage $VHDXFilePath | Get-Disk | Get-Partition + if (-not $mountedDisk) { + Write-Output "Failed to mount disk: $VHDXFilePath" + return + } + $driveLetter = $mountedDisk.DriveLetter + + # Cleanup temporary files + Write-Output "Cleaning up temporary files on drive $driveLetter..." + $tempPaths = @( + "$driveLetter\Windows\Temp", + "$driveLetter\Users\*\AppData\Local\Temp", + "$driveLetter\Users\*\AppData\Local\Microsoft\Windows\INetCache", + "$driveLetter\Users\*\AppData\Local\Microsoft\Windows\Temporary Internet Files" + + ) + foreach ($tempPath in $tempPaths) { + if (Test-Path $tempPath) { + Write-Output "Removing temporary files from $tempPath..." + Get-ChildItem $tempPath -Include * -Recurse | Remove-Item -Force -ErrorAction SilentlyContinue + } + } + + # Conditional cleanup of Downloads folder + if ($EnableDownloadCleanup) { + Write-Output "Cleaning up Downloads folder..." + $downloadPaths = "$driveLetter\Users\*\Downloads" + $timeLimit = (Get-Date).AddDays(-30) + $noticeFileName = "Downloads Notice - Files in this folder will be deleted regularly.txt" + + # Content of the notice in multiple languages + $noticeFileContent = @" +Les fichiers dans ce dossier seront supprimés régulièrement s'ils sont âgés de plus de 30 jours. +The files in this folder will be deleted regularly if they are older than 30 days. +"@ + + # Create or overwrite the notice file in each Downloads folder + foreach ($path in (Get-ChildItem -Path $downloadPaths -Directory -Recurse)) { + $noticeFilePath = Join-Path -Path $path.FullName -ChildPath $noticeFileName + if (Test-Path -Path $noticeFilePath) { + Remove-Item -Path $noticeFilePath -Force + } + Set-Content -Path $noticeFilePath -Value $noticeFileContent -Force + } + + # Clean up files older than 30 days + if (Test-Path $downloadPaths) { + Write-Output "Removing files older than 30 days from $downloadPaths..." + Get-ChildItem $downloadPaths -Include * -Recurse | Where-Object { $_.LastWriteTime -lt $timeLimit -and $_.Name -ne $noticeFileName } | Remove-Item -Force -ErrorAction SilentlyContinue + } + } + + # Defragment profile disk + Write-Output "Defragmenting profile disk on drive $driveLetter..." + Optimize-Volume -DriveLetter $driveLetter -Defrag -Verbose + + # Compact disk using DISKPART + Write-Output "Compacting profile disk on drive $driveLetter..." + $diskpartScript = @" +select vdisk file="$VHDXFilePath" +compact vdisk +"@ + + $diskpartScript | diskpart + + # Unmount VHDX + Write-Output "Unmounting VHDX file: $VHDXFilePath..." + Dismount-DiskImage $VHDXFilePath -ErrorAction SilentlyContinue +} + +# Process VHDX files based on environment variables +if ($Target) { + $targetFile = Join-Path $Path $Target + if (Test-Path $targetFile) { + Optimize-VHDX -VHDXFilePath $targetFile + } else { + Write-Output "Specified target file '$Target' does not exist." + } +} else { + $vhdxFiles = Get-ChildItem -Path "$Path\*.vhdx" -File + if ($vhdxFiles.Count -eq 0) { + Write-Output "No VHDX files found in the specified path: $Path" + } else { + if ($RandomPicks) { + $randomFiles = $vhdxFiles | Get-Random -Count ($vhdxFiles.Count / 2) + foreach ($file in $randomFiles) { + Optimize-VHDX -VHDXFilePath $file.FullName + } + } else { + $vhdxFiles | ForEach-Object { + Optimize-VHDX -VHDXFilePath $_.FullName + } + } + } +} diff --git a/scripts_wip/DUPE_Win_Network_Wifi_SSID_and_Password_Retrieval.ps1 b/scripts_wip/DUPE_Win_Network_Wifi_SSID_and_Password_Retrieval.ps1 deleted file mode 100644 index 60cda271..00000000 --- a/scripts_wip/DUPE_Win_Network_Wifi_SSID_and_Password_Retrieval.ps1 +++ /dev/null @@ -1,7 +0,0 @@ -# Dupe of Win_Wifi_SSID_and_Password_Retrieval.ps1 -<# - .SYNOPSIS - This Will Retrieve All Wifi SSIDs and passwords on a client - #> - -(netsh wlan show profiles) | Select-String "\:(.+)$" | % { $name = $_.Matches.Groups[1].Value.Trim(); $_ } | % { (netsh wlan show profile name="$name" key=clear) } | Select-String "Key Content\W+\:(.+)$" | % { $pass = $_.Matches.Groups[1].Value.Trim(); $_ } | % { [PSCustomObject]@{ PROFILE_NAME = $name; PASSWORD = $pass } } | Format-Table -AutoSize diff --git a/scripts_wip/Disk_Speedmultitest.py b/scripts_wip/Disk_Speedmultitest.py new file mode 100644 index 00000000..1cf04fc9 --- /dev/null +++ b/scripts_wip/Disk_Speedmultitest.py @@ -0,0 +1,187 @@ +#!/usr/bin/python3 + +# v1.0 8/28/2024 silversword411 Testing drives for read speed +# v1.1 8/28/2024 silversword411 Fixing sporadic problems, added Linux support, adding error when below 200MB/s + +import ctypes +import time +import sys +import platform +import os +import subprocess + +GENERIC_READ = 0x80000000 +OPEN_EXISTING = 3 +FILE_SHARE_READ = 1 +FILE_SHARE_WRITE = 2 +FILE_SHARE_DELETE = 4 + +warnbelowspeed = 200 # MB/s + + +def get_drive_size_windows(drive_path, retries=5): + for attempt in range(retries): + try: + handle = ctypes.windll.kernel32.CreateFileW( + drive_path, + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + None, + OPEN_EXISTING, + 0, + None, + ) + if handle == -1: + raise ctypes.WinError() + + size = ctypes.c_ulonglong() + ctypes.windll.kernel32.GetDiskFreeSpaceExW( + drive_path, None, ctypes.byref(size), None + ) + ctypes.windll.kernel32.CloseHandle(handle) + return size.value + except PermissionError: + if attempt < retries - 1: + print( + f"Retrying to access the drive... (Attempt {attempt + 1}/{retries})" + ) + time.sleep(1) + else: + raise + + +def get_drive_size_linux(drive_path): + with open(drive_path, "rb") as f: + f.seek(0, os.SEEK_END) + return f.tell() + + +def detect_linux_drive(): + try: + result = subprocess.run( + ["lsblk", "-dpno", "NAME,TYPE"], stdout=subprocess.PIPE, text=True + ) + drives = [ + line.split()[0] for line in result.stdout.splitlines() if "disk" in line + ] + return drives[0] if drives else None + except Exception as e: + print(f"Error detecting drive: {e}") + sys.exit(1) + + +def read_speed_test_windows(drive_path, offset, length): + handle = ctypes.windll.kernel32.CreateFileW( + drive_path, + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + None, + OPEN_EXISTING, + 0, + None, + ) + if handle == -1: + raise ctypes.WinError() + + high_offset = ctypes.c_long(offset >> 32) + low_offset = ctypes.c_long(offset & 0xFFFFFFFF) + ctypes.windll.kernel32.SetFilePointer( + handle, low_offset, ctypes.byref(high_offset), 0 + ) + + buffer = ctypes.create_string_buffer(length) + bytes_read = ctypes.c_ulong(0) + + start_time = time.time() + success = ctypes.windll.kernel32.ReadFile( + handle, buffer, length, ctypes.byref(bytes_read), None + ) + end_time = time.time() + + ctypes.windll.kernel32.CloseHandle(handle) + + if not success: + raise ctypes.WinError() + + read_time = end_time - start_time + read_speed = bytes_read.value / read_time + return read_speed + + +def read_speed_test_linux(drive_path, offset, length): + with open(drive_path, "rb") as f: + f.seek(offset) + start_time = time.time() + buffer = f.read(length) + end_time = time.time() + + read_time = end_time - start_time + read_speed = len(buffer) / read_time + return read_speed + + +def check_speed_difference(speed1, speed2): + difference = abs(speed1 - speed2) / ((speed1 + speed2) / 2) * 100 + return difference + + +def main(): + if platform.system() == "Windows": + drive_path = r"\\.\PhysicalDrive0" + drive_size = get_drive_size_windows(drive_path) + read_speed_test = read_speed_test_windows + elif platform.system() == "Linux": + drive_path = detect_linux_drive() + if not drive_path: + print("No suitable drive found on the system.") + sys.exit(1) + drive_size = get_drive_size_linux(drive_path) + read_speed_test = read_speed_test_linux + else: + print("Unsupported OS") + sys.exit(1) + + read_length = 500 * 1024 * 1024 # 500 MB + + front_offset = 0 + middle_offset = drive_size // 2 + back_offset = drive_size - read_length + + front_speed = read_speed_test(drive_path, front_offset, read_length) / (1024 * 1024) + middle_speed = read_speed_test(drive_path, middle_offset, read_length) / ( + 1024 * 1024 + ) + back_speed = read_speed_test(drive_path, back_offset, read_length) / (1024 * 1024) + + print(f"Front read speed: {front_speed:.2f} MB/s") + print(f"Middle read speed: {middle_speed:.2f} MB/s") + print(f"Back read speed: {back_speed:.2f} MB/s") + + # Flag to track if any condition for exit is met + error_detected = False + + # Check if any speed is below the warning threshold + if ( + front_speed < warnbelowspeed + or middle_speed < warnbelowspeed + or back_speed < warnbelowspeed + ): + print(f"Error: One or more read speeds are below {warnbelowspeed} MB/s.") + error_detected = True + + # Check if speed differences exceed 20% + if ( + check_speed_difference(front_speed, middle_speed) > 20 + or check_speed_difference(middle_speed, back_speed) > 20 + or check_speed_difference(front_speed, back_speed) > 20 + ): + print("Error: Read speeds differ by more than 20%.") + error_detected = True + + # Exit with error if any condition is met + if error_detected: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts_wip/TRMM_Management_AddClient.ps1 b/scripts_wip/TRMM_Management_AddClient.ps1 new file mode 100644 index 00000000..399a6a30 --- /dev/null +++ b/scripts_wip/TRMM_Management_AddClient.ps1 @@ -0,0 +1,111 @@ +<# +.SYNOPSIS +A tool to create clients, and sites, programitcally to enable "bulk" creation of clients & sites. + +.DESCRIPTION +A tool to create clients, and sites, programitcally to enable "bulk" creation of clients & sites. + +Creates a site called "Site" by default as the first site. + +Assumes there is a client wide custom field for storing valid domains for that client. By default the name is assumed to be "Domains" and can be overridden using the -clientCustomFieldName parameter. + +.EXAMPLE +Create a CSV file with the headers "clientName" and "domains". If there are multiple valid domains, separate them with a semicolon (;). + +$csvFilePath = (Read-Host "Provide the full file path for the CSV.") +$apiKey = (Read-Host "Enter your API key") +$apiFQDN = (Read-Host "Enter the FQDN portion of your TRMM API URI - e.g. api.example.com") +Import-CSV -Path $csvFilePath | New-TrmmClient.ps1 -apiKey $apiKey -apiFQDN $apiFQDN + +.NOTES +v1.0 2026-01-14 Owen Conti + +#> +[cmdletbinding()] +Param( + [Parameter(ValueFromPipelineByPropertyName)][string] + $clientName, + + [Parameter(ValueFromPipelineByPropertyName)][string[]]#Uses a custom field to store the domains that M365 users will be using as part of their UPNs. When importing by CSV, have the list of Domains separated by a semicolon (;). + $domains, + + [string[]]#"Site" is created first by default. This is a list of additional Site names to create. + $sites, + + [string]#The API Key value to use + $apiKey, + + [string]#The FQDN of your API end point (the URI is built later). E.g. "api.example.com" + $apiFQDN, + + [string]#The exact name of the Client level Custom Field used for storing the valid Domains + $clientCustomFieldName = "Domains" +) + +BEGIN { + $headers = @{ + 'Content-Type' = 'application/json' + 'X-API-KEY' = $apiKey + } #These are common to all our API calls + + #Get the initial list of clients + $clients = Invoke-RestMethod -Uri "https://$apiFQDN/clients/" -Method GET -Headers $headers + + #Get CustomField ID for the given name + $customFieldDetails = Invoke-RestMethod -Uri "https://$apiFQDN/core/customfields/" -Method GET -Headers $headers + $customFieldDetails = $customFieldDetails | Where-Object -FilterScript {$_.model -eq "client" -and $_.name -eq $clientCustomFieldName} +} + +PROCESS{ + + If($domains.Contains(";")){ + $domains = $domains.Split(";") + } + + #First check if the client already exists + $existingClient = $clients | Where-Object -FilterScript {$_.name -eq $clientName} + + If($existingClient){ + Write-Error "$clientName already exists (ID $($existingClient.ID))." + } else { + #Create the Client + $clientPayload = (@{ + client = @{ + name = $clientName + } + custom_fields = @( + @{ + field = $customFieldDetails.id + string_value = $domains -join "\n" + } + ) + site = @{ + name = "Site" + } + } | ConvertTo-Json).Replace("\\n","\n") + + Invoke-RestMethod -Uri "https://$apiFQDN/clients/" -Method POST -Headers $headers -body $clientPayload + + $clients = Invoke-RestMethod -Uri "https://$apiFQDN/clients/" -Method GET -Headers $headers + #The API doesn't return the new Client ID, so we need to collect all clients again. Has the added advantage of preventing us from trying to create duplicates if the CSV has duplicated rows. + $newClientDetails = $clients | Where-Object -FilterScript {$_.name -eq $clientName} + + #Add the Sites + If($sites){ + $existingSites = Invoke-RestMethod -Uri "https://$apiFQDN/clients/sites/" -Method GET -Headers $headers + Foreach($site in $sites){ + If($existingSites | Where-Object -FilterScript {$_.client -eq $newClientDetails.id -and $_.name -eq $site}){ + "Site already exists" + Continue + } + $sitePayload = @{ + site = @{ + client = $newClientDetails.id + name = $site + } + } | ConvertTo-Json + Invoke-RestMethod -Uri "https://$apiFQDN/clients/sites/" -Method POST -Headers $headers -body $sitePayload + } + } + } +} \ No newline at end of file diff --git a/scripts_wip/Win_3rdparty_Urbackup_Monitor.ps1 b/scripts_wip/Win_3rdparty_Urbackup_Monitor.ps1 new file mode 100644 index 00000000..fee729b9 --- /dev/null +++ b/scripts_wip/Win_3rdparty_Urbackup_Monitor.ps1 @@ -0,0 +1,110 @@ +<# +.SYNOPSIS + Script to check the status of Urbackup file backup and log events. + +.DESCRIPTION + This script checks the status of Urbackup file backup and logs events in the Windows Event Log. It performs the following steps: + - Checks if the UrbackupCheck parameter is enabled. If enabled, the script exits. + - Checks if the UrBackup client is installed. If not installed, the script exits. + - Checks if the Urbackup postfile exists. If not, it creates the file. + - Checks if the "Write event to Event Log" line already exists in the file. If not, it adds the line. + - Retrieves Urbackup events from the Application event log that match a specific description. + - Determines the days elapsed since the latest event and compares it with the NumberOfDaysBeforeError parameter. + - Displays the relevant event log information if the event is found and within the specified number of days. + - Exits with a status code of 1 if the event is older than the specified number of days. + +.PARAMETER UrbackupCheck + Specifies whether Urbackup check is enabled or disabled. Use Custom Fields to enable or disable as needed + +.PARAMETER NumberOfDaysBeforeError + Specifies the number of days before considering an event as an error. + +.EXAMPLE + -UrbackupCheck {{agent.UrbackupDisableCheck}} -NumberOfDaysBeforeError 30 + +.NOTES + Version: 1.5 6/20/2024 silversword411 +#> + +param ( + [Int]$UrbackupCheck, + [Int]$NumberOfDaysBeforeError +) + + + +#Write-Output "NumberOfDaysBeforeError: $NumberOfDaysBeforeError" + +# See if Custom Field has disabled VeeamCheck +#Write-Output "VeeamCheck: $VeeamCheck" +if ($UrbackupCheck) { + Write-Output "Urbackup check disabled." + Exit 0 +} + +# Stop if Urbackup is not installed +$clientExecutable = 'C:\Program Files\UrBackup\UrBackupClient.exe' +if (-not (Test-Path -Path $clientExecutable)) { + Write-Output "UrBackup client is not installed. Quitting" + exit 0 +} + +function UpdateUrbackupPostFile { + $file = 'C:\Program Files\UrBackup\postfilebackup.bat' + $lineToAdd = 'EVENTCREATE /T SUCCESS /L APPLICATION /SO URBACKUP /ID 100 /D "File backup succeeded."' + + # Check if the Urbackup postfile exists + if (-not (Test-Path -Path $file)) { + # Create the file if it doesn't exist + New-Item -Path $file -ItemType File | Out-Null + Write-Output "Post backup .bat file has been created." + } + + # Check if the line already exists in the file + $lineExists = Get-Content -Path $file | Select-String -Pattern $lineToAdd + + if ($lineExists) { + Write-Output "Write event to Event Log already exists in the file." + } + else { + # Add the line to the file + Add-Content -Path $file -Value $lineToAdd + Write-Output "Write event to Event Log line has been added to the file." + } +} + +UpdateUrbackupPostFile + +######################################################################### +Write-Output "------------ CHECK FOR LOG ------------" +$source = "URBACKUP" +$logName = "Application" +$eventID = 100 +$description = "File backup succeeded." + +$UrbackupEvents = Get-WinEvent -FilterHashtable @{ + LogName = $logName + ProviderName = $source + ID = $eventID +} | Where-Object { $_.Message -like "*$description*" } | Sort-Object TimeCreated -Descending + +if ($UrbackupEvents -ne $null) { + $latestEvent = $UrbackupEvents[0] + $daysSinceEvent = (Get-Date) - $latestEvent.TimeCreated + if ($daysSinceEvent.Days -gt $NumberOfDaysBeforeError) { + Write-Output "WARNING: The last event is older than $NumberOfDaysBeforeError days." + Write-Output "Last Backup: $($latestEvent.TimeCreated)" + exit 1 + } + else { + Write-Output "ALL GOOD: The last event is newer than $NumberOfDaysBeforeError days." + #Write-Output "Event Log found:" + #Write-Output "Source: $($latestEvent.ProviderName)" + #Write-Output "Event ID: $($latestEvent.Id)" + #Write-Output "Message: $($latestEvent.Message)" + Write-Output "Last Backup: $($latestEvent.TimeCreated)" + } +} +else { + Write-Output "Event Log not found." +} \ No newline at end of file diff --git a/scripts_wip/Win_3rdparty_Urbackup_Uninstall.bat b/scripts_wip/Win_3rdparty_Urbackup_Uninstall.bat new file mode 100644 index 00000000..3dac42a0 --- /dev/null +++ b/scripts_wip/Win_3rdparty_Urbackup_Uninstall.bat @@ -0,0 +1 @@ +"C:\Program Files\UrBackup\Uninstall.exe" /S \ No newline at end of file diff --git a/scripts_wip/Win_3rdparty_Urbackup_restorepermfixer.bat b/scripts_wip/Win_3rdparty_Urbackup_restorepermfixer.bat new file mode 100644 index 00000000..c55fd331 --- /dev/null +++ b/scripts_wip/Win_3rdparty_Urbackup_restorepermfixer.bat @@ -0,0 +1,27 @@ +rem Use environment variables +rem eg pcname=pcname username=username + +rem Display the values of environment variables +echo pcname: %pcname% +echo Username: %username% + +takeown /s %pcname% /u %pcname%\%username% /f "c:\users\%username%\Desktop" /r /d Y +icacls "c:\users\%username%\Desktop" /reset /T + +takeown /s %pcname% /u %pcname%\%username% /f "c:\users\%username%\Documents" /r /d Y +icacls "c:\users\%username%\Documents" /reset /T + +takeown /s %pcname% /u %pcname%\%username% /f "c:\users\%username%\Downloads" /r /d Y +icacls "c:\users\%username%\Downloads" /reset /T + +takeown /s %pcname% /u %pcname%\%username% /f "c:\users\%username%\Favorites" /r /d Y +icacls "c:\users\%username%\Favorites" /reset /T + +takeown /s %pcname% /u %pcname%\%username% /f "c:\users\%username%\Music" /r /d Y +icacls "c:\users\%username%\Music" /reset /T + +takeown /s %pcname% /u %pcname%\%username% /f "c:\users\%username%\Pictures" /r /d Y +icacls "c:\users\%username%\Pictures" /reset /T + +takeown /s %pcname% /u %pcname%\%username% /f "c:\users\%username%\Videos" /r /d Y +icacls "c:\users\%username%\Videos" /reset /T \ No newline at end of file diff --git a/scripts_wip/Win_ASUS_debloater.ps1 b/scripts_wip/Win_ASUS_debloater.ps1 new file mode 100644 index 00000000..738a0230 --- /dev/null +++ b/scripts_wip/Win_ASUS_debloater.ps1 @@ -0,0 +1,35 @@ +<# +.SYNOPSIS + Stop and disable specified ASUS services + +.DESCRIPTION + This script stops and disables a list of specified ASUS services on the local machine. + It loops through each service name provided, attempts to stop the service, and then disables it. + The script outputs the status of each operation. + +.EXAMPLE + "asusappservice", "asusoptimization", "ASUSSoftwareManager", "ASUSSwitch", "ASUSSystemAnalysis", "ASUSSystemDiagnosis" + +.NOTES + v1.0 7/17/2024 silversword411 Initial release Get rid of that ASUS crap that installs because of Armoury-crate autoinstaller that's enabled in BIOS +#> + +# Define the variable containing the service names +$serviceNames = "asusappservice", "asusoptimization", "ASUSSoftwareManager", "ASUSSwitch", "ASUSSystemAnalysis", "ASUSSystemDiagnosis" + +# Loop through each service name in the variable +foreach ($serviceName in $serviceNames) { + # Stop the service + Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue + + # Disable the service + Set-Service -Name $serviceName -StartupType Disabled -ErrorAction SilentlyContinue + + # Output the status of the operation + if ((Get-Service -Name $serviceName).Status -eq 'Stopped') { + Write-Output "$serviceName has been stopped and disabled successfully." + } + else { + Write-Output "Failed to stop and disable $serviceName." + } +} \ No newline at end of file diff --git a/scripts_wip/Win_AutoElevate_Manage.ps1 b/scripts_wip/Win_AutoElevate_Manage.ps1 index 037647aa..93af44e3 100644 --- a/scripts_wip/Win_AutoElevate_Manage.ps1 +++ b/scripts_wip/Win_AutoElevate_Manage.ps1 @@ -8,7 +8,7 @@ .EXAMPLE Win_AutoElevate_Manage -LicenseKey "abcdefg" -CompanyName "MyCompany" -LocationName "Main" -AgentMode live .EXAMPLE - Win_AutoElevate_Manage -LicenseKey "abcdefg" -CompanyName "MyCompany" -LocationName "Main" -AgentMode live -Uninstall + Win_AutoElevate_Manage -Uninstall .EXAMPLE Win_AutoElevate_Manage -LicenseKey "abcdefg" -CompanyName "MyCompany" -CompanyInitials "MC" -LocationName "Main" -AgentMode live .INSTRUCTIONS @@ -28,22 +28,19 @@ .NOTES Version: 1.0 Author: redanthrax - Creation Date: 2022-04-12 + Creation Date: 2023-04-12 + Updated Date: 2024-03-22 #> Param( - [Parameter(Mandatory)] [string]$LicenseKey, - [Parameter(Mandatory)] [string]$CompanyName, [string]$CompanyInitials, - [Parameter(Mandatory)] [string]$LocationName, - [Parameter(Mandatory)] [ValidateSet("live", "policy", "audit", "technician")] $AgentMode, @@ -51,45 +48,65 @@ Param( ) function Win_AutoElevate_Manage { - [CmdletBinding()] + [CmdletBinding(DefaultParameterSetName = 'InstallSet')] Param( - [Parameter(Mandatory)] + [Parameter(Mandatory=$true, ParameterSetName='InstallSet')] [string]$LicenseKey, - [Parameter(Mandatory)] + [Parameter(Mandatory=$true, ParameterSetName='InstallSet')] [string]$CompanyName, [string]$CompanyInitials, - [Parameter(Mandatory)] + [Parameter(Mandatory=$true, ParameterSetName='InstallSet')] [string]$LocationName, - [Parameter(Mandatory)] + [Parameter(Mandatory=$true, ParameterSetName='InstallSet')] [ValidateSet("live", "policy", "audit", "technician")] $AgentMode, + [Parameter(Mandatory=$true, ParameterSetName='UninstallSet')] [switch]$Uninstall ) Begin { + $Apps = @() + $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" + $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" if ($null -ne (Get-Service | Where-Object { $_.DisplayName -Match "AutoElevate" }) -and -Not($Uninstall)) { Write-Output "AutoElevate already installed." Exit 0 } + if($Uninstall -and $null -eq (Get-Service | Where-Object { $_.DisplayName -Match "AutoElevate" })) { + Write-Output "AutoElevate already uninstalled." + Exit 0 + } + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $random = ([char[]]([char]'a'..[char]'z') + 0..9 | sort { get-random })[0..12] -join '' - if (-not(Test-Path "C:\packages$random")) { New-Item -ItemType Directory -Force -Path "C:\packages$random" } + if (-not(Test-Path "C:\packages$random")) { New-Item -ItemType Directory -Force -Path "C:\packages$random" | Out-Null } } Process { Try { if ($Uninstall) { - (Get-WmiObject -Class Win32_Product -Filter "Name = 'AutoElevate'").Uninstall() - Write-Output "Uninstalled AutoElevate" - Exit 0 + Write-Output "Uninstalling AutoElevate" + $uninstallString = ($Apps | Where-Object { $_.DisplayName -Match "AutoElevate" }).UninstallString + if ($uninstallString) { + $unst = $uninstallString -Split " " + $unst[1] = $unst[1] -Replace '/I', '/X' + Start-Process $unst[0] -ArgumentList $unst[1], "/quiet", "/qn", "/noreboot" -Wait -NoNewWindow + Write-Output "Uninstalled AutoElevate" + return + } + else { + Write-Error "Could not find uninstall string" + return + } } + Write-Output "Installing AutoElevate" $source = "https://autoelevate-installers.s3.us-east-2.amazonaws.com/current/AESetup.msi" $destination = "C:\packages$random\AESetup.msi" Invoke-WebRequest -Uri $source -OutFile $destination @@ -102,19 +119,18 @@ function Win_AutoElevate_Manage { $process | Wait-Process -Timeout 300 -ErrorAction SilentlyContinue -ErrorVariable timedOut if ($timedOut) { $process | kill - Write-Output "Install timed out after 300 seconds." - Exit 1 + Write-Error "Install timed out after 300 seconds." } elseif ($process.ExitCode -ne 0) { $code = $process.ExitCode - Write-Output "Install error code: $code." - Exit 1 + Write-Error "Install error code: $code." } + + Write-Output "AutoElevate installation complete" } Catch { $exception = $_.Exception - Write-Output "Error: $exception" - Exit 1 + Write-Error "Error: $exception" } } @@ -123,6 +139,10 @@ function Win_AutoElevate_Manage { Remove-Item -Path "C:\packages$random" -Recurse -Force } + if($error) { + Exit 1 + } + Exit 0 } } @@ -131,13 +151,20 @@ if (-not(Get-Command 'Win_AutoElevate_Manage' -errorAction SilentlyContinue)) { . $MyInvocation.MyCommand.Path } -$scriptArgs = @{ - LicenseKey = $LicenseKey - CompanyName = $CompanyName - CompanyInitials = $CompanyInitials - LocationName = $LocationName - AgentMode = $AgentMode - Uninstall = $Uninstall +$scriptArgs = @{ } +if($Uninstall) { + $scriptArgs = @{ + Uninstall = $Uninstall + } +} +else { + $scriptArgs = @{ + LicenseKey = $LicenseKey + CompanyName = $CompanyName + CompanyInitials = $CompanyInitials + LocationName = $LocationName + AgentMode = $AgentMode + } } Win_AutoElevate_Manage @scriptArgs \ No newline at end of file diff --git a/scripts_wip/Win_Battery_Capacity_Check.ps1 b/scripts_wip/Win_Battery_Capacity_Check.ps1 new file mode 100644 index 00000000..f2bc2619 --- /dev/null +++ b/scripts_wip/Win_Battery_Capacity_Check.ps1 @@ -0,0 +1,49 @@ +<# +.Synopsis + Checks the battery full charge capacity VS the design capacity +.DESCRIPTION + This was written specifically for use as a "Script Check" in mind, where it the output is deliberaly light unless a warning or error condition is found that needs more investigation. + + If the total full charge capacity is less than the minimum capacity amount, an error is returned. +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $false)] + [int]#The minimum battery full charge capacity (as a percentage of design capacity by default). Defaults to 85 percent. + $minimumBatteryCapacity = 85, + + [Parameter(Mandatory = $false)] + [switch]#Set the check condition to absolute mWh values instead of a percentage + $absoluteValues +) + +try{ + $searcher = New-Object System.Management.ManagementObjectSearcher("root\wmi","SELECT * FROM BatteryStaticData") + $batteryStatic = $searcher.Get() + #CIM approach threw errors when Get-WMIObject did not - WMI approach is not available in PSv7, so took .NET approach + $batteryCharge = Get-CimInstance -Namespace "root\wmi" -ClassName "BatteryFullChargedCapacity" -ErrorAction Stop +} catch { + Write-Output "No battery detected" + exit 0 +} + +if (-not $batteryStatic -or -not $batteryCharge) { + Write-Output "No battery detected" + exit 0 +} + +$chargeCapacity = $batteryCharge.FullChargedCapacity +$designCapacity = $batteryStatic.DesignedCapacity + +$available = [math]::Round(($chargeCapacity / $designCapacity) * 100,2) +$label = "%" +if ($absoluteValues) { + $available = $chargeCapacity + $label = "mWh" +} + +"Full charge capacity $available$label of $designCapacity mWh." + +If($available -le $minimumBatteryCapacity){ Exit 1 } +Exit 0 \ No newline at end of file diff --git a/scripts_wip/Win_Bitlocker_AIO.ps1 b/scripts_wip/Win_Bitlocker_AIO.ps1 new file mode 100644 index 00000000..fda834fb --- /dev/null +++ b/scripts_wip/Win_Bitlocker_AIO.ps1 @@ -0,0 +1,345 @@ +<# +.SYNOPSIS + Manages bitlocker encryption. +.DESCRIPTION + A script to manage bitlocker on a workstation. Get information on volumes, keys, + tpm, and tpm health. Encrypt, Decrypt, Suspend, Resume, and backup. HealBitlocker is + for the circumstance when you receive an odd error when trying to get the bitlocker + volume "Get-CimInstance : Invalid property" +.EXAMPLE + .\Win_Bitlocker_AIO.ps1 -Info Keys + .\Win_Bitlocker_AIO.ps1 -Info Tpm,TpmHealth + .\Win_Bitlocker_AIO.ps1 -Operation Encrypt,Backup + .\Win_Bitlocker_AIO.ps1 -Operation HealBitlocker +.INSTRUCTIONS +.NOTES + Version: 1.0 + Author: red + Creation Date: 2024-03-13 +#> + +Param( + [Parameter(HelpMessage = "Output volumes in Json format")] + [switch]$Json, + + [Parameter(HelpMessage = "Info: Volumes, Keys, Tpm, TpmHealth, Status")] + [AllowNull()] + [AllowEmptyCollection()] + [string[]]$Info, + + [Parameter(HelpMessage = "Operation: Encrypt, Decrypt, Suspend, Resume, + Backup, HealBitlocker, HealKeyBackup")] + [AllowNull()] + [AllowEmptyCollection()] + [string[]]$Operation +) + +function Win_Bitlocker_AIO { + [CmdletBinding()] + Param( + [Parameter(HelpMessage = "Output volumes in Json format")] + [switch]$Json, + + [Parameter(HelpMessage = "Info: Volumes, Keys, Tpm, TpmHealth, Status")] + [AllowNull()] + [AllowEmptyCollection()] + [string[]]$Info, + + [Parameter(HelpMessage = "Operation: Encrypt, Decrypt, Suspend, Resume, + Backup, HealBitlocker, HealKeyBackup")] + [AllowNull()] + [AllowEmptyCollection()] + [string[]]$Operation + ) + + Begin {} + + Process { + Try { + #Info Section - Information Gathering + foreach ($item in $Info) { + $volumes = Get-BitlockerVolume + $tpm = Get-Tpm + switch ($item) { + "Volumes" { + if ($Json) { + Write-Output $volumes | ConvertTo-Json -Depth 100 + } + else { + Write-Output $volumes | Format-List + } + } + "Keys" { + foreach ($vol in $volumes) { + $keys = $vol | Get-BitlockerVolume | Select-Object -ExpandProperty KeyProtector + foreach ($key in $keys) { + if ($key.KeyProtectorType -eq "RecoveryPassword") { + Write-Output $key.RecoveryPassword + } + } + } + } + "Tpm" { + if ($Json) { + Write-Output $tpm | ConvertTo-Json -Depth 100 + } + else { + Write-Output $tpm + } + } + "TpmHealth" { + if (-Not($tpm.TpmPresent -or $tpm.TpmReady -or $tpm.TpmEnabled -or + $tpm.TpmActivated)) { + Write-Error "Tpm State: Unhealthy" + } + else { + Write-Output "Tpm State: Healthy" + } + } + "Status" { + foreach ($vol in $volumes) { + if ($vol.VolumeType -eq "OperatingSystem") { + $status = @{ + Volume = [string]$vol.VolumeStatus + Percentage = $vol.EncryptionPercentage + Status = [string]$vol.ProtectionStatus + } + if ($Json) { + $status | ConvertTo-Json + } + else { + Write-Output "Status: $($status.Status), Volume: $($status.Volume), Percentage: $($status.Percentage)" + } + } + } + } + } + } + + #Operation Section - Taking action + #Only OS encryption + foreach ($item in $Operation) { + $volumes = Get-BitlockerVolume + $tpm = Get-Tpm + switch ($item) { + "Encrypt" { + foreach ($vol in $volumes) { + if ($vol.VolumeType -eq "OperatingSystem") { + if ($vol.VolumeStatus -eq "FullyDecrypted") { + if (($tpm.TpmPresent -or $tpm.TpmReady -or $tpm.TpmEnabled -or $tpm.TpmActivated)) { + Remove-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\FVE" -Name "UseAdvancedStartup" -ErrorAction SilentlyContinue + Write-Output "Generating recovery password" + $vol | Add-BitLockerKeyProtector -RecoveryPasswordProtector -InformationAction SilentlyContinue | Out-Null + Write-Output "Encrypting volume" + $vol | Enable-Bitlocker -TpmProtector -UsedSpaceOnly -SkipHardwareTest + } + else { + Write-Error "Tpm not in healthy state" + } + } + else { + Write-Output "Volume already encrypted or in process" + } + + #Check for recovery password, add if missing and we have Tpm + $tpmProtector = $vol.KeyProtector | Where-Object { $_.KeyProtectorType -eq "Tpm" } + $recoveryPassword = $vol.KeyProtector | Where-Object { $_.KeyProtectorType -eq "RecoveryPassword" } + if (-Not($recoveryPassword) -and $tpmProtector) { + Write-Output "Adding recovery password" + $vol | Add-BitLockerKeyProtector -RecoveryPasswordProtector -InformationAction SilentlyContinue | Out-Null + } + } + } + } + "Decrypt" { + foreach ($vol in $volumes) { + if ($vol.VolumeType -eq "OperatingSystem") { + if ($vol.VolumeStatus -eq "FullyEncrypted") { + Write-Output "Clearing automatic unlocking keys" + Clear-BitLockerAutoUnlock | Out-Null + Write-Output "Decrypting Bitlocker volumes" + $vol | Disable-BitLocker | Out-Null + } + else { + Write-Error "Volume not in FullyEncrypted state" + } + } + } + } + "Backup" { + foreach ($vol in $volumes) { + if ($vol.VolumeType -eq "OperatingSystem") { + $key = $vol.KeyProtector | Where-Object { $_.KeyProtectorType -eq "RecoveryPassword" } + if ($key) { + Write-Output "Attempting key protector backup for AD and AAD" + #use jobs to ignore errors + $ad = Start-Job -ScriptBlock { + param($vol, $key) + $vol | Backup-BitLockerKeyProtector -KeyProtectorId $key.KeyProtectorId -ErrorAction SilentlyContinue | Out-Null + } -ArgumentList $vol, $key + Wait-Job $ad | Out-Null + Remove-Job -Job $ad + + $aad = Start-Job -ScriptBlock { + param($vol, $key) + $vol | BackupToAAD-BitLockerKeyProtector -KeyProtectorId $key.KeyProtectorId -ErrorAction SilentlyContinue | Out-Null + } -ArgumentList $vol, $key + Wait-Job $aad | Out-Null + Remove-Job -Job $aad + } + else { + Write-Error "No key protector found for backup" + } + } + } + } + "HealBitlocker" { + Set-Service vss -StartupType Manual + Set-Service smphost -StartupType Manual + Stop-Service SMPHost + Stop-Service vss + $mof = mofcomp.exe win32_encryptablevolume.mof + if ($mof -like "*not found*") { + # Set the Windows Management Instrumentation (WMI) service to start automatically + Set-Service winmgmt -StartupType Automatic + + # Add registry keys and values + Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Ole' -Name EnableDCOM -Value "Y" -Type String -Force + Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Ole' -Name LegacyAuthenticationLevel -Value 2 -Type DWord -Force + Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Ole' -Name LegacyImpersonationLevel -Value 3 -Type DWord -Force + + # Delete registry keys + Remove-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Ole' -Name DefaultLaunchPermission -Force -ErrorAction SilentlyContinue + Remove-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Ole' -Name MachineAccessRestriction -Force -ErrorAction SilentlyContinue + Remove-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Ole' -Name MachineLaunchRestriction -Force -ErrorAction SilentlyContinue + + # Stop services + Stop-Service -Name SharedAccess -Force -ErrorAction SilentlyContinue + Stop-Service -Name winmgmt -Force -ErrorAction SilentlyContinue + + # Clear the Wbem Repository + Remove-Item "$env:WINDIR\System32\Wbem\Repository\*.*" -Force -Recurse + + # Register DLLs + $system32Path = Join-Path -Path $env:WINDIR -ChildPath "system32\wbem" + Set-Location $system32Path + regsvr32 /s scecli.dll + regsvr32 /s userenv.dll + + # Compile MOF files + mofcomp cimwin32.mof + mofcomp cimwin32.mfl + mofcomp rsop.mof + mofcomp rsop.mfl + + # Register all DLLs and compile all MOF and MFL files in the current directory and its subdirectories + Get-ChildItem -Path $system32Path -Recurse -Filter *.dll | ForEach-Object { regsvr32 /s $_.FullName } + Get-ChildItem -Path $system32Path -Filter *.mof | ForEach-Object { mofcomp $_.Name } + Get-ChildItem -Path $system32Path -Filter *.mfl | ForEach-Object { mofcomp $_.Name } + + # Additional MOF compilations + mofcomp exwmi.mof + mofcomp -n:root\cimv2\applications\exchange wbemcons.mof + mofcomp -n:root\cimv2\applications\exchange smtpcons.mof + mofcomp exmgmt.mof + + # Upgrade the WMI repository + rundll32 wbemupgd, UpgradeRepository + + # Clear the catroot2 directory and security logs + Stop-Service Cryptsvc -Force -ErrorAction SilentlyContinue + Remove-Item "$env:WINDIR\System32\catroot2\*.*" -Force -Recurse + Remove-Item "C:\WINDOWS\security\logs\*.log" -Force + Start-Service Cryptsvc + + # Reset the performance counter registry settings and rebuild the base performance counters + Set-Location "$env:WINDIR\system32" + lodctr /R + Set-Location "$env:WINDIR\sysWOW64" + lodctr /R + + # Resync WMI performance counters + winmgmt.exe /resyncperf + + # Unregister and reregister the Microsoft Installer + msiexec /unregister + msiexec /regserver + + # Register MSI DLL + regsvr32 /s msi.dll + + # Start the necessary services + Start-Service winmgmt + Start-Service SharedAccess + } + } + "Suspend" { + Write-Output "TODO" + } + "Resume" { + Write-Output "TODO" + } + "HealTpm" { + Write-Output "TODO" + } + "HealKeyBackup" { + #check for procs, use jobs to suppress errors + $dism = Start-Job -ScriptBlock { + Get-Process dism -ErrorAction SilentlyContinue + } + + $sfc = Start-Job -ScriptBlock { + Get-Process sfc -ErrorAction SilentlyContinue + } + + Wait-Job $dism, $sfc | Out-Null + $dismResult = Receive-Job $dism + $sfcResult = Receive-Job $sfc + if($dismResult -or $sfcResult) { + Write-Output "DISM or SFC still running, assume heal in progress" + Remove-Job $dism, $sfc + break + } + else { + $registryPath = "HKLM:\SYSTEM\CurrentControlSet\Control\MiniNT" + if (Test-Path $registryPath) { + Remove-Item $registryPath -Recurse + } + + Start-Process powershell.exe -ArgumentList ` + "-WindowStyle Hidden -Command & { + DISM.exe /Online /Cleanup-image /Restorehealth + sfc /scannow + }" + Write-Output "Started background job to heal key backup." + } + } + } + } + } + Catch { + Write-Error $_.Exception + } + } + + End { + if ($error) { + $error + Exit 1 + } + + Exit 0 + } +} + +if (-Not(Get-Command 'Win_Bitlocker_AIO' -ErrorAction SilentlyContinue)) { + . $MyInvocation.MyCommand.Path +} + +$scriptArgs = @{ + Json = $Json + Info = $Info + Operation = $Operation +} + +Win_Bitlocker_AIO @scriptArgs \ No newline at end of file diff --git a/scripts_wip/Win_Browser_KillFullscreen.ps1 b/scripts_wip/Win_Browser_KillFullscreen.ps1 new file mode 100644 index 00000000..c081f881 --- /dev/null +++ b/scripts_wip/Win_Browser_KillFullscreen.ps1 @@ -0,0 +1,54 @@ +# TODO Not functional, needs work. Trying to stop full screen tech scam sites from going fullscreen. + +Function InstallRequirements { + # Check if NuGet is installed + if (!(Get-PackageProvider -Name NuGet -ListAvailable)) { + Write-Output "Nuget installing" + Install-PackageProvider -Name NuGet -Force + } + else { + Write-Output "Nuget already installed" + } + if (-not (Get-Module -Name RunAsUser -ListAvailable)) { + Write-Output "RunAsUser installing" + Install-Module -Name RunAsUser -Force + } + else { + Write-Output "RunAsUser already installed" + } +} +InstallRequirements + +############# Machine Settings ############################# + +function Set-RegistryValue ($registryPath, $name, $value) { + if (!(Test-Path -Path $registryPath)) { + # Key does not exist, create it + New-Item -Path $registryPath -Force | Out-Null + } + # Set the value + Set-ItemProperty -Path $registryPath -Name $name -Value $value +} + +#FukOffAutoFullscreen +$RegistryPath = "HKLM:\SOFTWARE\Policies\Google\Chrome" +Set-RegistryValue -registryPath $RegistryPath -name "FullscreenAllowed" -value 0 + +############# User Settings ############################# + +Invoke-AsCurrentUser -scriptblock { + + function Set-RegistryValue ($registryPath, $name, $value) { + if (!(Test-Path -Path $registryPath)) { + # Key does not exist, create it + New-Item -Path $registryPath -Force | Out-Null + } + # Set the value + Set-ItemProperty -Path $registryPath -Name $name -Value $value + } + + # Kill Full screen in Firefox + $RegistryPath = "HKCU:\Software\Mozilla\Firefox\Preferences" + Set-RegistryValue -registryPath $RegistryPath -name "full-screen-browsing" -value 0 + +} \ No newline at end of file diff --git a/scripts_wip/Win_CPU_Uptime_Check.ps1 b/scripts_wip/Win_CPU_Uptime_Check.ps1 new file mode 100644 index 00000000..2cbd2638 --- /dev/null +++ b/scripts_wip/Win_CPU_Uptime_Check.ps1 @@ -0,0 +1,26 @@ +<# +.Synopsis + Checks Uptime of the computer +.DESCRIPTION + This was written specifically for use as a "Script Check" in mind, where it the output is deliberaly light unless a warning or error condition is found that needs more investigation. + + If the totalhours of uptime of the computer is greater than or equal to the warning limit, an error is returned. +#> + +[cmdletbinding()] +Param( + [Parameter(Mandatory = $false)] + [int]#Warn if the uptime total hours is over this limit. Defaults to 2.5 days. + $maximumUptimeHoursWarningLimit = 60 +) + +$uptime = (get-Date) - (Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -ExpandProperty LastBootUpTime) + #v7 introduces Get-Uptime, but using WMI is backwards compatiable with v5 + +$hiberbootEnabled = (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power' -Name 'HiberbootEnabled' -ErrorAction Stop).HiberbootEnabled +[bool]$FastStartupEnabled = ($hiberbootEnabled -eq 1) + +"CPU Uptime $([math]::Round($uptime.TotalHours,2)) hours. Fast Startup enabled: $FastStartupEnabled" + +If($uptime.TotalHours -ge $maximumUptimeHoursWarningLimit){ Exit 1 } +Exit 0 \ No newline at end of file diff --git a/scripts_wip/Win_Celldata.ps1 b/scripts_wip/Win_Celldata.ps1 new file mode 100644 index 00000000..2b5039d6 --- /dev/null +++ b/scripts_wip/Win_Celldata.ps1 @@ -0,0 +1,25 @@ +<# +.SYNOPSIS + Gets Cellular info + +.NOTES + v1.0 11/23/2024 silversword411 initial release +#> + +# Ensure the script is running with appropriate permissions to access WMI +try { + # Query the WMI class for cellular information + $WWAN_Data = Get-CimInstance -Namespace "root\cimv2\mdm\dmmap" -ClassName "MDM_DeviceStatus_CellularIdentities01_01" | + Select-Object -Property ICCID, IMSI, InstanceID, PhoneNumber + + if ($WWAN_Data) { + # Output the retrieved cellular data + Write-Output $WWAN_Data + } + else { + Write-Output "No cellular data found." + } +} +catch { + Write-Error "An error occurred while retrieving cellular data: $_" +} \ No newline at end of file diff --git a/scripts_wip/Win_Clear_Office_Cache.ps1 b/scripts_wip/Win_Clear_Office_Cache.ps1 new file mode 100644 index 00000000..fd702331 --- /dev/null +++ b/scripts_wip/Win_Clear_Office_Cache.ps1 @@ -0,0 +1,32 @@ +<# +.SYNOPSIS + Sets the registry setting to force office to clear the local cache of files. +.DESCRIPTION + The reason this script exists is to force applications to pull the cloud version + of a file instead of using the local cache version for files in OneDrive. +.NOTES + Version: 1.0 + Author: redanthrax + Creation Date: 2024-01-18 +#> + +$sids = Get-ChildItem -Path Registry::HKEY_USERS | ` + Where-Object { $_.Name -match 'S-\d-\d+-(\d+-){1,14}\d+$' } | ` + ForEach-Object { $_.Name } +$count = 0 +foreach ($sid in $sids) { + if (Test-Path "Registry::$sid\Software\Microsoft\Office\16.0\Common") { + $options = @{ + Path = "Registry::$sid\Software\Microsoft\Office\16.0\Common\FileIO" + Name = 'AgeOutPolicy' + Value = '1' + } + + Set-ItemProperty @options + $options["Name"] = 'DisableLongTermCaching' + Set-ItemProperty @options + $count += 1 + } +} + +Write-Output "Execution complete. Set for $count user(s)." \ No newline at end of file diff --git a/scripts_wip/Win_Crowdstrike.ps1 b/scripts_wip/Win_Crowdstrike.ps1 new file mode 100644 index 00000000..ec49a6ab --- /dev/null +++ b/scripts_wip/Win_Crowdstrike.ps1 @@ -0,0 +1,131 @@ +<# + .SYNOPSIS + .DESCRIPTION + .EXAMPLE + .NOTES +#> + +Param ( + [string]$InstallerUrl, + + [string]$CommunityCID, + + [switch]$Uninstall +) + +function Win_Crowdstrike { + [CmdletBinding(DefaultParameterSetName = 'InstallSet')] + Param ( + [Parameter(Mandatory = $true, ParameterSetName = 'InstallSet')] + [string]$InstallerUrl, + + [Parameter(Mandatory = $true, ParameterSetName = 'InstallSet')] + [string]$CommunityCID, + + [Parameter(Mandatory = $true, ParameterSetName = 'UninstallSet')] + [switch]$Uninstall + ) + + Begin { + $Apps = @() + $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" + $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" + if ($null -ne ($Apps | Where-Object { $_.DisplayName -Match "CrowdStrike" }) -and -Not($Uninstall)) { + Write-Output "CrowdStrike already installed." + Exit 0 + } + + if ($Uninstall -and $null -eq ($Apps | Where-Object { $_.DisplayName -Match "CrowdStrike" })) { + Write-Output "CrowdStrike already uninstalled" + Exit 0 + } + + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + $random = ([char[]]([char]'a'..[char]'z') + 0..9 | Sort-Object { get-random })[0..12] -join '' + if (-not(Test-Path "C:\packages$random")) { New-Item -ItemType Directory -Force -Path "C:\packages$random" | Out-Null } + } + + Process { + Try { + if ($Uninstall) { + Write-Output "Uninstalling CrowdStrike" + $uninstallString = ($Apps | Where-Object { $_.DisplayName -Match "CrowdStrike" }).UninstallString + foreach ($unstring in $uninstallString) { + if ($unstring -Match "msiexec") { + $unst = $unstring -Split " " + $unst[1] = $unst[1] -Replace '/I', '/X' + Start-Process $unst[0] -ArgumentList $unst[1], "/quiet", "/qn", "/noreboot" -Wait -NoNewWindow + Write-Output "Uninstalled CrowdStrike resource" + } + elseif ($unstring -Match "exe") { + $unstring = "$unstring /quiet" + $pattern = '".*?"' + $matches = [regex]::Matches($unstring, $pattern) + $run = $matches.value + Start-Process $run -ArgumentList "/uninstall", "/quiet" -Wait -NoNewWindow + Write-Output "Uninstalled CrowdStrike resource" + } + } + + Write-Output "Uninstall complete." + return + } + + Write-Output "Starting installation..." + $dest = "C:\packages$random\WindowsSensor.exe" + Write-Output "Downloading file..." + Invoke-WebRequest -Uri $InstallerUrl -OutFile $dest + $arguments = @("/install", "/quiet", "/norestart", "CID=$CommunityCID") + Write-Output "Starting install file..." + $process = Start-Process -NoNewWindow -FilePath $dest -ArgumentList $arguments -PassThru + $timedOut = $null + $process | Wait-Process -Timeout 500 -ErrorAction SilentlyContinue -ErrorVariable timedOut + if ($timedOut) { + $process | Stop-Process + Write-Output "Install timed out after 500 seconds." + } + elseif ($process.ExitCode -ne 0) { + $code = $process.ExitCode + Write-Output "Install error code: $code." + } + } + Catch { + $exception = $_.Exception + Write-Output "Error: $exception" + } + } + + End { + if (Test-Path "C:\packages$random") { + Remove-Item -Path "C:\packages$random" -Recurse -Force + } + + if ($error) { + Exit 1 + } + + Write-Output "Script complete." + Exit 0 + } +} + +if (-not(Get-Command 'Win_Crowdstrike' -ErrorAction SilentlyContinue)) { + . $MyInvocation.MyCommand.Path +} + +$scriptArgs = @{} + +if ($InstallerUrl) { + $scriptArgs = @{ + InstallerUrl = $InstallerUrl + CommunityCID = $CommunityCID + } +} + +if ($Uninstall) { + $scriptArgs = @{ + Uninstall = $Uninstall + } +} + +Win_Crowdstrike @scriptArgs \ No newline at end of file diff --git a/scripts_wip/Win_CyberCNS_Install.ps1 b/scripts_wip/Win_CyberCNS_Install.ps1 index 4c3907b3..a7fe5ec0 100644 --- a/scripts_wip/Win_CyberCNS_Install.ps1 +++ b/scripts_wip/Win_CyberCNS_Install.ps1 @@ -2,63 +2,34 @@ .Synopsis Installs CyberCNS Agent .DESCRIPTION - Downloads the CyberCNS Agent executable and installs based on selection. - Must specify -Type when installing. Probe for the CyberCNS Probe, LightWeight for CyberCNS Lightweight Agent, and Scan for a single scan. - Tenant expects your CyberCNS tenant name, the mycompany part of mycompany.cybercns.com (unless obtained through a third-party like Pax8, in which ase you may have to analyze your URL more closely). - Retrieve the CompanyID, ClientID, and ClientSecret from CyberCNS. + Downloads the CyberCNS Agent executable and installs. .INSTRUCTIONS - 1. Download the CyberCNS executable and upload to a location accessable by your clients. - 2. Navigate to your CyberCNS portal and create a Probe/Agent deployment. - 3. In Tactical RMM, Go to Settings >> Global Settings >> Key Store and create the following custom fields and fill with the required information: - a) CyberCNSExeLocation as type text - this is the location of the agent executable that you downloaded in step 1. - b) CyberCNSTenant as type text - this is your CyberCNS tenant, usually formatted like "tacticalrmm". - c) CyberCNSPortalHost as type text - this is your CyberCNS hostname from the URL like "portaluswest2.mycybercns.com". - 4. In Tactical RMM, Go to Settings >> Global Settings >> Custom Fields and under Clients, create the following custom fields: + 1. Navigate to your CyberCNS portal and create a Probe/Agent deployment. + 2. In Tactical RMM, Go to Settings >> Global Settings >> Custom Fields and under Clients, + create the following custom fields: a) CyberCNSCompanyID as type text - b) CyberCNSClientID as type text - c) CyberCNSClientSecret as type text - 4. In Tactical RMM, Right-click on each client and select Edit. Fill in the CyberCNSCompanyID, CyberCNSClientID, - and CyberCNSClientSecret. - 5. Create the follow script arguments - a) -ExecutableLocation {{global.CyberCNSExeLocation}} - b) -Tenant {{global.CyberCNSTenant}} - c) -CompanyID {{client.CyberCNSCompanyID}} - d) -ClientID {{client.CyberCNSClientID}} - e) -ClientSecret {{client.CyberCNSClientSecret}} - f) -Portal {{global.CyberCNSPortalHost}} - g) -Type Probe|LightWeight|Scan - 6. If you want to trigger an uninstall of the agent, add the following variable: + b) CyberCNSTenantID as type text + 3. In Tactical RMM, Right-click on each client and select Edit. Fill in the + CyberCNSCompanyID and CyberCNSTenantID. + 4. Create the follow script arguments + a) -CompanyID {{client.CyberCNSCompanyID}} + b) -TenantID {{client.CyberCNSTentantID}} + 5. If you want to trigger an uninstall of the agent, add the following variable: a) -Uninstall .NOTES - Version: 1.0 + Version: 1.2 Author: redanthrax Creation Date: 2022-04-07 Updated 2023-01-25 1.1 bionemesis + Updated 2024-01-01 redanthrax for ConnectSecure v4 #> Param( - - [Parameter(Mandatory)] - [string]$ExecutableLocation, - - [Parameter(Mandatory)] - [string]$Tenant, - [Parameter(Mandatory)] [string]$CompanyID, [Parameter(Mandatory)] - [string]$ClientID, - - [Parameter(Mandatory)] - [string]$ClientSecret, - - [Parameter(Mandatory)] - [string]$Portal, - - [Parameter(Mandatory)] - [ValidateSet("Probe", "LightWeight", "Scan")] - $Type, + [string]$TenantID, [switch]$Uninstall ) @@ -66,27 +37,11 @@ Param( function Win_CyberCNS_Install { [CmdletBinding()] Param( - [Parameter(Mandatory)] - [string]$ExecutableLocation, - - [Parameter(Mandatory)] - [string]$Tenant, - [Parameter(Mandatory)] [string]$CompanyID, [Parameter(Mandatory)] - [string]$ClientID, - - [Parameter(Mandatory)] - [string]$ClientSecret, - - [Parameter(Mandatory)] - [string]$Portal, - - [Parameter(Mandatory)] - [ValidateSet("Probe", "LightWeight", "Scan")] - $Type, + [string]$TenantID, [switch]$Uninstall ) @@ -104,38 +59,27 @@ function Win_CyberCNS_Install { return } if ($Uninstall) { - if (Test-Path "C:\Program Files (x86)\CyberCNSAgentV2\cybercnsagentv2.exe.new") { - Move-Item "C:\Program Files (x86)\CyberCNSAgentV2\cybercnsagentv2.exe.new" "C:\Program Files (x86)\CyberCNSAgentV2\cybercnsagentv2.exe" - } - - $monitor = Get-Service -Name "CyberCNSAgentMonitor" -ErrorAction SilentlyContinue - if ($monitor.Length -gt 0) { - Write-Output "Stopping service..." - Stop-Service -Name "CyberCNSAgentMonitor" - Write-Output "Removing service..." - & "sc.exe" delete 'CyberCNSAgentMonitor' - } - - $service = Get-Service -Name "CyberCNSAgentV2" -ErrorAction SilentlyContinue + $service = Get-Service -Name "CyberCNSAgent" -ErrorAction SilentlyContinue if ($service.Length -gt 0) { Write-Output "Stopping service..." - Stop-Service -Name "CyberCNSAgentV2" - & "sc.exe" delete 'CyberCNSAgentV2' + Stop-Service -Name "CyberCNSAgent" + & "sc.exe" delete 'CyberCNSAgent' } - if (Test-Path "C:\Program Files (x86)\CyberCNSAgentV2\cybercnsagentv2.exe") { + if (Test-Path "C:\Program Files (x86)\CyberCNSAgent\cybercnsagent.exe") { Write-Output "Running agent uninstaller..." - & "C:\Program Files (x86)\CyberCNSAgentV2\cybercnsagentv2.exe" -r + & "C:\Program Files (x86)\CyberCNSAgent\cybercnsagent.exe" -r } Write-Output "CyberCNS uninstall complete." return } - $source = $ExecutableLocation + + $source = Invoke-RestMethod "https://configuration.myconnectsecure.com/api/v4/configuration/agentlink?ostype=windows" $destination = "C:\packages$random\cybercnsagent.exe" Invoke-WebRequest -Uri $source -OutFile $destination - $arguments = @("-c $CompanyID", "-a $ClientID", "-s $ClientSecret", "-b $Portal", "-e $Tenant", "-i $Type") + $arguments = @("-c $CompanyID", "-e $TenantID", "-i") $process = Start-Process -NoNewWindow -FilePath $destination -ArgumentList $arguments -PassThru $timedOut = $null $process | Wait-Process -Timeout 300 -ErrorAction SilentlyContinue -ErrorVariable timedOut @@ -172,14 +116,9 @@ if (-not(Get-Command 'Win_CyberCNS_Install' -errorAction SilentlyContinue)) { } $scriptArgs = @{ - ExecutableLocation = $ExecutableLocation - Tenant = $Tenant - CompanyID = $CompanyID - ClientID = $ClientID - ClientSecret = $ClientSecret - Portal = $Portal - Type = $Type - Uninstall = $Uninstall + CompanyID = $CompanyID + TenantID = $TenantID + Uninstall = $Uninstall } Win_CyberCNS_Install @scriptArgs diff --git a/scripts_wip/Win_DISM_SFC_CheckandFix.ps1 b/scripts_wip/Win_DISM_SFC_CheckandFix.ps1 new file mode 100644 index 00000000..9bad5a93 --- /dev/null +++ b/scripts_wip/Win_DISM_SFC_CheckandFix.ps1 @@ -0,0 +1,74 @@ +<# + +.SYNOPSIS + Checks DISM and SFC. Repairs when needed. + +.DESCRIPTION + This is for checking to make sure the backend source files of Windows are in a good state and not corrupted. Also shrinks DISM if features have been removed from windows + +.NOTES + v1.0 11/23/2024 silversword411 Initial release. +#> + +# Perform DISM Health Check +$dismhealth = DISM /Online /Cleanup-Image /ScanHealth + +if ($dismhealth -match "The component store is repairable") { + # Attempt to restore health if repairable + $dismhealthfix = DISM /Online /Cleanup-Image /RestoreHealth + if ($dismhealthfix -match "The restore operation completed successfully") { + Log-Activity -Message "DISM Fixes Successful." -EventName "DISM Health" + Write-Output "DISM Fixes Performed." + } + else { + Write-Output "DISM RestoreHealth failed. Check logs for details." + } +} +elseif ($dismhealth -match "No component store corruption detected") { + Write-Output "DISM Health is good." +} +else { + Write-Output "DISM ScanHealth encountered an unexpected result. Check logs for details." +} + +# DISM Component Store Space Check +$dismspacecheck = DISM /Online /Cleanup-Image /AnalyzeComponentStore + +if ($dismspacecheck -match "Component Store Cleanup Recommended : Yes") { + if ($dismspacecheck -match "Reclaimable Packages : (\d+)") { + $reclaimablePackages = [int]$Matches[1] + if ($reclaimablePackages -gt 4) { + Write-Output "Cleanup needed. Performing cleanup..." + DISM /Online /Cleanup-Image /StartComponentCleanup + Log-Activity -Message "DISM Cleanup Performed" -EventName "DISM Cleanup" + } + else { + Write-Output "Cleanup recommended but reclaimable packages are minimal." + } + } + else { + Write-Output "Cleanup recommended, but reclaimable package count could not be determined." + } +} +else { + Write-Output "Cleanup not needed." +} + + +# SFC +$sfcverify = ($(sfc /verifyonly) -split '' | ? { $_ -and [byte][char]$_ -ne 0 }) -join '' +if ($sfcverify -like "*found integrity violations*") { + Write-Output("SFC found corrupt files. Fixing.") + $sfcfix = ($(sfc /scannow) -split '' | ? { $_ -and [byte][char]$_ -ne 0 }) -join '' + if ($sfcfix -like "*unable to fix*") { + Rmm-Alert -Category 'SFC' -Body 'SFC fixes failed!' + Write-Output("SFC was unable to fix the issues.") + } + else { + Write-Output("SFC repair successful.") + Log-Activity -Message "SFC Fixes Successful!" -EventName "SFC" + } +} +else { + Write-Output("SFC is all good.") +} diff --git a/scripts_wip/Win_Dell_RAIDmonitor.ps1 b/scripts_wip/Win_Dell_RAIDmonitor.ps1 new file mode 100644 index 00000000..5e1e2601 --- /dev/null +++ b/scripts_wip/Win_Dell_RAIDmonitor.ps1 @@ -0,0 +1,109 @@ +<# +.SYNOPSIS + Check Dell PERC RAID status using OpenManage command-line interface (OMSA). + +.DESCRIPTION + This script checks the RAID status of Dell systems using OMSA. It scans for issues in both virtual and physical disks on all controllers and outputs the results. If the `-debug` switch is provided, detailed disk information is also displayed. + +.PARAMETER debug + Switch to enable debug output. + +.NOTES + v1.3 7/17/2024 silversword411 Adding exit conditions, debug, cleaned output +#> + +param ( + [switch]$debug +) + + +# For setting debug output level. -debug switch will set $debug to true +if ($debug) { + $DebugPreference = "Continue" +} +else { + $DebugPreference = "SilentlyContinue" + $ErrorActionPreference = 'silentlycontinue' +} + +# Check Dell RAID status using OpenManage command-line interface (OMSA) + +# Define the OMSA installation directory +$omsaDir = "C:\Program Files\Dell\SysMgt\oma\bin" + +# Change to the OMSA installation directory +Set-Location $omsaDir + +# Initialize variables to track if there are any issues and their reasons +$hasProblems = $false +$problemReasons = @() + +# Get a list of all controllers +$controllerOutput = .\omreport storage controller + +# Extract controller IDs +$controllerIds = $controllerOutput | Select-String "ID" -Context 0, 1 | ForEach-Object { + if ($_.Line -match 'ID\s+:\s+(\d+)') { + $matches[1] + } +} + +# Iterate through each controller ID to list its vdisks and physical disks +foreach ($controllerId in $controllerIds) { + # List vdisks for the current controller + $vdiskList = .\omreport storage vdisk controller=$controllerId + # List physical disks for the current controller + $pdiskList = .\omreport storage pdisk controller=$controllerId + + # Check for issues in the virtual disks + $vdiskList -split "`r`n" | ForEach-Object { + if ($_ -match "Status\s+:\s+Failure Predicted\s+:\s+Yes|State\s+:\s+Failed") { + $hasProblems = $true + $problemReasons += "Virtual Disk issue on Controller ID ${controllerId}: $_" + } + } + + # Check for issues in the physical disks + $pdiskList -split "`r`n" | ForEach-Object { + if ($_ -match "Status\s+:\s+Failure Predicted\s+:\s+Yes|State\s+:\s+Failed") { + $hasProblems = $true + $problemReasons += "Physical Disk issue on Controller ID ${controllerId}: $_" + } + } +} + +function Display-ControllerDisks { + # Display the details after the check + Write-Debug "-----------------------" + foreach ($controllerId in $controllerIds) { + # List vdisks for the current controller + $vdiskList = .\omreport storage vdisk controller=$controllerId + # List physical disks for the current controller + $pdiskList = .\omreport storage pdisk controller=$controllerId + + # Format and display the vdisk list with the controller ID + Write-Host "Controller ID: $controllerId" + Write-Host "Virtual Disks:" + $vdiskList -split "`r`n" | ForEach-Object { " $_" } + + # Format and display the physical disk list for the controller + Write-Host "Physical Disks:" + $pdiskList -split "`r`n" | ForEach-Object { " $_" } + + Write-Host "-----------------------" + } +} + +# Output error or success message at the beginning +if ($hasProblems) { + Write-Host "Problems detected in RAID configuration. Exiting with status code 1." + Write-Host "Reasons:" + $problemReasons | ForEach-Object { Write-Host " $_" } + if ($debug) { Display-ControllerDisks } + exit 1 +} +else { + Write-Host "No problems detected in RAID configuration. Exiting with status code 0." + if ($debug) { Display-ControllerDisks } + exit 0 +} \ No newline at end of file diff --git a/scripts_wip/Win_Disk_HealthCheck.ps1 b/scripts_wip/Win_Disk_HealthCheck.ps1 new file mode 100644 index 00000000..87319f39 --- /dev/null +++ b/scripts_wip/Win_Disk_HealthCheck.ps1 @@ -0,0 +1,84 @@ +<# +.Synopsis + Outputs Drive Health +.DESCRIPTION + This was written specifically for use as a "Script Check" in mind, where it the output is deliberaly light unless a warning or error condition is found that needs more investigation. + + Uses the Windows Storage Reliabilty Counters first (the information behind Settings - Storage - Disks & Volumes - %DiskID% - Drive health) to report on drive health. + + Will exit if running on a virtual machine. + +.NOTES + Learing taken from "Win_Disk_SMART2.ps1" by nullzilla, and modified by: redanthrax +#> + +# Requires -Version 5.0 +# Requires -RunAsAdministrator +[cmdletbinding()] +Param( + [Parameter(Mandatory = $false)] + [int]#Warn if the temperature (in degrees C) is over this limit + $TemperatureWarningLimit = 60, + + [Parameter(Mandatory = $false)] + [int]#Warn if the "wear" of the drive (as a percentage) is above this + $maximumWearAllowance = 20 +) + +BEGIN { + # If this is a virtual machine, we don't need to continue + $Computer = Get-CimInstance -ClassName 'Win32_ComputerSystem' + if ($Computer.Model -like 'Virtual*') { + exit + } + } + +PROCESS { + Try{ + #Using Windows Storage Reliabilty Counters first (the information behind Settings - Storage - Disks & Volumes - %DiskID% - Drive health) + $physicalDisks = Get-PhysicalDisk -ErrorAction Stop + foreach ($disk in $physicalDisks) { + $reliabilityCounter = $null + try { + $reliabilityCounter = $disk | Get-StorageReliabilityCounter -ErrorAction Stop + } + catch { + Write-Error "No Storage Reliability Counter for '$($disk.FriendlyName)'. This usually means the driver/controller isn't exposing it." + } + $driveLetters = (get-disk -FriendlyName $Disk.FriendlyName | Get-Partition | Where-Object -FilterScript {$_.DriveLetter} | Select-Object -Expand DriveLetter) -join ", " + + <# + DriveLetters = $driveLetters + HealthStatus = $disk.HealthStatus + FriendlyName = $disk.FriendlyName + SerialNumber = $disk.SerialNumber + BusType = $disk.BusType + OperationalStatus = ($disk.OperationalStatus -join ", ") + Temperature = $reliabilityCounter.Temperature + Wear = $reliabilityCounter.Wear + ReadErrorsTotal = $reliabilityCounter.ReadErrorsTotal + WriteErrorsTotal = $reliabilityCounter.WriteErrorsTotal + ReallocatedSectors = $reliabilityCounter.ReallocatedSectors + PowerOnHours = $reliabilityCounter.PowerOnHours + #> + If( + $disk.HealthStatus.ToLower() -ne "healthy" -or + ($disk.OperationalStatus | Where-Object -FilterScript { $_.ToLower() -ne "ok" }) -or + $reliabilityCounter.Wear -gt $maximumWearAllowance -or + $reliabilityCounter.Temperature -gt $TemperatureWarningLimit + ){ + "Disk issue: $DriveLetters $($disk.HealthStatus) Status:$(($disk.OperationalStatus -join ", ")) $($reliabilityCounter.Temperature)*C $($reliabilityCounter.Wear)% wear" + Write-Error -Message "Disk issues need investigating" + } else { + "$DriveLetters $($disk.HealthStatus) Status:$(($disk.OperationalStatus -join ", ")) $($reliabilityCounter.Temperature)*C $($reliabilityCounter.Wear)% wear" + } + } + } catch { + Write-Error -Message "Get-PhysicalDisk failed. This can happen on older OS builds or restricted environments." + } +} + +END{ + if ($error) { Exit 1 } + Exit 0 +} \ No newline at end of file diff --git a/scripts_wip/Win_Disk_SMART.ps1 b/scripts_wip/Win_Disk_SMART.ps1 deleted file mode 100644 index fa5ea446..00000000 --- a/scripts_wip/Win_Disk_SMART.ps1 +++ /dev/null @@ -1,126 +0,0 @@ -# From nullzilla - -# Requires -Version 3.0 -# Requires -RunAsAdministrator - -# If this is a virtual machine, we don't need to continue -$Computer = Get-CimInstance -ClassName 'Win32_ComputerSystem' -if ($Computer.Model -like 'Virtual*') { - exit -} - -$disks = (Get-CimInstance -Namespace 'Root\WMI' -ClassName 'MSStorageDriver_FailurePredictStatus' | Select-Object 'InstanceName') - -$Warnings = @() - -foreach ($disk in $disks.InstanceName) { - # Retrieve SMART data - $SmartData = (Get-CimInstance -Namespace 'Root\WMI' -ClassName 'MSStorageDriver_ATAPISMartData' | Where-Object 'InstanceName' -eq $disk) - - [Byte[]]$RawSmartData = $SmartData | Select-Object -ExpandProperty 'VendorSpecific' - - # Starting at the third number (first two are irrelevant) - # get the relevant data by iterating over every 12th number - # and saving the values from an offset of the SMART attribute ID - [PSCustomObject[]]$Output = for ($i = 2; $i -lt $RawSmartData.Count; $i++) { - if (0 -eq ($i - 2) % 12 -and $RawSmartData[$i] -ne 0) { - # Construct the raw attribute value by combining the two bytes that make it up - [Decimal]$RawValue = ($RawSmartData[$i + 6] * [Math]::Pow(2, 8) + $RawSmartData[$i + 5]) - - $InnerOutput = [PSCustomObject]@{ - DiskID = $disk - ID = $RawSmartData[$i] - #Flags = $RawSmartData[$i + 1] - #Value = $RawSmartData[$i + 3] - Worst = $RawSmartData[$i + 4] - RawValue = $RawValue - } - - $InnerOutput - } - } - - # Reallocated Sectors Count - $Warnings += $Output | Where-Object ID -eq 5 | Where-Object RawValue -gt 1 | Format-Table - - # Spin Retry Count - $Warnings += $Output | Where-Object ID -eq 10 | Where-Object RawValue -ne 0 | Format-Table - - # Recalibration Retries - $Warnings += $Output | Where-Object ID -eq 11 | Where-Object RawValue -ne 0 | Format-Table - - # Used Reserved Block Count Total - $Warnings += $Output | Where-Object ID -eq 179 | Where-Object RawValue -gt 1 | Format-Table - - # Erase Failure Count - $Warnings += $Output | Where-Object ID -eq 182 | Where-Object RawValue -ne 0 | Format-Table - - # SATA Downshift Error Count or Runtime Bad Block - $Warnings += $Output | Where-Object ID -eq 183 | Where-Object RawValue -ne 0 | Format-Table - - # End-to-End error / IOEDC - $Warnings += $Output | Where-Object ID -eq 184 | Where-Object RawValue -ne 0 | Format-Table - - # Reported Uncorrectable Errors - $Warnings += $Output | Where-Object ID -eq 187 | Where-Object RawValue -ne 0 | Format-Table - - # Command Timeout - $Warnings += $Output | Where-Object ID -eq 188 | Where-Object RawValue -gt 2 | Format-Table - - # High Fly Writes - $Warnings += $Output | Where-Object ID -eq 189 | Where-Object RawValue -ne 0 | Format-Table - - # Temperature Celcius - $Warnings += $Output | Where-Object ID -eq 194 | Where-Object RawValue -gt 50 | Format-Table - - # Reallocation Event Count - $Warnings += $Output | Where-Object ID -eq 196 | Where-Object RawValue -ne 0 | Format-Table - - # Current Pending Sector Count - $Warnings += $Output | Where-Object ID -eq 197 | Where-Object RawValue -ne 0 | Format-Table - - # Uncorrectable Sector Count - $Warnings += $Output | Where-Object ID -eq 198 | Where-Object RawValue -ne 0 | Format-Table - - # UltraDMA CRC Error Count - $Warnings += $Output | Where-Object ID -eq 199 | Where-Object RawValue -ne 0 | Format-Table - - # Soft Read Error Rate - $Warnings += $Output | Where-Object ID -eq 201 | Where-Object Worst -lt 95 | Format-Table - - # SSD Life Left - $Warnings += $Output | Where-Object ID -eq 231 | Where-Object Worst -lt 50 | Format-Table - - # SSD Media Wear Out Indicator - $Warnings += $Output | Where-Object ID -eq 233 | Where-Object Worst -lt 50 | Format-Table - -} - -$Warnings += Get-CimInstance -Namespace 'Root\WMI' -ClassName 'MSStorageDriver_FailurePredictStatus' | -Select-Object InstanceName, PredictFailure, Reason | -Where-Object { $_.PredictFailure -ne $False } | Format-Table - -$Warnings += Get-CimInstance -ClassName 'Win32_DiskDrive' | -Select-Object Model, SerialNumber, Name, Size, Status | -Where-Object { $_.status -ne 'OK' } | Format-Table - -$Warnings += Get-PhysicalDisk | -Select-Object FriendlyName, Size, MediaType, OperationalStatus, HealthStatus | -Where-Object { $_.OperationalStatus -ne 'OK' -or $_.HealthStatus -ne 'Healthy' } | Format-Table - -if ($Warnings) { - $Warnings = $warnings | Out-String - $Warnings - Write-Host "$Warnings" - Exit 1 -} - -if ($Error) { - if ($Error -match "Not supported") { - $notsup = "You may need to switch from ACHI to RAID/RST mode, see the link for how to do this non-destructively: https://www.top-password.com/blog/switch-from-raid-to-ahci-without-reinstalling-windows/" - $notsup - } - Write-Host "$Error $notsup" - exit 1 -} - diff --git a/scripts_wip/Win_Disk_SMART2.ps1 b/scripts_wip/Win_Disk_SMART2.ps1 deleted file mode 100644 index c255e5a3..00000000 --- a/scripts_wip/Win_Disk_SMART2.ps1 +++ /dev/null @@ -1,215 +0,0 @@ -# Requires -Version 4.0 -# Requires -RunAsAdministrator - -<# -.Synopsis - Outputs SMART data -.DESCRIPTION - Checks the system for a comprehensive list of SMART data. - Will exit on finding a virtual machine. - Use the -Warning flag to only get warnings instead of all data. - Use the -Pretty flag to make the output pretty. -.EXAMPLE - Win_Hardware_Disk_SMART -.EXAMPLE - Win_Hardware_Disk_SMART -Warning -.EXAMPLE - Win_Hardware_Disk_SMART -Warning -Pretty -.NOTES - Version: 1.0 - Author: nullzilla - Modified by: redanthrax -#> - -Param( - [Parameter(Mandatory = $false)] - [switch]$Warning, - - [Parameter(Mandatory = $false)] - [switch]$Pretty -) - -function Win_Hardware_Disk_SMART { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $false)] - [switch]$Warning, - - [Parameter(Mandatory = $false)] - [switch]$Pretty - ) - - Begin { - # If this is a virtual machine, we don't need to continue - $Computer = Get-CimInstance -ClassName 'Win32_ComputerSystem' - if ($Computer.Model -like 'Virtual*') { - exit - } - } - - Process { - Try { - $data = @{} - $disks = (Get-CimInstance -Namespace 'Root\WMI' -ClassName 'MSStorageDriver_FailurePredictStatus' | Select-Object 'InstanceName') - foreach ($disk in $disks.InstanceName) { - $SmartData = (Get-CimInstance -Namespace 'Root\WMI' -ClassName 'MSStorageDriver_ATAPISMartData' | Where-Object 'InstanceName' -eq $disk) - [Byte[]]$RawSmartData = $SmartData | Select-Object -ExpandProperty 'VendorSpecific' - # Starting at the third number (first two are irrelevant) - # get the relevant data by iterating over every 12th number - # and saving the values from an offset of the SMART attribute ID - [PSCustomObject[]]$Output = for ($i = 2; $i -lt $RawSmartData.Count; $i++) { - if (0 -eq ($i - 2) % 12 -and $RawSmartData[$i] -ne 0) { - # Construct the raw attribute value by combining the two bytes that make it up - [Decimal]$RawValue = ($RawSmartData[$i + 6] * [Math]::Pow(2, 8) + $RawSmartData[$i + 5]) - - $InnerOutput = [PSCustomObject]@{ - ID = $RawSmartData[$i] - #Flags = $RawSmartData[$i + 1] - #Value = $RawSmartData[$i + 3] - Worst = $RawSmartData[$i + 4] - RawValue = $RawValue - } - - $InnerOutput - - } - } - - # View full table with - #$Output - - $diskData = [PSCustomObject]@{ - "Reallocated Sector Count" = ($Output | Where-Object ID -eq 5 | Select-Object -ExpandProperty RawValue) - "Spin Retry Count" = ($Output | Where-Object ID -eq 10 | Select-Object -ExpandProperty RawValue) - "Recalibration Retries" = ($Output | Where-Object ID -eq 11 | Select-Object -ExpandProperty RawValue) - "Used Reserved Block Count Total" = ($Output | Where-Object ID -eq 179 | Select-Object -ExpandProperty RawValue) - "Erase Failure Count" = ($Output | Where-Object ID -eq 182 | Select-Object -ExpandProperty RawValue) - "SATA Downshift Error Countor Runtime Bad Block" = ($Output | Where-Object ID -eq 183 | Select-Object -ExpandProperty RawValue) - "End-to-End error / IOEDC" = ($Output | Where-Object ID -eq 184 | Select-Object -ExpandProperty RawValue) - "Reported Uncorrectable Errors" = ($Output | Where-Object ID -eq 187 | Select-Object -ExpandProperty RawValue) - "Command Timeout" = ($Output | Where-Object ID -eq 188 | Select-Object -ExpandProperty RawValue) - "High Fly Writes" = ($Output | Where-Object ID -eq 189 | Select-Object -ExpandProperty RawValue) - "Temperature Celcius" = ($Output | Where-Object ID -eq 194 | Select-Object -ExpandProperty RawValue) - "Reallocation Event Count" = ($Output | Where-Object ID -eq 196 | Select-Object -ExpandProperty RawValue) - "Current Pending Sector Count" = ($Output | Where-Object ID -eq 197 | Select-Object -ExpandProperty RawValue) - "Uncorrectable Sector Count" = ($Output | Where-Object ID -eq 198 | Select-Object -ExpandProperty RawValue) - "UltraDMA CRC Error Count" = ($Output | Where-Object ID -eq 199 | Select-Object -ExpandProperty RawValue) - "Soft Read Error Rate" = ($Output | Where-Object ID -eq 201 | Select-Object -ExpandProperty RawValue) - "SSD Life Left" = ($Output | Where-Object ID -eq 231 | Select-Object -ExpandProperty RawValue) - "SSD Media Wear Out Indicator" = ($Output | Where-Object ID -eq 233 | Select-Object -ExpandProperty RawValue) - "Power On Hours" = ($Output | Where-Object ID -eq 9 | Select-Object -ExpandProperty RawValue) - "FailurePredictStatus" = ( - Get-CimInstance -Namespace 'Root\WMI' -ClassName 'MSStorageDriver_FailurePredictStatus' | - Select-Object PredictFailure, Reason - ) - "DiskDriveOkay" = ( - Get-CimInstance -ClassName 'Win32_DiskDrive' | - Select-Object -ExpandProperty Status - ) - "PhysicalDiskOkayAndHealthy" = ( - Get-PhysicalDisk | - Select-Object OperationalStatus, HealthStatus - ) - } - - $data.Add($disk, $diskData) - } - - #Only output warnings - if ($Warning) { - $warnings = @{} - $data.GetEnumerator() | Foreach-Object { - $diskWarnings = @{} - $_.Value.psobject.Members | ForEach-Object { - $item = $_ - switch ($_.Name) { - #Anything in this section will cause the script to return warning. - "Power On Hours" { if ($null -ne $item.Value -and $item.Value -gt 50000) { $diskWarnings.Add($item.Name, $item.Value) } } #Remove line or adjust 50000 number if you don't want Old drives to start returning Warnings - "Reallocated Sector Count" { if ($null -ne $item.Value -and $item.Value -gt 1) { $diskWarnings.Add($item.Name, $item.Value) } } - "Recalibration Retries" { if ($null -ne $item.Value -and $item.Value -ne 0) { $diskWarnings.Add($item.Name, $item.Value) } } - "Used Reserved Block Count Total" { if ($null -ne $item.Value -and $item.Value -gt 1) { $diskWarnings.Add($item.Name, $item.Value) } } - "Erase Failure Count" { if ($null -ne $item.Value -and $item.Value -ne 0) { $diskWarnings.Add($item.Name, $item.Value) } } - "SATA Downshift Error Countor Runtime Bad Block" { if ($null -ne $item.Value -and $item.Value -ne 0) { $diskWarnings.Add($item.Name, $item.Value) } } - "End-to-End error / IOEDC" { if ($null -ne $item.Value -and $item.Value -ne 0) { $diskWarnings.Add($item.Name, $item.Value) } } - "Reported Uncorrectable Errors" { if ($null -ne $item.Value -and $item.Value -ne 0) { $diskWarnings.Add($item.Name, $item.Value) } } - "Command Timeout" { if ($null -ne $item.Value -and $item.Value -gt 2) { $diskWarnings.Add($item.Name, $item.Value) } } - "High Fly Writes" { if ($null -ne $item.Value -and $item.Value -ne 0) { $diskWarnings.Add($item.Name, $item.Value) } } - "Temperature Celcius" { if ($null -ne $item.Value -and $item.Value -gt 50) { $diskWarnings.Add($item.Name, $item.Value) } } - "Reallocation Event Count" { if ($null -ne $item.Value -and $item.Value -ne 0) { $diskWarnings.Add($item.Name, $item.Value) } } - "Current Pending Sector Count" { if ($null -ne $item.Value -and $item.Value -ne 0) { $diskWarnings.Add($item.Name, $item.Value) } } - "Uncorrectable Sector Count" { if ($null -ne $item.Value -and $item.Value -ne 0) { $diskWarnings.Add($item.Name, $item.Value) } } - "UltraDMA CRC Error Count" { if ($null -ne $item.Value -and $item.Value -ne 0) { $diskWarnings.Add($item.Name, $item.Value) } } - "Soft Read Error Rate" { if ($null -ne $item.Value -and $item.Value -lt 95) { $diskWarnings.Add($item.Name, $item.Value) } } - "SSD Life Left" { if ($null -ne $item.Value -and $item.Value -lt 50) { $diskWarnings.Add($item.Name, $item.Value) } } - "SSD Media Wear Out Indicator" { if ($null -ne $item.Value -and $item.Value -lt 50) { $diskWarnings.Add($item.Name, $item.Value) } } - "FailurePredictStatus" { if ($item.Value | Where-Object PredictFailure -ne $False) { $diskWarnings.Add($item.Name, $item.Value) } } - "DiskDriveOkay" { if ($null -ne $item.Value -and $item.Value -ne 'OK') { $diskWarnings.Add($item.Name, $item.Value) } } - "PhysicalDiskOkayAndHealthy" { if ($item.Value | Where-Object { ($_.OperationalStatus -ne 'OK') -or ($_.HealthStatus -ne 'Healthy') }) { $diskWarnings.Add($item.Name, $item.Value) } } - } - } - - if ($diskWarnings.Count -gt 0) { - $warnings.Add($_.Key, $diskWarnings) - } - } - - if ($warnings.Count -gt 0) { - if ($Pretty) { - foreach ($key in $warnings.Keys) { - Write-Output "Disk: $key" - Write-Output $warnings[$key] - } - } - else { - $warnings - } - Write-Output "Done" - Exit 1 - } - } - else { - if ($Pretty) { - foreach ($key in $data.Keys) { - Write-Output "Disk: $key" - Write-Output $data[$key] - } - } - else { - $data - } - } - } - - Catch { - $exception = $_.Exception - Write-Output "Error: $exception" - Write-Output "Done" - Exit 1 - } - } - - End { - if ($Error) { - if ($Error -match "Not supported") { - Write-Output "You may need to switch from ACHI to RAID/RST mode, see the link for how to do this non-destructively: https://www.top-password.com/blog/switch-from-raid-to-ahci-without-reinstalling-windows/" - } - - Write-Output $Error - Write-Output "Done" - exit 1 - } - Write-Output "Done" - Exit 0 - } -} - -if (-not(Get-Command 'Win_Hardware_Disk_SMART' -errorAction SilentlyContinue)) { - . $MyInvocation.MyCommand.Path -} - -$scriptArgs = @{ - Warning = $Warning - Pretty = $Pretty -} - -Win_Hardware_Disk_SMART @scriptArgs \ No newline at end of file diff --git a/scripts_wip/Win_Disk_Space_Check.ps1 b/scripts_wip/Win_Disk_Space_Check.ps1 index 99a08eb6..d9bb2713 100644 --- a/scripts_wip/Win_Disk_Space_Check.ps1 +++ b/scripts_wip/Win_Disk_Space_Check.ps1 @@ -5,87 +5,60 @@ Long description Checks all FileSystem drives for an amount of space specified (amount is converted to Gigabytes). .EXAMPLE - Win_Disk_Space_Check -Size 10 + Confirm-DiskSpaceAvailable -Size 10 .EXAMPLE - Win_Disk_Space_Check -Size 10 -Percent + Confirm-DiskSpaceAvailable -Size 10 -Percent .NOTES Version: 1.0 Author: redanthrax Creation Date: 2022-04-05 + Updated: Owen Conti 2025-12-12 #> Param( - [Parameter(Mandatory)] - [int]$Size, + [Parameter(Mandatory = $false)] + [int]#The minimum amount of GB that should be available + $Size = 25, [Parameter(Mandatory = $false)] - [switch]$Percent + [switch]#Switches the Size to be a percentage instead of GB + $Percent ) -#Script Version -$sScriptVersion = "1.0" - -function Win_Disk_Space_Check { - [CmdletBinding()] - Param( - [Parameter(Mandatory)] - [int]$Size, +Begin {} - [Parameter(Mandatory = $false)] - [switch]$Percent - ) +Process { + Try { + $errors = 0 + $drives = Get-PSDrive | Where-Object { $_.Provider.Name -eq "FileSystem" -and $_.Used -gt 0 -and $_.Name.ToLower() -ne "temp" } + foreach ($drive in $drives) { + [string]$label = "GB" + [double]$available = 0 + if ($Percent) { + #Percent flag is set + #Calculate percent of free space left on drive + $available = [math]::Round(($drive.Free / ($drive.Free + $drive.Used)) * 100,2) + $label = "%" + } + else { + $available = [math]::Round($drive.Free / 1Gb, 2) + } - Begin {} + "$($drive.Name) $available $label space remaining." - Process { - Try { - $errors = 0 - $drives = Get-PSDrive | Where-Object { $_.Provider.Name -eq "FileSystem" -and $_.Used -gt 0 } - foreach ($drive in $drives) { - if ($Percent) { - #Percent flag is set - #Calculate percent of space left on drive - $remainingPercent = [math]::Round($drive.Used / ($drive.Free + $drive.Used)) - $name = $drive.Name - if ($Size -gt $remainingPercent) { - Write-Output "$remainingPercent% space remaining on $name." - $errors += 1 - } - } - else { - $free = [math]::Round($drive.Free / 1Gb, 2) - $name = $drive.Name - if ($Size -gt $free) { - Write-Output "${free}GB of space on $name." - $errors += 1 - } - } + if ($Size -gt $available) { + $errors += 1 } } - - Catch { - Write-Output "Error: ${$_.Exception}" - Exit 1 - } } - End { - if ($errors -gt 0) { - Exit 1 - } - - Write-Output "All disk space checked and clear." - Exit 0 + Catch { + "ERROR: ${$_.Exception}" + Exit 1 } } -if (-not(Get-Command 'Win_Disk_Space_Check' -errorAction SilentlyContinue)) { - . $MyInvocation.MyCommand.Path +End { + if ($errors -gt 0) { Exit 1 } + Exit 0 } - -$scriptArgs = @{ - Size = $Size - Percent = $Percent -} - -Win_Disk_Space_Check @scriptArgs \ No newline at end of file diff --git a/scripts_wip/Win_DuoAuthLogon_Manage.ps1 b/scripts_wip/Win_DuoAuthLogon_Manage.ps1 index 176b32e6..b73b6aff 100644 --- a/scripts_wip/Win_DuoAuthLogon_Manage.ps1 +++ b/scripts_wip/Win_DuoAuthLogon_Manage.ps1 @@ -27,16 +27,14 @@ Version: 1.1 Author: redanthrax Creation Date: 2022-04-12 + Update Date: 2024-03-22 #> Param( - [Parameter(Mandatory)] [string]$IntegrationKey, - [Parameter(Mandatory)] [string]$SecretKey, - [Parameter(Mandatory)] [string]$ApiHost, [string]$LatestVersion = "4.2.2.1755", @@ -128,15 +126,15 @@ function ConvertTo-StringData { } function Win_DuoAuthLogon_Manage { - [CmdletBinding()] + [CmdletBinding(DefaultParameterSetName = 'InstallSet')] Param( - [Parameter(Mandatory)] + [Parameter(Mandatory = $true, ParameterSetName = 'InstallSet')] [string]$IntegrationKey, - [Parameter(Mandatory)] + [Parameter(Mandatory = $true, ParameterSetName = 'InstallSet')] [string]$SecretKey, - [Parameter(Mandatory)] + [Parameter(Mandatory = $true, ParameterSetName = 'InstallSet')] [string]$ApiHost, [string]$LatestVersion, @@ -162,6 +160,7 @@ function Win_DuoAuthLogon_Manage { [ValidateSet("2", "1", "0")] $UsernameFormat = "1", + [Parameter(Mandatory = $true, ParameterSetName = 'UninstallSet')] [switch]$Uninstall ) @@ -172,6 +171,10 @@ function Win_DuoAuthLogon_Manage { $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" if ($null -ne ($Apps | Where-Object { $_.DisplayName -Match "Duo Authentication" }) -and -Not($Uninstall)) { $duo = $Apps | Where-Object { $_.DisplayName -Match "Duo Authentication" } + if ($duo.GetType().Name -eq "Object[]") { + $duo = $duo[0] + } + if (Compare-SoftwareVersion $duo.DisplayVersion $LatestVersion) { Write-Output "Duo Authentication $($duo.DisplayVersion) already installed." Exit 0 @@ -181,6 +184,11 @@ function Win_DuoAuthLogon_Manage { } } + if ($Uninstall -and $null -eq ($Apps | Where-Object { $_.DisplayName -Match "Duo Authentication" })) { + Write-Output "Duo Authentication already uninstalled" + Exit 0 + } + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $random = ([char[]]([char]'a'..[char]'z') + 0..9 | Sort-Object { get-random })[0..12] -join '' if (-not(Test-Path "C:\packages$random")) { New-Item -ItemType Directory -Force -Path "C:\packages$random" | Out-Null } @@ -188,20 +196,71 @@ function Win_DuoAuthLogon_Manage { Process { Try { - if ($Uninstall -or $Upgrade) { + if ($Uninstall) { + Write-Output "Uninstalling Duo Authentication for Windows" $uninstallString = ($Apps | Where-Object { $_.DisplayName -Match "Duo Authentication" }).UninstallString if ($uninstallString) { - $msiexec, $args = $uninstallString.Split(" ") - Start-Process $msiexec -ArgumentList $args, "/qn" -Wait -NoNewWindow - Write-Output "Uninstalled Duo Authentication for Windows" - if ($Uninstall) { - return + if ($uninstallString.GetType().Name -eq "Object[]") { + foreach ($unst in $uninstallString) { + $m = [regex]::Match($unst, '') + if ($unst -like "*`"*") { + $m = [regex]::Match($unst, '^"([^"]+)"\s*(.*)') + } + else { + $m = [regex]::Match($unst, '^(.*?)\s(.*)$') + } + + $path = $m.Groups[1].Value + $arguments = $m.Groups[2].Value + if ($path.ToLower() -like "*msiexec*") { + Start-Process $path -ArgumentList $arguments, "/quiet", "/qn", "/noreboot" -Wait -NoNewWindow + } + else { + Start-Process $path -ArgumentList $arguments, "/x", "/s", "/v/qn" -Wait -NoNewWindow + } + } + } + else { + $m = [regex]::Match($uninstallString, '^"([^"]+)"\s*(.*)') + $path = $m.Groups[1].Value + $arguments = $m.Groups[2].Value + if ($path.ToLower() -like "*msiexec*") { + Start-Process $path -ArgumentList $arguments, "/quiet", "/qn", "/noreboot" -Wait -NoNewWindow + } + else { + Start-Process $path -ArgumentList $arguments, "/x", "/s", "/v/qn" -Wait -NoNewWindow + } } + + Write-Output "Uninstalled Duo Authentication for Windows" } else { - Write-Output "No uninstall string found." - return + Write-Output "App uninstall via exe." + $destination = "C:\packages$random\duo-win-login-latest.exe" + Invoke-WebRequest -Uri "https://dl.duosecurity.com/duo-win-login-latest.exe" -OutFile $destination + $myargs = "/x", "/s", "/v/qn" + Start-Process "$destination" -ArgumentList $myargs + Start-Sleep -Seconds 5 + Write-Output "Uninstalled Duo Authentication for Windows" + } + + Write-Output "Validating Duo uninstall complete" + + $Apps = @() + $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" + $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" + if ($null -ne ($Apps | Where-Object { $_.DisplayName -Match "Duo Authentication" })) { + Write-Error "Duo detected, uninstall failed" } + else { + Write-Output "Duo not detected, uninstall complete" + } + + return + } + + if ($Upgrade) { + Write-Output "Attempting upgrade of Duo." } Write-Output "Starting installation." @@ -255,6 +314,10 @@ function Win_DuoAuthLogon_Manage { Remove-Item -Path "C:\packages$random" -Recurse -Force } + if ($error) { + Exit 1 + } + Write-Output "Management complete." Exit 0 } @@ -264,19 +327,27 @@ if (-not(Get-Command 'Win_DuoAuthLogon_Manage' -errorAction SilentlyContinue)) { . $MyInvocation.MyCommand.Path } -$scriptArgs = @{ - IntegrationKey = $IntegrationKey - SecretKey = $SecretKey - ApiHost = $ApiHost - LatestVersion = $LatestVersion - AutoPush = $AutoPush - FailOpen = $FailOpen - RdpOnly = $RdpOnly - Smartcard = $Smartcard - WrapSmartcard = $WrapSmartcard - EnableOffline = $EnableOffline - UsernameFormat = $UsernameFormat - Uninstall = $Uninstall +$scriptArgs = @{} + +if ($IntegrationKey) { + $scriptArgs = @{ + IntegrationKey = $IntegrationKey + SecretKey = $SecretKey + ApiHost = $ApiHost + LatestVersion = $LatestVersion + AutoPush = $AutoPush + FailOpen = $FailOpen + RdpOnly = $RdpOnly + Smartcard = $Smartcard + WrapSmartcard = $WrapSmartcard + EnableOffline = $EnableOffline + UsernameFormat = $UsernameFormat + } +} +if ($Uninstall) { + $scriptArgs = @{ + Uninstall = $Uninstall + } } Win_DuoAuthLogon_Manage @scriptArgs \ No newline at end of file diff --git a/scripts_wip/Win_FirefoxAddinInstallDisable.ps1 b/scripts_wip/Win_FirefoxAddinInstallDisable.ps1 new file mode 100644 index 00000000..4defd5df --- /dev/null +++ b/scripts_wip/Win_FirefoxAddinInstallDisable.ps1 @@ -0,0 +1,14 @@ +IF(!(Test-Path $registryPath)) + { + New-Item -Path $registryPath -Force | Out-Null + New-ItemProperty -Path $registryPath -Name $name -Value $value ` + -PropertyType DWORD -Force | Out-Null} + ELSE { + New-ItemProperty -Path $registryPath -Name $name -Value $value ` + -PropertyType DWORD -Force | Out-Null} + +# Disable Firefox Add-in installation +$registryPath = "HKLM:\SOFTWARE\Policies\Mozilla\Firefox\InstallAddonsPermission" +$Name = "Default" +$value = "0" +New-ItemProperty -Path $registryPath -Name $name -Value $value -PropertyType DWORD -Force | Out-Null \ No newline at end of file diff --git a/scripts_wip/Win_LocalAdmin_Manage.ps1 b/scripts_wip/Win_LocalAdmin_Manage.ps1 index f0b49ef6..38f1dd0d 100644 --- a/scripts_wip/Win_LocalAdmin_Manage.ps1 +++ b/scripts_wip/Win_LocalAdmin_Manage.ps1 @@ -4,11 +4,12 @@ .DESCRIPTION This script will check the local administrators group for a list of users in the group. It will try and select the user called "Administrator". The script will also make sure - the account is enabled. + the account is enabled. Once the admin account exists it will remove all others from the local + administrators group. .EXAMPLE Win_LocalAdmin_Manage -LocalAdminUser CompanyAdmin -LocalPassword Password123 .EXAMPLE - Win_LocalAdmin_Manage -LocalAdminUser CompanyAdmin -LocalPassword Password123 -Enforce + Win_LocalAdmin_Manage -LocalAdminUser CompanyAdmin -LocalPassword Password124 -Enforce .INSTRUCTIONS 1. In Tactical RMM, Go to Settings >> Global Settings >> Custom Fields and under Clients, create the following custom fields: @@ -23,6 +24,7 @@ Version: 1.0 Author: redanthrax Creation Date: 2022-05-04 + Updated: 2023-10-24 #> [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "RMM Only Has Cleartext")] @@ -52,18 +54,35 @@ function Win_LocalAdmin_Manage { Begin { $userPrincipal = $null - } + } Process { Try { $adminMembers = ([ADSI]"WinNT://localhost/Administrators,group").Members() | Foreach-Object { ([ADSI]$_).Path.Substring(8).split("/")[1] } - if($adminMembers | Where-Object { $_ -Match $LocalAdminUser }) { + if ($adminMembers | Where-Object { $_ -Match $LocalAdminUser }) { Write-Output "$LocalAdminUser exists." - if($Enforce) { - Write-Output "Disabling all other admins." - foreach($adminMember in $adminMembers) { - if(-Not($adminMember -Match $LocalAdminUser) -and -Not([string]::IsNullOrWhiteSpace($adminMember))) { - Disable-LocalUser -Name $adminMember + if ($Enforce) { + Write-Output "Removing all other admins from Administrator Group." + $adminMembers = $adminMembers | Where-Object { -Not([string]::IsNullOrWhiteSpace($_)) } + foreach ($adminMember in $adminMembers) { + if (-Not($adminMember -Match $LocalAdminUser)) { + if (Get-LocalUser | Where-Object { $_.Name -eq $adminMember }) { + if($adminMember -ne "Administrator") { + Add-LocalGroupMember -Group "Users" -Member $adminMember + Remove-LocalGroupMember -Group Administrators -Member $adminMember + } + } + } + else { + #ensure admin is the sid 500 account + Write-Output "Checking $LocalAdminUser account." + $matchAccount = Get-LocalUser | Where-Object { $_.Name -eq $adminMember } + if (-Not($matchAccount.SID -like "*-500")) { + #account exists but is not admin - remove + Write-Output "Removing $($matchAccount.Name) non-sid 500 account." + Remove-LocalUser $matchAccount + Exit 0 + } } } @@ -77,12 +96,7 @@ function Win_LocalAdmin_Manage { } $adminSID = (Get-WmiObject -Class Win32_UserAccount -Filter "SID like '%-500'").SID - if($null -ne $adminSID) { - Write-Output "Disabling all admins." - foreach($adminMember in $adminMembers) { - Disable-LocalUser -Name $adminMember - } - + if ($null -ne $adminSID) { Write-Output "Found administrator user." $userObject = Get-LocalUser -SID $adminSID Write-Output "Renaming local admin account to $LocalAdminUser." diff --git a/scripts_wip/Win_Login_Audit.ps1 b/scripts_wip/Win_Login_Audit.ps1 deleted file mode 100644 index c7255e11..00000000 --- a/scripts_wip/Win_Login_Audit.ps1 +++ /dev/null @@ -1,71 +0,0 @@ -# Define the Variables 1-3 - -# 1. Enter the beginning of the time range being reviewed. Use the same time format as configured in the endpoint's time & date settings (for example, for USA date&time: MM-DD-YYY hh:mm:ss). - -$StartTime = "12-01-2017 17:00:00" - -# 2. Enter the end of the time range being reviewed. Use the same time format as configured in the endpoint's time & date settings (for example, for USA date&time: MM-DD-YYY hh:mm:ss). - -$EndTime = "12-14-2017 17:00:00" - -# 3. Location of the result file. Make sure the file type is csv. - -$ResultFile = "C:\Temp\LoginAttemptsResultFile.csv" - -# Create the output file and define the column headers. - -"Time Created, Domain\Username, Login Attempt" | Add-Content $ResultFile - -# Query the server for the login events. - -$colEvents = Get-WinEvent -FilterHashtable @{logname='Security'; StartTime="$StartTime"; EndTime="$EndTime"} - -# Iterate through the collection of login events. - -Foreach ($Entry in $colEvents) - -{ - -If (($Entry.Id -eq "4624") -and ($Entry.Properties[8].value -eq "2")) - -{ - -$TimeCreated = $Entry.TimeCreated - -$Domain = $Entry.Properties[6].Value - -$Username = $Entry.Properties[5].Value - -$Result = "$TimeCreated,$Domain\$Username,Interactive Login Success" | Add-Content $ResultFile - -} - -If (($Entry.Id -eq "4624") -and ($Entry.Properties[8].value -eq "10")) - -{ - -$TimeCreated = $Entry.TimeCreated - -$Domain = $Entry.Properties[6].Value - -$Username = $Entry.Properties[5].Value - -$Result = "$TimeCreated,$Domain\$Username,Remote Login Success" | Add-Content $ResultFile - -} - -If ($Entry.Id -eq "4625") - -{ - -$TimeCreated = $Entry.TimeCreated - -$Domain = $Entry.Properties[6].Value - -$Username = $Entry.Properties[5].Value - -$Result = "$TimeCreated,$Domain\$Username,Login Failure" | Add-Content $ResultFile - -} - -} diff --git a/scripts_wip/Win_Login_Auditv2.py b/scripts_wip/Win_Login_Auditv2.py new file mode 100644 index 00000000..cdc9e69f --- /dev/null +++ b/scripts_wip/Win_Login_Auditv2.py @@ -0,0 +1,178 @@ +#!/usr/bin/python3 +# v2.2 11/23/2024 silversword411 Rewrite + +import win32evtlog +from datetime import datetime + + +def is_system_account(username): + """Check if the account is a system account to exclude.""" + system_accounts = [ + "DWM-", + "UMFD-", + "ANONYMOUS LOGON", + "LOCAL SERVICE", + "NETWORK SERVICE", + "SYSTEM", + "Font Driver Host", + ] + for sys_account in system_accounts: + if sys_account in username: + return True + return False + + +def process_events(): + """Collect Logon Type 2 and 10 and system startup/shutdown events into a timeline.""" + events_list = [] + logon_sessions = {} # Key: Logon ID, Value: Event Data + + # Define event IDs and log types + security_logtype = "Security" + system_logtype = "System" + logon_event_id = 4624 + logoff_event_id = 4634 + special_logon_event_id = 4672 + startup_event_ids = [6005] # System startup + shutdown_event_ids = [6006, 6008] # System shutdown and unexpected shutdown + + # Open event logs + server = "localhost" + security_handle = win32evtlog.OpenEventLog(server, security_logtype) + system_handle = win32evtlog.OpenEventLog(server, system_logtype) + + # Read Security events (logon/logoff and special logon) + flags = win32evtlog.EVENTLOG_FORWARDS_READ | win32evtlog.EVENTLOG_SEQUENTIAL_READ + events = True + while events: + events = win32evtlog.ReadEventLog(security_handle, flags, 0) + if events: + for event in events: + event_id = event.EventID & 0xFFFF + time_generated = event.TimeGenerated + strings = event.StringInserts + if event_id == logon_event_id: + # Process logon event + if strings: + logon_type = strings[8] # LogonType + logon_id = strings[7] # TargetLogonId + username = strings[5] # TargetUserName + domain = strings[6] # TargetDomainName + full_username = f"{domain}\\{username}" + # Include Logon Types 2 and 10 + if logon_type in ["2", "10"] and not is_system_account( + username + ): + event_dict = { + "time": time_generated, + "event_type": "Logon", + "user": full_username, + "logon_type": logon_type, + "logon_id": logon_id, + "is_admin": False, # Default to False + } + logon_sessions[logon_id] = event_dict + elif event_id == logoff_event_id: + # Process logoff event + if strings: + logon_type = strings[4] # LogonType + logon_id = strings[3] # SubjectLogonId + username = strings[1] # SubjectUserName + domain = strings[2] # SubjectDomainName + full_username = f"{domain}\\{username}" + if logon_type in ["2", "10"] and not is_system_account( + username + ): + # Logoff events are appended directly to the events list + event_dict = { + "time": time_generated, + "event_type": "Logoff", + "user": full_username, + "logon_type": logon_type, + "logon_id": logon_id, + } + events_list.append(event_dict) + elif event_id == special_logon_event_id: + # Process special logon event + if strings: + # The index for SubjectLogonId may vary, adjust if necessary + logon_id = strings[3] # SubjectLogonId + if logon_id in logon_sessions: + # Mark the session as admin + logon_sessions[logon_id]["is_admin"] = True + else: + break + + # Add the logon sessions to the events list + for logon_event in logon_sessions.values(): + events_list.append(logon_event) + + # Read System events (startup/shutdown) + flags = win32evtlog.EVENTLOG_FORWARDS_READ | win32evtlog.EVENTLOG_SEQUENTIAL_READ + events = True + while events: + events = win32evtlog.ReadEventLog(system_handle, flags, 0) + if events: + for event in events: + event_id = event.EventID & 0xFFFF + time_generated = event.TimeGenerated + if event_id in startup_event_ids: + event_dict = { + "time": time_generated, + "event_type": "System Startup", + "details": "The Event log service was started.", + } + events_list.append(event_dict) + elif event_id in shutdown_event_ids: + if event_id == 6006: + reason = "The Event log service was stopped." + elif event_id == 6008: + reason = "The previous system shutdown was unexpected." + event_dict = { + "time": time_generated, + "event_type": "System Shutdown", + "details": reason, + } + events_list.append(event_dict) + else: + break + + # Close event logs + win32evtlog.CloseEventLog(security_handle) + win32evtlog.CloseEventLog(system_handle) + + # Sort events by time + events_list.sort(key=lambda x: x["time"]) + + # Define logon type descriptions + logon_type_descriptions = { + "2": "Interactive (Console)", + "10": "Remote Interactive (RDP)", + } + + # Output events in chronological order + for event in events_list: + time_str = event["time"].Format() + if event["event_type"] == "Logon": + admin_status = "Admin" if event.get("is_admin", False) else "User" + logon_method = logon_type_descriptions.get(event["logon_type"], "Unknown") + print( + f"{event['event_type']} Event: {time_str}, User: {event['user']}, " + f"Logon Method: {logon_method}, Status: {admin_status}" + ) + elif event["event_type"] == "Logoff": + logon_method = logon_type_descriptions.get(event["logon_type"], "Unknown") + print( + f"{event['event_type']} Event: {time_str}, User: {event['user']}, " + f"Logon Method: {logon_method}" + ) + else: + print(f"{event['event_type']}: {time_str}, Details: {event['details']}") + + +def main(): + process_events() + + +if __name__ == "__main__": + main() diff --git a/scripts_wip/Win_MSStoreUpdates.ps1 b/scripts_wip/Win_MSStoreUpdates.ps1 new file mode 100644 index 00000000..c8c3f55b --- /dev/null +++ b/scripts_wip/Win_MSStoreUpdates.ps1 @@ -0,0 +1,66 @@ +<# +.Synopsis + Sets the MS Store Updates setting +.DESCRIPTION + Toggles auto updates in the MS Store. + Use the -Enabled parameter to enable and omit the parameter to disable. +.NOTES + Version: 1.0 + Author: redanthrax + Creation Date: 2024-04-16 +#> + +Param( + [switch]$Enabled +) + +function Win_MSStoreUpdates { + [CmdletBinding()] + Param( + [switch]$Enabled + ) + + Begin { + $path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsStore\WindowsUpdate" + if (-not (Test-Path $path)) { + New-Item -Path $path -Force | Out-Null + } + + if ($Enabled) { + Set-ItemProperty -Path $path -Name "AutoDownload" -Value 4 -Type DWord | Out-Null + Write-Output "Enabled MS Store Auto Updates" + } + else { + Set-ItemProperty -Path $path -Name "AutoDownload" -Value 2 -Type DWord | Out-Null + Write-Output "Disabled MS Store Auto Updates" + } + } + + Process { + Try { + + } + Catch { + $exception = $_.Exception + Write-Output "Error: $exception" + } + } + + End { + if ($error) { + Exit 1 + } + + Exit 0 + } +} + +if (-not(Get-Command "Win_MSStoreUpdates" -ErrorAction SilentlyContinue)) { + . $MyInvocation.MyCommand.Path +} + +$scriptArgs = @{ + Enabled = $Enabled +} + +Win_MSStoreUpdates @scriptArgs \ No newline at end of file diff --git a/scripts_wip/Win_PerchLogShipper_Install.ps1 b/scripts_wip/Win_PerchLogShipper_Install.ps1 index ca48d6bf..7052e002 100644 --- a/scripts_wip/Win_PerchLogShipper_Install.ps1 +++ b/scripts_wip/Win_PerchLogShipper_Install.ps1 @@ -13,34 +13,90 @@ 3. Create the follow script arguments a) -Token {{client.PerchToken}} .NOTES - Version: 1.0 + Version: 1.1 Author: redanthrax Creation Date: 2022-04-08 + Update Date: 2024-04-16 #> Param( - [Parameter(Mandatory)] [string]$Token, + [string]$LatestVersion = "2023.05.12", + [switch]$Uninstall ) +function Compare-SoftwareVersion { + param ( + [Parameter(Mandatory = $true)] + [string]$Version1, + + [Parameter(Mandatory = $true)] + [string]$Version2 + ) + + # Split the version strings into individual parts + $versionParts1 = $Version1 -split '\.' + $versionParts2 = $Version2 -split '\.' + + # Get the minimum number of parts between the two versions + $minParts = [Math]::Min($versionParts1.Count, $versionParts2.Count) + + # Compare the version parts + for ($i = 0; $i -lt $minParts; $i++) { + $part1 = [int]$versionParts1[$i] + $part2 = [int]$versionParts2[$i] + + if ($part1 -gt $part2) { + return $true + } + elseif ($part1 -lt $part2) { + return $false + } + } + + # If all parts are equal, check the length of the version strings + if ($versionParts1.Count -gt $versionParts2.Count) { + # Check the additional part in Version1 + $additionalPart = $versionParts1[$minParts..($versionParts1.Count - 1)] -join '.' + return ![string]::IsNullOrEmpty($additionalPart) + } + elseif ($versionParts1.Count -lt $versionParts2.Count) { + # Check the additional part in Version2 + $additionalPart = $versionParts2[$minParts..($versionParts2.Count - 1)] -join '.' + return [string]::IsNullOrEmpty($additionalPart) + } + + return $true +} + function Win_PerchLogShipper_Install { - [CmdletBinding()] + [CmdletBinding(DefaultParameterSetName = "InstallSet")] Param( - [Parameter(Mandatory)] + [Parameter(Mandatory = $true, ParameterSetName = "InstallSet")] [string]$Token, + [string]$LatestVersion, + + [Parameter(Mandatory = $true, ParameterSetName = "UninstallSet")] [switch]$Uninstall ) Begin { + $Upgrade = $false $Apps = @() $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" if ($null -ne (Get-Service | Where-Object { $_.DisplayName -Match "perch" }) -and -Not($Uninstall)) { - Write-Output "Perch already installed." - Exit 0 + $perch = $Apps | Where-Object { $_.DisplayName -Match "perch"} + if (Compare-SoftwareVersion $perch.DisplayVersion $LatestVersion) { + Write-Output "Perch $($perch.DisplayVersion) already installed." + Exit 0 + } + else { + $Upgrade = $true + } } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 @@ -64,6 +120,13 @@ function Win_PerchLogShipper_Install { } } + if ($Upgrade) { + Write-Output "Attempting upgrade of Perch Log Shipper" + } + else { + Write-Output "Installing Perch log shipper" + } + $source = "https://cdn.perchsecurity.com/downloads/perch-log-shipper-latest.exe" $destination = "C:\packages$random\perch-log-shipper-latest.exe" Invoke-WebRequest -Uri $source -OutFile $destination @@ -81,6 +144,13 @@ function Win_PerchLogShipper_Install { Write-Output "Install error code: $code." Exit 1 } + + if ($Upgrade) { + Write-Output "Perch log shipper upgraded" + } + else { + Write-Output "Perch log shipper installed" + } } Catch { $exception = $_.Exception @@ -94,6 +164,10 @@ function Win_PerchLogShipper_Install { Remove-Item -Path "C:\packages$random" -Recurse -Force } + if ($error) { + Exit 1 + } + Exit 0 } } @@ -101,10 +175,18 @@ function Win_PerchLogShipper_Install { if (-not(Get-Command 'Win_PerchLogShipper_Install' -errorAction SilentlyContinue)) { . $MyInvocation.MyCommand.Path } - -$scriptArgs = @{ - Token = $Token - Uninstall = $Uninstall + +$scriptArgs = @{} +if ($Token) { + $scriptArgs = @{ + Token = $Token + LatestVersion = $LatestVersion + } +} +if ($Uninstall) { + $scriptArgs = @{ + Uninstall = $Uninstall + } } Win_PerchLogShipper_Install @scriptArgs \ No newline at end of file diff --git a/scripts_wip/Win_PrinterInstaller.ps1 b/scripts_wip/Win_PrinterInstaller.ps1 new file mode 100644 index 00000000..8006d54b --- /dev/null +++ b/scripts_wip/Win_PrinterInstaller.ps1 @@ -0,0 +1,156 @@ +<# +.SYNOPSIS + Installs printers via name and IP +.DESCRIPTION +.INSTRUCTIONS +.NOTES + Version: 1.0 + Creation Date: 2022-02-14 +#> + +Param( + [Parameter(Mandatory)] + [string]$PrinterNames, + + [Parameter(Mandatory)] + [string]$PrinterIPs, + + [Parameter(Mandatory)] + [string]$DriverNames, + + [Parameter(Mandatory)] + [string]$DriverLocations, + + [switch]$Force +) + +function Win_PrinterInstaller { + [CmdletBinding()] + Param( + [Parameter(Mandatory)] + [string]$PrinterNames, + + [Parameter(Mandatory)] + [string]$PrinterIPs, + + [Parameter(Mandatory)] + [string]$DriverNames, + + [Parameter(Mandatory)] + [string]$DriverLocations, + + [switch]$Force + ) + + Begin { + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + $random = ([char[]]([char]'a'..[char]'z') + 0..9 | sort { get-random })[0..12] -join '' + if (-not(Test-Path "C:\packages$random")) { New-Item -ItemType Directory -Force -Path "C:\packages$random" | Out-Null } + #test for install + #check params, expecting comma separated values + $pn = @($PrinterNames -Split ",") + $pi = @($PrinterIPs -Split ",") + if ($pn.Length -ne $pi.Length) { + Write-Error "Printer names and IPs must have the same count." + return + } + else { + Write-Output "$($pn.Length) printer(s) specified." + } + + $dn = @($DriverNames -Split ",") + if ($pn.Length -ne $dn.Length) { + Write-Error "Printer names and Driver names must have the same count." + } + + $dl = @() + if ($DriverLocations.Length -gt 0) { + $dl = @($DriverLocations -Split ",") + if ($pn.Length -ne $dl.Length) { + Write-Error "Printer Names and Drivers must have the same count." + return + } + } + else { + Write-Error "No drivers specified." + } + } + + Process { + Try { + #do install + for ($i = 0; $i -le $pn.Length; $i++) { + #Check if driver location is web address for download + if ($dl[$i].StartsWith("https://")) { + Write-Output "Downloading printer driver zip." + Invoke-RestMethod $dl[$i] -OutFile "C:\packages$random\driver.zip" + $dl[$i] = "C:\packages$random\" + Expand-Archive -Path "C:\packages$random\driver.zip" -DestinationPath $dl[$i] -Force + } + + if ($Force) { + $port = Get-PrinterPort -Name "$($pn[$i]) Port" -ErrorAction SilentlyContinue + if ($port) { + Get-Printer | Where-Object { $_.PortName -eq "$($pn[$i]) Port" } | Remove-Printer + Remove-PrinterPort -Name "$($pn[$i]) Port" + } + } + + #add drivers to windows + Write-Output "Installing printer driver." + $inf = Get-ChildItem -Path $dl[$i] -Recurse -Filter "*.inf" | ForEach-Object { + $p = $_.FullName + $pnp = & pnputil.exe /add-driver $p /install | Out-String + $null = $pnp -match '(?m)^Published Name:\s+(.+)$' + $matches[1] + return $matches[1] + } + + $inf[-1] = $inf[-1] -replace "`t|`n|`r" + $loc = (Get-WindowsDriver -Online | Where-Object { $_.Driver -match $inf[-1] }).OriginalFileName + Add-PrinterDriver -Name $dn[$i] -InfPath $loc -ErrorAction Stop + Write-Output "Printer driver installation complete." + Write-Output "Adding printer port." + Add-PrinterPort -Name "$($pn[$i]) Port" -PrinterHostAddress $pi[$i] + Write-Output "Printer port added." + $printerArgs = @{ + DriverName = $dn[$i] + Name = $pn[$i] + PortName = "$($pn[$i]) Port" + } + + Write-Output "Installing printer." + Add-Printer @printerArgs + Write-Output "$($pn[$i]) added." + } + + Write-Output "Printer installation complete." + } + Catch { + $exception = $_.Exception + Write-Output "Error: $exception" + } + } + + End { + if (Test-Path "C:\packages$random") { + Remove-Item -Path "C:\packages$random" -Recurse -Force + } + + Exit 0 + } +} + +if (-Not(Get-Command 'Win_PrinterInstaller' -ErrorAction SilentlyContinue)) { + . $MyInvocation.MyCommand.Path +} + +$scriptArgs = @{ + PrinterNames = $PrinterNames + PrinterIPs = $PrinterIPs + DriverNames = $DriverNames + DriverLocations = $DriverLocations + Force = $Force +} + +Win_PrinterInstaller @scriptArgs \ No newline at end of file diff --git a/scripts_wip/Win_RAM_Available_Check.ps1 b/scripts_wip/Win_RAM_Available_Check.ps1 new file mode 100644 index 00000000..02532ffb --- /dev/null +++ b/scripts_wip/Win_RAM_Available_Check.ps1 @@ -0,0 +1,36 @@ +<# +.Synopsis + Checks the available amount of RAM on a computer +.DESCRIPTION + This was written specifically for use as a "Script Check" in mind, where it the output is deliberaly light unless a warning or error condition is found that needs more investigation. + + If the total available (free) amount of RAM is less than the warning limit, an error is returned. + +#> + +[cmdletbinding()] +Param( + [Parameter(Mandatory = $false)] + [double]#Warn if the amount of available RAM (defaults to GB) is below this limit. Defaults to 1 GB. + $minimumAvailableRAM = 0.75, + + [Parameter(Mandatory = $false)] + [switch]#Use percentage instead of absolute GB values + $percent +) + +$os = Get-CimInstance -ClassName Win32_OperatingSystem + +$available = [math]::Round(($os.FreePhysicalMemory * 1KB) / 1GB, 2) +$label = "GB" +if ($Percent) { + #Percent flag is set + #Calculate percent of free available RAM + $available = [math]::Round(($os.FreePhysicalMemory / $os.TotalVisibleMemorySize) * 100, 1) + $label = "%" +} + +"$available $label RAM available." + +If($minimumAvailableRAM -gt $available){ Exit 1 } +Exit 0 \ No newline at end of file diff --git a/scripts_wip/Win_Reboot_Request_via_toast.ps1 b/scripts_wip/Win_Reboot_Request_via_toast.ps1 new file mode 100644 index 00000000..59df1ee1 --- /dev/null +++ b/scripts_wip/Win_Reboot_Request_via_toast.ps1 @@ -0,0 +1,58 @@ +#Checking if ToastReboot:// protocol handler is present +New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT -erroraction silentlycontinue | out-null +$ProtocolHandler = get-item 'HKCR:\ToastReboot' -erroraction 'silentlycontinue' +if (!$ProtocolHandler) { + #create handler for reboot + New-item 'HKCR:\ToastReboot' -force + set-itemproperty 'HKCR:\ToastReboot' -name '(DEFAULT)' -value 'url:ToastReboot' -force + set-itemproperty 'HKCR:\ToastReboot' -name 'URL Protocol' -value '' -force + new-itemproperty -path 'HKCR:\ToastReboot' -propertytype dword -name 'EditFlags' -value 2162688 + New-item 'HKCR:\ToastReboot\Shell\Open\command' -force + set-itemproperty 'HKCR:\ToastReboot\Shell\Open\command' -name '(DEFAULT)' -value 'C:\Windows\System32\shutdown.exe -r -t 00' -force +} + +# Check if NuGet is installed +if (!(Get-PackageProvider -Name NuGet -ListAvailable)) { + Write-Output "Nuget installing" + Install-PackageProvider -Name NuGet -Force +} +else { + Write-Output "Nuget already installed" +} +if (-not (Get-Module -Name BurntToast -ListAvailable)) { + Write-Output "BurntToast installing" + Install-Module -Name BurntToast -Force +} +else { + Write-Output "BurntToast already installed" +} + +if (-not (Get-Module -Name RunAsUser -ListAvailable)) { + Write-Output "RunAsUser installing" + Install-Module -Name RunAsUser -Force +} +else { + Write-Output "RunAsUser already installed" +} + +invoke-ascurrentuser -scriptblock { + + $heroimage = New-BTImage -Source 'https://imageurl.png' -HeroImage + $Text1 = New-BTText -Content "Message from Computer Dudez" + $Text2 = New-BTText -Content "Updates have been installed and a reboot is needed. Please select if you'd like to reboot now, or snooze this message for later. Call if you have any questions. 867-5309" + $Button = New-BTButton -Content "Snooze" -snooze -id 'SnoozeTime' + $Button2 = New-BTButton -Content "Reboot now" -Arguments "ToastReboot:" -ActivationType Protocol + $5Min = New-BTSelectionBoxItem -Id 5 -Content '5 minutes' + $10Min = New-BTSelectionBoxItem -Id 10 -Content '10 minutes' + $1Hour = New-BTSelectionBoxItem -Id 60 -Content '1 hour' + $4Hour = New-BTSelectionBoxItem -Id 240 -Content '4 hours' + $8Hour = New-BTSelectionBoxItem -Id 480 -Content '8 hours' + $1Day = New-BTSelectionBoxItem -Id 1440 -Content '1 day' + $Items = $5Min, $10Min, $1Hour, $4Hour, $8Hour, $1Day + $SelectionBox = New-BTInput -Id 'SnoozeTime' -DefaultSelectionBoxItemId 10 -Items $Items + $action = New-BTAction -Buttons $Button, $Button2 -inputs $SelectionBox + $Binding = New-BTBinding -Children $text1, $text2 -HeroImage $heroimage + $Visual = New-BTVisual -BindingGeneric $Binding + $Content = New-BTContent -Visual $Visual -Actions $action + Submit-BTNotification -Content $Content +} \ No newline at end of file diff --git a/scripts_wip/Win_Reboot_Request_via_toast_andforce.ps1 b/scripts_wip/Win_Reboot_Request_via_toast_andforce.ps1 new file mode 100644 index 00000000..89d8772a --- /dev/null +++ b/scripts_wip/Win_Reboot_Request_via_toast_andforce.ps1 @@ -0,0 +1,57 @@ +#Checking if ToastReboot:// protocol handler is present +New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT -erroraction silentlycontinue | out-null +$ProtocolHandler = get-item 'HKCR:\ToastReboot' -erroraction 'silentlycontinue' +if (!$ProtocolHandler) { + #create handler for reboot + New-item 'HKCR:\ToastReboot' -force + set-itemproperty 'HKCR:\ToastReboot' -name '(DEFAULT)' -value 'url:ToastReboot' -force + set-itemproperty 'HKCR:\ToastReboot' -name 'URL Protocol' -value '' -force + new-itemproperty -path 'HKCR:\ToastReboot' -propertytype dword -name 'EditFlags' -value 2162688 + New-item 'HKCR:\ToastReboot\Shell\Open\command' -force + set-itemproperty 'HKCR:\ToastReboot\Shell\Open\command' -name '(DEFAULT)' -value 'C:\Windows\System32\shutdown.exe -r -t 00' -force +} + +# Always run the shutdown command +Invoke-Expression -Command "shutdown /r /t 600" + +# Check if NuGet is installed +if (!(Get-PackageProvider -Name NuGet -ListAvailable)) { + Write-Output "Nuget installing" + Install-PackageProvider -Name NuGet -Force +} +else { + Write-Output "Nuget already installed" +} +if (-not (Get-Module -Name BurntToast -ListAvailable)) { + Write-Output "BurntToast installing" + Install-Module -Name BurntToast -Force +} +else { + Write-Output "BurntToast already installed" +} + +if (-not (Get-Module -Name RunAsUser -ListAvailable)) { + Write-Output "RunAsUser installing" + Install-Module -Name RunAsUser -Force +} +else { + Write-Output "RunAsUser already installed" +} + +invoke-ascurrentuser -scriptblock { + + $heroimage = New-BTImage -Source 'https://imageurl.png' -HeroImage + $Text1 = New-BTText -Content "Message from Computer Dudez" + $Text2 = New-BTText -Content "Emergency Updates have been installed for a critical bug and a reboot is required. Please reboot now. Call if you have any questions. 867-5309" + $Button = New-BTButton -Content "Snooze" -snooze -id 'SnoozeTime' + $Button2 = New-BTButton -Content "Reboot now" -Arguments "ToastReboot:" -ActivationType Protocol + $5Min = New-BTSelectionBoxItem -Id 5 -Content '5 minutes' + $10Min = New-BTSelectionBoxItem -Id 10 -Content '10 minutes' + $Items = $5Min, $10Min + $SelectionBox = New-BTInput -Id 'SnoozeTime' -DefaultSelectionBoxItemId 10 -Items $Items + $action = New-BTAction -Buttons $Button, $Button2 -inputs $SelectionBox + $Binding = New-BTBinding -Children $text1, $text2 -HeroImage $heroimage + $Visual = New-BTVisual -BindingGeneric $Binding + $Content = New-BTContent -Visual $Visual -Actions $action + Submit-BTNotification -Content $Content +} \ No newline at end of file diff --git a/scripts_wip/Win_Reboot_usingIdleandUptime.ps1 b/scripts_wip/Win_Reboot_usingIdleandUptime.ps1 new file mode 100644 index 00000000..c671a89b --- /dev/null +++ b/scripts_wip/Win_Reboot_usingIdleandUptime.ps1 @@ -0,0 +1,351 @@ +#Reboot Device Upon The User’s Preferences: Wait, reboot at 18:00 or reboot now. The prompt mesage and colors can be changed upon your choice + + +$days = 7 +$system = Get-WmiObject win32_operatingsystem + +if($system.ConvertToDateTime($system.LastBootUpTime) -lt (Get-Date).AddDays(-$days)){ + #---------------------------------------------- +#region Import Assemblies +#---------------------------------------------- +[void][Reflection.Assembly]::Load('System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089') +[void][Reflection.Assembly]::Load('System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089') +[void][Reflection.Assembly]::Load('System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a') +#endregion Import Assemblies + + +#Define a Param block to use custom parameters in the project +#Param ($CustomParameter) + +function Main { +<# + .SYNOPSIS + The Main function starts the project application. + + .PARAMETER Commandline + $Commandline contains the complete argument string passed to the script packager executable. + + .NOTES + Use this function to initialize your script and to call GUI forms. + + .NOTES + To get the console output in the Packager (Forms Engine) use: + $ConsoleOutput (Type: System.Collections.ArrayList) +#> + Param ([String]$Commandline) + + #-------------------------------------------------------------------------- + #TODO: Add initialization script here (Load modules and check requirements) + + + #-------------------------------------------------------------------------- + + if((Call-MainForm_psf) -eq 'OK') + { + + } + + $global:ExitCode = 0 #Set the exit code for the Packager +} + + + + + + + +#endregion Source: Startup.pss + +#region Source: MainForm.psf +function Call-MainForm_psf +{ + + #---------------------------------------------- + #region Import the Assemblies + #---------------------------------------------- + [void][reflection.assembly]::Load('System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089') + [void][reflection.assembly]::Load('System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089') + [void][reflection.assembly]::Load('System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a') + #endregion Import Assemblies + + #---------------------------------------------- + #region Generated Form Objects + #---------------------------------------------- + [System.Windows.Forms.Application]::EnableVisualStyles() + $MainForm = New-Object 'System.Windows.Forms.Form' + $panel2 = New-Object 'System.Windows.Forms.Panel' + $ButtonCancel = New-Object 'System.Windows.Forms.Button' + $ButtonSchedule = New-Object 'System.Windows.Forms.Button' + $ButtonRestartNow = New-Object 'System.Windows.Forms.Button' + $panel1 = New-Object 'System.Windows.Forms.Panel' + $labelITSystemsMaintenance = New-Object 'System.Windows.Forms.Label' + $labelSecondsLeftToRestart = New-Object 'System.Windows.Forms.Label' + $labelTime = New-Object 'System.Windows.Forms.Label' + $labelInOrderToApplySecuri = New-Object 'System.Windows.Forms.Label' + $timerUpdate = New-Object 'System.Windows.Forms.Timer' + $InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState' + #endregion Generated Form Objects + + #---------------------------------------------- + # User Generated Script + #---------------------------------------------- + $TotalTime = 1500 #in seconds + + $MainForm_Load={ + #TODO: Initialize Form Controls here + $labelTime.Text = "{0:D2}" -f $TotalTime #$TotalTime + #Add TotalTime to current time + $script:StartTime = (Get-Date).AddSeconds($TotalTime) + #Start the timer + $timerUpdate.Start() + } + + + $timerUpdate_Tick={ + # Define countdown timer + [TimeSpan]$span = $script:StartTime - (Get-Date) + #Update the display + $labelTime.Text = "{0:N0}" -f $span.TotalSeconds + $timerUpdate.Start() + if ($span.TotalSeconds -le 0) + { + $timerUpdate.Stop() + Restart-Computer -Force + } + + } + + $ButtonRestartNow_Click = { + # Restart the computer immediately + Restart-Computer -Force + } + + $ButtonSchedule_Click={ + # Schedule restart for 6pm + if(Get-ScheduledTask -TaskName "auto shutdown my computer" -ErrorAction SilentlyContinue){Get-ScheduledTask -TaskName "auto shutdown my computer" | Unregister-ScheduledTask -Confirm:$false} + if((schtasks /create /sc once /tn "auto shutdown my computer" /tr "shutdown /r /d p:1:1 /c 'Initiating reboot since the device has not been rebooted for 7 days'" /st 18:00) -like "*Success*"){ + $SetT=Get-ScheduledTask -TaskName "auto shutdown my computer" + $SetT.Triggers[0].EndBoundary=[DateTime]::Now.Date.ToString("yyyy-MM-dd")+"T"+"19:00:00" + $SetT.Settings.DeleteExpiredTaskAfter ='PT0S' + Set-ScheduledTask -InputObject $SetT + } + $MainForm.Close() + } + + $ButtonCancel_Click={ + #TODO: Place custom script here + $MainForm.Close() + } + + $labelITSystemsMaintenance_Click={ + #TODO: Place custom script here + + } + + $panel2_Paint=[System.Windows.Forms.PaintEventHandler]{ + #Event Argument: $_ = [System.Windows.Forms.PaintEventArgs] + #TODO: Place custom script here + + } + + $labelTime_Click={ + #TODO: Place custom script here + + } + # --End User Generated Script-- + #---------------------------------------------- + #region Generated Events + #---------------------------------------------- + + $Form_StateCorrection_Load= + { + #Correct the initial state of the form to prevent the .Net maximized form issue + $MainForm.WindowState = $InitialFormWindowState + } + + $Form_StoreValues_Closing= + { + #Store the control values + } + + + $Form_Cleanup_FormClosed= + { + #Remove all event handlers from the controls + try + { + $ButtonCancel.remove_Click($buttonCancel_Click) + $ButtonSchedule.remove_Click($ButtonSchedule_Click) + $ButtonRestartNow.remove_Click($ButtonRestartNow_Click) + $panel2.remove_Paint($panel2_Paint) + $labelITSystemsMaintenance.remove_Click($labelITSystemsMaintenance_Click) + $labelTime.remove_Click($labelTime_Click) + $MainForm.remove_Load($MainForm_Load) + $timerUpdate.remove_Tick($timerUpdate_Tick) + $MainForm.remove_Load($Form_StateCorrection_Load) + $MainForm.remove_Closing($Form_StoreValues_Closing) + $MainForm.remove_FormClosed($Form_Cleanup_FormClosed) + } + catch [Exception] + { } + } + #endregion Generated Events + + #---------------------------------------------- + #region Generated Form Code + #---------------------------------------------- + $MainForm.SuspendLayout() + $panel2.SuspendLayout() + $panel1.SuspendLayout() + # + # MainForm + # + $MainForm.Controls.Add($panel2) + $MainForm.Controls.Add($panel1) + $MainForm.Controls.Add($labelSecondsLeftToRestart) + $MainForm.Controls.Add($labelTime) + $MainForm.Controls.Add($labelInOrderToApplySecuri) + $MainForm.AutoScaleDimensions = '6, 13' + $MainForm.AutoScaleMode = 'Font' + $MainForm.BackColor = 'White' + $MainForm.ClientSize = '373, 279' + $MainForm.MaximizeBox = $False + $MainForm.MinimizeBox = $False + $MainForm.Name = 'MainForm' + $MainForm.ShowIcon = $False + $MainForm.ShowInTaskbar = $False + $MainForm.StartPosition = 'CenterScreen' + $MainForm.Text = 'MSP Name' + $MainForm.TopMost = $True + $MainForm.add_Load($MainForm_Load) + # + # panel2 + # + $panel2.Controls.Add($ButtonCancel) + $panel2.Controls.Add($ButtonSchedule) + $panel2.Controls.Add($ButtonRestartNow) + $panel2.BackColor = 'ScrollBar' + $panel2.Location = '0, 205' + $panel2.Name = 'panel2' + $panel2.Size = '378, 80' + $panel2.TabIndex = 9 + $panel2.add_Paint($panel2_Paint) + # + # ButtonCancel + # + $ButtonCancel.Location = '250, 17' + $ButtonCancel.Name = 'ButtonCancel' + $ButtonCancel.Size = '77, 45' + $ButtonCancel.TabIndex = 7 + $ButtonCancel.Text = 'Wait' + $ButtonCancel.UseVisualStyleBackColor = $True + $ButtonCancel.add_Click($buttonCancel_Click) + # + # ButtonSchedule + # + $ButtonSchedule.Font = 'Microsoft Sans Serif, 8.25pt, style=Bold' + $ButtonSchedule.Location = '139, 17' + $ButtonSchedule.Name = 'ButtonSchedule' + $ButtonSchedule.Size = '105, 45' + $ButtonSchedule.TabIndex = 6 + $ButtonSchedule.Text = 'Reboot at 18:00' + $ButtonSchedule.UseVisualStyleBackColor = $True + $ButtonSchedule.add_Click($ButtonSchedule_Click) + # + # ButtonRestartNow + # + $ButtonRestartNow.Font = 'Microsoft Sans Serif, 8.25pt, style=Bold' + $ButtonRestartNow.ForeColor = 'DarkRed' + $ButtonRestartNow.Location = '42, 17' + $ButtonRestartNow.Name = 'ButtonRestartNow' + $ButtonRestartNow.Size = '91, 45' + $ButtonRestartNow.TabIndex = 0 + $ButtonRestartNow.Text = 'Reboot' + $ButtonRestartNow.UseVisualStyleBackColor = $True + $ButtonRestartNow.add_Click($ButtonRestartNow_Click) + # + # panel1 + # + $panel1.Controls.Add($labelITSystemsMaintenance) + $panel1.BackColor = '22, 54, 36' + $panel1.Location = '0, 0' + $panel1.Name = 'panel1' + $panel1.Size = '375, 67' + $panel1.TabIndex = 8 + # + # labelITSystemsMaintenance + # + $labelITSystemsMaintenance.Font = 'Microsoft Sans Serif, 14.25pt' + $labelITSystemsMaintenance.ForeColor = 'White' + $labelITSystemsMaintenance.Location = '11, 18' + $labelITSystemsMaintenance.Name = 'labelITSystemsMaintenance' + $labelITSystemsMaintenance.Size = '269, 23' + $labelITSystemsMaintenance.TabIndex = 1 + $labelITSystemsMaintenance.Text = 'MSP Name' + $labelITSystemsMaintenance.TextAlign = 'MiddleLeft' + $labelITSystemsMaintenance.add_Click($labelITSystemsMaintenance_Click) + # + # labelSecondsLeftToRestart + # + $labelSecondsLeftToRestart.AutoSize = $True + $labelSecondsLeftToRestart.Font = 'Microsoft Sans Serif, 9pt, style=Bold' + $labelSecondsLeftToRestart.Location = '87, 176' + $labelSecondsLeftToRestart.Name = 'labelSecondsLeftToRestart' + $labelSecondsLeftToRestart.Size = '155, 15' + $labelSecondsLeftToRestart.TabIndex = 5 + $labelSecondsLeftToRestart.Text = 'Seconds to reboot :' + # + # labelTime + # + $labelTime.AutoSize = $True + $labelTime.Font = 'Microsoft Sans Serif, 9pt, style=Bold' + $labelTime.ForeColor = '192, 0, 0' + $labelTime.Location = '237, 176' + $labelTime.Name = 'labelTime' + $labelTime.Size = '43, 15' + $labelTime.TabIndex = 3 + $labelTime.Text = '00:60' + $labelTime.TextAlign = 'MiddleCenter' + $labelTime.add_Click($labelTime_Click) + # + # labelInOrderToApplySecuri + # + $labelInOrderToApplySecuri.Font = 'Microsoft Sans Serif, 9pt' + $labelInOrderToApplySecuri.Location = '12, 84' + $labelInOrderToApplySecuri.Name = 'labelInOrderToApplySecuri' + $labelInOrderToApplySecuri.Size = '350, 83' + $labelInOrderToApplySecuri.TabIndex = 2 + $labelInOrderToApplySecuri.Text = 'Every 7 days your PC should be restarted for maintenance and updates. + +If this does not fit, you can press wait or restart at. 6:00 p.m.' + # + # timerUpdate + # + $timerUpdate.add_Tick($timerUpdate_Tick) + $panel1.ResumeLayout() + $panel2.ResumeLayout() + $MainForm.ResumeLayout() + #endregion Generated Form Code + + #---------------------------------------------- + + #Save the initial state of the form + $InitialFormWindowState = $MainForm.WindowState + #Init the OnLoad event to correct the initial state of the form + $MainForm.add_Load($Form_StateCorrection_Load) + #Clean up the control events + $MainForm.add_FormClosed($Form_Cleanup_FormClosed) + #Store the control values when form is closing + $MainForm.add_Closing($Form_StoreValues_Closing) + #Show the Form + return $MainForm.ShowDialog() + +} +#endregion Source: MainForm.psf + +#Start the application +Main ($CommandLine) +}else{ + Write-Host "Machine was rebooted less than $days days ago" + +} \ No newline at end of file diff --git a/scripts_wip/Win_RemoteDesktopApp.ps1 b/scripts_wip/Win_RemoteDesktopApp.ps1 new file mode 100644 index 00000000..9dc0bbd5 --- /dev/null +++ b/scripts_wip/Win_RemoteDesktopApp.ps1 @@ -0,0 +1,114 @@ +<# + .SYNOPSIS + Install Remote Desktop App + .DESCRIPTION + This script is used to install the Remote Desktop App + from a direct link at Microsoft. This is the app required + for an Azure Virtual Desktop environment. + .EXAMPLE + Win_RemoteDesktopApp + .EXAMPLE + Win_RemoteDesktopApp -ShowLog + .EXAMPLE + Win_RemoteDesktopApp -Timeout 600 + .EXAMPLE + Win_RemoteDesktopApp -ShowLog -Timeout 600 + .NOTES + Version: 0.0.1 + Author: redanthrax + Creation Date: 11/14/2023 +#> + +Param( + $Timeout = 300, + [switch]$ShowLog +) + +$dir = "$env:AppData\remoteapp" + +function Win_RemoteDesktopApp { + [CmdletBinding()] + Param( + $Timeout = 300, + [switch]$ShowLog + ) + + Begin { + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + if (-not(Test-Path $dir)) { + New-Item -ItemType Directory -Force -Path "$env:AppData\remoteapp" | Out-Null + } + } + + Process { + Try { + Write-Output "Downloading Remote App installation..." + $source = "https://go.microsoft.com/fwlink/?linkid=2139369" + $destination = "$dir\RemoteDesktop.msi" + Invoke-WebRequest -Uri $source -OutFile $destination + Write-Output "File download complete. Starting install with $Timeout second timeout..." + $arguments = @("/i $destination", "/quiet", "/lv $dir\install.log") + $process = Start-Process -NoNewWindow "msiexec.exe" -ArgumentList $arguments -PassThru + $timedOut = $null + $process | Wait-Process -Timeout $Timeout -ErrorAction SilentlyContinue -ErrorVariable timedOut + if ($timedOut) { + $process | Stop-Process + Write-Error "Installed timed out after $Timeout seconds." -ErrorAction SilentlyContinue + } + elseif ($process.ExitCode -ne 0) { + $code = $process.ExitCode + Write-Error "Install error code: $code" -ErrorAction SilentlyContinue + } + + Write-Output "Creating shortcut." + New-item -ItemType Directory -Path "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\RemoteApp" | Out-Null + $WshShell = New-Object -ComObject WScript.Shell + $shortcutPath = "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\RemoteApp\Remote Desktop App.lnk" + $Shortcut = $WshShell.CreateShortcut($shortcutPath) + $target = "C:\Program Files\Remote Desktop\msrdcw.exe" + $Shortcut.TargetPath = $target + $description = "Remote Desktop App" + $Shortcut.Description = $description + $workingdirectory = (Get-ChildItem $target).DirectoryName + $shortcut.WorkingDirectory = $workingdirectory + $Shortcut.Save() + } + Catch { + $exception = $_.Exception + Write-Error "Error: $exception" -ErrorAction SilentlyContinue + } + } + + End { + if ($ShowLog) { + Write-Output "===Install Log===" + Get-Content "$dir\install.log" + } + + if (Test-Path $dir) { + Remove-Item -Path $dir -Recurse -Force + } + + if ($Error) { + foreach ($err in $Error) { + Write-Output $err + } + + Exit 1 + } + + Write-Output "Installation complete." + Exit 0 + } +} + +if (-not(Get-Command 'Win_RemoteDesktopApp' -ErrorAction SilentlyContinue)) { + . $MyInvocation.MyCommand.Path +} + +$scriptArgs = @{ + Timeout = $Timeout + ShowLog = $ShowLog +} + +Win_RemoteDesktopApp @scriptArgs \ No newline at end of file diff --git a/scripts_wip/Win_SMB1_CheckIfEnabled.ps1 b/scripts_wip/Win_SMB1_CheckIfEnabled.ps1 new file mode 100644 index 00000000..a7ed5261 --- /dev/null +++ b/scripts_wip/Win_SMB1_CheckIfEnabled.ps1 @@ -0,0 +1,29 @@ +#Check if enabled + +try { + # Check SMB1 Server status + $smbServerConfig = Get-SmbServerConfiguration -ErrorAction Stop + if ($smbServerConfig.EnableSMB1Protocol -eq $true) { + Write-Host "SMB1 Server is enabled." + exit 1 + } else { + Write-Host "SMB1 Server is not enabled." + } +} +catch { + Write-Host "Error checking SMB1 Server status. It may not be applicable on this system." +} + +try { + # Check SMB1 Client status + $smbClientConfig = Get-SmbClientConfiguration -ErrorAction Stop + if ($smbClientConfig.EnableSMB1Protocol -eq $true) { + Write-Host "SMB1 Client is enabled." + exit 1 + } else { + Write-Host "SMB1 Client is not enabled." + } +} +catch { + Write-Host "Error checking SMB1 Client status. It may not be applicable on this system." +} \ No newline at end of file diff --git a/scripts_wip/Win_Screenconnect_Detectothers.ps1 b/scripts_wip/Win_Screenconnect_Detectothers.ps1 new file mode 100644 index 00000000..9db56d17 --- /dev/null +++ b/scripts_wip/Win_Screenconnect_Detectothers.ps1 @@ -0,0 +1,84 @@ +<# + +.NOTES + v1.6 silversword411 Making into a function + v1.7 silversword411 Adding Custom Field AuditSCOtherDisable Disables check + +TODO: Add detection of other remote access systems +-AuditSCOtherDisable {{agent.AuditSCOtherDisable}} +#> +param ( + [string] $SCURLtocheck, # The URL to check against the service path + [Int] $AuditSCOtherDisable, # Disable + [switch] $debug +) + +# check for Win7 and exit if true +$OSVersion = (Get-WmiObject Win32_OperatingSystem).Version +if ($OSVersion.StartsWith("6.1")) { + Write-Output "Running on Windows 7. Exiting..." + Exit +} + +# See if Custom Field has disabled AuditSCOtherDisable +Write-Debug "AuditSCOtherDisable: $AuditSCOtherDisable" +if ($AuditSCOtherDisable) { + Write-Output "Other SC check disabled." + Exit 0 +} + +function Check-SCServicePath { + + Write-Output "################# Check ScreenConnect Service Path #################" + + # For setting debug output level. -debug switch will set $debug to true + if ($debug) { + $DebugPreference = "Continue" + $ErrorActionPreference = 'Continue' + Write-Debug "Debug mode enabled" + } + else { + $DebugPreference = "SilentlyContinue" + $ErrorActionPreference = 'SilentlyContinue' + } + + # Get all ScreenConnect services + $SCServices = Get-Service | Where-Object { $_.Name -match "ScreenConnect Client*" } + + $servicesNotContainingUrl = @() + + foreach ($service in $SCServices) { + $serviceDetail = Get-CimInstance -ClassName Win32_Service -Filter "Name = '$($service.Name)'" + if ($serviceDetail.PathName -notlike "*$SCURLtocheck*") { + $servicesNotContainingUrl += $service + } + } + + if ($servicesNotContainingUrl.Count -gt 0) { + Write-Output "WARNING: ScreenConnect services do not contain '$SCURLtocheck' in their path." + + foreach ($service in $servicesNotContainingUrl) { + $serviceDetail = Get-CimInstance -ClassName Win32_Service -Filter "Name = '$($service.Name)'" + Write-Debug "serviceDetail: $serviceDetail" + $path = $serviceDetail.PathName + Write-Debug "Path: $path" + # Extract the text between "&h=" and "&p" + $startIndex = $path.IndexOf("&h=") + 3 + if ($startIndex -gt 2) { + # Check if "&h=" exists + $endIndex = $path.IndexOf("&p", $startIndex) + if ($endIndex -gt $startIndex) { + # Check if "&p" exists after "&h=" + $extractedText = $path.Substring($startIndex, $endIndex - $startIndex) + Write-Output "Other SC server URLs: $($extractedText)" + } + } + } + Exit 1 + } + else { + Write-Output "AllGood: All ScreenConnect services contain '$SCURLtocheck' in their path." + } +} + +Check-SCServicePath \ No newline at end of file diff --git a/scripts_wip/Win_Security_Vuln_scanner.ps1 b/scripts_wip/Win_Security_Vuln_scanner.ps1 new file mode 100644 index 00000000..f730afa3 --- /dev/null +++ b/scripts_wip/Win_Security_Vuln_scanner.ps1 @@ -0,0 +1,187 @@ +# from Discord #scripts d.0_0.b 2/5/2024 +# Vulnerability scanner + +# Create a Text custom field for the overall status: +$VulStatusField = 'vulnerabilityStatus' + +# Create a Multi-Line custom field for the detailed output. +$VulDetails = 'vulnerabilityDetails' + +# Add any CVEs you wish to ignore here. +$ExcludeCVES = @('CVE-3000-123','CVE-3000-456') + +$registry_paths = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', 'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall' +$vulMapScannerUri = 'https://vulmon.com/scannerapi_vv211' + +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +[System.Collections.Generic.List[PSCustomObject]]$Inventory = @() + +function Get-ProductList () { + Write-Verbose "Reading installed software from registry." + foreach ($registry_path in $registry_paths) { + $subkeys = Get-ChildItem -Path $registry_path -ErrorAction SilentlyContinue + + if ($subkeys) { + ForEach ($key in $subkeys) { + $DisplayName = $key.getValue('DisplayName') + + if ($null -ne $DisplayName) { + $DisplayVersion = $key.GetValue('DisplayVersion') + + $Inventory.add([PSCustomObject]@{ + PSTypeName = 'System.Software.Inventory' + DisplayName = $DisplayName.Trim() + DisplayVersion = $DisplayVersion + NameVersionPair = $DisplayName.Trim() + $DisplayVersion + Installed = 'Machine Wide' + }) + } + } + } + } +} + +function Get-UsersProductList () { + Write-Verbose "Reading installed software from registry." + + # Define a Provider Drive to access HKEy_Users + New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS -ErrorAction SilentlyContinue | out-null + + # define the user keys to be skipped (system / service keys) + $Skip_User_Keys = @('.default') + + # open / connect to the registry / read the user subkeys + $hkeyUsersSubkeys = $( + ([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('USERS', $env:COMPUTERNAME)).GetSubKeyNames() | + # skip any undesirable keys + ForEach-Object { if ($_ -notin $Skip_User_Keys) { $_ } } | + ForEach-Object { if ($_.indexof('_Classes') -LT 0) { $_ } } + ) + + # Loop through the users + $hkeyUsersSubkeys | ForEach-Object { + + $UserSIDPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$_" + $Username = (Get-ItemProperty -Path $UserSIDPath).ProfileImagePath.Split('\')[-1] + + $UsersKeys = "REGISTRY::HKEY_USERS\$_\Software\Microsoft\Windows\CurrentVersion\Uninstall", "REGISTRY::HKEY_USERS\$_\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" + + foreach ($registry_path in $UsersKeys) { + $subkeys = Get-ChildItem -Path $registry_path -ErrorAction SilentlyContinue + + if ($subkeys) { + ForEach ($key in $subkeys) { + $DisplayName = $key.getValue('DisplayName') + + if ($null -ne $DisplayName) { + $DisplayVersion = $key.GetValue('DisplayVersion') + Write-Host "Adding $($DisplayName.Trim())" + $Inventory.add([PSCustomObject]@{ + PSTypeName = 'System.Software.Inventory' + DisplayName = $DisplayName.Trim() + DisplayVersion = $DisplayVersion + NameVersionPair = $DisplayName.Trim() + $DisplayVersion + Installed = $Username + }) + } + } + } + } + } +} + +function Get-JsonRequestBatches ($inventory) { + $numberOfBatches = [math]::Ceiling(@($inventory).count / 100) + + for ($i = 0; $i -lt $numberOfBatches; $i++) { + Write-Verbose "Submitting software to vulmon.com api, batch '$i' of '$numberOfBatches'." + $productList = $inventory | + Select-Object -First 100 | + ForEach-Object { + [pscustomobject]@{ + product = $_.DisplayName + version = if ($_.DisplayVersion) { $_.DisplayVersion } else { '' } + } + } + + $inventory = $inventory | Select-Object -Skip 100 + + $json_request_data = [ordered]@{ + os = (Get-CimInstance Win32_OperatingSystem -Verbose:$false).Caption + product_list = @($productList) + } | ConvertTo-Json + + $webRequestSplat = @{ + Uri = $vulMapScannerUri + Method = 'POST' + Body = @{ querydata = $json_request_data } + UseBasicParsing = $True + } + + if ($Proxy) { + $webRequestSplat.Proxy = $Proxy + } + + (Invoke-WebRequest @webRequestSplat).Content | ConvertFrom-Json + } +} + +function Resolve-RequestResponses ($responses) { + $count = 0 + foreach ($response in $responses) { + foreach ($vuln in ($response | Select-Object -ExpandProperty results -ErrorAction SilentlyContinue)) { + Write-Verbose "Parsing results from vulmon.com api." + $interests = $vuln | + Select-Object -Property query_string -ExpandProperty vulnerabilities | where-object {$_.cveid -notin $ExcludeCVES} | + ForEach-Object { + [PSCustomObject]@{ + Product = $_.query_string + 'CVE ID' = $_.cveid + 'Risk Score' = $_.cvssv2_basescore + 'Vulnerability Detail' = $_.url + 'Name' = $vuln.user_provided_product + 'Version' = $vuln.user_provided_version + 'Installed In' = ($Inventory | Where-Object {$_.DisplayName -eq $vuln.user_provided_product -and $_.DisplayVersion -eq $vuln.user_provided_version}).Installed -join ', ' + } + } + + + $count += $interests.Count + Write-Verbose "Found '$count' vulnerabilities so far." + + $interests + } + } +} + +function Invoke-VulnerabilityScan ($Inventory) { + Write-Host 'Vulnerability scanning started...' + $responses = Get-JsonRequestBatches $inventory + $vuln_list = Resolve-RequestResponses $responses + Write-Host "Checked $(@($inventory).count) items" -ForegroundColor Green + + if ($null -like $vuln_list) { + Write-Host "No vulnerabilities detected - Checked $(@($inventory).count) items" + + } else { + $VulnText = ForEach ($Vul in $vuln_list){ + if ($null -ne $Vul.'Risk Score'){ + $Risk = $Vul.'Risk Score' + } else { + $Risk = 'Unknown' + } + "------------------------------------`rProduct: $($Vul.Name) - $($Vul.Version)`rCVE ID: $($Vul.'CVE ID')`rRisk Score: $($Risk)`rInstalled For: $($Vul.'Installed In')`rDetail: $($Vul.'Vulnerability Detail')`r" + } -join '' + Write-Host "$($vuln_list.Count) Vulnerabilities Found" + Write-Host "Please run the original script on the host from:" + Write-Host "https://raw.githubusercontent.com/vulmon/Vulmap/master/Vulmap-Windows/vulmap-windows.ps1" + Write-Host "To have more details:" + Write-Host "SAN Notes: the original script does not work in the rmm if i have some time i could maybe fix it." + exit 1 + } + +} + +Get-ProductList +Get-UsersProductList +Invoke-VulnerabilityScan -inventory ($inventory | Sort-Object NameVersionPair -Unique) diff --git a/scripts_wip/Win_Software_AddRemovelog.ps1 b/scripts_wip/Win_Software_AddRemovelog.ps1 new file mode 100644 index 00000000..6d11cce3 --- /dev/null +++ b/scripts_wip/Win_Software_AddRemovelog.ps1 @@ -0,0 +1,114 @@ +<# +.Synopsis + Software Install and Removal Detection - Reports new installs and removals without considering version numbers. +.DESCRIPTION + This script compares the current installed software list from the registry with a previous state. +.VERSION + v1.0 11/23/2024 +#> + +Function Foldercreate { + param ( + [Parameter(Mandatory = $false)] + [String[]]$Paths + ) + + foreach ($Path in $Paths) { + if (!(Test-Path $Path)) { + New-Item -ItemType Directory -Force -Path $Path + } + } +} +Foldercreate -Paths "$env:ProgramData\TacticalRMM\temp", "$env:ProgramData\TacticalRMM\logs" + +# Define file paths +$previousStateFile = "$env:ProgramData\TacticalRMM\installed_software.json" +$logFile = "$env:ProgramData\TacticalRMM\logs\software_changes.log" + +# Function to get installed software from the registry +function Get-InstalledSoftware { + $installedSoftware = @() + + # Get software from 64-bit and 32-bit registry paths + $installedSoftware += Get-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' | + Select-Object DisplayName, DisplayVersion + $installedSoftware += Get-ItemProperty 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' | + Select-Object DisplayName, DisplayVersion + + # Filter out entries without a valid DisplayName + $installedSoftware = $installedSoftware | Where-Object { $_.DisplayName -ne $null -and $_.DisplayName -ne '' } + + # Strip version number patterns from DisplayName and remove duplicates + $installedSoftware = $installedSoftware | ForEach-Object { + if ($_.DisplayVersion -and $_.DisplayName -like "*$($_.DisplayVersion)*") { + $_.DisplayName = $_.DisplayName -replace [regex]::Escape($_.DisplayVersion), '' # Strip DisplayVersion + $_.DisplayName = $_.DisplayName.Trim() # Remove trailing spaces + } + $_ + } | Sort-Object DisplayName -Unique + + return $installedSoftware +} + +# Function to log changes to a file, ensuring proper logging +function LogChange { + param([string]$message) + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + $logEntry = "$timestamp - $message" + + # Write the log entry to the file + Add-Content -Path $logFile -Value $logEntry +} + +# Get current installed software +$currentSoftware = Get-InstalledSoftware + +# Check if the previous state file exists +if (Test-Path $previousStateFile) { + # Load the previous state + $previousSoftware = Get-Content $previousStateFile | ConvertFrom-Json + + # Compare current and previous software lists + $newSoftware = Compare-Object -ReferenceObject $previousSoftware -DifferenceObject $currentSoftware -Property DisplayName -PassThru | + Where-Object { $_.SideIndicator -eq '=>' } + + $removedSoftware = Compare-Object -ReferenceObject $previousSoftware -DifferenceObject $currentSoftware -Property DisplayName -PassThru | + Where-Object { $_.SideIndicator -eq '<=' } + + # Report new installs + if ($newSoftware) { + Write-Output "New software installed:" + $newSoftware | ForEach-Object { + Write-Output " - $($_.DisplayName)" + LogChange "Installed: $($_.DisplayName)" + } + } + + # Report removals + if ($removedSoftware) { + Write-Output "The following software(s) were removed:" + $removedSoftware | ForEach-Object { + Write-Output " - $($_.DisplayName)" + LogChange "Removed: $($_.DisplayName)" + } + } + + # Save the current state (overwrite the existing file) + $currentSoftware | ConvertTo-Json | Out-File -FilePath $previousStateFile -Encoding UTF8 + + # Exit with status code based on changes + if ($newSoftware -or $removedSoftware) { + exit 1 + } + else { + Write-Output "No new software installations or removals detected." + exit 0 + } +} +else { + # Save the current state if no previous state exists (overwrite if needed) + $currentSoftware | ConvertTo-Json | Out-File -FilePath $previousStateFile -Encoding UTF8 + LogChange "Initial software inventory saved." + Write-Output "Initial software inventory saved." + exit 0 +} diff --git a/scripts_wip/Win_Software_Uninstall.ps1 b/scripts_wip/Win_Software_Uninstall.ps1 new file mode 100644 index 00000000..a4fa68b0 --- /dev/null +++ b/scripts_wip/Win_Software_Uninstall.ps1 @@ -0,0 +1,133 @@ +<# +.SYNOPSIS + Uninstalls a specified application from Windows. + +.DESCRIPTION + This script uninstalls an application from a Windows system. It searches for the + application in the system's registry to find its uninstall string and then uses + msiexec.exe or the apps spec to perform the uninstallation. + +.PARAMETER Application + The name of the application to be uninstalled. It is a mandatory string parameter. + +.EXAMPLE + -Application "ExampleApp" + -Application "ExampleApp","AnotherApp" + Uninstalls the application named "ExampleApp". + +.NOTES + Version: 1.0 + Author: redanthrax + Creation Date: 2023-11-27 + Updated Date: 2024-03-22 +#> + +Param( + [string[]]$Application +) + +function Win_Software_Uninstall { + [CmdletBinding()] + Param ( + [Parameter(Mandatory)] + [string[]]$Application + ) + + Begin { + $Apps = @() + $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" + $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" + } + + Process { + try { + foreach($app in $Application) { + Write-Output "Getting $app data" + if ($null -ne ($Apps | Where-Object { $_.DisplayName -Match [regex]::Escape($app)})) { + Write-Output "Found $app in the registry, uninstalling..." + $uninstallString = ($Apps | Where-Object { $_.DisplayName -Match [regex]::Escape($app) }).UninstallString + if ($uninstallString) { + if ($uninstallString.GetType().Name -eq "Object[]") { + foreach ($unst in $uninstallString) { + $m = [regex]::Match($unst, '') + if ($unst -like "*`"*") { + $m = [regex]::Match($unst, '^"([^"]+)"\s*(.*)') + } + else { + $m = [regex]::Match($unst, '^(.*?)\s(.*)$') + } + + $path = $m.Groups[1].Value + $arguments = $m.Groups[2].Value + if ($path.ToLower() -like "*msiexec*") { + Write-Output "Executing: $path $arguments /quiet /qn /noreboot" + Start-Process $path -ArgumentList $arguments, "/quiet", "/qn", "/noreboot" -Wait -NoNewWindow + } + else { + Write-Output "Executing: $path $arguments /x /s /v/qn" + Start-Process $path -ArgumentList $arguments, "/x", "/s", "/v/qn" -Wait -NoNewWindow + } + } + } + else { + $m = [regex]::Match($uninstallString, '^(\S+)\s(.+)$') + $path = $m.Groups[1].Value + $arguments = $m.Groups[2].Value + if ($path.ToLower() -like "*msiexec*") { + $arguments = $arguments -Replace '/I', '/X' + Write-Output "Executing: $path $arguments /quiet /qn /noreboot" + Start-Process $path -ArgumentList $arguments, "/quiet", "/qn", "/noreboot" -Wait -NoNewWindow + } + else { + Write-Output "Executing: $path $arguments /x /s /v/qn" + Start-Process $path -ArgumentList $arguments, "/x", "/s", "/v/qn" -Wait -NoNewWindow + } + } + + Write-Output "Validating uninstall complete" + $Apps = Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" + $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" + + if ($null -eq ($Apps | Where-Object { $_.DisplayName -Match [regex]::Escape($app)})) { + Write-Output "$app uninstall verified" + } + else { + Write-Error "Could not uninstall $app" + } + } + else { + Write-Output "Did not find uninstall string for $app" + } + } + else { + Write-Output "Did not find $app in the registry" + } + } + } + Catch { + Write-Error "Error $($_.Exception)" + } + + } + + End { + if ($error) { + Exit 1 + } + + Exit 0 + } +} + +if (-Not(Get-Command 'Win_Software_Uninstall' -ErrorAction SilentlyContinue)) { + . $MyInvocation.MyCommand.Path +} + +$scriptArgs = @{} +if($Application) { + $scriptArgs = @{ + Application = $Application + } +} + +Win_Software_Uninstall @scriptArgs \ No newline at end of file diff --git a/scripts_wip/Win_SpywareKillerv1.2.ps1 b/scripts_wip/Win_SpywareKiller.ps1 similarity index 65% rename from scripts_wip/Win_SpywareKillerv1.2.ps1 rename to scripts_wip/Win_SpywareKiller.ps1 index 6b45648e..f9217fc5 100644 --- a/scripts_wip/Win_SpywareKillerv1.2.ps1 +++ b/scripts_wip/Win_SpywareKiller.ps1 @@ -3,7 +3,7 @@ Spyware killer script .DESCRIPTION - Death to all spyware! This scans for Wavebrowser and Onelaunch + Death to all spyware! This scans for Wavebrowser, Onelaunch, and Webcompanion .PARAMETER Days The number of days to look back for installers in the Downloads folder. Default is 1000. @@ -29,6 +29,8 @@ Added Onelaunch v1.2 7/2023 silversword411 Refining, adding debug output, adding autodelete switch, reformatting outlook for easier reading + v1.3 and v1.42 /2024 silversword411 + Adding Webcompanion and Write-Debug #> param( @@ -69,17 +71,17 @@ else { } function Wavebrowser-Scan { - Write-Output "" - Write-Output "################### Scanning for Wavebrowser ##################" + Write-Debug "" + Write-Debug "################### Scanning for Wavebrowser ##################" $targetProgDir = "c:\users\$currentuser\Wavesor Software\" $targetDir = "c:\users\$currentuser\Downloads\" Write-Debug "targetDir is $targetDir" $pattern = "wave br*.exe" # Look for Wavebrowser installer in downloads folder - Write-Output "##########" + Write-Debug "##########" If (!(get-ChildItem $targetDir | Where-Object { ($_.name -like $pattern) -and ($_.CreationTime -gt (Get-Date).AddDays(-$Days)) })) { - Write-Output "No Wavebrowser installers in the downloads folder in the last $Days days" + Write-Debug "No Wavebrowser installers in the downloads folder in the last $Days days" } else { Write-Output "WARNING-WARNING-WARNING - WaveBrowser installer found in downloads folder!" @@ -96,9 +98,9 @@ function Wavebrowser-Scan { } # Look for installed Wavebrowser - Write-Output "##########" + Write-Debug "##########" If (!(get-ChildItem $targetProgDir)) { - Write-Output "No installed Wavebrowser" + Write-Debug "No installed Wavebrowser" } else { Write-Output "WARNING - WaveBrowser was installed in c:\users\$currentuser\Wavesor Software\" @@ -111,17 +113,17 @@ function Wavebrowser-Scan { Wavebrowser-Scan function Onelaunch-Scan { - Write-Output "" - Write-Output "################### Scanning for Onelaunch ##################" + Write-Debug "" + Write-Debug "################### Scanning for Onelaunch ##################" $targetProgDir = "c:\users\$currentuser\appdata\local\Onelaunch" $targetDir = "c:\users\$currentuser\Downloads\" Write-Debug "targetDir is $targetDir" $pattern = "onelaunch*.exe" # Look for Onelaunch installer in downloads folder - Write-Output "##########" + Write-Debug "##########" If (!(get-ChildItem $targetDir | Where-Object { ($_.name -like $pattern) -and ($_.CreationTime -gt (Get-Date).AddDays(-$Days)) })) { - Write-Output "No Onelaunch installers in the downloads folder in the last $Days days" + Write-Debug "No Onelaunch installers in the downloads folder in the last $Days days" } else { Write-Output "WARNING-WARNING-WARNING - Onelaunch installer found in downloads folder!" @@ -138,21 +140,64 @@ function Onelaunch-Scan { } # Look for installed Onelaunch - Write-Output "##########" + Write-Debug "##########" If (!(get-ChildItem $targetProgDir)) { - Write-Output "No installed Onelaunch" + Write-Debug "No installed Onelaunch" } else { - Write-Output "WARNING - OneLaunch was installed in c:\users\$currentuser\appdata\local\Onelaunch" - $dirdate = (Get-Item "c:\users\$currentuser\appdata\local\Onelaunc").CreationTime - Write-Output "DirDate is $($dirdate)" + Write-Debug "WARNING - OneLaunch was installed in c:\users\$currentuser\appdata\local\Onelaunch" + $dirdate = (Get-Item "c:\users\$currentuser\appdata\local\Onelaunch").CreationTime + Write-Debug "DirDate is $($dirdate)" $script:ErrorCount += 1 Write-Debug "ErrorCount increased. Total is $ErrorCount" } } Onelaunch-Scan -Write-Output "" + +function WebCompanion-Scan { + Write-Debug "" + Write-Debug "################### Scanning for Onelaunch ##################" + $targetProgDir = "c:\users\$currentuser\appdata\local\Onelaunch" + $targetDir = "c:\users\$currentuser\Downloads\" + Write-Debug "targetDir is $targetDir" + $pattern = "*Webcompanion.exe" + + # Look for WebCompanion installer in downloads folder + Write-Debug "##########" + If (!(get-ChildItem $targetDir | Where-Object { ($_.name -like $pattern) -and ($_.CreationTime -gt (Get-Date).AddDays(-$Days)) })) { + Write-Debug "No WebCompanion installers in the downloads folder in the last $Days days" + } + else { + Write-Output "WARNING-WARNING-WARNING - WebCompanion installer found in downloads folder!" + Get-ChildItem $targetDir | Where-Object { ($_.name -like $pattern) -and ($_.CreationTime -gt (Get-Date).AddDays(-$Days)) } | ForEach-Object { + if ($AutoDelete) { + $_ | Remove-Item -Confirm:$false + } + else { + Write-Output $_ + } + } + $script:ErrorCount += 1 + Write-Debug "ErrorCount increased. Total is $ErrorCount" + } + + # Look for installed WebCompanion + Write-Debug "##########" + If (!(get-ChildItem $targetProgDir)) { + Write-Debug "No installed WebCompanion" + } + else { + Write-Output "WARNING - WebCompanion was installed in c:\users\$currentuser\appdata\local\Onelaunch" + $dirdate = (Get-Item "c:\users\$currentuser\appdata\local\Onelaunch").CreationTime + Write-Output "DirDate is $($dirdate)" + $script:ErrorCount += 1 + Write-Debug "ErrorCount increased. Total is $ErrorCount" + } +} +WebCompanion-Scan + +Write-Debug "" Write-Debug "Finished Tests" if ($ErrorCount -gt 0) { @@ -164,4 +209,4 @@ else { Write-Debug "Total ErrorCount is $ErrorCount." Write-Output "No spyware detected" Exit 0 -} +} \ No newline at end of file diff --git a/scripts_wip/Win_TRMM_Agent_Installer_and_Locker.ps1 b/scripts_wip/Win_TRMM_Agent_Installer_and_Locker.ps1 new file mode 100644 index 00000000..d6cb2f77 --- /dev/null +++ b/scripts_wip/Win_TRMM_Agent_Installer_and_Locker.ps1 @@ -0,0 +1,81 @@ +<# +.SYNOPSIS + Script to install and configure the Tactical RMM (TRMM) Agent. + +.DESCRIPTION + This script performs several tasks to install and secure the Tactical RMM (TRMM) Agent on a Windows machine. + It includes setting up necessary prerequisites, installing the TRMM agent, configuring Windows Defender exclusions, + locking down services, and preventing access to specific folders. + +.PARAMETER RMMurl + The deployment URL to download the Tactical RMM Agent installer. + +.EXAMPLE + $RMMurl = "https://example.com/path/to/agent.exe" + # (Run the script with the specified URL) + # This will download and install the TRMM agent, configure exclusions, lock services, and secure folders. + +.NOTES + v1.0 8/22/2024 CBG_ITSUP Initial version +#> + +############################################### +###### Prerequisites #### +############################################### + +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 + +$RMMurl = "Insert RMM agent URL here" + +$Path = Test-Path -Path "C:\Program Files\TacticalAgent\tacticalrmm.exe" + +############################################### +############ Install TRMM Agent ######## +############################################### + +If ($Path -eq $false) { + + Add-MpPreference -ExclusionPath "C:\ProgramData" + + Invoke-WebRequest $RMMurl -OutFile "C:\ProgramData\trmm-agent.exe" + + Start-Process -Wait "C:\ProgramData\trmm-agent.exe" -ArgumentList '-silent' + + Remove-MpPreference -ExclusionPath "C:\ProgramData" + + Remove-Item "C:\ProgramData\trmm-agent.exe" -Force + +} +############################################### +### Exclude TRMM paths in Windows Defender #### +############################################### + +Add-MpPreference -ExclusionPath "C:\Program Files\Mesh Agent\*" +Add-MpPreference -ExclusionPath "C:\Program Files\TacticalAgent\*" +Add-MpPreference -ExclusionPath "C:\ProgramData\TacticalRMM\*" + +############################################### +#### Lock Down Services #### +############################################### + +Start-Process -FilePath "$env:comspec" -ArgumentList "/c sc config tacticalrmm start=auto" + +Start-Process -FilePath "$env:comspec" -ArgumentList "/c sc start tacticalrmm" + +Start-Process -FilePath "$env:comspec" -ArgumentList "/c sc.exe sdset tacticalrmm D:AR(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;SY)(A;;CCDCLCSWLOCRRC;;;BA)(A;;CCLCSWLOCRRC;;;IU)S:(AU;FA;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;WD)" + +Start-Process -FilePath "$env:comspec" -ArgumentList '/c sc config "Mesh Agent" start=auto' + +Start-Process -FilePath "$env:comspec" -ArgumentList '/c sc start "Mesh Agent"' + +Start-Process -FilePath "$env:comspec" -ArgumentList '/c sc.exe sdset "Mesh Agent" D:AR(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;SY)(A;;CCDCLCSWLOCRRC;;;BA)(A;;CCLCSWLOCRRC;;;IU)S:(AU;FA;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;WD)' + +############################################### +##### Prevent access to TRMM folders ### +############################################### + +Invoke-Expression -Command:"icacls ""C:\Program Files\TacticalAgent"" /T /setowner system" +Invoke-Expression -Command:"icacls ""C:\Program Files\TacticalAgent\unins000.exe"" /inheritance:d /grant System:F /deny Administrators:F" +Invoke-Expression -Command:"icacls ""C:\Program Files\TacticalAgent"" /T /inheritance:d /grant System:F /deny Administrators:F" + +Exit 0 \ No newline at end of file diff --git a/scripts_wip/Win_TRMM_Agent_Locker.ps1 b/scripts_wip/Win_TRMM_Agent_Locker.ps1 new file mode 100644 index 00000000..04fef694 --- /dev/null +++ b/scripts_wip/Win_TRMM_Agent_Locker.ps1 @@ -0,0 +1,37 @@ +<# +.SYNOPSIS + Lock down services and prevent access to TRMM folders. + +.DESCRIPTION + This script configures and starts the "tacticalrmm" and "Mesh Agent" services, setting security descriptors to enforce security. Additionally, it restricts access to the TacticalAgent directory and its executable to prevent unauthorized access. + +.NOTES + v1.0 8/22/2024 CBG_ITSUP Initial version +#> + + +############################################### +#### Lock Down Services #### +############################################### + +Start-Process -FilePath "$env:comspec" -ArgumentList "/c sc config tacticalrmm start=auto" + +Start-Process -FilePath "$env:comspec" -ArgumentList "/c sc start tacticalrmm" + +Start-Process -FilePath "$env:comspec" -ArgumentList "/c sc.exe sdset tacticalrmm D:AR(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;SY)(A;;CCDCLCSWLOCRRC;;;BA)(A;;CCLCSWLOCRRC;;;IU)S:(AU;FA;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;WD)" + +Start-Process -FilePath "$env:comspec" -ArgumentList '/c sc config "Mesh Agent" start=auto' + +Start-Process -FilePath "$env:comspec" -ArgumentList '/c sc start "Mesh Agent"' + +Start-Process -FilePath "$env:comspec" -ArgumentList '/c sc.exe sdset "Mesh Agent" D:AR(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;SY)(A;;CCDCLCSWLOCRRC;;;BA)(A;;CCLCSWLOCRRC;;;IU)S:(AU;FA;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;WD)' + +############################################### +##### Prevent access to TRMM folders ### +############################################### + +Invoke-Expression -Command:"icacls ""C:\Program Files\TacticalAgent"" /T /setowner system" +Invoke-Expression -Command:"icacls ""C:\Program Files\TacticalAgent\unins000.exe"" /inheritance:d /grant System:F /deny Administrators:F" +Invoke-Expression -Command:"icacls ""C:\Program Files\TacticalAgent"" /T /inheritance:d /grant System:F /deny Administrators:F" + +Exit 0 \ No newline at end of file diff --git a/scripts_wip/Win_TRMM_Agent_unLocker.ps1 b/scripts_wip/Win_TRMM_Agent_unLocker.ps1 new file mode 100644 index 00000000..eb12761d --- /dev/null +++ b/scripts_wip/Win_TRMM_Agent_unLocker.ps1 @@ -0,0 +1,59 @@ +<# +.SYNOPSIS + Unlock TacticalRMM Agent and optionally remove it. + +.DESCRIPTION + This script unlocks the TacticalRMM Agent by modifying folder permissions and resetting service security descriptors. Additionally, it includes an optional parameter to remove the TacticalRMM Agent if specified. + +.PARAMETER remove + A boolean parameter that, if set to $True, will trigger the removal of the TacticalRMM Agent. + +.OUTPUTS + None + +.EXAMPLE + .\script.ps1 -remove $False + - Unlocks the TacticalRMM Agent by adjusting permissions and resetting service security descriptors without removing the agent. + +.EXAMPLE + .\script.ps1 -remove $True + - Unlocks the TacticalRMM Agent and then removes it using its uninstaller. + +.NOTES + v1.0 8/22/2024 CBG_ITSUP Initial version + +#> + + +param ( + + [Parameter()] + [string]$remove +) + +####################################################### +############ UnLock TacticalRMM Agent ################# +####################################################### + +#################### App Folder ####################### + +Invoke-Expression -Command:"icacls ""C:\Program Files\TacticalAgent"" /T /inheritance:d /grant System:F /grant Administrators:F" + +Invoke-Expression -Command:"icacls ""C:\Program Files\TacticalAgent\unins000.exe"" /inheritance:d /grant System:F /grant Administrators:F" + +Invoke-Expression -Command:"icacls ""C:\Program Files\TacticalAgent"" /T /inheritance:d /grant System:F /grant Administrators:F" + +##################### Services ######################## + +Start-Process -FilePath "$env:comspec" -ArgumentList "/c sc.exe sdset tacticalrmm D:AR(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;SY)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;IU)S:(AU;FA;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;WD)" + +Start-Process -FilePath "$env:comspec" -ArgumentList '/c sc.exe sdset "Mesh Agent" D:AR(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;SY)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;IU)S:(AU;FA;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;WD)' + + +####################################################### +######### Optional: Remove TacticalRMM Agent ########## +####################################################### + +If ($remove -eq $True) { + Start-Process -Wait -FilePath "$env:comspec" -ArgumentList '/c ""C:\Program Files\TacticalAgent\unins000.exe"" /VERYSILENT' +} \ No newline at end of file diff --git a/scripts_wip/Win_TRMM_GetLoggedOnUPN.ps1 b/scripts_wip/Win_TRMM_GetLoggedOnUPN.ps1 new file mode 100644 index 00000000..b3af54eb --- /dev/null +++ b/scripts_wip/Win_TRMM_GetLoggedOnUPN.ps1 @@ -0,0 +1 @@ +whoami /upn \ No newline at end of file diff --git a/scripts_wip/Win_TRMM_MoveAgentToCorrectSite.ps1 b/scripts_wip/Win_TRMM_MoveAgentToCorrectSite.ps1 new file mode 100644 index 00000000..f68c9648 --- /dev/null +++ b/scripts_wip/Win_TRMM_MoveAgentToCorrectSite.ps1 @@ -0,0 +1,113 @@ +<# +.DESCRIPTION +Moves an Windows Workstation Agent to the first Site created for the Client with a match in the Client's "Domains" Custom Field. + +Assumptions made: + - The User will be on a Windows client machine and logged in with their Microsoft 365 account, therefore their UPN will be their M365 username (UPN) + - The lowest value ID site per Client is the one you want the Agent to be pushed to (or Clients only have 1 site) + - - If this is not true, you'll need to update the "Determine the Site..." region with logic that returns the desired site(s) + - There is a Client Custom Field called "Domains" and it's for all of the the domains that could be valid for the UPN + +You will need: + - A script in TRMM that runs "whoami /upn" and the ID of that script (hover over it) - E.g. "Win_TRMM_GetLoggedOnUPN.ps1" + - - The ID will be the value supplied to the "-whoAmIScriptId" script arguement + - - This is deliberately not done as a script snippit because it must be run as the USER (and not SYSTEM) + - A Client Custom Field for storing domains exists, and is populated with the domains that any valid UPN may have for the given Client + - - The name of this field can be overridden by supplying the "-clientCustomFieldName" script argument with a value + +.SYNOPSIS +Moves an Agent to the first Site created for the Client with a match in the Client's "Domains" Custom Field. (Assumes Windows & the user is logged in with their M365 account, and requires an additional script (see Description).) + +.NOTES +v1.0 2026-01-12 Owen Conti + +#> + +[cmdletbinding()] +Param( + [string]#Provided by using the script arguement "-agentId {{agent.agent_id}}" + $agentId, + [string]#The API Key value to use. A better approach is to pass this with an ENVIRONMENT VARIABLE in the script triggering so your API key is not logged in the Windows Event Log on all the Clients that run this. If you provide a value here it will override any environment variable you provide. + $apiKey, + [int]#The ID of the "WhoAmI" script on your environment + $whoAmIScriptId, + [string]#The FQDN of your API end point (the URI is built later). E.g. "api.example.com" + $apiFQDN, + [string]#The exact name of the Client level Custom Field used for storing the valid Domains + $clientCustomFieldName = "Domains" +) + +#region script wide parameters +If($apiKey){ + $apiAuthKey = $apiKey +} else { + $apiAuthKey = $env:apiKey +} + +$headers = @{ + 'Content-Type' = 'application/json' + 'X-API-KEY' = $apiAuthKey +} #These are common to all our API calls +#endRegion + + +#region Collect the User's UPN using the "Who Am I" script +$whoamiPayload = @{ + output = "wait" + emails = @() + emailMode = "default" + custom_field = $null + save_all_output = $false + script = $whoAmIScriptId + args = @() + env_vars = @() + run_as_user = $false + timeout = 10 +} | ConvertTo-Json + +$upn = Invoke-RestMethod -Uri "https://$apiFQDN/agents/$agentId/runscript/" -Method POST -Body $whoamiPayload -Headers $headers +If($upn.Contains("ERROR:")) +{ + #The user is not logged in with an account that presents the UPN to whoami.exe + Write-Error "An error occured when trying to collect the UPN of the user: $upn" + Exit 1 +} +#endRegion + +#region Determine the Site (and therefore Client) the user belongs to +$domain = $upn.Split("@")[1].Trim() #The trim is required as trailing spaces cause problems +#Need to define a method for doing a lookup for UPN domain to Customer ID to correct Site in TRMM +$customFields = Invoke-RestMethod -Uri "https://$apiFQDN/core/customfields" -Method GET -Headers $headers +$domainFieldId = $customFields | Where-Object -FilterScript {$_.model -eq "client" -and $_.name -eq $clientCustomFieldName} | Select-Object -ExpandProperty id + +$clients = Invoke-RestMethod -Uri "https://$apiFQDN/clients/" -Method GET -Headers $headers +$siteId = $clients | + Where-Object {$_.custom_fields.field -eq $domainFieldId -and $_.custom_fields.value -match $domain} | + Select-Object -ExpandProperty sites | + Sort-Object -Property id | + Select-Object -ExpandProperty id -First 1 +#endRegion + +#region Move the agent to the correct site +If($siteId){ + + $body = @{ + site = $siteId + } | ConvertTo-Json + + $moveResult = Invoke-RestMethod -Method PUT -Uri "https://$apiFQDN/agents/$agentId" -Headers $headers -Body $body + If($moveResult -eq "The agent was updated successfully") + { + $moveResult + Exit 0 + } else { + Write-Error "There was a problem with the API call to move the Agent" + $moveResult + Exit 1 + } + +} else { + Write-Error "There was a problem collecting the Site ID. The domain value is between the asterisks: ***$domain*** Check the Clients and ensure this domain value is in the correct place." + Exit 1 +} +#endRegion \ No newline at end of file diff --git a/scripts_wip/Win_Teams_Upgrade.ps1 b/scripts_wip/Win_Teams_Upgrade.ps1 new file mode 100644 index 00000000..a5e0ba4a --- /dev/null +++ b/scripts_wip/Win_Teams_Upgrade.ps1 @@ -0,0 +1,105 @@ +<# +.SYNOPSIS + A script to install or remove the new Teams from Microsoft. +.DESCRIPTION + Downloads the bootstrap and installs or uninstalls the new Teams. Set new Teams + as the default in the Teams Admin Center. This will become the default in the future. +.NOTES + Version: 1.0 + Author: redanthrax + Creation Date: 2024-1-18 +#> + +Param( + [switch]$Uninstall +) + +function Win_Teams_Upgrade { + [CmdletBinding()] + Param( + [switch]$Uninstall + ) + + Begin { + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + $random = ([char[]]([char]'a'..[char]'z') + 0..9 | sort { get-random })[0..12] -join '' + if (-not(Test-Path "C:\packages$random")) { + New-Item -ItemType Directory -Force -Path "C:\packages$random" | Out-Null + } + } + + Process { + Try { + $destination = "C:\packages$random\teamsbootstrapper.exe" + $request = @{ + Uri = "https://go.microsoft.com/fwlink/?linkid=2243204&clcid=0x409" + OutFile = $destination + } + + Invoke-WebRequest @request + $arguments = @("-p") + + if ($Uninstall) { + Write-Output "Uninstalling new Teams..." + $arguments = @("-x") + } + else { + Write-Output "Installing new Teams..." + } + + $options = @{ + NoNewWindow = $true + FilePath = $destination + ArgumentList = $arguments + PassThru = $true + } + + $process = Start-Process @options + $timedOut = $null + $options = @{ + Timeout = 300 + ErrorAction = "SilentlyContinue" + ErrorVariable = $timedOut + } + + $process | Wait-Process @options + if ($timedOut) { + $process | Stop-Process + Write-Output "Install timed out after 300 seconds." + } + elseif ($process.ExitCode -ne 0) { + $code = $process.ExitCode + Write-Output "Install error code: $code" + } + } + Catch { + $exception = $_.Exception + Write-Output "Error: $exception" + } + } + + End { + Start-Sleep -Seconds 3 + if (Test-Path "C:\packages$random") { + Remove-Item -Path "C:\packages$random" -Recurse -Force + } + + if($error) { + Write-Output "Error: $error" + Exit 1 + } + + Write-Output "Execution complete." + Exit 0 + } +} + +if (-Not(Get-Command 'Win_Teams_Upgrade' -ErrorAction SilentlyContinue)) { + . $MyInvocation.MyCommand.Path +} + +$scriptArgs = @{ + Uninstall = $Uninstall +} + +Win_Teams_Upgrade @scriptArgs \ No newline at end of file diff --git a/scripts_wip/Win_User_Admin_check_if_admin.ps1 b/scripts_wip/Win_User_Admin_check_if_admin.ps1 new file mode 100644 index 00000000..773d63d9 --- /dev/null +++ b/scripts_wip/Win_User_Admin_check_if_admin.ps1 @@ -0,0 +1,72 @@ +<# +.SYNOPSIS + Reports if the currently logged-in interactive user has local administrator rights. + This script is designed to be run from the SYSTEM account. + +.DESCRIPTION + When run as SYSTEM, the script first identifies the user who is actively + logged into the console session. It then uses the reliable ADSI provider to + query the local 'Administrators' group and checks if the detected user is a member. + + This revised version avoids potential name resolution errors encountered with the + [System.Security.Principal.WindowsIdentity] .NET class when run as SYSTEM. + +.NOTES + v1 2025-7-22 silversword411 initial release +#> +[CmdletBinding()] +param() + +# Suppress errors for the initial check in case no one is logged in +$ErrorActionPreference = 'SilentlyContinue' + +# --- Step 1: Find the currently logged-in user from the SYSTEM context --- +Write-Verbose "Querying Win32_ComputerSystem to find the interactive user..." +$loggedInUser = (Get-CimInstance -ClassName Win32_ComputerSystem).UserName +$ErrorActionPreference = 'Continue' # Reset error preference + +# --- Step 2: Check if a user was found --- +if ([string]::IsNullOrWhiteSpace($loggedInUser)) { + Write-Output "Status: No interactive user is currently logged in to the console." + exit 0 +} + +# The user is typically returned as "DOMAIN\user" or "COMPUTERNAME\user". +# We only need the username part for the group check. +$usernameOnly = $loggedInUser.Split('\')[-1] +Write-Output "Detected logged-in user: $loggedInUser (Checking for account: $usernameOnly)" + + +# --- Step 3 (Revised): Check group membership using ADSI --- +try { + # Define the well-known name for the local Administrators group + $adminGroupName = "Administrators" + + # Use the ADSI provider to connect to the local Administrators group. + # The "WinNT://" provider is used for local machine resources. + # The "." represents the local computer. + $group = [ADSI]"WinNT://./$adminGroupName,group" + + # Get all members of the group. + $members = $group.psbase.Invoke("Members") | ForEach-Object { + # For each member object, get its 'Name' property + $_.GetType().InvokeMember("Name", "GetProperty", $null, $_, $null) + } + + # Now, check if the username is in the list of administrator members. + # We use the username part we extracted earlier ($usernameOnly). + if ($members -contains $usernameOnly) { + Write-Output "Result: The user '$loggedInUser' IS an Administrator." + $host.SetShouldExit(1) + } + else { + Write-Output "Result: The user '$loggedInUser' is NOT an Administrator." + # You could exit with a specific code, e.g., exit 0 for "Not Admin" + } +} +catch { + Write-Error "An error occurred while checking Administrators group membership." + Write-Error "Error details: $($_.Exception.Message)" + # Exit with an error code + exit 99 +} \ No newline at end of file diff --git a/scripts_wip/Win_Veeam_BackupRun.ps1 b/scripts_wip/Win_Veeam_BackupRun.ps1 new file mode 100644 index 00000000..fcfec1cc --- /dev/null +++ b/scripts_wip/Win_Veeam_BackupRun.ps1 @@ -0,0 +1,3 @@ +rem https://helpcenter.veeam.com/docs/agentforwindows/userguide/backup_cmd.html?ver=60 + +"C:\Program Files\Veeam\Endpoint Backup\Veeam.EndPoint.Manager.exe" /backup \ No newline at end of file diff --git a/scripts_wip/Win_Veeam_CheckBackup.ps1 b/scripts_wip/Win_Veeam_CheckBackup.ps1 index c38d1121..dc9bea1c 100644 --- a/scripts_wip/Win_Veeam_CheckBackup.ps1 +++ b/scripts_wip/Win_Veeam_CheckBackup.ps1 @@ -1,56 +1,315 @@ <# .SYNOPSIS - Using Events log "Veeam Agent", gets date of last backup and + Using Events log "Veeam Agent", makes sure veeam is installed. Then make sure you haven't disabled Veeam checks. Then looks to see if there's a warning about last backup (in the last 24hrs). If no warning, then gets date of last backup and displays. Needs to run every 24hrs. .DESCRIPTION - Run it daily, it'll output Veeam version, and return 1 if last backup failed. Will also list last good backup - .PARAMETERS - -VeeamCheck {{agent.DisableVeeamCheck}} + Run it daily. It'll error and return 1 if any of these conditions: No backup in 10 days (customizable per agent), backup drive not NTFS, increases Veeam log max size for more data, errors if less than 10GB free space on backup drive. + .PARAMETER -VeeamCheck + -VeeamCheck {{agent.VeeamDisableCheck}} + Make a Custom Field | For Agents | Called "VeeamDisableCheck" of type checkbox, with default of false. When you don't want the veeamcheck to run on an agent flip the switch and the script won't error, it'll just bypass that agent completely. + .PARAMETER -NumberOfDaysBeforeError + -VeeamCheck {{agent.VeeamDaysBeforeError}} + Make a Custom Field | For Agents | Called "VeeamDaysBeforeError" of type Number, with default of empty. Use this to set the number of days with no backup before script goes from pass to error. Line 40 is number of days by default: 10 .NOTES 2/2022 v1 Initial release by @silversword411 - If you want to be able to disable per-agent the check, create a custom field switch on agents and use the VeeamCheck variable + 6/22/2023 v1.1 setting NumberOfDaysBeforeError using Custom Fields + 10/2023 v1.5 Toast function added. Still needs regression testing before activating + 12/20/2023 v1.8 Adding CheckBackupDriveSpace Script will error if backup drive free space less than 10GB + 12/28/2023 v1.9 Adding Set-EventLogMaxSize to change Veeam Event log max size from 512k to 10MB #> - param( - [Int]$VeeamCheck +#-VeeamCheck {{agent.VeeamDisableCheck}} +#-NumberOfDaysBeforeError {{agent.VeeamDaysBeforeError}} + +param( + [Int]$VeeamCheck, + [Int]$NumberOfDaysBeforeError, + [Int]$VeeamEventLogSize, + [switch]$debug ) -#$ErrorActionPreference= 'silentlycontinue' +#$PSBoundParameters + +if ($debug) { + $DebugPreference = "Continue" +} +else { + $DebugPreference = "SilentlyContinue" + $ErrorActionPreference = 'silentlycontinue' +} + +if ($NumberOfDaysBeforeError -eq "") { + $NumberOfDaysBeforeError = 10 +} + + +$logName = "Veeam Agent" +# ------------------------------------ + + +Write-Debug "NumberOfDaysBeforeError: $NumberOfDaysBeforeError" +#Write-Debug "Command line arguments splatting `$args:", $($args) +Write-Debug "args: $args" +Write-Output "----------------- INFO AND CHECK FOR PROBLEMS ----------------" +# Look for backup drive and make sure it's NTFS. Anything else and any restore will fail +#$Drive = get-psdrive | where { $_.Root -match ":" } | % { if (Test-Path ($_.Root + "VeeamBackup")) { $_.Root } } +$Drive = Get-PSDrive | Where-Object { $_.Root -match ":" } | ForEach-Object { + if (Test-Path ($_.Root + "VeeamBackup")) { + $_.Root.Substring(0, 1) # return only the first letter of the root + #break innerloop + } +} | Select-Object -Unique -# List last 20 Veeam Agent Log Items -# Get-EventLog "Veeam Agent" -newest 20 -After (Get-Date).AddDays(-1) -Write-Output "VeeamCheck: $VeeamCheck" +Write-Debug "Backup drive is $Drive" +if ([string]::IsNullOrEmpty($Drive)) { + Write-Debug "Backup drive not connected. Test for FileSystem type later." +} +else { + $DriveFS = (Get-Volume -DriveLetter $Drive).FileSystem + Write-Debug "Backup Drive File System: $DriveFS" + + if ($DriveFS -ne "NTFS") { + Write-Output "WARNING*WARNING*WARNING: Backup Drive isn't NTFS. Rebuild backup drive!!!" + Exit 1 + } +} + +# See if Custom Field has disabled VeeamCheck +Write-Debug "VeeamCheck: $VeeamCheck" if ($VeeamCheck) { - Write-Output "Veeam check disabled" + Write-Output "Veeam check disabled." + Exit 0 +} + +# Make sure Veeam is installed +if (-not(Test-Path -Path "C:\Program Files\Veeam\Endpoint Backup")) { + Write-Output "Veeam not installed." Exit 0 } -if (Test-Path -Path "C:\Program Files\Veeam\Endpoint Backup") { - Write-Output "Veeam Installed" - $Path = "C:\Program Files\Veeam\Endpoint Backup\Veeam.Backup.Core.dll" # Path to Veeam.Backup.Core.dll by default it's located in C:\Program Files\Veeam\Backup and Replication\Backup\Veeam.Backup.Core.dll - $Item = Get-Item -Path $Path - $Item.VersionInfo.ProductVersion -# $item.VersionInfo.FileVersion - $item.VersionInfo.Comments - $event = Get-EventLog "Veeam Agent" -newest 1 -After (Get-Date).AddDays(-1) | Where-Object { $_.InstanceID -eq 191 } +function CheckBackupDriveSpace { + # Get the drive information + $driveInfo = Get-PSDrive -Name $drive + + # Check if the drive exists + if ($null -eq $driveInfo) { + Write-Output "Drive $drive does not exist." + return + } + + # Convert free space to GB and format it with two decimal places + $freeSpaceGB = [math]::Round(($driveInfo.Free / 1GB), 2) + + # Convert 10GB to bytes (1GB = 1073741824 bytes) + $requiredSpace = 10 * 1073741824 + + # Check if the free space is less than 10GB + if ($driveInfo.Free -lt $requiredSpace) { + Write-Output "WARNING*WARNING*WARNING Drive $drive has only $freeSpaceGB GB free space, which is less than 10GB." + } + else { + Write-Debug "Drive $drive has $freeSpaceGB GB free space." + } +} + + + +# Get Veeam version +$Path = "C:\Program Files\Veeam\Endpoint Backup\Veeam.Backup.Core.dll" # Path to Veeam.Backup.Core.dll by default it's located in C:\Program Files\Veeam\Backup and Replication\Backup\Veeam.Backup.Core.dll +$Item = Get-Item -Path $Path +#$Item.VersionInfo.ProductVersion +#$item.VersionInfo.FileVersion +#$item.VersionInfo.Comments +Write-Debug "Veeam Installed: v$($Item.VersionInfo.ProductVersion)" + +function ToastAlerts { + #Needs testing + Write-Debug "last_successful_backup: $last_successful_backup" + Write-Debug "backup_time: $backup_time" + Write-Debug "days_since_last_backup: $days_since_last_backup" + Write-Debug "days_since_last_backup_dint: $days_since_last_backup_dint" + + function InstallToastRequirements { + # Check if NuGet is installed + if (!(Get-PackageProvider -Name NuGet -ListAvailable)) { + Write-Output "Nuget installing" + Install-PackageProvider -Name NuGet -Force + } + else { + Write-Debug "Nuget already installed" + } + + if (-not (Get-Module -Name BurntToast -ListAvailable)) { + Write-Output "BurntToast installing" + Install-Module -Name BurntToast -Force + } + else { + Write-Debug "BurntToast already installed" + } + + if (-not (Get-Module -Name RunAsUser -ListAvailable)) { + Write-Output "RunAsUser installing" + Install-Module -Name RunAsUser -Force + } + else { + Write-Debug "RunAsUser already installed" + } + } + InstallToastRequirements + + function TRMMTempFolder { + # Make sure the temp folder exists + If (!(test-path $env:ProgramData\TacticalRMM\temp)) { + New-Item -ItemType Directory -Force -Path "$env:ProgramData\TacticalRMM\temp" + } + Else { + Write-Debug "TRMM Temp folder exists" + } + } + TRMMTempFolder + + # Used to store text to show user and use inside the script block. Currently untested 2/22/2024 + Set-Content -Path $env:ProgramData\TacticalRMM\temp\toastmessage.txt -Value "Your external backup hasn't run since $backup_time ($days_since_last_backup_dint days). Please connect drive so it can update. Call if you have questions: 770-778-1672" + + Invoke-AsCurrentUser -scriptblock { + $messagetext = Get-Content -Path $env:ProgramData\TacticalRMM\temp\toastmessage.txt + $heroimage = New-BTImage -Source 'https://fixme/Logo9a.png' -HeroImage + $Text1 = New-BTText -Content "Message from xyz" + $Text2 = New-BTText -Content "$messagetext" + $Button = New-BTButton -Content "Snooze" -snooze -id 'SnoozeTime' + $Button2 = New-BTButton -Content "Dismiss" -dismiss + $5Min = New-BTSelectionBoxItem -Id 5 -Content '5 minutes' + $10Min = New-BTSelectionBoxItem -Id 10 -Content '10 minutes' + $1Hour = New-BTSelectionBoxItem -Id 60 -Content '1 hour' + $4Hour = New-BTSelectionBoxItem -Id 240 -Content '4 hours' + $1Day = New-BTSelectionBoxItem -Id 1440 -Content '1 day' + $Items = $5Min, $10Min, $1Hour, $4Hour, $1Day + $SelectionBox = New-BTInput -Id 'SnoozeTime' -DefaultSelectionBoxItemId 10 -Items $Items + $action = New-BTAction -Buttons $Button, $Button2 -inputs $SelectionBox + $Binding = New-BTBinding -Children $Text1, $Text2 -HeroImage $heroimage + $Visual = New-BTVisual -BindingGeneric $Binding + $Content = New-BTContent -Visual $Visual -Actions $action + Submit-BTNotification -Content $Content + } + + # Cleanup temp file for message variables + Remove-Item -Path $env:ProgramData\TacticalRMM\temp\toastmessage.txt +} +# ToastAlerts + + +If ($Debug) { + Write-Output "=================== DEBUG ===================" + + $ErrorActionPreference = 'silentlycontinue' + + $total_events = Get-EventLog -LogName $logName | Measure-Object | Select-Object -ExpandProperty Count + Write-Output "Total Events in Veeam Log: $total_events" + + $currentMaxSize = (Get-EventLog -List | Where-Object { $_.Log -eq $logName }).MaximumKilobytes + Write-Output "Current Maximum Size: $currentMaxSize KB" - if ($event.entrytype -eq "Warning") { - write-Output "Latest Veeam Backup Failed" - Get-EventLog "Veeam Agent" -newest 1 -After (Get-Date).AddDays(-1) | Format-List TimeGenerated, InstanceID, EntryType, Message - write-Output "Last Successful Backup was" - Get-EventLog "Veeam Agent" -EntryType Information,Warning -InstanceId 190 -newest 1 | Format-List TimeGenerated, InstanceID, EntryType, Message - Exit 1 + + $oldest_event = Get-WinEvent -FilterHashtable @{LogName = $logName } | Sort-Object -Property TimeCreated | Select-Object -First 1 -ExpandProperty TimeCreated + Write-Output "Oldest Event in Veeam Log: $oldest_event" + + Write-Output "-----------------------" + $oldest_errorevent = Get-EventLog $logName -EntryType Error -InstanceId 190 -newest 1 + if ($oldest_errorevent) { + $lasterrortime = $oldest_errorevent.TimeGenerated + Write-Output "Last Error Backup: $lasterrortime" + Get-EventLog $logName -EntryType Error -InstanceId 190 -newest 1 | Format-List TimeGenerated, InstanceID, EntryType, Message + } + else { + Write-Output "No error events found." + } + + Write-Output "-----------------------" + $last_warning_event = Get-EventLog $logName -EntryType Information, Warning -InstanceId 190 -newest 1 + if ($last_warning_event) { + $last_warning_time = $last_warning_event.TimeGenerated + Write-Output "Last Warning Successful Backup: $last_warning_time" + $last_warning_event | Format-List TimeGenerated, InstanceID, EntryType, Message } else { - write-host "Veeam Backup ok, time of last backup:" - Get-EventLog "Veeam Agent" -EntryType Information,Warning -InstanceId 190 -newest 1 | Format-List TimeGenerated - Exit 0 + Write-Output "No warning events found." } + + Write-Output "-----------------------" + $last_success_event = Get-EventLog $logName -EntryType Information -InstanceId 190 -newest 1 + if ($last_success_event) { + $last_success_time = $last_success_event.TimeGenerated + Write-Output "Last Successful Backup: $last_success_time" + $last_success_event | Format-List TimeGenerated, InstanceID, EntryType, Message + } + else { + Write-Output "No successful backup events found." + } + Write-Output "================= END DEBUG =================" +} + +function Set-EventLogMaxSize { + param ( + [int]$NewMaxSizeMB = 10 + ) + $logName = "Veeam Agent" + + $currentMaxSize = (Get-EventLog -List | Where-Object { $_.Log -eq $LogName }).MaximumKilobytes + Write-Debug "Current Maximum Size: $currentMaxSize KB" + + $desiredMaxSize = $NewMaxSizeMB * 1MB + + if (($currentMaxSize * 1024) -ne $desiredMaxSize) { + Write-Output "Changing to $NewMaxSizeMB MB." + Limit-EventLog -LogName $LogName -MaximumSize $desiredMaxSize + } else { + Write-Debug "No change necessary." + } } -else { - Write-Output "Veeam not Installed" - exit 0 + +$currentMaxSize = (Get-EventLog -List | Where-Object { $_.Log -eq $LogName }).MaximumKilobytes +If ($currentMaxSize -eq 512) { + Write-Output "Current Size test = 512KB, going to make it bigger" + Set-EventLogMaxSize } + + +Write-Output "------------- Veeam Backup Data --------------" + +Write-Debug "Error if no backup within this number of days: $NumberOfDaysBeforeError" +$date_to_check = (Get-Date).AddDays(-$NumberOfDaysBeforeError) + +$oldest_event = Get-WinEvent -FilterHashtable @{LogName = $logName } | Sort-Object -Property TimeCreated | Select-Object -First 1 -ExpandProperty TimeCreated +$oldest_event_formatted = $oldest_event.ToString("yyyy-MM-dd HH:mm:ss") +Write-Debug "Oldest Event in Veeam Log: $oldest_event_formatted" + +$date_to_check_formatted = $date_to_check.ToString("yyyy-MM-dd HH:mm:ss") +Write-Debug "Date to Check back to: $date_to_check_formatted" + +$last_successful_backup = Get-EventLog $logName -EntryType Information, Warning -InstanceId 190 -newest 1 +$backup_time = $last_successful_backup.TimeGenerated.ToString("yyyy-MM-dd HH:mm:ss") +Write-Output "Last Successful backup: $backup_time" + +if ($last_successful_backup.TimeGenerated -lt $date_to_check) { + if ($backup_time -eq $null) { + Write-Output "WARNING*WARNING*WARNING: Last successful backup was UNKNOWN. Investigate!" + } + else { + $days_since_last_backup = (Get-Date) - $last_successful_backup.TimeGenerated + $days_since_last_backup = $days_since_last_backup.Days + Write-Output "WARNING*WARNING*WARNING: Last successful backup was $($last_successful_backup.TimeGenerated) : $days_since_last_backup days ago" + Write-Output "That's more than $NumberOfDaysBeforeError days ago. Investigate!" + + Get-EventLog "Veeam Agent" -newest 1 -After (Get-Date).AddDays(-1) | Format-List TimeGenerated, InstanceID, EntryType, Message + } + CheckBackupDriveSpace + Exit 1 +} +else { + Write-Output "GOOD: Last successful backup on $($last_successful_backup.TimeGenerated) was less than $NumberOfDaysBeforeError days ago. All good!" + #$last_successful_backup.TimeGenerated + Exit 0 +} \ No newline at end of file diff --git a/scripts_wip/Win_Veeam_CollectorLastBackupDate.ps1 b/scripts_wip/Win_Veeam_CollectorLastBackupDate.ps1 new file mode 100644 index 00000000..90075c37 --- /dev/null +++ b/scripts_wip/Win_Veeam_CollectorLastBackupDate.ps1 @@ -0,0 +1,4 @@ +$logName = "Veeam Agent" + +$last_successful_backup = Get-EventLog $logName -EntryType Information, Warning -InstanceId 190 -newest 1 +$last_successful_backup.TimeGenerated \ No newline at end of file diff --git a/scripts_wip/Win_Veeam_RecoveryMediaCreate.ps1 b/scripts_wip/Win_Veeam_RecoveryMediaCreate.ps1 new file mode 100644 index 00000000..aa428ed7 --- /dev/null +++ b/scripts_wip/Win_Veeam_RecoveryMediaCreate.ps1 @@ -0,0 +1,34 @@ +# + +param( + [string] $Drive +) + +# Set variable for "USB drive" through a search for a unique directory only available on a USB drive. +$Drive = get-psdrive | where {$_.Root -match ":"} |% {if (Test-Path ($_.Root + "VeeamBackup")){$_.Root}} + +Write-output "Drive is $Drive" + +# $param="/createrecoverymediaiso /f:$Drive:\VeeamRecovery$ENV:COMPUTERNAME.iso" +# "C:\Program Files\Veeam\Endpoint Backup\Veeam.EndPoint.Manager.exe" /createrecoverymediaiso /f:$Drive:\VeeamRecovery$ENV:COMPUTERNAME.iso + +# Write-output "C:\Program Files\Veeam\Endpoint Backup\Veeam.EndPoint.Manager.exe /createrecoverymediaiso /f:${Drive}:\VeeamRecovery$ENV:COMPUTERNAME.iso" +# Write-output $param + +#Get version number +$Path = "C:\Program Files\Veeam\Endpoint Backup\Veeam.Backup.Core.dll" # Path to Veeam.Backup.Core.dll by default it's located in C:\Program Files\Veeam\Backup and Replication\Backup\Veeam.Backup.Core.dll +$Item = Get-Item -Path $Path +$Item.VersionInfo.ProductVersion +$item.VersionInfo.Comments + + +$proc = Start-Process "C:\Program Files\Veeam\Endpoint Backup\Veeam.EndPoint.Manager.exe" -ArgumentList "/createrecoverymediaiso /f:${Drive}\VeeamRecovery$ENV:COMPUTERNAME.iso" -PassThru + Wait-Process -InputObject $proc + if ($proc.ExitCode -ne 0) { + Write-Warning "Exited with error code: $($proc.ExitCode)" + Write-output $proc + } + else { + Write-Output "Successful install with exit code: $($proc.ExitCode)" + Write-output $proc + } diff --git a/scripts_wip/Win_WinREFix.ps1 b/scripts_wip/Win_WinREFix.ps1 new file mode 100644 index 00000000..5f96872e --- /dev/null +++ b/scripts_wip/Win_WinREFix.ps1 @@ -0,0 +1,72 @@ +<# +.SYNOPSIS + Move WinRE partition to end and increase size by 250MB every run. +.DESCRIPTION + This script resolves the error caused by the January 1st 2024 update for KB5034441 +#> +Write-Output "Diagnosing Windows RE Issues..." +Write-Output "Getting disk info" +$diskpart = [System.Text.StringBuilder]::new() +$osdisk = (Get-disk | Where-Object { $_.IsBoot }).Number +[void]$diskpart.AppendLine("sel disk $osdisk") +[void]$diskpart.AppendLine("list part") +Remove-Item diskpart.txt -ErrorAction SilentlyContinue | Out-Null +New-Item diskpart.txt | Out-Null +Add-Content diskpart.txt $diskpart.ToString() +$output = & diskpart /s diskpart.txt +$primaryPart = ( -split ($output -match "Primary"))[1] +$winREPart = ( -split ($output -match "Recovery"))[1] +$winRESize = ( -split ($output -match "Recovery"))[3] +$winREOffset = ( -split ($output -match "Recovery"))[5] +$winREOffsetType = ( -split ($output -match "Recovery"))[6] +Write-Output "WinRE Partition: Part: $winREPart - Size: $winRESize - Offset: $winREOffset - Type: $winREOffsetType" +if (-Not(Test-Path "C:\Windows\System32\Recovery\Winre.wim")) { + Write-Output "WinRE image missing. Disabling RE Agent..." + & reagentc /disable +} + +if (-Not(Test-Path "C:\Windows\System32\Recovery\Winre.wim")) { + Write-Error "WinRE image still missing. Download Recovery wim file to expected location..." + exit 1 +} +else { + Write-Output "WinRE image available" +} + +$diskInfo = ("list disk" | diskpart) +Write-Output "Fixing WinRE Drive Configuration..." +#check if gpt or mbr +[void]$diskpart.Clear() +[void]$diskpart.AppendLine("sel disk $osdisk") +if ($winREPart) { + #has winre partition + [void]$diskpart.AppendLine("sel part $winREPart") + [void]$diskpart.AppendLine("delete partition override") +} + +if ($diskInfo -match "\*") { + Write-Output "Disk is GPT" + [void]$diskpart.AppendLine("sel part $primaryPart") + [void]$diskpart.AppendLine("shrink desired=250 minimum=250") + [void]$diskpart.AppendLine("create partition primary id=de94bba4-06d1-4d40-a16a-bfd50179d6ac") + [void]$diskpart.AppendLine("gpt attributes =0x8000000000000001") + [void]$diskpart.AppendLine("format quick fs=ntfs label=`"Windows RE tools`"") +} +else { + Write-Output "Disk is MBR" + [void]$diskpart.AppendLine("sel part $primaryPart") + [void]$diskpart.AppendLine("shrink desired=250 minimum=250") + [void]$diskpart.AppendLine("create partition primary id=27") + [void]$diskpart.AppendLine("format quick fs=ntfs label=`"Windows RE tools`"") +} + +Clear-Content diskpart.txt +Add-Content diskpart.txt $diskpart.ToString() +$output = & diskpart /s diskpart.txt +Remove-Item diskpart.txt +$output + +Write-Output "Enabling reagent..." +& reagentc /enable +& reagentc /info +Write-Output "System must be rebooted before patching." \ No newline at end of file diff --git a/scripts_wip/Win_Windows_11_Upgrade.ps1 b/scripts_wip/Win_Windows_11_Upgrade.ps1 new file mode 100644 index 00000000..2a303cc9 --- /dev/null +++ b/scripts_wip/Win_Windows_11_Upgrade.ps1 @@ -0,0 +1,1028 @@ +<# +.SYNOPSIS + Upgrades Windows 10 to Windows 11 after validating system requirements. +.DESCRIPTION + This script checks system compatibility, downloads the Windows 11 Installation Assistant, + and initiates the upgrade process. It includes detailed error checking and reporting. +.AUTHOR + redanthrax +.DATE + May 6, 2025 +.VERSION + 1.1 (Optimized) +.EXAMPLE + .\Win_Windows_11_Upgrade.ps1 + This command checks system compatibility and downloads the Windows 11 Installation Assistant to install Windows 11. +.EXAMPLE + .\Win_Windows_11_Upgrade.ps1 -Force + This command forces the upgrade to Windows 11 and deletes drivers that are blocking the install. +.EXAMPLE + .\Win_Windows_11_Upgrade.ps1 -Force -IsoLocation "C:\Path\To\Windows11.iso" + This command specifies a custom ISO location for the Windows 11 installation and forces the install. +#> + +# Define parameters +param( + [switch]$Force, + [string]$IsoLocation +) + +#--------------------------------- +# Configuration +#--------------------------------- +$Config = @{ + Win11SetupUrl = "https://go.microsoft.com/fwlink/?linkid=2171764" + SetupPath = "$env:TEMP\Win11InstallationAssistant.exe" + IsoLocation = $IsoLocation + IsoPath = "$env:TEMP\Windows11.iso" + MountPath = "$env:TEMP\Mount" + LogPath = "C:\SetupLogs" + LogPaths = @{ + PantherDir = "C:\`$WINDOWS.~BT\Sources\Panther" + SetupAct = "C:\`$WINDOWS.~BT\Sources\Panther\setupact.log" + SetupErr = "C:\`$WINDOWS.~BT\Sources\Panther\setuperr.log" + ScanResult = "C:\`$WINDOWS.~BT\Sources\Panther\ScanResult.xml" + CompatData = "C:\`$WINDOWS.~BT\Sources\Panther\CompatData.xml" + UpdateLogDir = "$env:SystemRoot\Logs\MoSetup" + WindowsUpdate = "$env:SystemRoot\Logs\WindowsUpdate" + SetupDiag = "$env:SystemRoot\Logs\SetupDiag" + } + TimeoutMinutes = 180 + StatusIntervalSec = 30 + ErrorPatterns = @('error', 'failure', 'failed', 'crash', 'compatibility', 'blockage', 'block found', 'not supported', 'rollback', 'could not complete', 'blockmigration', 'migration block', 'Result = 0x', 'HRESULT', 'Error code:') +} + +# Windows Setup Error Code Dictionary +$SetupErrorCodes = @{ + # Migration errors + "0x0000007E" = @{ + Description = "Failed to load migration components" + Explanation = "Windows could not load the migration module (migcore.dll) required for the upgrade" + Solution = "Run SFC /scannow to repair system files, ensure Windows Update is fully updated, and try again" + Category = "Migration" + Severity = "High" + } + "0xC1900204" = @{ + Description = "Migration choice not available" + Explanation = "System settings or configurations are not compatible with migration to Windows 11" + Solution = "Check system compatibility, ensure drivers are updated, and remove blocking applications" + Category = "Migration" + Severity = "High" + } + + # Resource management errors + "0xD0000003" = @{ + Description = "Resource management error (EcoQos)" + Explanation = "Windows couldn't allocate necessary system resources to perform the upgrade" + Solution = "Close other applications, ensure sufficient disk space, and try restarting the system" + Category = "Resources" + Severity = "Medium" + } + + # COM/Interface errors + "0x80040154" = @{ + Description = "Interface not registered (REGDB_E_CLASSNOTREG)" + Explanation = "A required component or interface wasn't properly registered in the system" + Solution = "Run the System File Checker (sfc /scannow) and DISM to repair Windows components" + Category = "Component" + Severity = "Medium" + } + + # Setup process errors + "0xC1800104" = @{ + Description = "Setup process suspension error" + Explanation = "The upgrade process was suspended due to a critical error or compatibility issue" + Solution = "Check setup logs for specific compatibility issues and resolve them before retrying" + Category = "Setup" + Severity = "High" + } + "0x800704D3" = @{ + Description = "Process interrupted or terminated" + Explanation = "The upgrade process was interrupted, possibly by another application or service" + Solution = "Close all non-essential applications and services before attempting the upgrade" + Category = "Setup" + Severity = "Medium" + } + + # Hardware compatibility errors + "0xC1900200" = @{ + Description = "System doesn't meet minimum requirements" + Explanation = "The device doesn't meet Windows 11 hardware requirements" + Solution = "Check CPU, TPM, RAM, WinRE, and disk space requirements for Windows 11" + Category = "Hardware" + Severity = "Critical" + } + "0xC1900202" = @{ + Description = "System doesn't meet minimum requirements for update" + Explanation = "System configuration doesn't meet Windows 11 requirements" + Solution = "Ensure TPM 2.0 is enabled, Secure Boot is enabled, and all hardware meets requirements" + Category = "Hardware" + Severity = "Critical" + } + + # Storage errors + "0x80070070" = @{ + Description = "Insufficient disk space" + Explanation = "Not enough free space on the system drive for the upgrade" + Solution = "Free up at least 20GB of space on the system drive and try again" + Category = "Storage" + Severity = "Medium" + } + + # Generic errors + "0xC1900101" = @{ + Description = "Driver compatibility error" + Explanation = "A driver on your system is incompatible with Windows 11" + Solution = "Update all device drivers to the latest versions, especially graphics, network, and storage drivers" + Category = "Driver" + Severity = "High" + } + "0x80004005" = @{ + Description = "Unspecified error (E_FAIL)" + Explanation = "A general failure occurred during the upgrade process" + Solution = "Check for driver updates, ensure sufficient disk space, and remove third-party security software" + Category = "General" + Severity = "Medium" + } +} + +#--------------------------------- +# Utility Functions +#--------------------------------- + +function Write-Log { + <# + .SYNOPSIS + Writes a log message to the console with timestamp and severity. + #> + param ( + [Parameter(Mandatory)] + [string]$Message, + [ValidateSet("INFO", "WARNING", "ERROR")] + [string]$Level = "INFO" + ) + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + $color = switch ($Level) { + "INFO" { "Green" } + "WARNING" { "Yellow" } + "ERROR" { "Red" } + } + Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color +} + +function Handle-Error { + <# + .SYNOPSIS + Centralized error handling function. + #> + param ( + [Parameter(Mandatory)] + [string]$Message, + [Parameter(Mandatory = $false)] + $Exception + ) + + Write-Log "Error: $Message" "ERROR" + if ($Exception) { + $errorMessage = if ($Exception -is [System.Management.Automation.ErrorRecord]) { + $Exception.Exception.Message + } + else { + $Exception.Message + } + Write-Log "Details: $errorMessage" "ERROR" + } +} + +function Test-Admin { + <# + .SYNOPSIS + Checks if the script is running with administrative privileges. + #> + $user = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() + if (-not $user.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + Write-Log "Script requires administrative privileges." "ERROR" + Start-Process PowerShell.exe -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`"" -Verb RunAs + exit + } + + Write-Log "Running with administrative privileges." "INFO" +} + +#--------------------------------- +# Core Functions +#--------------------------------- + +function Test-WinREStatus { + <# + .SYNOPSIS + Checks if Windows Recovery Environment (WinRE) is enabled and properly configured. + .OUTPUTS + Boolean indicating if WinRE is enabled and properly configured. + #> + try { + $reagentInfo = reagentc /info + $winREEnabled = $reagentInfo | Select-String "Windows RE Status:\s+Enabled" + + if ($winREEnabled) { + # Verify WinRE image is registered + $imageInfo = reagentc /info | Select-String "Windows RE location" + if ($imageInfo -and $imageInfo.Line -notmatch "Not found") { + Write-Log "WinRE: Enabled and properly configured" "INFO" + return $true + } + else { + Write-Log "WinRE: Enabled but image not properly registered" "WARNING" + return $false + } + } + else { + Write-Log "WinRE: Not enabled" "ERROR" + return $false + } + } + catch { + Handle-Error -Message "Failed to check WinRE status." -Exception $_ + return $false + } +} + +function Test-SystemCompatibility { + <# + .SYNOPSIS + Validates system requirements for Windows 11, including WinRE status. + #> + $results = @{ TPM = $false; SecureBoot = $false; System = $false; WinRE = $false } + + # Check TPM + try { + $tpm = Get-Tpm + if ($tpm.TpmPresent -and $tpm.TpmReady) { + $tpmVersion = (Get-WmiObject -Namespace "root\CIMV2\Security\MicrosoftTpm" -Class "Win32_Tpm").SpecVersion.Split(",")[0] + $results.TPM = $tpmVersion -ge 2 + Write-Log "TPM: $(if ($results.TPM) { 'Version 2.0 or higher detected' } else { 'Failed requirements' })" "INFO" + } + else { + Write-Log "TPM not present or not ready." "ERROR" + } + } + catch { + Handle-Error -Message "Failed to check TPM." -Exception $_ + } + + # Check Secure Boot + try { + $results.SecureBoot = Confirm-SecureBootUEFI + Write-Log "Secure Boot: $(if ($results.SecureBoot) { 'Enabled' } else { 'Not enabled' })" "INFO" + } + catch { + Handle-Error -Message "Failed to check Secure Boot." -Exception $_ + } + + # Check system requirements + try { + $processor = Get-CimInstance Win32_Processor + $memory = Get-CimInstance Win32_ComputerSystem + $disk = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='$env:SystemDrive'" + $results.System = ($processor.NumberOfCores -ge 2) -and + ([math]::Round($memory.TotalPhysicalMemory / 1GB, 2) -ge 4) -and + ([math]::Round($disk.FreeSpace / 1GB, 2) -ge 64) + Write-Log "System: $(if ($results.System) { 'Meets requirements' } else { 'Insufficient CPU, RAM, or disk space' })" "INFO" + } + catch { + Handle-Error -Message "Failed to check system requirements." -Exception $_ + } + + # Check WinRE status + try { + $results.WinRE = Test-WinREStatus + } + catch { + Handle-Error -Message "Failed to check WinRE status." -Exception $_ + } + + return $results +} + +function Get-DriverBlocks { + <# + .SYNOPSIS + Checks for driver migration blocks in ScanResult.xml. + #> + $blockedDrivers = @() + $scanResultPath = $Config.LogPaths.ScanResult + + if (-not (Test-Path $scanResultPath)) { + Write-Log "ScanResult.xml not found." "INFO" + return $blockedDrivers + } + + try { + [xml]$scanResult = Get-Content $scanResultPath -ErrorAction Stop + $blockedDrivers = $scanResult.CompatReport.DriverPackages.DriverPackage | Where-Object { $_.BlockMigration -eq 'True' } + + # check if $blockedDrivers is an object or an array + if ($blockedDrivers -is [System.Xml.XmlNode]) { + $blockedDrivers = @($blockedDrivers) + } + + foreach ($driver in $blockedDrives) { + $blockedDrivers += [PSCustomObject]@{ + InfFile = $driver.Inf + HasSignedBinaries = $driver.HasSignedBinaries + BlockReason = "Migration block" + } + } + + Write-Log "Found $($blockedDrivers.Count) driver blocks." "WARNING" + } + catch { + Handle-Error -Message "Failed to parse ScanResult.xml." -Exception $_ + } + + return $blockedDrivers +} + + +function Check-PreviousUpgradeAttempt { + <# + .SYNOPSIS + Checks for evidence of previous Windows 11 upgrade attempts. + .PARAMETER Force + If specified, proceeds with the upgrade but still reports critical issues. + .OUTPUTS + Hashtable containing details of previous attempts. + #> + param( + [switch]$Force + ) + + $result = @{ + PreviousAttemptFound = $false + FailureDetected = $false + FailureReason = $null + UpgradeDate = $null + LogsExist = $false + BlockedDrivers = @() + ErrorEntries = @() + } + + Write-Log "Checking for previous Windows 11 upgrade attempts..." "INFO" + + # Check for setup directories + $setupDirs = @( + $Config.LogPaths.PantherDir, + "$env:SystemRoot\Panther", + "$env:SystemDrive\ESD\Windows" + ) + + foreach ($dir in $setupDirs) { + if (Test-Path $dir) { + $result.PreviousAttemptFound = $true + $result.LogsExist = $true + Write-Log "Found setup directory: $dir" "INFO" + + # Get upgrade date from newest log + $logFiles = Get-ChildItem -Path $dir -Filter "*.log" -ErrorAction SilentlyContinue + if ($logFiles) { + $newestLog = $logFiles | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + $result.UpgradeDate = $newestLog.LastWriteTime + Write-Log "Previous attempt detected on: $($newestLog.LastWriteTime)" "INFO" + } + break + } + } + + # Always check for driver blocks and log errors, even with -Force + $blockedDrivers = Get-DriverBlocks + if ($blockedDrivers.Count -gt 0) { + $result.FailureDetected = $true + $result.FailureReason = "Driver Compatibility Issues" + $result.BlockedDrivers = $blockedDrivers + Write-Log "Previous upgrade failed due to $($blockedDrivers.Count) driver blocks." "ERROR" + } + + $errorEntries = Get-UpgradeLogErrors + if ($errorEntries.Count -gt 0) { + $result.ErrorEntries = $errorEntries + if (-not $result.FailureDetected) { + $result.FailureDetected = $true + $result.FailureReason = "Upgrade Errors Detected" + } + + Write-Log "Previous upgrade failed due to $($errorEntries.Count) errors in logs." "ERROR" + Show-UpgradeFailureInfo -ErrorEntries $errorEntries + } + + if ($result.PreviousAttemptFound -and $result.FailureDetected -and $Force) { + Write-Log "-Force specified: Proceeding despite previous issues. Resolve reported issues to ensure success." "WARNING" + } + elseif ($result.PreviousAttemptFound -and -not $Force) { + Write-Log "Previous attempt detected. Use -Force to proceed or resolve issues." "WARNING" + } + else { + Write-Log "No critical issues detected from previous attempts." "INFO" + } + + return $result +} + +function Show-PreviousUpgradeAttemptInfo { + <# + .SYNOPSIS + Displays information about a previous upgrade attempt. + #> + param( + [Parameter(Mandatory)] + [hashtable]$PreviousAttempt + ) + + if (-not $PreviousAttempt.PreviousAttemptFound) { + return + } + + Write-Log "Previous Windows 11 upgrade attempt detected." "WARNING" + if ($PreviousAttempt.UpgradeDate) { + Write-Log "Date: $($PreviousAttempt.UpgradeDate)" "INFO" + } + + if ($PreviousAttempt.FailureDetected) { + Write-Log "Status: Failed - $($PreviousAttempt.FailureReason)" "ERROR" + if ($PreviousAttempt.BlockedDrivers.Count -gt 0) { + Show-BlockedDriverInfo -BlockedDrivers $PreviousAttempt.BlockedDrivers + } + + if ($PreviousAttempt.ErrorEntries.Count -gt 0) { + Show-UpgradeFailureInfo -ErrorEntries $PreviousAttempt.ErrorEntries + } + + Write-Log "Action: Address issues above. Use -Force to retry." "INFO" + } + else { + Write-Log "Status: Incomplete or canceled." "WARNING" + } +} + +function Get-UpgradeLogErrors { + <# + .SYNOPSIS + Parses Windows setup logs for errors and returns error entries. + .OUTPUTS + Array of PSCustomObjects containing error details. + #> + [CmdletBinding()] + param() + + Write-Log "Analyzing Windows setup logs for errors..." "INFO" + $errorEntries = @() + + # Check Panther directory + $pantherDir = $Config.LogPaths.PantherDir + if (Test-Path $pantherDir) { + Write-Log "Found setup logs in $pantherDir" "INFO" + $logFiles = Get-ChildItem -Path $pantherDir -Filter "*.log" -ErrorAction SilentlyContinue + + foreach ($logFile in $logFiles) { + Write-Log "Scanning $($logFile.Name)..." "INFO" + $matches = Select-String -Path $logFile.FullName -Pattern $Config.ErrorPatterns -Context 2, 2 -ErrorAction SilentlyContinue + foreach ($match in $matches) { + $errorEntries += [PSCustomObject]@{ + LogFile = $logFile.Name + LineNumber = $match.LineNumber + Context = $match.Context | Out-String + Line = $match.Line + TimeFound = Get-Date + } + } + } + } + else { + Write-Log "No setup logs found in $pantherDir" "INFO" + } + + # Check additional log directories + $additionalDirs = @($Config.LogPaths.UpdateLogDir, $Config.LogPaths.WindowsUpdate) + foreach ($dir in $additionalDirs) { + if (Test-Path $dir) { + $logFiles = Get-ChildItem -Path $dir -Filter "*.log" -Recurse -ErrorAction SilentlyContinue | + Where-Object { $_.LastWriteTime -gt (Get-Date).AddHours(-24) } + foreach ($logFile in $logFiles) { + $matches = Select-String -Path $logFile.FullName -Pattern $Config.ErrorPatterns -Context 2, 2 -ErrorAction SilentlyContinue + foreach ($match in $matches) { + $errorEntries += [PSCustomObject]@{ + LogFile = $logFile.Name + LineNumber = $match.LineNumber + Context = $match.Context | Out-String + Line = $match.Line + TimeFound = Get-Date + } + } + } + } + } + + Write-Log "Found $($errorEntries.Count) error entries in logs." "INFO" + return $errorEntries +} + +function Show-BlockedDriverInfo { + <# + .SYNOPSIS + Displays details about blocked drivers preventing the upgrade. + #> + param ( + [Parameter(Mandatory)] + [Array]$BlockedDrivers + ) + + if ($BlockedDrivers.Count -eq 0) { + return + } + + Write-Log "Critical: $($BlockedDrivers.Count) incompatible drivers detected." "ERROR" + # match the driver Inf to the driver from Parse-PnpUtilDrivers + + $pnpDrivers = Parse-PnpUtilDrivers -RunCommand + + foreach ($driver in $BlockedDrivers) { + Write-Log "Driver: $($driver.Inf)" "ERROR" + Write-Log "Matched Driver: $($pnpDrivers | Where-Object { $_."Published Name" -eq $driver.Inf } | Select-Object -ExpandProperty "Original Name")" "ERROR" + Write-Log "Signed: $($driver.HasSignedBinaries)" "INFO" + } + + Write-Log "Resolve these driver issues before retrying the upgrade." "WARNING" +} + +function Show-UpgradeFailureInfo { + <# + .SYNOPSIS + Displays detailed information about upgrade failures. + #> + param ( + [Parameter(Mandatory)] + [System.Collections.ArrayList]$ErrorEntries + ) + + if ($ErrorEntries.Count -eq 0) { + Write-Log "No errors found in logs." "INFO" + return + } + + Write-Log "Found $($ErrorEntries.Count) errors in upgrade logs:" "WARNING" + foreach ($error in $ErrorEntries | Select-Object -First 3) { + Write-Log "Log: $($error.LogFile), Line: $($error.LineNumber)" "INFO" + Write-Log "Error: $($error.Line)" "ERROR" + if ($error.Context) { + Write-Log "Context:" "INFO" + ($error.Context -split "`n" | Where-Object { $_ -match '\S' }) | ForEach-Object { Write-Log " $_" "INFO" } + } + } + + if ($ErrorEntries.Count -gt 3) { + Write-Log "... and $($ErrorEntries.Count - 3) more errors." "INFO" + } +} + +function Get-ErrorCodesFromLogs { + <# + .SYNOPSIS + Extracts error codes from log entries. + #> + param ( + [Parameter(Mandatory)] + [System.Collections.ArrayList]$ErrorEntries + ) + + $errorCodes = @() + $errorCodeRegex = '0x[0-9A-F]{8}|0x[0-9A-F]{4,6}' + + foreach ($entry in $ErrorEntries) { + if ($entry.Line -match $errorCodeRegex) { + $matches = [regex]::Matches($entry.Line, $errorCodeRegex) + foreach ($match in $matches) { + if (-not $errorCodes.Contains($match.Value)) { + $errorCodes += $match.Value + } + } + } + } + + return $errorCodes +} + +function Get-LastSetupError { + <# + .SYNOPSIS + Analyzes the SetupAct.log file to find the last setup error and provides details about it. + .DESCRIPTION + This function parses the Windows Setup log (setupact.log) to identify the last error code, + then uses the SetupErrorCodes dictionary to provide a description, explanation, and solution. + .OUTPUTS + PSCustomObject containing error details including the error code, timestamp, description, explanation, and solution. + #> + [CmdletBinding()] + param() + + $setupActLogPath = $Config.LogPaths.SetupAct + $errorCodeRegex = '0x[0-9A-F]{8}|0x[0-9A-F]{4,6}' + $result = [PSCustomObject]@{ + ErrorFound = $false + ErrorCode = $null + Timestamp = $null + LogLine = $null + Description = $null + Explanation = $null + Solution = $null + Category = $null + Severity = $null + } + + if (-not (Test-Path $setupActLogPath)) { + Write-Log "SetupAct.log not found at $setupActLogPath" "WARNING" + return $result + } + + Write-Log "Analyzing SetupAct.log for the last error..." "INFO" + + try { + # Get all lines containing error codes + $errorLines = Select-String -Path $setupActLogPath -Pattern $errorCodeRegex -ErrorAction Stop + + if ($errorLines.Count -eq 0) { + Write-Log "No error codes found in SetupAct.log" "INFO" + return $result + } + + # Get the last error line + $lastErrorLine = $errorLines | Select-Object -Last 1 + + # Extract the error code + $matches = [regex]::Matches($lastErrorLine.Line, $errorCodeRegex) + if ($matches.Count -eq 0) { + Write-Log "Error code pattern matched but no code extracted" "WARNING" + return $result + } + + $errorCode = $matches[0].Value + + # Try to extract timestamp from the log line + $timestamp = if ($lastErrorLine.Line -match '^\s*\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]') { + try { + [datetime]::ParseExact($matches[1], "yyyy-MM-dd HH:mm:ss", [System.Globalization.CultureInfo]::InvariantCulture) + } + catch { + Get-Date + } + } + else { + Get-Date + } + + # Look up error code in SetupErrorCodes dictionary + $errorInfo = $SetupErrorCodes[$errorCode] + if ($errorInfo) { + $result.ErrorFound = $true + $result.ErrorCode = $errorCode + $result.Timestamp = $timestamp + $result.LogLine = $lastErrorLine.Line + $result.Description = $errorInfo.Description + $result.Explanation = $errorInfo.Explanation + $result.Solution = $errorInfo.Solution + $result.Category = $errorInfo.Category + $result.Severity = $errorInfo.Severity + + Write-Log "Found last error code $errorCode in SetupAct.log" "WARNING" + Write-Log "Error: $($errorInfo.Description)" "ERROR" + Write-Log "Explanation: $($errorInfo.Explanation)" "WARNING" + Write-Log "Solution: $($errorInfo.Solution)" "INFO" + } + else { + $result.ErrorFound = $true + $result.ErrorCode = $errorCode + $result.Timestamp = $timestamp + $result.LogLine = $lastErrorLine.Line + $result.Description = "Unknown error code" + $result.Explanation = "This error code is not documented in the SetupErrorCodes dictionary" + $result.Solution = "Search online for more information about this error code" + $result.Category = "Unknown" + $result.Severity = "Unknown" + + Write-Log "Found unknown error code $errorCode in SetupAct.log" "WARNING" + } + } + catch { + Handle-Error -Message "Failed to analyze SetupAct.log" -Exception $_ + } + + return $result +} + +function Parse-PnpUtilDrivers { + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true, Position = 0)] + [string[]]$InputData, + + [Parameter()] + [switch]$RunCommand + ) + + begin { + $rawOutput = @() + $drivers = @() + } + + process { + # Collect all input lines + if ($InputData) { + $rawOutput += $InputData + } + } + + end { + # If the RunCommand switch is specified, execute pnputil and get its output + if ($RunCommand) { + $rawOutput = pnputil /enum-drivers + } + + # If we have no input, exit + if (-not $rawOutput) { + Write-Warning "No input data provided." + return + } + + # Join all lines into a single string + $outputText = $rawOutput -join "`n" + + # Split the output into blocks for each driver + # Each driver entry is separated by one or more blank lines + $driverBlocks = $outputText -split '(?m)^\s*$\s*(?=Published Name:|$)' | Where-Object { $_ -match '\S' } + + foreach ($block in $driverBlocks) { + # Create a hashtable to store driver properties + $driverProps = [ordered]@{} + + # Get each line in the block + $lines = $block -split "`n" + $currentKey = $null + $currentValue = $null + + foreach ($line in $lines) { + # Check if this is a key-value line + if ($line -match '^\s*([^:]+):\s*(.*)$') { + # If we have a stored key and value, add them to the properties + if ($currentKey) { + $driverProps[$currentKey] = $currentValue.Trim() + } + + # Store the new key and value + $currentKey = $matches[1].Trim() + $currentValue = $matches[2] + } + # If this is a continuation of a value (indented line) + elseif ($line -match '^\s+(.+)$' -and $currentKey) { + $currentValue += " " + $matches[1] + } + } + + # Add the last key-value pair + if ($currentKey) { + $driverProps[$currentKey] = $currentValue.Trim() + } + + # Create a custom object from the properties + $driverObj = [PSCustomObject]$driverProps + $drivers += $driverObj + } + + return $drivers + } +} + +function Start-IsoUpgrade { + try { + # Check for existing driver blocks + Write-Log "Checking for driver compatibility issues before starting ISO upgrade..." "INFO" + $blockedDrivers = Get-DriverBlocks + if ($blockedDrivers.Count -gt 0) { + Write-Log "Critical: $($blockedDrivers.Count) driver blocks detected." "ERROR" + if (-not $Force) { + Write-Log "Upgrade cannot proceed due to driver blocks. Use -Force to override after resolving issues." "ERROR" + return $false + } + else { + Write-Log "-Force specified: Proceeding despite driver blocks. Ensure drivers are resolved." "WARNING" + } + } + + # Ensure directories exist + $dirs = @($Config.IsoPath, $Config.MountPath, $Config.LogPath) + foreach ($dir in $dirs) { + New-Item -Path $dir -ItemType Directory -Force | Out-Null + } + + # Download the ISO + Write-Log "Downloading Windows 11 ISO from $IsoLocation..." "INFO" + $retryCount = 3 + $success = $false + for ($i = 1; $i -le $retryCount; $i++) { + try { + (New-Object System.Net.WebClient).DownloadFile($IsoLocation, $Config.IsoPath) + $success = $true + break + } + catch { + Write-Log "Download attempt $i failed: $($_.Exception.Message)" "WARNING" + if ($i -eq $retryCount) { + Write-Log "Failed to download ISO after $retryCount attempts." "ERROR" + return $false + } + + Start-Sleep -Seconds 5 + } + } + + if (-not $success -or -not (Test-Path $Config.IsoPath)) { + Write-Log "Failed to download ISO." "ERROR" + return $false + } + + Write-Log "Download completed: $($Config.IsoPath)" "INFO" + # Mount the ISO + Write-Log "Mounting ISO at $($Config.MountPath)..." "INFO" + if (-not (Test-Path $Config.MountPath)) { + New-Item -Path $Config.MountPath -ItemType Directory -Force | Out-Null + } + + Mount-DiskImage -ImagePath $Config.IsoPath -ErrorAction Stop + $driveLetter = (Get-DiskImage -ImagePath $Config.IsoPath | Get-Volume).DriveLetter + if (-not $driveLetter) { + Write-Log "Failed to mount ISO. No drive letter assigned." "ERROR" + return $false + } + + Write-Log "ISO mounted at drive $driveLetter" "INFO" + $systemDrive = $env:SystemDrive + Write-Log "Checking BitLocker status for $systemDrive..." "INFO" + try { + $bitLockerVolume = Get-BitLockerVolume -MountPoint $systemDrive -ErrorAction Stop + $protectionStatus = $bitLockerVolume.ProtectionStatus + Write-Log "BitLocker Protection Status: $protectionStatus" "INFO" + } + catch { + Write-Log "Error checking BitLocker status: $_" "ERROR" + } + + if ($protectionStatus -eq 'On') { + try { + Write-Log "Suspending BitLocker protection for $systemDrive..." "WARNING" + Suspend-BitLocker -MountPoint $systemDrive -ErrorAction Stop + # Verify suspension + $newStatus = (Get-BitLockerVolume -MountPoint $systemDrive).ProtectionStatus + if ($newStatus -eq 'Off') { + Write-Log "BitLocker protection successfully suspended for $systemDrive." "WARNING" + } + else { + Write-Log "Failed to confirm BitLocker suspension. Please check manually with 'Get-BitLockerVolume -MountPoint $systemDrive'." "WARNING" + } + } + catch { + Write-Log "Error suspending BitLocker: $_" "ERROR" + return $false + } + } + + + $isoRoot = "${driveLetter}:\" + # Run setup.exe + $setupExe = Join-Path $isoRoot "setup.exe" + $setupArgs = "/auto upgrade /compat ignorewarning /eula accept /dynamicupdate disable /bitlocker alwayssuspend /migratedrivers none /copylogs $($Config.LogPath)" + Write-Log "Starting Windows 11 ISO upgrade..." "INFO" + $process = Start-Process -FilePath $setupExe -ArgumentList $setupArgs -PassThru -ErrorAction Stop + $process.WaitForExit() + Write-Log "Upgrade process completed with exit code: $($process.ExitCode)" "INFO" + + # Dismount ISO + Write-Log "Dismounting ISO..." "INFO" + Dismount-DiskImage -ImagePath $Config.IsoPath -ErrorAction SilentlyContinue + + return $process.ExitCode -eq 0 + } + catch { + Handle-Error -Message "Failed to initiate ISO upgrade." -Exception $_ + if (Get-DiskImage -ImagePath $Config.IsoPath -ErrorAction SilentlyContinue) { + Dismount-DiskImage -ImagePath $Config.IsoPath -ErrorAction SilentlyContinue + } + return $false + } +} + +function Start-Upgrade { + try { + # Check for existing driver blocks + Write-Log "Checking for driver compatibility issues before starting upgrade..." "INFO" + $blockedDrivers = Get-DriverBlocks + if ($blockedDrivers.Count -gt 0) { + Write-Log "Critical: $($blockedDrivers.Count) driver blocks detected." "ERROR" + if (-not $Force) { + Write-Log "Upgrade cannot proceed due to driver blocks. Use -Force to override after resolving issues." "ERROR" + return $false + } + else { + Write-Log "-Force specified: Proceeding despite driver blocks. Ensure drivers are resolved." "WARNING" + } + } + + Write-Log "Downloading Windows 11 Installation Assistant..." "INFO" + $retryCount = 3 + $success = $false + for ($i = 1; $i -le $retryCount; $i++) { + try { + (New-Object System.Net.WebClient).DownloadFile($Config.Win11SetupUrl, $Config.SetupPath) + $success = $true + break + } + catch { + Write-Log "Download attempt $i failed: $($_.Exception.Message)" "WARNING" + if ($i -eq $retryCount) { throw "Failed to download after $retryCount attempts." } + Start-Sleep -Seconds 5 + } + } + + if (-not $success -or -not (Test-Path $Config.SetupPath)) { + throw "Failed to download Installation Assistant." + } + + Write-Log "Starting Windows 11 upgrade process..." "INFO" + $dir = "$($env:SystemDrive)\_Windows_FU\packages" + $process = Start-Process -FilePath $Config.SetupPath -ArgumentList "/quietinstall /skipeula /skipcompatcheck /skipselfupdate /auto upgrade /copylogs $dir" -PassThru -ErrorAction Stop + $process.WaitForExit() + Write-Log "Upgrade process completed with exit code: $($process.ExitCode)" "INFO" + return $process.ExitCode -eq 0 + } + catch { + Handle-Error -Message "Failed to initiate upgrade." -Exception $_ + return $false + } +} + +#--------------------------------- +# Main Execution +#--------------------------------- + +Write-Log "Starting Windows 11 Script..." "INFO" +Test-Admin + +# Check for previous upgrade attempts +$previousAttempt = Check-PreviousUpgradeAttempt -Force:$Force +if ($previousAttempt.PreviousAttemptFound -and -not $Force) { + Show-PreviousUpgradeAttemptInfo -PreviousAttempt $previousAttempt + if ($previousAttempt.FailureDetected) { + Get-LastSetupError + Write-Log "Previous upgrade failure detected. Use -Force to retry." "ERROR" + Write-Log "Attempting to download SetupDiag for further analysis..." "INFO" + $setupDiagUrl = "https://go.microsoft.com/fwlink/?linkid=870142" + $setupDiagPath = "$PSScriptRoot\SetupDiag.exe" + Invoke-WebRequest -Uri $setupDiagUrl -OutFile $setupDiagPath + Write-Log "SetupDiag downloaded to $setupDiagPath, executing SetupDiag" "INFO" + Start-Process -FilePath $setupDiagPath -ArgumentList "/Output:$PSScriptRoot\SetupDiagResults.log" -Wait + Get-Content "$PSScriptRoot\SetupDiagResults.log" + exit 1 + } +} +elseif ($previousAttempt.PreviousAttemptFound -and $Force) { + Show-PreviousUpgradeAttemptInfo -PreviousAttempt $previousAttempt + if ($previousAttempt.FailureDetected) { + Write-Log "Force specified: Attempting auto remediation" "WARNING" + if ($previousAttempt.BlockedDrivers.Count -gt 0) { + $previousAttempt.BlockedDrivers | ForEach-Object { + Write-Log "Attempting to remove driver: $($_.Inf)" "INFO" + pnputil /delete-driver $_.Inf /force + } + } + } +} + +# Validate system compatibility +$compat = Test-SystemCompatibility +if (-not ($compat.TPM -and $compat.SecureBoot -and $compat.System -and $compat.WinRE)) { + Write-Log "System does not meet Windows 11 requirements." "ERROR" + exit 1 +} + +# Start upgrade +$upgradeSuccess = if ($IsoLocation) { + Write-Log "IsoLocation specified ($IsoLocation). Using ISO-based upgrade." "INFO" + Start-IsoUpgrade +} +else { + Write-Log "No IsoLocation specified. Using Windows 11 Installation Assistant." "INFO" + Start-Upgrade +} + +if (-not $upgradeSuccess) { + Write-Log "Upgrade failed. Check logs for details." "ERROR" + exit 1 +} + +Write-Log "Upgrade started successfully." "INFO" \ No newline at end of file diff --git a/scripts_wip/Win_Winget_InstallIfMissing.ps1 b/scripts_wip/Win_Winget_InstallIfMissing.ps1 new file mode 100644 index 00000000..5067c6e6 --- /dev/null +++ b/scripts_wip/Win_Winget_InstallIfMissing.ps1 @@ -0,0 +1,80 @@ +#Install Winget if missing + +#Setup +Set-ExecutionPolicy RemoteSigned -Scope Process -Force +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +#Setup temp folder +$InstallerFolder = "c:\temp" +if (!(Test-Path $InstallerFolder)) { + New-Item -Path $InstallerFolder -ItemType Directory -Force -Confirm:$false +} + +#If Visual C++ Redistributable 2022 not present, download and install. (Winget Dependency) +if (Get-WmiObject -Class Win32_Product -Filter "Name LIKE '%Visual C++ 2022%'") { + Write-Host "VC++ Redistributable 2022 already installed" +} +else { + Write-Host "Installing Visual C++ Redistributable" + #Permalink for latest supported x64 version + Invoke-Webrequest -uri https://aka.ms/vs/17/release/vc_redist.x64.exe -Outfile $InstallerFolder\vc_redist.x64.exe + Start-Process "$InstallerFolder\vc_redist.x64.exe" -Wait -ArgumentList "/q /norestart" +} + +#Check Winget Install +$TestWinget = Get-AppxProvisionedPackage -Online | Where-Object { $_.DisplayName -eq "Microsoft.DesktopAppInstaller" } +If ([Version]$TestWinGet. Version -gt "2022.506.16.0") { + Write-Host "WinGet is already installed" -ForegroundColor Green +} +Else { + #Download WinGet MSIXBundle + Write-Host "Winget is not installed. Downloading WinGet..." + Invoke-Webrequest -uri https://aka.ms/getwinget -Outfile $InstallerFolder\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle + + #Install WinGet MSIXBundle + Try { + Write-Host "Installing MSIXBundle for App Installer..." + Add-AppxProvisionedPackage -Online -PackagePath "$InstallerFolder\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle" -SkipLicense + Write-Host "Installed MSIXBundle for App Installer" -ForegroundColor Green + } + Catch { + Write-Host "Failed to install MSIXBundle for App Installer..." -ForegroundColor Red + } +} + +#Remove downloaded files +if (Test-Path "$InstallerFolder\vc_redist.x64.exe") { + Remove-Item -Path "$InstallerFolder\vc_redist.x64.exe" -Force -ErrorAction Continue +} +if (Test-Path "$InstallerFolder\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle") { + Remove-Item -Path "$InstallerFolder\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle" -Force -ErrorAction Continue +} + +Start-Sleep -seconds 5 +#Find the Winget path, and peel off winget.exe +$ResolveWingetPath = Resolve-Path "C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_*_x64__8wekyb3d8bbwe\winget.exe" +if ($null -eq $ResolveWingetPath) { + write-host "ERROR: Winget path was not found." + exit 1 +} +$WingetPath = $ResolveWingetPath[-1].Path +$WingetPath = Split-Path -Path $WingetPath -Parent + +#Add Winget to the System path environment variable if it doesn't exist +if ([Environment]::GetEnvironmentVariable("PATH", "Machine") -notlike "*$WingetPath*") { + + #Set system path environment variable + $SystemPath = [Environment]::GetEnvironmentVariable("PATH", "Machine") + [IO.Path]::PathSeparator + $WingetPath + [Environment]::SetEnvironmentVariable( "Path", $SystemPath, "Machine" ) + + #Check if path successfully added + if ([Environment]::GetEnvironmentVariable("PATH", "Machine") -like "*$WingetPath*") { + Write-Host "Successfully added winget to the Environment Variables for System Path. Computer must be rebooted before this takes effect." + exit + } + else { + Write-Host "Failed to add winget to the Environment Variables for System Path" + exit 1 + } +} +Write-Host "Environment Variable for system path already exists for winget." \ No newline at end of file diff --git a/scripts_wip/Win_screenshottaker.py b/scripts_wip/Win_screenshottaker.py new file mode 100644 index 00000000..224463b6 --- /dev/null +++ b/scripts_wip/Win_screenshottaker.py @@ -0,0 +1,133 @@ +#!/usr/bin/python3 +""" +Script Name: Screenshot Capturer +Description: This script captures screenshots of the computer screen and saves them to a specified folder. + It can also take continuous screenshots at a specified interval. Additionally, it provides an + option to remove all pictures in the screenshots folder. +Notes: Uses 19MB of disk space a minute at 1080p + +Screenshots are saved in the following format: + - File name: COMPUTERNAME_USERNAME_TIMESTAMP.png + - Location: PROGRAMDATA/TacticalRMM/screenshots/ + +Usage Example: + - Capture continuous screenshots every 5 seconds for 119 seconds: + --dofor2 + + - Remove all pictures in the screenshots folder: + --clean +""" + +import os +from datetime import datetime +import time # Import the time module +import argparse # Import the argparse module +import shutil # Import the shutil module to remove files +import sys +import psutil # Import psutil for disk space check + +# Define the minimum free disk space in bytes (1GB) +MIN_FREE_DISK_SPACE = 1 * 1024 * 1024 * 1024 + +# Check available disk space +available_space = psutil.disk_usage(os.getenv("PROGRAMDATA")).free + +# Check if available disk space is less than the minimum required +if available_space < MIN_FREE_DISK_SPACE: + print("Aborting script: Insufficient free disk space (less than 1GB).") + sys.exit(1) + +# Try to import PIL.Image. If it fails, install Pillow using pip. +try: + from PIL import ImageGrab +except ImportError: + import subprocess + import sys + + print("Pillow is not installed. Installing Pillow...") + try: + subprocess.check_call([sys.executable, "-m", "pip", "install", "Pillow"]) + from PIL import ImageGrab + except subprocess.CalledProcessError: + print("Failed to install Pillow. Please install it manually using 'pip install Pillow'") + sys.exit(1) + +# Create an argument parser +parser = argparse.ArgumentParser(description="Take a screenshot") + +# Add an optional argument to take continuous screenshots +parser.add_argument( + "--dofor2", + action="store_true", + help="Take a screenshot every 5 seconds for 119 seconds", +) + +# Add an optional argument to clean the screenshots folder +parser.add_argument( + "--clean", + action="store_true", + help="Remove all pictures in the screenshots folder", +) + +# Parse the command line arguments +args = parser.parse_args() + +# If the --clean parameter is provided, remove all pictures in the screenshots folder +if args.clean: + screenshots_folder = os.path.join(os.getenv("PROGRAMDATA"), "TacticalRMM", "screenshots") + for filename in os.listdir(screenshots_folder): + file_path = os.path.join(screenshots_folder, filename) + try: + if os.path.isfile(file_path): + os.unlink(file_path) + except Exception as e: + print(f"Error deleting {file_path}: {e}") + print(f"All cleaned") + sys.exit(0) + +# Capture the screen +screenshot = ImageGrab.grab() + +# Save to file +filename = os.path.join( + os.getenv("PROGRAMDATA"), + "TacticalRMM", + "screenshots", + f"{os.environ['COMPUTERNAME']}_{os.environ['USERNAME']}_{datetime.now().strftime('%Y.%m.%d-%H.%M.%S')}.png", +) + +# Ensure the screenshots directory exists +os.makedirs(os.path.dirname(filename), exist_ok=True) + +# If the dofor2 parameter is provided, take additional screenshots +if args.dofor2: + total_time = 119 + capture_interval = 5 + num_captures = total_time // capture_interval + + for i in range(num_captures): + # Capture the screen + screenshot = ImageGrab.grab() + + # Save to file + filename = os.path.join( + os.getenv("PROGRAMDATA"), + "TacticalRMM", + "screenshots", + f"{os.environ['COMPUTERNAME']}_{os.environ['USERNAME']}_{datetime.now().strftime('%Y.%m.%d-%H.%M.%S')}.png", + ) + + # Ensure the screenshots directory exists + os.makedirs(os.path.dirname(filename), exist_ok=True) + + # Save as PNG + screenshot.save(filename, format="PNG") + + # Wait for the next capture interval + time.sleep(capture_interval) + # Exit the script if --dofor2 was used + sys.exit() + + +# Save as PNG +screenshot.save(filename, format="PNG") \ No newline at end of file diff --git a/scripts_wip/linux_sshserver_check.sh b/scripts_wip/linux_sshserver_check.sh new file mode 100644 index 00000000..315ee6b3 --- /dev/null +++ b/scripts_wip/linux_sshserver_check.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# +# With love from Stefan Lousberg 10/29/2023 +# + +SSH_STATUS=$(systemctl is-active sshd) + +if [ "$SSH_STATUS" == "active" ]; then + echo "SSH server (sshd) is running" + exit 0 +else + echo "SSH server (sshd) is not running" + exit 1 +fi diff --git a/scripts_wip/linux_website_keywordmonitor.sh b/scripts_wip/linux_website_keywordmonitor.sh new file mode 100644 index 00000000..b3db21aa --- /dev/null +++ b/scripts_wip/linux_website_keywordmonitor.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# +# With love from Stefan Lousberg 10/29/2023 +# Env Var: URL=https://www.google.nl +# Args: words +# + +# URL to monitor (specified as an environment variable) +URL="$URL" + +# Keywords to monitor for (passed as script arguments) +KEYWORDS=("$@") + +# Perform the cURL request and store the page content in a variable +PAGE_CONTENT=$(curl -s "$URL") + +found_all_keywords=1 + +# Loop through each keyword and check if it exists in the page content +for keyword in "${KEYWORDS[@]}"; do + if [[ $PAGE_CONTENT != *"$keyword"* ]]; then + echo "Keyword '$keyword' not found on $URL" + found_all_keywords=0 + fi +done + +if [ "$found_all_keywords" -eq 1 ]; then + echo "All keywords found on $URL" + exit 0 +else + exit 1 +fi diff --git a/scripts_wip/nix_bash_HP_CPU_Status.sh b/scripts_wip/nix_bash_HP_CPU_Status.sh new file mode 100644 index 00000000..57777824 --- /dev/null +++ b/scripts_wip/nix_bash_HP_CPU_Status.sh @@ -0,0 +1,21 @@ +#!/bin/bash +#Get Server/CPU status +RESULT=$(hpasmcli -s "show server" | grep -i status) +RETURN=0 +#Loop through each CPU and fail if any is not OK +while IFS= read -r line; do + echo "$line" + if [[ $line == *"Status : Ok"* ]]; + then echo "Good"; + else echo "Bad"; RETURN=1; + fi +done <<< "$RESULT" +echo $RETURN +#Return result to TRMM +if [ $RETURN == 0 ]; then + echo "CPUs are Healthy" + #exit 0 +else + echo "CPU Fault" + #exit 2 +fi \ No newline at end of file diff --git a/scripts_wip/nix_bash_HP_Memory_Status.sh b/scripts_wip/nix_bash_HP_Memory_Status.sh new file mode 100644 index 00000000..12a53b63 --- /dev/null +++ b/scripts_wip/nix_bash_HP_Memory_Status.sh @@ -0,0 +1,21 @@ +#!/bin/bash +#Get DIMM status +RESULT=$(hpasmcli -s "show dimm" | grep -i status) +RETURN=0 +#Loop through each DIMM and fail if any is not OK +while IFS= read -r line; do + echo "$line" + if [[ $line == *"Status: Ok"* ]]; + then echo "Good"; + else echo "Bad"; RETURN=1; + fi +done <<< "$RESULT" +echo $RETURN +#Return result to TRMM +if [ $RETURN == 0 ]; then + echo "Memory Modules are Healthy" + exit 0 +else + echo "Memory Fault" + exit 2 +fi \ No newline at end of file diff --git a/scripts_wip/nix_bash_HP_Power_Supply_Status.sh b/scripts_wip/nix_bash_HP_Power_Supply_Status.sh new file mode 100644 index 00000000..18141e7b --- /dev/null +++ b/scripts_wip/nix_bash_HP_Power_Supply_Status.sh @@ -0,0 +1,21 @@ +#!/bin/bash +#Get DIMM status +RESULT=$(hpasmcli -s "show powersupply" | grep -i condition) +RETURN=0 +#Loop through each DIMM and fail if any is not OK +while IFS= read -r line; do + echo "$line" + if [[ $line == *"Condition: Ok"* ]]; + then echo "Good"; + else echo "Bad"; RETURN=1; + fi +done <<< "$RESULT" +echo $RETURN +#Return result to TRMM +if [ $RETURN == 0 ]; then + echo "Power Supplies are Healthy" + exit 0 +else + echo "Power Supply Fault" + exit 2 +fi \ No newline at end of file diff --git a/scripts_wip/nix_bash_HP_RAID_Battery_Status.sh b/scripts_wip/nix_bash_HP_RAID_Battery_Status.sh new file mode 100644 index 00000000..fac1210c --- /dev/null +++ b/scripts_wip/nix_bash_HP_RAID_Battery_Status.sh @@ -0,0 +1,10 @@ +#!/bin/bash +CONTROLLER=$(hpssacli ctrl all show status | grep -i battery) +echo $CONTROLLER +if [[ $CONTROLLER == *"Battery/Capacitor Status: OK"* ]]; then + echo "RAID Battery is Healthy" + exit 0 +else + echo "RAID Battery has Error" + exit 2 +fi \ No newline at end of file diff --git a/scripts_wip/nix_bash_HP_RAID_Cache_Status.sh b/scripts_wip/nix_bash_HP_RAID_Cache_Status.sh new file mode 100644 index 00000000..e818445c --- /dev/null +++ b/scripts_wip/nix_bash_HP_RAID_Cache_Status.sh @@ -0,0 +1,10 @@ +#!/bin/bash +CONTROLLER=$(hpssacli ctrl all show status | grep -i cache) +echo $CONTROLLER +if [[ $CONTROLLER == *"Cache Status: OK"* ]]; then + echo "RAID Cache is Healthy" + exit 0 +else + echo "RAID Cache has Error" + exit 2 +fi \ No newline at end of file diff --git a/scripts_wip/nix_bash_HP_RAID_Controller_Status.sh b/scripts_wip/nix_bash_HP_RAID_Controller_Status.sh new file mode 100644 index 00000000..c81dcb34 --- /dev/null +++ b/scripts_wip/nix_bash_HP_RAID_Controller_Status.sh @@ -0,0 +1,10 @@ +#!/bin/bash +CONTROLLER=$(hpssacli ctrl all show status | grep -i controller) +echo $CONTROLLER +if [[ $CONTROLLER == *"Controller Status: OK"* ]]; then + echo "RAID Controller is Healthy" + exit 0 +else + echo "RAID Controller has Error" + exit 2 +fi \ No newline at end of file diff --git a/scripts_wip/nix_bash_Install_HP_Server_Health_Tools.sh b/scripts_wip/nix_bash_Install_HP_Server_Health_Tools.sh new file mode 100644 index 00000000..c49a0791 --- /dev/null +++ b/scripts_wip/nix_bash_Install_HP_Server_Health_Tools.sh @@ -0,0 +1,6 @@ +#!/bin/bash +cd /tmp +wget https://downloads.linux.hpe.com/SDR/repo/mcp/centos/6/x86_64/10.40/hpssacli-2.40-13.0.x86_64.rpm +yum install -y --nogpgcheck hpssacli-2.40-13.0.x86_64.rpm +wget https://downloads.linux.hpe.com/SDR/repo/mcp/centos/6/x86_64/10.40/hp-health-10.40-1777.17.rhel6.x86_64.rpm +yum install -y --nogpgcheck hp-health-10.40-1777.17.rhel6.x86_64.rpm \ No newline at end of file