From dd66982d04adac147bdce68343340843f1dd7d7f Mon Sep 17 00:00:00 2001 From: redanthrax Date: Wed, 25 Oct 2023 11:27:32 -0700 Subject: [PATCH 001/447] remove accounts from admin group, not disable --- scripts_wip/Win_LocalAdmin_Manage.ps1 | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/scripts_wip/Win_LocalAdmin_Manage.ps1 b/scripts_wip/Win_LocalAdmin_Manage.ps1 index f0b49ef6..74c50761 100644 --- a/scripts_wip/Win_LocalAdmin_Manage.ps1 +++ b/scripts_wip/Win_LocalAdmin_Manage.ps1 @@ -4,7 +4,8 @@ .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 @@ -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,19 @@ 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." + foreach ($adminMember in $adminMembers) { + if (-Not($adminMember -Match $LocalAdminUser) -and -Not([string]::IsNullOrWhiteSpace($adminMember))) { + Add-LocalGroupMember -Group "Users" -Member $adminMember + Remove-LocalGroupMember -Group Administrators -Member $adminMember } } @@ -77,12 +80,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." From dd440eca3a31748a029179dd888ed3017a9a0acb Mon Sep 17 00:00:00 2001 From: Joel DeTeves Date: Wed, 25 Oct 2023 15:21:12 -0700 Subject: [PATCH 002/447] Remove cleanmgr and PromptForScan functions --- scripts/Win_Start_Cleanup.ps1 | 41 +---------------------------------- 1 file changed, 1 insertion(+), 40 deletions(-) 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 From 6f4483ea3b71ae366fc2ae16d02f417e3ed604d7 Mon Sep 17 00:00:00 2001 From: redanthrax Date: Thu, 26 Oct 2023 14:58:27 -0700 Subject: [PATCH 003/447] updated to only do users, was doing groups --- scripts_wip/Win_LocalAdmin_Manage.ps1 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts_wip/Win_LocalAdmin_Manage.ps1 b/scripts_wip/Win_LocalAdmin_Manage.ps1 index 74c50761..486105c8 100644 --- a/scripts_wip/Win_LocalAdmin_Manage.ps1 +++ b/scripts_wip/Win_LocalAdmin_Manage.ps1 @@ -65,8 +65,10 @@ function Win_LocalAdmin_Manage { Write-Output "Removing all other admins from Administrator Group." foreach ($adminMember in $adminMembers) { if (-Not($adminMember -Match $LocalAdminUser) -and -Not([string]::IsNullOrWhiteSpace($adminMember))) { - Add-LocalGroupMember -Group "Users" -Member $adminMember - Remove-LocalGroupMember -Group Administrators -Member $adminMember + if(Get-LocalUser | Where-Object { $_.Name -eq $adminMember }) { + Add-LocalGroupMember -Group "Users" -Member $adminMember + Remove-LocalGroupMember -Group Administrators -Member $adminMember + } } } From 019cfd64647019cba5d67e7802450a03598f05ad Mon Sep 17 00:00:00 2001 From: silversword411 Date: Sun, 29 Oct 2023 14:43:04 -0400 Subject: [PATCH 004/447] From Discord #scripts Thanks Stefan! --- scripts_wip/linux_sshserver_check.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 scripts_wip/linux_sshserver_check.sh 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 From e4d04ad81014b328a3e08bb96a6d34066fcc442f Mon Sep 17 00:00:00 2001 From: silversword411 Date: Sun, 29 Oct 2023 14:52:04 -0400 Subject: [PATCH 005/447] missed one From Discord #scripts Thanks Stefan! --- scripts_wip/linux_website_keywordmonitor.sh | 32 +++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 scripts_wip/linux_website_keywordmonitor.sh 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 From 72ecf5a46b1c2613fa412da88ad80942cfe616b2 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 9 Nov 2023 14:03:17 -0500 Subject: [PATCH 006/447] WIP add firefox kill addin install --- scripts_wip/Win_FirefoxAddinInstallDisable.ps1 | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 scripts_wip/Win_FirefoxAddinInstallDisable.ps1 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 From 7be4a3010cc2dbe321ce71ec52ee2ecb2afc9199 Mon Sep 17 00:00:00 2001 From: redanthrax Date: Tue, 14 Nov 2023 09:49:36 -0800 Subject: [PATCH 007/447] Install Remote App for Azure Virtual Desktop Added shortcut install --- scripts_wip/Win_RemoteDesktopApp.ps1 | 114 +++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 scripts_wip/Win_RemoteDesktopApp.ps1 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 From 845bce0dcad376901a3e9b1dc009df188e6e0b59 Mon Sep 17 00:00:00 2001 From: brisksystems-us <101900157+brisksystems-us@users.noreply.github.com> Date: Wed, 15 Nov 2023 15:58:42 -0600 Subject: [PATCH 008/447] Scripts to monitor HP Proliant hardware status 1 script downloads and installs 2 official HP tools from their website (RPM/Yum only at this time) 6 scripts that can be used for script checks for various bits of hardware; Return code 2 for errors. --- nix_bash_HP_CPU_Status.sh | 21 +++++++++++++++++++++ nix_bash_HP_Memory_Status.sh | 21 +++++++++++++++++++++ nix_bash_HP_Power_Supply_Status.sh | 21 +++++++++++++++++++++ nix_bash_HP_RAID_Battery_Status.sh | 10 ++++++++++ nix_bash_HP_RAID_Cache_Status.sh | 10 ++++++++++ nix_bash_HP_RAID_Controller_Status.sh | 10 ++++++++++ nix_bash_Install_HP_Server_Health_Tools.sh | 6 ++++++ 7 files changed, 99 insertions(+) create mode 100644 nix_bash_HP_CPU_Status.sh create mode 100644 nix_bash_HP_Memory_Status.sh create mode 100644 nix_bash_HP_Power_Supply_Status.sh create mode 100644 nix_bash_HP_RAID_Battery_Status.sh create mode 100644 nix_bash_HP_RAID_Cache_Status.sh create mode 100644 nix_bash_HP_RAID_Controller_Status.sh create mode 100644 nix_bash_Install_HP_Server_Health_Tools.sh diff --git a/nix_bash_HP_CPU_Status.sh b/nix_bash_HP_CPU_Status.sh new file mode 100644 index 00000000..57777824 --- /dev/null +++ b/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/nix_bash_HP_Memory_Status.sh b/nix_bash_HP_Memory_Status.sh new file mode 100644 index 00000000..12a53b63 --- /dev/null +++ b/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/nix_bash_HP_Power_Supply_Status.sh b/nix_bash_HP_Power_Supply_Status.sh new file mode 100644 index 00000000..18141e7b --- /dev/null +++ b/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/nix_bash_HP_RAID_Battery_Status.sh b/nix_bash_HP_RAID_Battery_Status.sh new file mode 100644 index 00000000..fac1210c --- /dev/null +++ b/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/nix_bash_HP_RAID_Cache_Status.sh b/nix_bash_HP_RAID_Cache_Status.sh new file mode 100644 index 00000000..e818445c --- /dev/null +++ b/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/nix_bash_HP_RAID_Controller_Status.sh b/nix_bash_HP_RAID_Controller_Status.sh new file mode 100644 index 00000000..c81dcb34 --- /dev/null +++ b/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/nix_bash_Install_HP_Server_Health_Tools.sh b/nix_bash_Install_HP_Server_Health_Tools.sh new file mode 100644 index 00000000..c49a0791 --- /dev/null +++ b/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 From 2e6c6d6e462aa244a56c33844c0bc7300dae5f65 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 16 Nov 2023 13:10:11 -0500 Subject: [PATCH 009/447] Fixing syntax on 2 scripts --- community_scripts.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/community_scripts.json b/community_scripts.json index bcbd48ef..fb31ab85 100644 --- a/community_scripts.json +++ b/community_scripts.json @@ -904,7 +904,7 @@ "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}]", + "syntax": "-PackageName \n[-Hosts ]\n[-mode {(install) | upgrade | uninstall}]", "shell": "powershell", "category": "TRMM (Win):3rd Party Software>Chocolatey", "supported_platforms": [ @@ -918,7 +918,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": [ From e90c437502e4f9e95e9974d9babcf069c5b9a9d7 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 17 Nov 2023 14:11:02 -0500 Subject: [PATCH 010/447] wip urbackup perm fixer --- ...Win_3rdparty_Urbackup_restorepermfixer.bat | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 scripts_wip/Win_3rdparty_Urbackup_restorepermfixer.bat 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 From 48faf5e6f34ebc32e1637c44b8988eded1c03a1c Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 17 Nov 2023 14:12:23 -0500 Subject: [PATCH 011/447] screenshot taker --- scripts_wip/Win_screenshottaker.py | 133 +++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 scripts_wip/Win_screenshottaker.py 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 From 9872a9a83fb48eb9171577347d14cc373d6479af Mon Sep 17 00:00:00 2001 From: silversword411 Date: Mon, 20 Nov 2023 23:36:26 -0500 Subject: [PATCH 012/447] WIP Adding winget install script --- scripts_wip/Win_Winget_InstallIfMissing.ps1 | 80 +++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 scripts_wip/Win_Winget_InstallIfMissing.ps1 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 From 44cfdc89d9d39aa24efa8b11a2e18ac0c322ea00 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 24 Nov 2023 12:52:53 -0500 Subject: [PATCH 013/447] Cleaning up Runasuser template --- scripts/Win_RunAsUser_Example.ps1 | 33 +++++++++++++------------------ 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/scripts/Win_RunAsUser_Example.ps1 b/scripts/Win_RunAsUser_Example.ps1 index 9c2fcf27..5bbc6dd4 100644 --- a/scripts/Win_RunAsUser_Example.ps1 +++ b/scripts/Win_RunAsUser_Example.ps1 @@ -20,43 +20,38 @@ 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 + $raulogPath = "c:\ProgramData\TacticalRMM\temp\raulog.txt" + $exit1Path = "c:\ProgramData\TacticalRMM\temp\exit1.txt" + + Write-Output "Hello from Userland" | Out-File -append -FilePath $raulogPath 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" | Out-File -append -FilePath $raulogPath } 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" | Out-File -append -FilePath $raulogPath # 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" +$exitdata = Get-Content -Path "c:\ProgramData\TacticalRMM\temp\raulog.txt" -ErrorAction SilentlyContinue Write-Output $exitdata # Cleanup raulog.txt File -Remove-Item -path "c:\ProgramData\TacticalRMM\temp\raulog.txt" +Remove-Item -Path "c:\ProgramData\TacticalRMM\temp\raulog.txt" -ErrorAction SilentlyContinue # 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 } - From 3a9aee8bde33b3c07abc70586ee2c950107badf4 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Mon, 27 Nov 2023 12:08:23 -0500 Subject: [PATCH 014/447] WIP software uninstall script. Thx red! --- scripts_wip/Win_Software_Uninstall.ps1 | 50 ++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 scripts_wip/Win_Software_Uninstall.ps1 diff --git a/scripts_wip/Win_Software_Uninstall.ps1 b/scripts_wip/Win_Software_Uninstall.ps1 new file mode 100644 index 00000000..8fb5a7ef --- /dev/null +++ b/scripts_wip/Win_Software_Uninstall.ps1 @@ -0,0 +1,50 @@ +<# +.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 to perform the uninstallation. + +.PARAMETER Application + The name of the application to be uninstalled. It is a mandatory string parameter. + +.EXAMPLE + -Application "ExampleApp" + Uninstalls the application named "ExampleApp". + +.NOTES + Version: 1.0 + Author: redanthrax + Creation Date: 2023-11-27 + +#> + +Param( + [Parameter(Mandatory)] + [string]$Application +) + +Write-Output "Attempting to uninstall $Application" +$Paths = @("HKLM:\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\*", + "HKLM:\SOFTWARE\\Wow6432node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\*") +if ($Application -ne "") { + foreach ($app in Get-ItemProperty $Paths | Where-Object { $_.Displayname -match [regex]::Escape($Application) } | Sort-Object DisplayName) { + if ($app.UninstallString) { + Write-Output "Found $Application uninstall string" + $MsiArguments = $app.UninstallString -Replace "MsiExec.exe ", "" -Replace "/I", "/X" + Write-Output "Executing msiexec $MsiArguments /quiet /norestart /qn" + Start-Process -FilePath msiexec.exe -ArgumentList "$MsiArguments /quiet /norestart /qn" -Wait + Start-Sleep -Seconds 20 + $UninstallTest = (Get-ItemProperty $Paths | Where-object { $_.UninstallString -match [regex]::Escape($Application) }).DisplayName + Write-Output "Uninstall Test: $UninstallTest" + If ($UninstallTest) { + Write-Output "$Application Uninstall Failed" + } + else { + Write-Output "$Application Uninstalled" + } + } + + break + } +} \ No newline at end of file From 5560f659a32d78f1a17b8e2f7ba48f389b81e434 Mon Sep 17 00:00:00 2001 From: ConvexSERV Date: Wed, 6 Dec 2023 12:00:30 -0500 Subject: [PATCH 015/447] Create scripts_stagingWin_HPE-SSA_Status.py Checks the status of RAID array(s) on HPE servers with Smart Array controllers - Requires SSACLI --- scripts_stagingWin_HPE-SSA_Status.py | 746 +++++++++++++++++++++++++++ 1 file changed, 746 insertions(+) create mode 100644 scripts_stagingWin_HPE-SSA_Status.py diff --git a/scripts_stagingWin_HPE-SSA_Status.py b/scripts_stagingWin_HPE-SSA_Status.py new file mode 100644 index 00000000..5a2f1996 --- /dev/null +++ b/scripts_stagingWin_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) From 39e77d0e115dcb22facb5f76f801f66a7be982c8 Mon Sep 17 00:00:00 2001 From: ConvexSERV Date: Wed, 6 Dec 2023 12:11:58 -0500 Subject: [PATCH 016/447] Create Win_StorageCraftSPX_Status.ps1 Checks to see of scheduled SPX backups are completing or failing. Returns Result. Utilizes PSSQLLite Module to query the SPX database. Will attempt to install PSSQLLite if it is not installed. An optional parameter can be passed in to skip this check, and speed up the script. --- .../Win_StorageCraftSPX_Status.ps1 | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 scripts_staging/Win_StorageCraftSPX_Status.ps1 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 From 7ab7922457461eedae4e24732aa6170eaac8c823 Mon Sep 17 00:00:00 2001 From: ConvexSERV Date: Thu, 7 Dec 2023 17:26:01 -0500 Subject: [PATCH 017/447] Create Win_StorageCraftImageManager_Status.ps1 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. --- .../Win_StorageCraftImageManager_Status.ps1 | 983 ++++++++++++++++++ 1 file changed, 983 insertions(+) create mode 100644 scripts_staging/Win_StorageCraftImageManager_Status.ps1 diff --git a/scripts_staging/Win_StorageCraftImageManager_Status.ps1 b/scripts_staging/Win_StorageCraftImageManager_Status.ps1 new file mode 100644 index 00000000..6dd397df --- /dev/null +++ b/scripts_staging/Win_StorageCraftImageManager_Status.ps1 @@ -0,0 +1,983 @@ +<# +.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.3 11/30/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 = '' + +####-------------------------------### +# Database Checks # +####-------------------------------### + +#Image Manager will have the DB open exclusively. Copy the DB to Temp. +$IMDBPath = "c:\ProgramData\TacticalRMM\temp\Imagemanager.mdb" +copy "C:\Program Files (x86)\StorageCraft\ImageManager\ImageManager.mdb" "c:\ProgramData\TacticalRMM\temp\Imagemanager.mdb" + +#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:\Windows\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 +Del "c:\ProgramData\TacticalRMM\temp\Imagemanager.mdb" + +#Report back to the RMM +if ($AlertLevel -gt 0) { + Write-Output $AlertText +} +$Host.SetShouldExit($AlertLevel) +Exit From e29bf9346af50dd05e7b195a9394ad6b0a509832 Mon Sep 17 00:00:00 2001 From: ConvexSERV Date: Thu, 7 Dec 2023 17:42:23 -0500 Subject: [PATCH 018/447] Create Win_HPE-SSACLI_Installl Downloads and installs the HPE SSACLI --- scripts_staging/Win_HPE-SSACLI_Installl | 63 +++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 scripts_staging/Win_HPE-SSACLI_Installl diff --git a/scripts_staging/Win_HPE-SSACLI_Installl b/scripts_staging/Win_HPE-SSACLI_Installl new file mode 100644 index 00000000..4e3ef0ef --- /dev/null +++ b/scripts_staging/Win_HPE-SSACLI_Installl @@ -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 + } +} From 4dc7f20d45dab464ce62feb834cd7b976dea462c Mon Sep 17 00:00:00 2001 From: ConvexSERV Date: Thu, 7 Dec 2023 17:44:49 -0500 Subject: [PATCH 019/447] Rename Win_HPE-SSACLI_Installl to Win_HPE-SSACLI_Install.ps1 Fix Typo in file name. --- .../{Win_HPE-SSACLI_Installl => Win_HPE-SSACLI_Install.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scripts_staging/{Win_HPE-SSACLI_Installl => Win_HPE-SSACLI_Install.ps1} (100%) diff --git a/scripts_staging/Win_HPE-SSACLI_Installl b/scripts_staging/Win_HPE-SSACLI_Install.ps1 similarity index 100% rename from scripts_staging/Win_HPE-SSACLI_Installl rename to scripts_staging/Win_HPE-SSACLI_Install.ps1 From 4edaff4549a5244dcbf65986aa4d8a3f64f05f62 Mon Sep 17 00:00:00 2001 From: ConvexSERV Date: Thu, 7 Dec 2023 18:32:41 -0500 Subject: [PATCH 020/447] Update Win_StorageCraftImageManager_Status.ps1 Added functionality to add the c:\ProgramData\TacticalRMM\temp\ if it does't exist. Catch error of we try to delete the temp copy of the ImageManager.mdb and it's still in use. This usually only happens during debugging, but it is worth handling. --- .../Win_StorageCraftImageManager_Status.ps1 | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts_staging/Win_StorageCraftImageManager_Status.ps1 b/scripts_staging/Win_StorageCraftImageManager_Status.ps1 index 6dd397df..4c564ddb 100644 --- a/scripts_staging/Win_StorageCraftImageManager_Status.ps1 +++ b/scripts_staging/Win_StorageCraftImageManager_Status.ps1 @@ -131,6 +131,11 @@ $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 # ####-------------------------------### @@ -973,7 +978,12 @@ $conn.Close() Start-Sleep -Seconds 1 #Delete the Database Copy -Del "c:\ProgramData\TacticalRMM\temp\Imagemanager.mdb" +try{ + Del "c:\ProgramData\TacticalRMM\temp\Imagemanager.mdb" +} +catch{ + Write-Output "Couldn't delete DB." +} #Report back to the RMM if ($AlertLevel -gt 0) { From a3cd13cf2185bad41ac9048cc077b3b444c50156 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 8 Dec 2023 10:43:32 -0500 Subject: [PATCH 021/447] Moving file into folder --- .../Win_HPE-SSA_Status.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scripts_stagingWin_HPE-SSA_Status.py => scripts_staging/Win_HPE-SSA_Status.py (100%) diff --git a/scripts_stagingWin_HPE-SSA_Status.py b/scripts_staging/Win_HPE-SSA_Status.py similarity index 100% rename from scripts_stagingWin_HPE-SSA_Status.py rename to scripts_staging/Win_HPE-SSA_Status.py From b119cdc2d59c1fc01878455aadda3fc3376460dc Mon Sep 17 00:00:00 2001 From: ConvexSERV Date: Fri, 8 Dec 2023 13:48:59 -0500 Subject: [PATCH 022/447] Update Win_StorageCraftImageManager_Status.ps1 Improved Error Handling when taking a copy of the ImageManager.mdb fie. --- .../Win_StorageCraftImageManager_Status.ps1 | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/scripts_staging/Win_StorageCraftImageManager_Status.ps1 b/scripts_staging/Win_StorageCraftImageManager_Status.ps1 index 4c564ddb..54b7b786 100644 --- a/scripts_staging/Win_StorageCraftImageManager_Status.ps1 +++ b/scripts_staging/Win_StorageCraftImageManager_Status.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS - Monitor Image Manager Status + Monitor Image Manager .DESCRIPTION Checks to see of scheduled Image Manager jobs are completing or failing. @@ -35,7 +35,7 @@ #No Parameters, defaults apply .NOTES - v1.3 11/30/2023 ConvexSERV + v1.4 12/8/2023 ConvexSERV Requires Access 2010 Runtime. Script will attempt to download/install #> @@ -140,9 +140,19 @@ if (-not(test-path "c:\ProgramData\TacticalRMM\temp\")){ # Database Checks # ####-------------------------------### -#Image Manager will have the DB open exclusively. Copy the DB to Temp. +#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" -copy "C:\Program Files (x86)\StorageCraft\ImageManager\ImageManager.mdb" "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{ @@ -153,7 +163,7 @@ 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:\Windows\Temp\AccessDatabaseEngine_X64.exe" + 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." @@ -984,7 +994,6 @@ try{ catch{ Write-Output "Couldn't delete DB." } - #Report back to the RMM if ($AlertLevel -gt 0) { Write-Output $AlertText From a11f76050418c38e830760e2ee5141d20baa3b99 Mon Sep 17 00:00:00 2001 From: ConvexSERV Date: Fri, 8 Dec 2023 13:50:43 -0500 Subject: [PATCH 023/447] Update Win_StorageCraftImageManager_Status.ps1 Fixed title --- scripts_staging/Win_StorageCraftImageManager_Status.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts_staging/Win_StorageCraftImageManager_Status.ps1 b/scripts_staging/Win_StorageCraftImageManager_Status.ps1 index 54b7b786..d30c0431 100644 --- a/scripts_staging/Win_StorageCraftImageManager_Status.ps1 +++ b/scripts_staging/Win_StorageCraftImageManager_Status.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS - Monitor Image Manager + Monitor Image Manager Status .DESCRIPTION Checks to see of scheduled Image Manager jobs are completing or failing. From dc1ea6e31aedcf6110d5f11951f0fbb7bb76cd48 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 8 Dec 2023 16:17:54 -0500 Subject: [PATCH 024/447] Refactor choco and add static choco paths --- community_scripts.json | 4 +- scripts/Win_Chocolatey_List_Installed.bat | 4 +- scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 | 125 ++++++++++++-------- 3 files changed, 78 insertions(+), 55 deletions(-) diff --git a/community_scripts.json b/community_scripts.json index fb31ab85..b1ffc58f 100644 --- a/community_scripts.json +++ b/community_scripts.json @@ -902,9 +902,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", + "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 Hosts x", - "syntax": "-PackageName \n[-Hosts ]\n[-mode {(install) | upgrade | uninstall}]", + "syntax": "-PackageName \n[-Hosts ]\n[-mode {(install) | upgrade | uninstall | list}]", "shell": "powershell", "category": "TRMM (Win):3rd Party Software>Chocolatey", "supported_platforms": [ diff --git a/scripts/Win_Chocolatey_List_Installed.bat b/scripts/Win_Chocolatey_List_Installed.bat index 3b6e2bc3..e04c238a 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 +$chocoExePath = "$env: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..bbf592ac 100644 --- a/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 +++ b/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 @@ -1,71 +1,92 @@ <# - .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 + 4 modes: 'install' (default), 'uninstall', 'upgrade', or 'list'. + + .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 + .\script.ps1 -Hosts 20 -PackageName googlechrome + + .EXAMPLE + .\script.ps1 -Mode upgrade -Hosts 50 -PackageName chocolatey + + .EXAMPLE + .\script.ps1 -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 +#> param ( - [Int] $Hosts = "0", + [Parameter(Mandatory=$false)] + [int] $Hosts = 0, + + [Parameter(Mandatory=$false)] [string[]] $PackageName, + + [Parameter(Mandatory=$false)] + [ValidateSet("install", "uninstall", "upgrade", "list")] [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-Output "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 "list" -and -not $PackageName) { + Write-Output "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 } + +Start-Sleep -Seconds $randDelay + +switch ($Mode) { + "install" { + if ($PackageName) { + foreach ($package in $PackageName) { + & $chocoExePath install $package -y + } + } } - else { - foreach ($package in $PackageName) - { - choco upgrade $package -y + "uninstall" { + if ($PackageName) { + foreach ($package in $PackageName) { + & $chocoExePath uninstall $package -y + } } } - # Write-Output "Running upgrade" - Exit 0 + "upgrade" { + if ($PackageName) { + foreach ($package in $PackageName) { + & $chocoExePath upgrade $package -y + } + } else { + & $chocoExePath upgrade all -y + } + } + "list" { + & $chocoExePath list + } } -# write-output "Running install/uninstall mode" -Start-Sleep -Seconds $rnd; -choco $Mode $PackageName -y Exit 0 From cc6d9a4cf54d1a8ef76e03e59f83e178bdf540be Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 8 Dec 2023 16:49:38 -0500 Subject: [PATCH 025/447] choco bulk - adding seconds debug info --- scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 b/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 index bbf592ac..d6527d97 100644 --- a/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 +++ b/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 @@ -58,6 +58,7 @@ if ($Mode -ne "upgrade" -and $Mode -ne "list" -and -not $PackageName) { # 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-Output "Sleeping $randDelay seconds" Start-Sleep -Seconds $randDelay switch ($Mode) { From 3a128d84df926cce7114e54162d3d6095a496f96 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 22 Dec 2023 06:14:32 -0500 Subject: [PATCH 026/447] choco fixing comment headers --- scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 b/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 index d6527d97..8d43f765 100644 --- a/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 +++ b/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 @@ -15,13 +15,13 @@ Use this to specify which software('s) to install eg: PackageName googlechrome. You can use multiple values using comma separated. .EXAMPLE - .\script.ps1 -Hosts 20 -PackageName googlechrome + -Hosts 20 -PackageName googlechrome .EXAMPLE - .\script.ps1 -Mode upgrade -Hosts 50 -PackageName chocolatey + -Mode upgrade -Hosts 50 -PackageName chocolatey .EXAMPLE - .\script.ps1 -Mode list + -Mode list .NOTES 9/2021 v1 Initial release by @silversword411 and @bradhawkins @@ -30,13 +30,13 @@ #> param ( - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] [int] $Hosts = 0, - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] [string[]] $PackageName, - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] [ValidateSet("install", "uninstall", "upgrade", "list")] [string] $Mode = "install" ) @@ -81,7 +81,8 @@ switch ($Mode) { foreach ($package in $PackageName) { & $chocoExePath upgrade $package -y } - } else { + } + else { & $chocoExePath upgrade all -y } } From 19f9a1561e77801e1506575ff08f99c7b1ab6275 Mon Sep 17 00:00:00 2001 From: Aidan-abss <141785712+Aidan-abss@users.noreply.github.com> Date: Tue, 2 Jan 2024 13:20:58 -0800 Subject: [PATCH 027/447] Create Win_Failed_Logon_Check Created a script to query the event log for failed logon events and gather information about them. --- scripts/Win_Failed_Logon_Check | 59 ++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 scripts/Win_Failed_Logon_Check diff --git a/scripts/Win_Failed_Logon_Check b/scripts/Win_Failed_Logon_Check new file mode 100644 index 00000000..d9776fc0 --- /dev/null +++ b/scripts/Win_Failed_Logon_Check @@ -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 +} From ff52c505c1b6282e9ef6dd77474efdf9f4af25ef Mon Sep 17 00:00:00 2001 From: Aidan-abss <141785712+Aidan-abss@users.noreply.github.com> Date: Tue, 2 Jan 2024 13:21:53 -0800 Subject: [PATCH 028/447] Rename Win_Failed_Logon_Check to Win_Failed_Logon_Check.ps1 Added file extension --- scripts/{Win_Failed_Logon_Check => Win_Failed_Logon_Check.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scripts/{Win_Failed_Logon_Check => Win_Failed_Logon_Check.ps1} (100%) diff --git a/scripts/Win_Failed_Logon_Check b/scripts/Win_Failed_Logon_Check.ps1 similarity index 100% rename from scripts/Win_Failed_Logon_Check rename to scripts/Win_Failed_Logon_Check.ps1 From a3739c31256d36e5bd23274b22d9fe4dd761c486 Mon Sep 17 00:00:00 2001 From: redanthrax Date: Wed, 3 Jan 2024 15:40:41 -0800 Subject: [PATCH 029/447] Updated for CyberCNS v4 updated to not need executable --- scripts_wip/Win_CyberCNS_Install.ps1 | 113 ++++++--------------------- 1 file changed, 26 insertions(+), 87 deletions(-) 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 From 0dce5b0b610bfa5105de2364f14e5ddf0eb7b290 Mon Sep 17 00:00:00 2001 From: redanthrax Date: Thu, 18 Jan 2024 08:31:47 -0800 Subject: [PATCH 030/447] added script for setting office to clear cache --- scripts_wip/Win_Clear_Office_Cache.ps1 | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 scripts_wip/Win_Clear_Office_Cache.ps1 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 From 59f04fc0bf24f6e069fe520ea69c1f75f077bb26 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 18 Jan 2024 12:12:07 -0500 Subject: [PATCH 031/447] Moving script to wip folder --- .../nix_bash_HP_CPU_Status.sh | 0 .../nix_bash_HP_Memory_Status.sh | 0 .../nix_bash_HP_Power_Supply_Status.sh | 0 .../nix_bash_HP_RAID_Battery_Status.sh | 0 .../nix_bash_HP_RAID_Cache_Status.sh | 0 .../nix_bash_HP_RAID_Controller_Status.sh | 0 .../nix_bash_Install_HP_Server_Health_Tools.sh | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename nix_bash_HP_CPU_Status.sh => scripts_wip/nix_bash_HP_CPU_Status.sh (100%) rename nix_bash_HP_Memory_Status.sh => scripts_wip/nix_bash_HP_Memory_Status.sh (100%) rename nix_bash_HP_Power_Supply_Status.sh => scripts_wip/nix_bash_HP_Power_Supply_Status.sh (100%) rename nix_bash_HP_RAID_Battery_Status.sh => scripts_wip/nix_bash_HP_RAID_Battery_Status.sh (100%) rename nix_bash_HP_RAID_Cache_Status.sh => scripts_wip/nix_bash_HP_RAID_Cache_Status.sh (100%) rename nix_bash_HP_RAID_Controller_Status.sh => scripts_wip/nix_bash_HP_RAID_Controller_Status.sh (100%) rename nix_bash_Install_HP_Server_Health_Tools.sh => scripts_wip/nix_bash_Install_HP_Server_Health_Tools.sh (100%) diff --git a/nix_bash_HP_CPU_Status.sh b/scripts_wip/nix_bash_HP_CPU_Status.sh similarity index 100% rename from nix_bash_HP_CPU_Status.sh rename to scripts_wip/nix_bash_HP_CPU_Status.sh diff --git a/nix_bash_HP_Memory_Status.sh b/scripts_wip/nix_bash_HP_Memory_Status.sh similarity index 100% rename from nix_bash_HP_Memory_Status.sh rename to scripts_wip/nix_bash_HP_Memory_Status.sh diff --git a/nix_bash_HP_Power_Supply_Status.sh b/scripts_wip/nix_bash_HP_Power_Supply_Status.sh similarity index 100% rename from nix_bash_HP_Power_Supply_Status.sh rename to scripts_wip/nix_bash_HP_Power_Supply_Status.sh diff --git a/nix_bash_HP_RAID_Battery_Status.sh b/scripts_wip/nix_bash_HP_RAID_Battery_Status.sh similarity index 100% rename from nix_bash_HP_RAID_Battery_Status.sh rename to scripts_wip/nix_bash_HP_RAID_Battery_Status.sh diff --git a/nix_bash_HP_RAID_Cache_Status.sh b/scripts_wip/nix_bash_HP_RAID_Cache_Status.sh similarity index 100% rename from nix_bash_HP_RAID_Cache_Status.sh rename to scripts_wip/nix_bash_HP_RAID_Cache_Status.sh diff --git a/nix_bash_HP_RAID_Controller_Status.sh b/scripts_wip/nix_bash_HP_RAID_Controller_Status.sh similarity index 100% rename from nix_bash_HP_RAID_Controller_Status.sh rename to scripts_wip/nix_bash_HP_RAID_Controller_Status.sh diff --git a/nix_bash_Install_HP_Server_Health_Tools.sh b/scripts_wip/nix_bash_Install_HP_Server_Health_Tools.sh similarity index 100% rename from nix_bash_Install_HP_Server_Health_Tools.sh rename to scripts_wip/nix_bash_Install_HP_Server_Health_Tools.sh From a344f937b4ab2365afb7b57f94875cff100b65a4 Mon Sep 17 00:00:00 2001 From: redanthrax Date: Thu, 18 Jan 2024 10:08:59 -0800 Subject: [PATCH 032/447] Added script to upgrade Teams to New Teams --- scripts_wip/Win_Teams_Upgrade.ps1 | 105 ++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 scripts_wip/Win_Teams_Upgrade.ps1 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 From dc53bf66551840319675f92e96107fce0403720b Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 19 Jan 2024 16:40:41 -0500 Subject: [PATCH 033/447] WIP SMB1 checker --- scripts_wip/Win_SMB1_CheckIfEnabled.ps1 | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 scripts_wip/Win_SMB1_CheckIfEnabled.ps1 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 From 274a4cdb590b48deb6091388f444eddd2c0ef124 Mon Sep 17 00:00:00 2001 From: dinger1986 Date: Mon, 22 Jan 2024 22:47:56 +0000 Subject: [PATCH 034/447] Update Win_Win11_Ready.ps1 --- scripts/Win_Win11_Ready.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/Win_Win11_Ready.ps1 b/scripts/Win_Win11_Ready.ps1 index cbcf0658..abed6448 100644 --- a/scripts/Win_Win11_Ready.ps1 +++ b/scripts/Win_Win11_Ready.ps1 @@ -481,4 +481,5 @@ if (0 -eq $outObject.returncode) { } else { "Not Windows 11 Ready" -} \ No newline at end of file + Write-Output $outObject.returnReason +} From 39ee66d6b47987149556f8345bb576f12ff0726c Mon Sep 17 00:00:00 2001 From: silversword411 Date: Mon, 5 Feb 2024 13:53:16 -0500 Subject: [PATCH 035/447] WIP: Vuln scanner --- scripts_wip/Win_Security_Vuln_scanner.ps1 | 187 ++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 scripts_wip/Win_Security_Vuln_scanner.ps1 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) From 193631a370a430df90cac2809cd269d336e721ef Mon Sep 17 00:00:00 2001 From: silversword411 Date: Mon, 5 Feb 2024 13:55:06 -0500 Subject: [PATCH 036/447] Choco List Converting PS to bat --- scripts/Win_Chocolatey_List_Installed.bat | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/Win_Chocolatey_List_Installed.bat b/scripts/Win_Chocolatey_List_Installed.bat index e04c238a..d6e659dd 100644 --- a/scripts/Win_Chocolatey_List_Installed.bat +++ b/scripts/Win_Chocolatey_List_Installed.bat @@ -1,5 +1,5 @@ rem List apps installed by Chocolatey -$chocoExePath = "$env:PROGRAMDATA\chocolatey\choco.exe" +set "chocoExePath=%PROGRAMDATA%\chocolatey\choco.exe" -$chocoExePath list \ No newline at end of file +"%chocoExePath%" list \ No newline at end of file From 78420c0423a6b7efc4b62e392fa336399305e377 Mon Sep 17 00:00:00 2001 From: Malte Kiefer <59220985+MalteKiefer@users.noreply.github.com> Date: Wed, 7 Feb 2024 10:30:55 +0100 Subject: [PATCH 037/447] Update Win_WinGet_Manage_Apps.ps1 You must replace upgrade with update, else an upgrade of all packages is not possible --- scripts/Win_WinGet_Manage_Apps.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 } From a7d403dff1fee8b45d4d87a5bfc26af8456dc605 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Sun, 11 Feb 2024 11:33:52 -0500 Subject: [PATCH 038/447] NIC enable/disable script. Thx https://github.com/orbitturner ! --- scripts_staging/Win_Network_DisableEnable.ps1 | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 scripts_staging/Win_Network_DisableEnable.ps1 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 +} From 5668448a3b249dfff5ebddfb07e72300ce6dfea9 Mon Sep 17 00:00:00 2001 From: redanthrax Date: Wed, 14 Feb 2024 14:26:21 -0800 Subject: [PATCH 039/447] created printer installer script added fixes for multiple printers --- scripts_wip/Win_PrinterInstaller.ps1 | 156 +++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 scripts_wip/Win_PrinterInstaller.ps1 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 From 2b8876ab779d53098afb7746b450a95f3b6612ed Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 16 Feb 2024 11:42:41 -0500 Subject: [PATCH 040/447] Fixing Recycle bin empty --- scripts/Win_RecycleBin_Empty.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From 0fb38c2e872062b72d61d34aa62bb943e702d715 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 16 Feb 2024 11:55:25 -0500 Subject: [PATCH 041/447] WIP: Spywarekiller v1.3 --- ...reKillerv1.2.ps1 => Win_SpywareKiller.ps1} | 81 ++++++++++++++----- 1 file changed, 63 insertions(+), 18 deletions(-) rename scripts_wip/{Win_SpywareKillerv1.2.ps1 => Win_SpywareKiller.ps1} (65%) 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..4e612306 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 2/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 From 408accb1842717dfefd2db50753675903fb702d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BClken?= Date: Thu, 22 Feb 2024 16:50:53 +0100 Subject: [PATCH 042/447] Update Win_Chocolatey_Manage_Apps_Bulk.ps1 Adding mode parameter "upgrade-only-installed" to force choco to upgrade only packets which are installed. Default behaviour of choco would be to install packages which are not found on the target although we only want to upgrade. --- scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 | 25 ++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 b/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 index 8d43f765..e530cba0 100644 --- a/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 +++ b/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 @@ -6,7 +6,12 @@ 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 - 4 modes: 'install' (default), 'uninstall', 'upgrade', or 'list'. + 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). You can specify multiple one or more packages seperated by comma. 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 .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 @@ -20,6 +25,9 @@ .EXAMPLE -Mode upgrade -Hosts 50 -PackageName chocolatey + .EXAMPLE + -Mode upgrade-only-installed -Hosts 20 -PackageName googlechrome,firefox + .EXAMPLE -Mode list @@ -27,6 +35,7 @@ 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 #> param ( @@ -37,7 +46,7 @@ param ( [string[]] $PackageName, [Parameter(Mandatory = $false)] - [ValidateSet("install", "uninstall", "upgrade", "list")] + [ValidateSet("install", "uninstall", "upgrade", "upgrade-only-installed", "list")] [string] $Mode = "install" ) @@ -50,7 +59,7 @@ if (-not (Test-Path $chocoExePath)) { $ErrorCount = 0 -if ($Mode -ne "upgrade" -and $Mode -ne "list" -and -not $PackageName) { +if ($Mode -ne "upgrade" -and $Mode -ne "upgrade-only-installed" -and $Mode -ne "list" -and -not $PackageName) { Write-Output "Error: No package name provided. Please specify a package name, e.g., `-PackageName googlechrome`." Exit 1 } @@ -86,6 +95,16 @@ switch ($Mode) { & $chocoExePath upgrade all -y } } + "upgrade-only-installed" { + if ($PackageName) { + foreach ($package in $PackageName) { + & $chocoExePath upgrade $package --failonnotinstalled -y + } + } + else { + & $chocoExePath upgrade all --failonnotinstalled -y + } + } "list" { & $chocoExePath list } From 94756d06a8bade5262f9ea3a8f301af54df4ae80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BClken?= Date: Thu, 22 Feb 2024 19:18:42 +0100 Subject: [PATCH 043/447] Add new mode Adding new mode to syntax and description --- community_scripts.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/community_scripts.json b/community_scripts.json index b1ffc58f..70c6f281 100644 --- a/community_scripts.json +++ b/community_scripts.json @@ -903,8 +903,8 @@ "filename": "Win_Chocolatey_Manage_Apps_Bulk.ps1", "submittedBy": "https://github.com/silversword411", "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 Hosts x", - "syntax": "-PackageName \n[-Hosts ]\n[-mode {(install) | upgrade | uninstall | list}]", + "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": [ @@ -1706,4 +1706,4 @@ ], "category": "TRMM (All):3rd Party Software" } -] \ No newline at end of file +] From d8d3d518f77c00b503ba424f8a11d1c517829e5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BClken?= Date: Thu, 22 Feb 2024 19:30:28 +0100 Subject: [PATCH 044/447] fix wording fix wording in mdoe description --- scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 b/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 index e530cba0..dfab136d 100644 --- a/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 +++ b/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 @@ -9,7 +9,7 @@ 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). You can specify multiple one or more packages seperated by comma. If no PackageName is given all installed packages are being updated + 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 From 477d18a36937670c9333aa8b50c74b8a9b7608b0 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 23 Feb 2024 00:00:37 -0500 Subject: [PATCH 045/447] Veeam Collection --- scripts_wip/Win_Veeam_BackupRun.ps1 | 3 + scripts_wip/Win_Veeam_CheckBackup.ps1 | 323 ++++++++++++++++-- .../Win_Veeam_CollectorLastBackupDate.ps1 | 4 + scripts_wip/Win_Veeam_RecoveryMediaCreate.ps1 | 34 ++ 4 files changed, 332 insertions(+), 32 deletions(-) create mode 100644 scripts_wip/Win_Veeam_BackupRun.ps1 create mode 100644 scripts_wip/Win_Veeam_CollectorLastBackupDate.ps1 create mode 100644 scripts_wip/Win_Veeam_RecoveryMediaCreate.ps1 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 + } From 54f022cb5d2805fd8cccdf006d900643deecb5f1 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 23 Feb 2024 00:06:18 -0500 Subject: [PATCH 046/447] Updating official defender exclusions --- scripts/Win_TRMM_AV_Update_Exclusion.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/Win_TRMM_AV_Update_Exclusion.ps1 b/scripts/Win_TRMM_AV_Update_Exclusion.ps1 index 97cae9e8..fba0740d 100644 --- a/scripts/Win_TRMM_AV_Update_Exclusion.ps1 +++ b/scripts/Win_TRMM_AV_Update_Exclusion.ps1 @@ -2,3 +2,5 @@ Add-MpPreference -ExclusionPath "C:\Program Files\Mesh Agent\*" Add-MpPreference -ExclusionPath "C:\Program Files\TacticalAgent\*" Add-MpPreference -ExclusionPath "C:\ProgramData\TacticalRMM\*" +# For agent updates. Inno setup temp directory +Add-MpPreference -ExclusionPath "%TEMPDIR%\is-*.tmp\tacticalagent*" From 6c885be7974df1bbb5b9f8d4d3aa286f2b625083 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 23 Feb 2024 00:08:04 -0500 Subject: [PATCH 047/447] WIP - try and kill Firefox full screening for Tech Support scammer --- scripts_wip/Win_Browser_KillFullscreen.ps1 | 54 ++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 scripts_wip/Win_Browser_KillFullscreen.ps1 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 From 5528749ef6ce5c8a5b9394bdd501216d4e240d00 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 23 Feb 2024 00:10:43 -0500 Subject: [PATCH 048/447] staged new speed test script - Test pls --- scripts_staging/Win_Network_Speed_Test.ps1 | 81 ++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 scripts_staging/Win_Network_Speed_Test.ps1 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 +} From 933e97e4c65a2c0af063282a37440856faff7753 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 23 Feb 2024 00:29:10 -0500 Subject: [PATCH 049/447] Moving to staging and renaming for further testing --- .../Win_Logon_FailsCheck.ps1 | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scripts/Win_Failed_Logon_Check.ps1 => scripts_staging/Win_Logon_FailsCheck.ps1 (100%) diff --git a/scripts/Win_Failed_Logon_Check.ps1 b/scripts_staging/Win_Logon_FailsCheck.ps1 similarity index 100% rename from scripts/Win_Failed_Logon_Check.ps1 rename to scripts_staging/Win_Logon_FailsCheck.ps1 From f6210fb29eb2d6a4d7b2d2d0c3d566179a87e6c3 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Tue, 5 Mar 2024 10:10:31 -0500 Subject: [PATCH 050/447] Updating choco bulk to use --no-progress --- scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 b/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 index 8d43f765..7a20e113 100644 --- a/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 +++ b/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 @@ -26,7 +26,8 @@ .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 + 12/8/2023 v1.3 silversword411 Adding list, making choco full path + 3/5/2024 v1.4 silversword411 Adding --no-progress to minimize output #> param ( @@ -65,7 +66,7 @@ switch ($Mode) { "install" { if ($PackageName) { foreach ($package in $PackageName) { - & $chocoExePath install $package -y + & $chocoExePath install $package -y --no-progress } } } @@ -79,11 +80,11 @@ switch ($Mode) { "upgrade" { if ($PackageName) { foreach ($package in $PackageName) { - & $chocoExePath upgrade $package -y + & $chocoExePath upgrade $package -y --no-progress } } else { - & $chocoExePath upgrade all -y + & $chocoExePath upgrade all -y --no-progress } } "list" { From 20e3755c7fc55cf044eaef1eb2ba35a2f4e9b5a9 Mon Sep 17 00:00:00 2001 From: redanthrax Date: Thu, 7 Mar 2024 10:26:57 -0800 Subject: [PATCH 051/447] changed upgrade to not uninstall first --- scripts_wip/Win_DuoAuthLogon_Manage.ps1 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts_wip/Win_DuoAuthLogon_Manage.ps1 b/scripts_wip/Win_DuoAuthLogon_Manage.ps1 index 176b32e6..8572e82c 100644 --- a/scripts_wip/Win_DuoAuthLogon_Manage.ps1 +++ b/scripts_wip/Win_DuoAuthLogon_Manage.ps1 @@ -172,6 +172,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 @@ -188,7 +192,7 @@ function Win_DuoAuthLogon_Manage { Process { Try { - if ($Uninstall -or $Upgrade) { + if ($Uninstall) { $uninstallString = ($Apps | Where-Object { $_.DisplayName -Match "Duo Authentication" }).UninstallString if ($uninstallString) { $msiexec, $args = $uninstallString.Split(" ") @@ -204,6 +208,10 @@ function Win_DuoAuthLogon_Manage { } } + if ($Upgrade) { + Write-Output "Attempting upgrade of Duo." + } + Write-Output "Starting installation." $source = "https://dl.duosecurity.com/duo-win-login-latest.exe" $destination = "C:\packages$random\duo-win-login-latest.exe" From 20c9eaaf6f01cc3d465037bb6bd7a855044930de Mon Sep 17 00:00:00 2001 From: dinger1986 Date: Mon, 11 Mar 2024 20:14:16 +0000 Subject: [PATCH 052/447] Update linux_os_update.sh --- scripts_staging/linux_os_update.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts_staging/linux_os_update.sh b/scripts_staging/linux_os_update.sh index fa38e8ae..7297fd83 100644 --- a/scripts_staging/linux_os_update.sh +++ b/scripts_staging/linux_os_update.sh @@ -2,7 +2,9 @@ #Update script to run for most common linux distros -if [[ `which yum` ]]; then +if [[ `which dnf` ]]; then + dnf -y update +elif [[ `which yum` ]]; then yum -y update elif [[ `which apt` ]]; then apt-get -y update From 0ee108e83d3b05f27c1018fe92e0e3ee790737ae Mon Sep 17 00:00:00 2001 From: dinger1986 Date: Mon, 11 Mar 2024 20:24:05 +0000 Subject: [PATCH 053/447] Update linux_os_update.sh --- scripts_staging/linux_os_update.sh | 43 +++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/scripts_staging/linux_os_update.sh b/scripts_staging/linux_os_update.sh index 7297fd83..d30eab66 100644 --- a/scripts_staging/linux_os_update.sh +++ b/scripts_staging/linux_os_update.sh @@ -1,20 +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 dnf` ]]; 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 [[ `which yum` ]]; then +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 From 00a373beb342e5145ec05a90bae35bfaf47b6469 Mon Sep 17 00:00:00 2001 From: dinger1986 Date: Mon, 11 Mar 2024 20:30:17 +0000 Subject: [PATCH 054/447] Create linux_os_update_check.sh --- scripts_staging/linux_os_update_check.sh | 65 ++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 scripts_staging/linux_os_update_check.sh 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 From 0c845d55126ef45e0dd12ac3e44c3f153bf4ec91 Mon Sep 17 00:00:00 2001 From: dinger1986 Date: Wed, 13 Mar 2024 22:20:53 +0000 Subject: [PATCH 055/447] Create Windows_Clear_cookies.ps1 --- scripts_staging/Windows_Clear_cookies.ps1 | 58 +++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 scripts_staging/Windows_Clear_cookies.ps1 diff --git a/scripts_staging/Windows_Clear_cookies.ps1 b/scripts_staging/Windows_Clear_cookies.ps1 new file mode 100644 index 00000000..20169750 --- /dev/null +++ b/scripts_staging/Windows_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." From a1dd608b0f4f087e4448c71324e2a7a9603900c3 Mon Sep 17 00:00:00 2001 From: dinger1986 Date: Wed, 13 Mar 2024 22:21:46 +0000 Subject: [PATCH 056/447] Rename Windows_Clear_cookies.ps1 to Win_Clear_cookies.ps1 --- .../{Windows_Clear_cookies.ps1 => Win_Clear_cookies.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scripts_staging/{Windows_Clear_cookies.ps1 => Win_Clear_cookies.ps1} (100%) diff --git a/scripts_staging/Windows_Clear_cookies.ps1 b/scripts_staging/Win_Clear_cookies.ps1 similarity index 100% rename from scripts_staging/Windows_Clear_cookies.ps1 rename to scripts_staging/Win_Clear_cookies.ps1 From a637392d1b84d46e9c71c7293963ac5553fe5a71 Mon Sep 17 00:00:00 2001 From: redanthrax Date: Wed, 31 Jan 2024 17:52:22 -0800 Subject: [PATCH 057/447] Added initial script that is WIP --- scripts_wip/Win_ManageBitlocker.ps1 | 303 ++++++++++++++++++++++++++++ 1 file changed, 303 insertions(+) create mode 100644 scripts_wip/Win_ManageBitlocker.ps1 diff --git a/scripts_wip/Win_ManageBitlocker.ps1 b/scripts_wip/Win_ManageBitlocker.ps1 new file mode 100644 index 00000000..fb91bb45 --- /dev/null +++ b/scripts_wip/Win_ManageBitlocker.ps1 @@ -0,0 +1,303 @@ +<# +.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_ManageBitlocker.ps1 -Info Keys + .\Win_ManageBitlocker.ps1 -Info Tpm,TpmHealth + .\Win_ManageBitlocker.ps1 -Operation Encrypt,Backup + .\Win_ManageBitlocker.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")] + [AllowNull()] + [AllowEmptyCollection()] + [string[]]$Operation +) + +function Win_ManageBitlocker { + [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")] + [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" + } + } + } + } + "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" + } + } + } + } + Catch { + Write-Error $_.Exception + } + } + + End { + if ($error) { + $error + Exit 1 + } + + Exit 0 + } +} + +if (-Not(Get-Command 'Win_ManageBitlocker' -ErrorAction SilentlyContinue)) { + . $MyInvocation.MyCommand.Path +} + +$scriptArgs = @{ + Json = $Json + Info = $Info + Operation = $Operation +} + +Win_ManageBitlocker @scriptArgs \ No newline at end of file From 27c365fc95c7d93337298dfce9cfd23a00a0afef Mon Sep 17 00:00:00 2001 From: redanthrax Date: Tue, 19 Mar 2024 10:49:22 -0700 Subject: [PATCH 058/447] added recovery password set --- scripts_wip/Win_ManageBitlocker.ps1 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts_wip/Win_ManageBitlocker.ps1 b/scripts_wip/Win_ManageBitlocker.ps1 index fb91bb45..00dc917b 100644 --- a/scripts_wip/Win_ManageBitlocker.ps1 +++ b/scripts_wip/Win_ManageBitlocker.ps1 @@ -138,6 +138,13 @@ function Win_ManageBitlocker { else { Write-Output "Volume already encrypted or in process" } + + #Check for recovery password, add if missing + $recoveryPassword = $vol.KeyProtector | Where-Object { $_.KeyProtectorType -eq "RecoveryPassword" } + if (-Not($recoveryPassword)) { + Write-Output "Adding recovery password" + $vol | Add-BitLockerKeyProtector -RecoveryPasswordProtector -InformationAction SilentlyContinue | Out-Null + } } } } From 36c57da4a988f33a5bf1b9f98346f41785537979 Mon Sep 17 00:00:00 2001 From: redanthrax Date: Tue, 19 Mar 2024 11:18:18 -0700 Subject: [PATCH 059/447] added check for tpm --- scripts_wip/Win_ManageBitlocker.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts_wip/Win_ManageBitlocker.ps1 b/scripts_wip/Win_ManageBitlocker.ps1 index 00dc917b..50b7fd0a 100644 --- a/scripts_wip/Win_ManageBitlocker.ps1 +++ b/scripts_wip/Win_ManageBitlocker.ps1 @@ -139,9 +139,10 @@ function Win_ManageBitlocker { Write-Output "Volume already encrypted or in process" } - #Check for recovery password, add if missing + #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)) { + if (-Not($recoveryPassword) -and $tpmProtector) { Write-Output "Adding recovery password" $vol | Add-BitLockerKeyProtector -RecoveryPasswordProtector -InformationAction SilentlyContinue | Out-Null } From 637a579d32d16089bc913e14997489aa23ad4a2f Mon Sep 17 00:00:00 2001 From: silversword411 Date: Tue, 19 Mar 2024 23:32:16 -0400 Subject: [PATCH 060/447] Screenconnect AIO - adding debug and formatting --- scripts/Win_ScreenConnectAIO.ps1 | 167 ++++++++++++++++--------------- 1 file changed, 86 insertions(+), 81 deletions(-) diff --git a/scripts/Win_ScreenConnectAIO.ps1 b/scripts/Win_ScreenConnectAIO.ps1 index f654eefd..2317d71c 100644 --- a/scripts/Win_ScreenConnectAIO.ps1 +++ b/scripts/Win_ScreenConnectAIO.ps1 @@ -5,19 +5,33 @@ url is the path the download the exe version of the ScreenConnect Access install 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. #> 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) { @@ -30,122 +44,113 @@ if (!$url) { } if (!$ErrorCount -eq 0) { -exit 1 + exit 1 } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 if ($action -eq "uninstall") { - $MyApp = Get-WmiObject -Class Win32_Product | Where-Object{$_.Name -eq "$serviceName"} - $MyApp.Uninstall() -} elseif ($action -eq "stop") { + $MyApp = Get-WmiObject -Class Win32_Product | Where-Object { $_.Name -eq "$serviceName" } + Write-Debug "MyApp: $MyApp" + $MyApp.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 + } + Finally { + } + + } +} +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 + } + Finally { + } + + } +} +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 - { - } + } + Finally { + } - } Else { + } + Else { - Try - { - Write-Host "Starting $serviceName" - Set-Service -Name $serviceName -Status running -StartupType automatic - exit 0 + Try { + Write-Host "Starting $serviceName" + Set-Service -Name $serviceName -Status running -StartupType automatic + exit 0 } - Catch - { + Catch { $ErrorMessage = $_.Exception.Message $FailedItem = $_.Exception.ItemName Write-Error -Message "$ErrorMessage $FailedItem" exit 1 - } - Finally - { - } + } + Finally { + } } - } Else { + } + Else { $OutPath = $env:TMP $output = "screenconnect.exe" - 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)" + 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 - $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)" + $start_time = Get-Date + $wc = New-Object System.Net.WebClient + Start-Process -FilePath $OutPath\$output -Wait + Write-Debug "Time taken to install: $((Get-Date).Subtract($start_time).Seconds) second(s)" exit 0 } - Catch - { + Catch { $ErrorMessage = $_.Exception.Message $FailedItem = $_.Exception.ItemName Write-Error -Message "$ErrorMessage $FailedItem" exit 1 } - Finally - { + Finally { Remove-Item -Path $OutPath\$output } From aa99076612547cce65b798b3b9c0d9fc5a92e012 Mon Sep 17 00:00:00 2001 From: redanthrax Date: Wed, 20 Mar 2024 07:59:08 -0700 Subject: [PATCH 061/447] added fix scenario for an issue preventing key backup, end output --- scripts_wip/Win_ManageBitlocker.ps1 | 40 +++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/scripts_wip/Win_ManageBitlocker.ps1 b/scripts_wip/Win_ManageBitlocker.ps1 index 50b7fd0a..8d80ee4d 100644 --- a/scripts_wip/Win_ManageBitlocker.ps1 +++ b/scripts_wip/Win_ManageBitlocker.ps1 @@ -27,7 +27,8 @@ Param( [AllowEmptyCollection()] [string[]]$Info, - [Parameter(HelpMessage = "Operation: Encrypt, Decrypt, Suspend, Resume, Backup, HealBitlocker")] + [Parameter(HelpMessage = "Operation: Encrypt, Decrypt, Suspend, Resume, + Backup, HealBitlocker, HealKeyBackup")] [AllowNull()] [AllowEmptyCollection()] [string[]]$Operation @@ -44,7 +45,8 @@ function Win_ManageBitlocker { [AllowEmptyCollection()] [string[]]$Info, - [Parameter(HelpMessage = "Operation: Encrypt, Decrypt, Suspend, Resume, Backup, HealBitlocker")] + [Parameter(HelpMessage = "Operation: Encrypt, Decrypt, Suspend, Resume, + Backup, HealBitlocker, HealKeyBackup")] [AllowNull()] [AllowEmptyCollection()] [string[]]$Operation @@ -280,8 +282,42 @@ function Win_ManageBitlocker { "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." + } + } } } + + Write-Output "Bitlocker Management Complete" } Catch { Write-Error $_.Exception From aa88e2cea2e951476fa92104340dd7142ca98742 Mon Sep 17 00:00:00 2001 From: redanthrax Date: Thu, 21 Mar 2024 07:22:22 -0700 Subject: [PATCH 062/447] removed end output --- scripts_wip/Win_ManageBitlocker.ps1 | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts_wip/Win_ManageBitlocker.ps1 b/scripts_wip/Win_ManageBitlocker.ps1 index 8d80ee4d..73e6cf4e 100644 --- a/scripts_wip/Win_ManageBitlocker.ps1 +++ b/scripts_wip/Win_ManageBitlocker.ps1 @@ -316,8 +316,6 @@ function Win_ManageBitlocker { } } } - - Write-Output "Bitlocker Management Complete" } Catch { Write-Error $_.Exception From 35c44ab69f1dd1ad7846f692d6bac1b573b97691 Mon Sep 17 00:00:00 2001 From: redanthrax Date: Fri, 22 Mar 2024 09:56:05 -0700 Subject: [PATCH 063/447] updated script to handle uninstall scenarios better --- scripts_wip/Win_DuoAuthLogon_Manage.ps1 | 117 ++++++++++++++++++------ 1 file changed, 90 insertions(+), 27 deletions(-) diff --git a/scripts_wip/Win_DuoAuthLogon_Manage.ps1 b/scripts_wip/Win_DuoAuthLogon_Manage.ps1 index 8572e82c..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 ) @@ -185,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 } @@ -193,19 +197,66 @@ function Win_DuoAuthLogon_Manage { Process { Try { 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 "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 "No uninstall string found." - return + Write-Output "Duo not detected, uninstall complete" } + + return } if ($Upgrade) { @@ -263,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 } @@ -272,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 From 39f1a0574bf26bb1f75168d67b35c05d9dff6290 Mon Sep 17 00:00:00 2001 From: redanthrax Date: Fri, 22 Mar 2024 13:10:04 -0700 Subject: [PATCH 064/447] updated for better uninstall functionality --- scripts_wip/Win_AutoElevate_Manage.ps1 | 83 +++++++++++++++++--------- 1 file changed, 55 insertions(+), 28 deletions(-) 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 From 6c456c13528aaee386a2089564dec81fc372baeb Mon Sep 17 00:00:00 2001 From: redanthrax Date: Fri, 22 Mar 2024 15:22:58 -0700 Subject: [PATCH 065/447] improved uninstall, added ability for multiple apps, improved output --- scripts_wip/Win_Software_Uninstall.ps1 | 139 ++++++++++++++++++++----- 1 file changed, 111 insertions(+), 28 deletions(-) diff --git a/scripts_wip/Win_Software_Uninstall.ps1 b/scripts_wip/Win_Software_Uninstall.ps1 index 8fb5a7ef..a4fa68b0 100644 --- a/scripts_wip/Win_Software_Uninstall.ps1 +++ b/scripts_wip/Win_Software_Uninstall.ps1 @@ -3,48 +3,131 @@ 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 to perform the uninstallation. + 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 - + Version: 1.0 + Author: redanthrax + Creation Date: 2023-11-27 + Updated Date: 2024-03-22 #> Param( - [Parameter(Mandatory)] - [string]$Application + [string[]]$Application ) -Write-Output "Attempting to uninstall $Application" -$Paths = @("HKLM:\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\*", - "HKLM:\SOFTWARE\\Wow6432node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\*") -if ($Application -ne "") { - foreach ($app in Get-ItemProperty $Paths | Where-Object { $_.Displayname -match [regex]::Escape($Application) } | Sort-Object DisplayName) { - if ($app.UninstallString) { - Write-Output "Found $Application uninstall string" - $MsiArguments = $app.UninstallString -Replace "MsiExec.exe ", "" -Replace "/I", "/X" - Write-Output "Executing msiexec $MsiArguments /quiet /norestart /qn" - Start-Process -FilePath msiexec.exe -ArgumentList "$MsiArguments /quiet /norestart /qn" -Wait - Start-Sleep -Seconds 20 - $UninstallTest = (Get-ItemProperty $Paths | Where-object { $_.UninstallString -match [regex]::Escape($Application) }).DisplayName - Write-Output "Uninstall Test: $UninstallTest" - If ($UninstallTest) { - Write-Output "$Application Uninstall Failed" - } - else { - Write-Output "$Application Uninstalled" +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" + } } } - - break + Catch { + Write-Error "Error $($_.Exception)" + } + + } + + End { + if ($error) { + Exit 1 + } + + Exit 0 } -} \ No newline at end of file +} + +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 From 401197e8e87848fc3a83afc4638a205a2c0845da Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 28 Mar 2024 02:21:06 -0400 Subject: [PATCH 066/447] Screenconnect AIO Update json for syntax --- community_scripts.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/community_scripts.json b/community_scripts.json index 70c6f281..c856ce32 100644 --- a/community_scripts.json +++ b/community_scripts.json @@ -1064,11 +1064,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" ], @@ -1706,4 +1706,4 @@ ], "category": "TRMM (All):3rd Party Software" } -] +] \ No newline at end of file From 583050940354eec301904fe579a65c23d8651a21 Mon Sep 17 00:00:00 2001 From: redanthrax Date: Wed, 10 Apr 2024 13:58:34 -0700 Subject: [PATCH 067/447] crowdstrike script --- scripts_wip/Win_Crowdstrike.ps1 | 131 ++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 scripts_wip/Win_Crowdstrike.ps1 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 From da9d1e39fe10a9c110b1fbb154c9c612d2f4703b Mon Sep 17 00:00:00 2001 From: redanthrax Date: Tue, 16 Apr 2024 08:38:19 -0700 Subject: [PATCH 068/447] added logic for upgrade, updated params --- scripts_wip/Win_PerchLogShipper_Install.ps1 | 102 ++++++++++++++++++-- 1 file changed, 92 insertions(+), 10 deletions(-) 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 From 3ae85a020b5c8d2cea9c034247dbfb338d6a5f6d Mon Sep 17 00:00:00 2001 From: redanthrax Date: Tue, 16 Apr 2024 09:05:42 -0700 Subject: [PATCH 069/447] MS Store Update Setting --- scripts_wip/Win_MSStoreUpdates.ps1 | 66 ++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 scripts_wip/Win_MSStoreUpdates.ps1 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 From 5e17c71e4ecb4e24bd99075b43eb37bc08cffeec Mon Sep 17 00:00:00 2001 From: dinger1986 Date: Fri, 19 Apr 2024 17:34:28 +0100 Subject: [PATCH 070/447] Update Win_Duplicati_Status.ps1 --- scripts/Win_Duplicati_Status.ps1 | 34 ++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/scripts/Win_Duplicati_Status.ps1 b/scripts/Win_Duplicati_Status.ps1 index 6a883d41..ecdf19c4 100644 --- a/scripts/Win_Duplicati_Status.ps1 +++ b/scripts/Win_Duplicati_Status.ps1 @@ -32,17 +32,31 @@ # 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 -} +# Define the time spans for the last 24 hours and the last 5 days +$Last24Hours = (Get-Date) - (New-TimeSpan -Days 1) +$Last5Days = (Get-Date) - (New-TimeSpan -Days 5) +# Fetch events from the last 5 days and last 24 hours +$eventsLast5Days = Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = 200; StartTime = $Last5Days} -ErrorAction SilentlyContinue | Sort-Object TimeCreated +$eventsLast24Hours = Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = 200; StartTime = $Last24Hours} -ErrorAction SilentlyContinue | Sort-Object TimeCreated -else { - Write-Output "Duplicati Backup Is Working Correctly" - Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = '205', '200', '201' } - exit 0 +# Check for any successful backup in the last 5 days +if ($eventsLast5Days) { + $lastSuccessfulBackup = $eventsLast5Days | Select-Object -Last 1 + $lastBackupTime = $lastSuccessfulBackup.TimeCreated + Write-Output "Last successful Duplicati Backup in the last 5 days was at $lastBackupTime" + + # Check if there was a successful backup in the last 24 hours + if ($eventsLast24Hours) { + Write-Output "There has been a successful backup in the last 24 hours." + $host.SetShouldExit(0) # Exit code 0 for success + } else { + Write-Output "No successful backup in the last 24 hours." + $host.SetShouldExit(1) # Exit code 1 for error + } +} else { + Write-Output "No successful Duplicati Backup found in the last 5 days." + $host.SetShouldExit(1) # Exit code 1 for error } + From 76e10a57a86df63ba53eeb7bc25cbcf94f165699 Mon Sep 17 00:00:00 2001 From: dinger1986 Date: Fri, 19 Apr 2024 17:49:17 +0100 Subject: [PATCH 071/447] Update Win_Duplicati_Status.ps1 --- scripts/Win_Duplicati_Status.ps1 | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/scripts/Win_Duplicati_Status.ps1 b/scripts/Win_Duplicati_Status.ps1 index ecdf19c4..224a2c47 100644 --- a/scripts/Win_Duplicati_Status.ps1 +++ b/scripts/Win_Duplicati_Status.ps1 @@ -33,6 +33,17 @@ $ErrorActionPreference = 'silentlycontinue' +# Name of the service to check +$serviceName = 'Duplicati' # change this to the actual service name you want to check + +# 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) # 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) $Last5Days = (Get-Date) - (New-TimeSpan -Days 5) @@ -46,17 +57,21 @@ if ($eventsLast5Days) { $lastSuccessfulBackup = $eventsLast5Days | Select-Object -Last 1 $lastBackupTime = $lastSuccessfulBackup.TimeCreated Write-Output "Last successful Duplicati Backup in the last 5 days was at $lastBackupTime" + Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = '205', '200', '201' } # Check if there was a successful backup in the last 24 hours if ($eventsLast24Hours) { Write-Output "There has been a successful backup in the last 24 hours." + Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = '205', '201', '202'; StartTime = $eventsLast24Hours } $host.SetShouldExit(0) # Exit code 0 for success } else { Write-Output "No successful backup in the last 24 hours." + Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = '205', '200', '201' } $host.SetShouldExit(1) # Exit code 1 for error } } else { Write-Output "No successful Duplicati Backup found in the last 5 days." + Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = '205', '200', '201' } $host.SetShouldExit(1) # Exit code 1 for error } From 401548b2fffad2023910d9afdc030e755e191e1c Mon Sep 17 00:00:00 2001 From: dinger1986 Date: Fri, 19 Apr 2024 18:09:34 +0100 Subject: [PATCH 072/447] Update Win_Duplicati_Status.ps1 --- scripts/Win_Duplicati_Status.ps1 | 50 ++++++++++++++++---------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/scripts/Win_Duplicati_Status.ps1 b/scripts/Win_Duplicati_Status.ps1 index 224a2c47..fed467fc 100644 --- a/scripts/Win_Duplicati_Status.ps1 +++ b/scripts/Win_Duplicati_Status.ps1 @@ -34,44 +34,44 @@ $ErrorActionPreference = 'silentlycontinue' # Name of the service to check -$serviceName = 'Duplicati' # change this to the actual service name you want 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) # Exit code 0 for "service not found" + $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) -$Last5Days = (Get-Date) - (New-TimeSpan -Days 5) +$Last10Days = (Get-Date) - (New-TimeSpan -Days 10) -# Fetch events from the last 5 days and last 24 hours -$eventsLast5Days = Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = 200; StartTime = $Last5Days} -ErrorAction SilentlyContinue | Sort-Object TimeCreated -$eventsLast24Hours = Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = 200; StartTime = $Last24Hours} -ErrorAction SilentlyContinue | Sort-Object TimeCreated +# 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 successful backup in the last 5 days -if ($eventsLast5Days) { - $lastSuccessfulBackup = $eventsLast5Days | Select-Object -Last 1 - $lastBackupTime = $lastSuccessfulBackup.TimeCreated - Write-Output "Last successful Duplicati Backup in the last 5 days was at $lastBackupTime" - Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = '205', '200', '201' } - - # Check if there was a successful backup in the last 24 hours - if ($eventsLast24Hours) { - Write-Output "There has been a successful backup in the last 24 hours." - Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = '205', '201', '202'; StartTime = $eventsLast24Hours } - $host.SetShouldExit(0) # Exit code 0 for success - } else { - Write-Output "No successful backup in the last 24 hours." - Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = '205', '200', '201' } - $host.SetShouldExit(1) # Exit code 1 for error +# 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' } } -} else { - Write-Output "No successful Duplicati Backup found in the last 5 days." - Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = '205', '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 + +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 +} From e72330f493f9494429cd2e657986f41d0151d00e Mon Sep 17 00:00:00 2001 From: redanthrax Date: Thu, 2 May 2024 14:16:28 -0700 Subject: [PATCH 073/447] WinRE Fix Script --- scripts_wip/Win_WinREFix.ps1 | 72 ++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 scripts_wip/Win_WinREFix.ps1 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 From 71e0b7004dbf6c5f8772dcfb6ce02ebffd8c74ed Mon Sep 17 00:00:00 2001 From: redanthrax Date: Thu, 2 May 2024 14:20:03 -0700 Subject: [PATCH 074/447] Updated Bitlocker Script Name --- ...n_ManageBitlocker.ps1 => Win_Bitlocker_AIO.ps1} | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) rename scripts_wip/{Win_ManageBitlocker.ps1 => Win_Bitlocker_AIO.ps1} (97%) diff --git a/scripts_wip/Win_ManageBitlocker.ps1 b/scripts_wip/Win_Bitlocker_AIO.ps1 similarity index 97% rename from scripts_wip/Win_ManageBitlocker.ps1 rename to scripts_wip/Win_Bitlocker_AIO.ps1 index 73e6cf4e..fda834fb 100644 --- a/scripts_wip/Win_ManageBitlocker.ps1 +++ b/scripts_wip/Win_Bitlocker_AIO.ps1 @@ -7,10 +7,10 @@ for the circumstance when you receive an odd error when trying to get the bitlocker volume "Get-CimInstance : Invalid property" .EXAMPLE - .\Win_ManageBitlocker.ps1 -Info Keys - .\Win_ManageBitlocker.ps1 -Info Tpm,TpmHealth - .\Win_ManageBitlocker.ps1 -Operation Encrypt,Backup - .\Win_ManageBitlocker.ps1 -Operation HealBitlocker + .\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 @@ -34,7 +34,7 @@ Param( [string[]]$Operation ) -function Win_ManageBitlocker { +function Win_Bitlocker_AIO { [CmdletBinding()] Param( [Parameter(HelpMessage = "Output volumes in Json format")] @@ -332,7 +332,7 @@ function Win_ManageBitlocker { } } -if (-Not(Get-Command 'Win_ManageBitlocker' -ErrorAction SilentlyContinue)) { +if (-Not(Get-Command 'Win_Bitlocker_AIO' -ErrorAction SilentlyContinue)) { . $MyInvocation.MyCommand.Path } @@ -342,4 +342,4 @@ $scriptArgs = @{ Operation = $Operation } -Win_ManageBitlocker @scriptArgs \ No newline at end of file +Win_Bitlocker_AIO @scriptArgs \ No newline at end of file From a32be81e8b38c92d25f8a055219d21458b0b78c9 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Mon, 6 May 2024 13:13:42 -0400 Subject: [PATCH 075/447] fix spywarekiller version comment headers --- scripts_wip/Win_SpywareKiller.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts_wip/Win_SpywareKiller.ps1 b/scripts_wip/Win_SpywareKiller.ps1 index 4e612306..f9217fc5 100644 --- a/scripts_wip/Win_SpywareKiller.ps1 +++ b/scripts_wip/Win_SpywareKiller.ps1 @@ -29,7 +29,7 @@ Added Onelaunch v1.2 7/2023 silversword411 Refining, adding debug output, adding autodelete switch, reformatting outlook for easier reading - v1.3 2/2024 silversword411 + v1.3 and v1.42 /2024 silversword411 Adding Webcompanion and Write-Debug #> From 068f67572cdb25d52e1ca8853dcc6c3439b1039f Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 9 May 2024 11:33:45 -0400 Subject: [PATCH 076/447] TRMM tasks --- scripts_wip/Win_TRMM_ScheduledTasks_List.ps1 | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 scripts_wip/Win_TRMM_ScheduledTasks_List.ps1 diff --git a/scripts_wip/Win_TRMM_ScheduledTasks_List.ps1 b/scripts_wip/Win_TRMM_ScheduledTasks_List.ps1 new file mode 100644 index 00000000..67f91289 --- /dev/null +++ b/scripts_wip/Win_TRMM_ScheduledTasks_List.ps1 @@ -0,0 +1,4 @@ +# List TRMM Scheduled tasks. Should match info in Tasks tab of TRMM admin + +(Get-ScheduledTask | Where-Object { $_.TaskName -like "Tac*" }).Count +Get-ScheduledTask -TaskName "Tac*" | Select-Object TaskName, Date | Format-table \ No newline at end of file From 43e8c88160e33549b5bcb3a93a7ebe7ff7f86b30 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Mon, 13 May 2024 13:45:28 -0400 Subject: [PATCH 077/447] Staged: Windows backup Monitor --- scripts_staging/Win_WindowsBackup_Monitor.ps1 | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 scripts_staging/Win_WindowsBackup_Monitor.ps1 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 From e114b3dc0b011bc2df5453cf33142dbcbbe05522 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 16 May 2024 13:01:18 -0400 Subject: [PATCH 078/447] Windows Network scanner via python v1.5 --- scripts_staging/Win_NetworkScanner.py | 110 ++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 scripts_staging/Win_NetworkScanner.py diff --git a/scripts_staging/Win_NetworkScanner.py b/scripts_staging/Win_NetworkScanner.py new file mode 100644 index 00000000..5c67ade4 --- /dev/null +++ b/scripts_staging/Win_NetworkScanner.py @@ -0,0 +1,110 @@ +#!/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. +v1.1 2/2024 silversword411 +v1.4 added open port checker +v1.5 5/2/2024 integrated reverse DNS lookup into the ping function with 1-second timeout + +TODO: Make subnet get automatically detected instead of assuming /24 +TODO: Compatible with Linux as well +""" + +import socket +import threading +import subprocess +import ipaddress +from collections import defaultdict +import argparse + +# Function to get the IP address of the primary network interface +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 + +# Function to ping an IP address, check if it is alive, and optionally perform a reverse DNS lookup +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) + 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)) + except Exception: + pass + +# Function to check for open ports +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 + +# Parse command-line arguments +def parse_arguments(): + parser = argparse.ArgumentParser(description="Scan network subnet for alive hosts, open ports, and optionally perform reverse DNS lookup.") + parser.add_argument("--hostname", help="Perform reverse DNS lookup", action="store_true") + return parser.parse_args() + +# Main function to detect the subnet and scan it +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() + + # Sort the alive hosts numerically + alive_hosts.sort(key=lambda x: x[0]) + + # Launch port checks + 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() + + print(f"Alive hosts in the subnet {subnet}:") + for host, hostname in alive_hosts: + ports = ', '.join(str(port) for port in open_ports[str(host)]) + print(f"IP: {host}, {hostname}, Open Ports: {ports}") + + print(f"\nTotal count of alive hosts: {len(alive_hosts)}") + +if __name__ == "__main__": + main() \ No newline at end of file From efca24c76f78684413f277f1a52970e98a8fb1c5 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 16 May 2024 13:03:06 -0400 Subject: [PATCH 079/447] Move TRMM tasks --- {scripts_wip => scripts_staging}/Win_TRMM_ScheduledTasks_List.ps1 | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {scripts_wip => scripts_staging}/Win_TRMM_ScheduledTasks_List.ps1 (100%) diff --git a/scripts_wip/Win_TRMM_ScheduledTasks_List.ps1 b/scripts_staging/Win_TRMM_ScheduledTasks_List.ps1 similarity index 100% rename from scripts_wip/Win_TRMM_ScheduledTasks_List.ps1 rename to scripts_staging/Win_TRMM_ScheduledTasks_List.ps1 From f338fabe0cb8ba8680a4f0b4b27c315498fe5f53 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 17 May 2024 13:43:48 -0400 Subject: [PATCH 080/447] New Community Script: Reboot --- community_scripts.json | 14 ++++++++++++++ scripts/Win_Reboot.ps1 | 28 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 scripts/Win_Reboot.ps1 diff --git a/community_scripts.json b/community_scripts.json index c856ce32..490359a9 100644 --- a/community_scripts.json +++ b/community_scripts.json @@ -1028,6 +1028,20 @@ ], "default_timeout": 30 }, + { + "guid": "5d905886-9eb1-4129-8b81-a013f842eb24", + "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": 30 + }, { "guid": "f396dae2-c768-45c5-bd6c-176e56ed3614", "filename": "Win_Power_RestartorShutdown.ps1", diff --git a/scripts/Win_Reboot.ps1 b/scripts/Win_Reboot.ps1 new file mode 100644 index 00000000..9f201c3c --- /dev/null +++ b/scripts/Win_Reboot.ps1 @@ -0,0 +1,28 @@ +<# +.SYNOPSIS + Restarts the computer with an optional wait time (in seconds) before restarting. + +.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 +#> + +param( + [int]$Wait +) + +if ($Wait) { + Restart-Computer -Force -Delay $Wait +} +else { + Restart-Computer -Force +} From c426a5a5b6387cb3d3f0899b51b591566b4ecb07 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 17 May 2024 13:58:08 -0400 Subject: [PATCH 081/447] derp...forgot a new guid --- community_scripts.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/community_scripts.json b/community_scripts.json index 490359a9..864b1a02 100644 --- a/community_scripts.json +++ b/community_scripts.json @@ -1029,7 +1029,7 @@ "default_timeout": 30 }, { - "guid": "5d905886-9eb1-4129-8b81-a013f842eb24", + "guid": "5bc815a0-d349-416f-8c3d-ac499d4da2e8", "filename": "Win_Reboot.ps1", "submittedBy": "https://github.com/silversword411", "name": "Reboot/Restart Computer", From 79af8f0bb500cd6b05685be5d16d62e4498e0434 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 23 May 2024 11:56:17 -0400 Subject: [PATCH 082/447] Fixing reboot script and timeouts --- community_scripts.json | 2 +- scripts/Win_Reboot.ps1 | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/community_scripts.json b/community_scripts.json index 864b1a02..af94a372 100644 --- a/community_scripts.json +++ b/community_scripts.json @@ -1040,7 +1040,7 @@ "supported_platforms": [ "windows" ], - "default_timeout": 30 + "default_timeout": 86400 }, { "guid": "f396dae2-c768-45c5-bd6c-176e56ed3614", diff --git a/scripts/Win_Reboot.ps1 b/scripts/Win_Reboot.ps1 index 9f201c3c..53ae638e 100644 --- a/scripts/Win_Reboot.ps1 +++ b/scripts/Win_Reboot.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS - Restarts the computer with an optional wait time (in seconds) before restarting. + Reboots/Restarts the computer with an optional wait time before restarting. Max wait 24hrs .DESCRIPTION This script restarts the computer forcefully. @@ -21,7 +21,8 @@ param( ) if ($Wait) { - Restart-Computer -Force -Delay $Wait + Sleep $Wait + Restart-Computer -Force } else { Restart-Computer -Force From 054a21475b162d3e9fd9dd9a5396656998e85798 Mon Sep 17 00:00:00 2001 From: redanthrax Date: Tue, 28 May 2024 12:41:54 -0700 Subject: [PATCH 083/447] fixed issue with account existing --- scripts_wip/Win_LocalAdmin_Manage.ps1 | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/scripts_wip/Win_LocalAdmin_Manage.ps1 b/scripts_wip/Win_LocalAdmin_Manage.ps1 index 486105c8..38f1dd0d 100644 --- a/scripts_wip/Win_LocalAdmin_Manage.ps1 +++ b/scripts_wip/Win_LocalAdmin_Manage.ps1 @@ -9,7 +9,7 @@ .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: @@ -63,11 +63,25 @@ function Win_LocalAdmin_Manage { Write-Output "$LocalAdminUser exists." 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) -and -Not([string]::IsNullOrWhiteSpace($adminMember))) { - if(Get-LocalUser | Where-Object { $_.Name -eq $adminMember }) { - Add-LocalGroupMember -Group "Users" -Member $adminMember - Remove-LocalGroupMember -Group Administrators -Member $adminMember + 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 } } } From 83b637b07ef6e6c0071478937729341f53207ccf Mon Sep 17 00:00:00 2001 From: silversword411 Date: Wed, 29 May 2024 09:44:10 -0400 Subject: [PATCH 084/447] Fix reboot script --- scripts/Win_Reboot.ps1 | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/Win_Reboot.ps1 b/scripts/Win_Reboot.ps1 index 53ae638e..6ea23c76 100644 --- a/scripts/Win_Reboot.ps1 +++ b/scripts/Win_Reboot.ps1 @@ -13,7 +13,7 @@ Waits for 60 seconds and then restarts the computer. .NOTES - v1.0 5/17/2024 Created by silversword411 + v1.0 5/17/2024 Created by silversword411 and dinger1986 #> param( @@ -21,8 +21,7 @@ param( ) if ($Wait) { - Sleep $Wait - Restart-Computer -Force + shutdown -r -t $Wait } else { Restart-Computer -Force From a42aaf1e5610671d6302af765db003ea9c3b8306 Mon Sep 17 00:00:00 2001 From: bbrendon Date: Sat, 1 Jun 2024 14:11:27 -0700 Subject: [PATCH 085/447] Update Win_Speedtest.ps1 --- scripts/Win_Speedtest.ps1 | 59 +++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/scripts/Win_Speedtest.ps1 b/scripts/Win_Speedtest.ps1 index 2eb02c23..03c41826 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,55 @@ $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://files.xlawgaming.com/10mb.bin", + "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 +68,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 } From 484743af4a0b4321d5077a5eb138f7c14c76f508 Mon Sep 17 00:00:00 2001 From: Dan <7434746+wh1te909@users.noreply.github.com> Date: Sat, 1 Jun 2024 19:48:47 -0700 Subject: [PATCH 086/447] remove dead url --- scripts/Win_Speedtest.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/Win_Speedtest.ps1 b/scripts/Win_Speedtest.ps1 index 03c41826..5717d069 100644 --- a/scripts/Win_Speedtest.ps1 +++ b/scripts/Win_Speedtest.ps1 @@ -7,7 +7,6 @@ $minuploadspeed = 4 # File to download you can find download links for other files here https://speedtest.flonix.net $downloadurls = @( - "https://files.xlawgaming.com/10mb.bin", "https://raw.githubusercontent.com/jamesward/play-load-tests/master/public/10mb.txt" ) From 76c58b27d91bc54cd2175299ccb3bfd03ff6a0e5 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 14 Jun 2024 15:00:23 -0400 Subject: [PATCH 087/447] Updating RunAsUser template --- scripts/Win_RunAsUser_Example.ps1 | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/scripts/Win_RunAsUser_Example.ps1 b/scripts/Win_RunAsUser_Example.ps1 index 5bbc6dd4..c1d95c60 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" @@ -27,28 +28,21 @@ If (!(Test-Path "c:\ProgramData\TacticalRMM\temp\")) { Write-Output "Hello from Systemland" -Invoke-AsCurrentUser -ScriptBlock { +Invoke-AsCurrentUser -CaptureOutput -ScriptBlock { # Put all Userland code here - $raulogPath = "c:\ProgramData\TacticalRMM\temp\raulog.txt" $exit1Path = "c:\ProgramData\TacticalRMM\temp\exit1.txt" - Write-Output "Hello from Userland" | Out-File -append -FilePath $raulogPath + 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 $raulogPath + 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 $raulogPath + 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 $exit1Path } } -# Get userland return info for Tactical Script History -$exitdata = Get-Content -Path "c:\ProgramData\TacticalRMM\temp\raulog.txt" -ErrorAction SilentlyContinue -Write-Output $exitdata -# Cleanup raulog.txt File -Remove-Item -Path "c:\ProgramData\TacticalRMM\temp\raulog.txt" -ErrorAction SilentlyContinue - # Checking for Userland Exit 1 If (Test-Path -Path "c:\ProgramData\TacticalRMM\temp\exit1.txt" -PathType Leaf) { Write-Output 'Return Exit 1 to Tactical from Userland' From 40ac6642e238d4012d3e9fdaa02bc5e13942d48c Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 14 Jun 2024 15:02:03 -0400 Subject: [PATCH 088/447] Rename MDM for keyword searching --- scripts_wip/{Win_WipeviaMDM.ps1 => Win_ResetviaMDM.ps1} | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) rename scripts_wip/{Win_WipeviaMDM.ps1 => Win_ResetviaMDM.ps1} (85%) diff --git a/scripts_wip/Win_WipeviaMDM.ps1 b/scripts_wip/Win_ResetviaMDM.ps1 similarity index 85% rename from scripts_wip/Win_WipeviaMDM.ps1 rename to scripts_wip/Win_ResetviaMDM.ps1 index 0f33953f..33c09909 100644 --- a/scripts_wip/Win_WipeviaMDM.ps1 +++ b/scripts_wip/Win_ResetviaMDM.ps1 @@ -1,4 +1,4 @@ -#Uses MDM features of windows to perform a Windows Reset clearing all data +# Uses MDM features of windows to perform a Windows Reset clearing all data $namespaceName = "root\cimv2\mdm\dmmap" $className = "MDM_RemoteWipe" @@ -10,12 +10,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 From c6a8e1be08a3fde1d6b3ba56914e7081aacd7d2e Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 20 Jun 2024 16:25:37 -0400 Subject: [PATCH 089/447] WIP: Adding urbackup scripts --- scripts_wip/Win_3rdparty_Urbackup_Monitor.ps1 | 110 ++++++++++++++++++ .../Win_3rdparty_Urbackup_Uninstall.bat | 1 + 2 files changed, 111 insertions(+) create mode 100644 scripts_wip/Win_3rdparty_Urbackup_Monitor.ps1 create mode 100644 scripts_wip/Win_3rdparty_Urbackup_Uninstall.bat 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 From b81e7509ee6dddc2f25a28f2cd1ef93d9003aa95 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Wed, 17 Jul 2024 07:02:39 -0400 Subject: [PATCH 090/447] chore: Refactor Win_RunAsUser_Example.ps1 to simplify CaptureOutput --- scripts/Win_RunAsUser_Example.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/Win_RunAsUser_Example.ps1 b/scripts/Win_RunAsUser_Example.ps1 index c1d95c60..1daea9b7 100644 --- a/scripts/Win_RunAsUser_Example.ps1 +++ b/scripts/Win_RunAsUser_Example.ps1 @@ -28,7 +28,7 @@ If (!(Test-Path "c:\ProgramData\TacticalRMM\temp\")) { Write-Output "Hello from Systemland" -Invoke-AsCurrentUser -CaptureOutput -ScriptBlock { +Invoke-AsCurrentUser -ScriptBlock { # Put all Userland code here $exit1Path = "c:\ProgramData\TacticalRMM\temp\exit1.txt" @@ -41,7 +41,7 @@ Invoke-AsCurrentUser -CaptureOutput -ScriptBlock { # Writing exit1.txt for Userland Exit 1 passing to Systemland for returning to Tactical Write-Output "Exit 1" | Out-File -append -FilePath $exit1Path } -} +} -CaptureOutput # Checking for Userland Exit 1 If (Test-Path -Path "c:\ProgramData\TacticalRMM\temp\exit1.txt" -PathType Leaf) { From fe843cd6b1a570a62c1f8d9bacdad02401a896ae Mon Sep 17 00:00:00 2001 From: silversword411 Date: Wed, 17 Jul 2024 11:25:03 -0400 Subject: [PATCH 091/447] WIP: Add Dell RAID monitoring script --- scripts_wip/Win_Dell_RAIDmonitor.ps1 | 109 +++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 scripts_wip/Win_Dell_RAIDmonitor.ps1 diff --git a/scripts_wip/Win_Dell_RAIDmonitor.ps1 b/scripts_wip/Win_Dell_RAIDmonitor.ps1 new file mode 100644 index 00000000..b2f031c0 --- /dev/null +++ b/scripts_wip/Win_Dell_RAIDmonitor.ps1 @@ -0,0 +1,109 @@ +<# +.SYNOPSIS + Check Dell 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 From f287fbda6cdd918535be2bf00f34ab5390df775a Mon Sep 17 00:00:00 2001 From: silversword411 Date: Wed, 17 Jul 2024 11:43:46 -0400 Subject: [PATCH 092/447] WIP: ASUS debloater --- scripts_wip/Win_ASUS_debloater.ps1 | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 scripts_wip/Win_ASUS_debloater.ps1 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 From cb39181a0b50937f3bfe9def4662e45cff28e41d Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 18 Jul 2024 15:22:24 -0400 Subject: [PATCH 093/447] chore: Refactor add folder creation function --- scripts_staging/Win_Template.ps1 | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) 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 From c97842f2685b987230e68fd7057f0741868c1433 Mon Sep 17 00:00:00 2001 From: Spam Me <295566+styx-tdo@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:28:35 +0200 Subject: [PATCH 094/447] Fix variable name certfileMY Add check for friendly name, use Subject if not present --- scripts/Win_IIS_Check_SSL_Certs.ps1 | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) 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 +} From 9ff865aaac32206067b4425143df6501941b1e5b Mon Sep 17 00:00:00 2001 From: silversword411 Date: Mon, 12 Aug 2024 12:50:41 -0400 Subject: [PATCH 095/447] chore: Refactor Win_TRMM_ScheduledTasks_List.ps1 to improve task information retrieval --- .../Win_TRMM_ScheduledTasks_List.ps1 | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/scripts_staging/Win_TRMM_ScheduledTasks_List.ps1 b/scripts_staging/Win_TRMM_ScheduledTasks_List.ps1 index 67f91289..2a203924 100644 --- a/scripts_staging/Win_TRMM_ScheduledTasks_List.ps1 +++ b/scripts_staging/Win_TRMM_ScheduledTasks_List.ps1 @@ -1,4 +1,22 @@ -# List TRMM Scheduled tasks. Should match info in Tasks tab of TRMM admin +<# -(Get-ScheduledTask | Where-Object { $_.TaskName -like "Tac*" }).Count -Get-ScheduledTask -TaskName "Tac*" | Select-Object TaskName, Date | Format-table \ No newline at end of file +.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 From 513a529e85788f11866b84ff6f5d98a1eb245615 Mon Sep 17 00:00:00 2001 From: Charlie Powell Date: Thu, 15 Aug 2024 00:46:22 -0400 Subject: [PATCH 096/447] Proposed work for amidaware/community-scripts#245 Just add the env and run_as_user keys to the schema. --- community_scripts.schema.json | 11 +++++++++++ 1 file changed, 11 insertions(+) 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" From e44c7a74f4c3bb6879b21638a0fbad40e3277f99 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 22 Aug 2024 08:18:14 -0400 Subject: [PATCH 097/447] add WIP: TRMM agent installer and lock/unlock scripts. Thx CBG_ITSUP --- .../Win_TRMM_Agent_Installer_and_Locker.ps1 | 81 +++++++++++++++++++ scripts_wip/Win_TRMM_Agent_Locker.ps1 | 37 +++++++++ scripts_wip/Win_TRMM_Agent_unLocker.ps1 | 59 ++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 scripts_wip/Win_TRMM_Agent_Installer_and_Locker.ps1 create mode 100644 scripts_wip/Win_TRMM_Agent_Locker.ps1 create mode 100644 scripts_wip/Win_TRMM_Agent_unLocker.ps1 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 From 9e193e8a54a0263cba321bde8ebedb5cd30424e6 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 23 Aug 2024 16:24:32 -0400 Subject: [PATCH 098/447] chore: Refactor Win_Wifi_SSID_and_Password_Retrieval.ps1 to add autoconnect column --- .../Win_Wifi_SSID_and_Password_Retrieval.ps1 | 33 +++++++++++++++++-- ...twork_Wifi_SSID_and_Password_Retrieval.ps1 | 7 ---- 2 files changed, 30 insertions(+), 10 deletions(-) delete mode 100644 scripts_wip/DUPE_Win_Network_Wifi_SSID_and_Password_Retrieval.ps1 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_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 From 98fa5b52619a475b13d32c4107d92b90d0da8c3a Mon Sep 17 00:00:00 2001 From: silversword411 Date: Wed, 28 Aug 2024 11:26:24 -0400 Subject: [PATCH 099/447] add WIP: Add Win_Screenconnect_Detectothers.ps1 script for detecting other remote access systems --- .../Win_Screenconnect_Detectothers.ps1 | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 scripts_wip/Win_Screenconnect_Detectothers.ps1 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 From 2f15cf518a327e381a006dc5104f62d39e5c4cf2 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 29 Aug 2024 09:51:26 -0400 Subject: [PATCH 100/447] WIP add: Win_Reboot_usingIdleandUptime.ps1 script for rebooting device --- scripts_wip/Win_Reboot_usingIdleandUptime.ps1 | 351 ++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 scripts_wip/Win_Reboot_usingIdleandUptime.ps1 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 From 930c8337bebf939b3366926b31fc7733d8dfa749 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 29 Aug 2024 09:58:00 -0400 Subject: [PATCH 101/447] WIP: Add reboot via toast requests --- scripts_wip/Win_Reboot_Request_via_toast.ps1 | 58 +++++++++++++++++++ .../Win_Reboot_Request_via_toast_andforce.ps1 | 57 ++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 scripts_wip/Win_Reboot_Request_via_toast.ps1 create mode 100644 scripts_wip/Win_Reboot_Request_via_toast_andforce.ps1 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 From b0e632d331ae68c7c96af970b8b8c479ea36bc2a Mon Sep 17 00:00:00 2001 From: silversword411 Date: Tue, 3 Sep 2024 09:05:59 -0400 Subject: [PATCH 102/447] Dell PERC add keyword --- scripts_wip/Win_Dell_RAIDmonitor.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts_wip/Win_Dell_RAIDmonitor.ps1 b/scripts_wip/Win_Dell_RAIDmonitor.ps1 index b2f031c0..5e1e2601 100644 --- a/scripts_wip/Win_Dell_RAIDmonitor.ps1 +++ b/scripts_wip/Win_Dell_RAIDmonitor.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS - Check Dell RAID status using OpenManage command-line interface (OMSA). + 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. From dd29b84cfae6460ddb7540e563ab2dfdfa58cddd Mon Sep 17 00:00:00 2001 From: silversword411 Date: Wed, 11 Sep 2024 18:01:48 -0400 Subject: [PATCH 103/447] Staging: Add script to enable exclusions for specific applications and processes in Windows Defender. Thx @dinger1986 for giving us a starting point --- .../Win_Defender_Enable_Exclusions.ps1 | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 scripts_staging/Win_Defender_Enable_Exclusions.ps1 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 From 3c50009c2cb4867213da5fc24784cc8180820979 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Wed, 11 Sep 2024 18:10:10 -0400 Subject: [PATCH 104/447] PROD Update: Refactor choco bulk to add support for listing upgradeable packages --- scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 | 25 ++++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 b/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 index 22955472..27df7b40 100644 --- a/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 +++ b/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 @@ -12,7 +12,8 @@ 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 @@ -37,6 +38,7 @@ 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 ( @@ -47,28 +49,28 @@ param ( [string[]] $PackageName, [Parameter(Mandatory = $false)] - [ValidateSet("install", "uninstall", "upgrade", "upgrade-only-installed", "list")] + [ValidateSet("install", "uninstall", "upgrade", "upgrade-only-installed", "list", "list-upgradeable")] [string] $Mode = "install" ) $chocoExePath = "$env:PROGRAMDATA\chocolatey\choco.exe" if (-not (Test-Path $chocoExePath)) { - Write-Output "Chocolatey is not installed." + Write-Host "Chocolatey is not installed." Exit 1 } $ErrorCount = 0 -if ($Mode -ne "upgrade" -and $Mode -ne "upgrade-only-installed" -and $Mode -ne "list" -and -not $PackageName) { - Write-Output "Error: No package name provided. Please specify a package name, e.g., `-PackageName googlechrome`." +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 } # 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-Output "Sleeping $randDelay seconds" +Write-Host "Sleeping $randDelay seconds" Start-Sleep -Seconds $randDelay switch ($Mode) { @@ -76,6 +78,7 @@ switch ($Mode) { if ($PackageName) { foreach ($package in $PackageName) { & $chocoExePath install $package -y --no-progress + if ($LASTEXITCODE -ne 0) { $ErrorCount++ } } } } @@ -83,6 +86,7 @@ switch ($Mode) { if ($PackageName) { foreach ($package in $PackageName) { & $chocoExePath uninstall $package -y + if ($LASTEXITCODE -ne 0) { $ErrorCount++ } } } } @@ -90,6 +94,7 @@ switch ($Mode) { if ($PackageName) { foreach ($package in $PackageName) { & $chocoExePath upgrade $package -y --no-progress + if ($LASTEXITCODE -ne 0) { $ErrorCount++ } } } else { @@ -100,6 +105,7 @@ switch ($Mode) { if ($PackageName) { foreach ($package in $PackageName) { & $chocoExePath upgrade $package --failonnotinstalled -y + if ($LASTEXITCODE -ne 0) { $ErrorCount++ } } } else { @@ -109,6 +115,13 @@ switch ($Mode) { "list" { & $chocoExePath list } + "list-upgradeable" { + & $chocoExePath outdated + } +} + +if ($ErrorCount -gt 0) { + Write-Host "$ErrorCount errors occurred during the operation." } Exit 0 From cf5f18f09c1ecba11965d97a191872c0f354b1fc Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 15 Nov 2024 14:40:27 -0500 Subject: [PATCH 105/447] Staging: Windows Agent Troubleshooting script --- scripts_staging/Win_Troubleshooting_Agent.ps1 | 458 ++++++++++++++++++ 1 file changed, 458 insertions(+) create mode 100644 scripts_staging/Win_Troubleshooting_Agent.ps1 diff --git a/scripts_staging/Win_Troubleshooting_Agent.ps1 b/scripts_staging/Win_Troubleshooting_Agent.ps1 new file mode 100644 index 00000000..d889e78b --- /dev/null +++ b/scripts_staging/Win_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 From 7927b0422f37d93b52c0c51f853f3e9991ebd85a Mon Sep 17 00:00:00 2001 From: silversword411 Date: Sat, 23 Nov 2024 07:04:28 -0500 Subject: [PATCH 106/447] Staging: Rename troubleshooting script --- ...oubleshooting_Agent.ps1 => Win_TRMM_Troubleshooting_Agent.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scripts_staging/{Win_Troubleshooting_Agent.ps1 => Win_TRMM_Troubleshooting_Agent.ps1} (100%) diff --git a/scripts_staging/Win_Troubleshooting_Agent.ps1 b/scripts_staging/Win_TRMM_Troubleshooting_Agent.ps1 similarity index 100% rename from scripts_staging/Win_Troubleshooting_Agent.ps1 rename to scripts_staging/Win_TRMM_Troubleshooting_Agent.ps1 From e3757cb2293d37779e0acb82cf3a2a01e3e396db Mon Sep 17 00:00:00 2001 From: silversword411 Date: Sat, 23 Nov 2024 07:12:59 -0500 Subject: [PATCH 107/447] wip: DISM and SFC checker and fixer --- scripts_wip/Win_DISM_SFC_CheckandFix.ps1 | 74 ++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 scripts_wip/Win_DISM_SFC_CheckandFix.ps1 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.") +} From ed3fe182bca15ed14fea7056f2ea66441397a191 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Sat, 23 Nov 2024 07:34:14 -0500 Subject: [PATCH 108/447] wip: Add Disk Speed Multitest script with cross-platform support --- scripts_wip/Disk_Speedmultitest.py | 187 +++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 scripts_wip/Disk_Speedmultitest.py 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() From 3dfffa38a912e6cf07da5d559784a76993b8e93a Mon Sep 17 00:00:00 2001 From: silversword411 Date: Sat, 23 Nov 2024 07:38:37 -0500 Subject: [PATCH 109/447] wip: Add script to retrieve cellular data using WMI --- scripts_wip/Win_Celldata.ps1 | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 scripts_wip/Win_Celldata.ps1 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 From 13e8ad3b5c55095b5e7905940006fb7e2fe9519e Mon Sep 17 00:00:00 2001 From: silversword411 Date: Sat, 23 Nov 2024 08:34:34 -0500 Subject: [PATCH 110/447] refactor: Replace PowerShell login audit script with Python version --- scripts_wip/Win_Login_Audit.ps1 | 71 ------------ scripts_wip/Win_Login_Auditv2.py | 178 +++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 71 deletions(-) delete mode 100644 scripts_wip/Win_Login_Audit.ps1 create mode 100644 scripts_wip/Win_Login_Auditv2.py 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() From 9be69b5a5d6c2500096cb85f09ba841715771444 Mon Sep 17 00:00:00 2001 From: dinger1986 Date: Mon, 25 Nov 2024 17:49:02 +0000 Subject: [PATCH 111/447] Update Win_Bluescreen_Report.ps1 --- scripts/Win_Bluescreen_Report.ps1 | 37 ++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 13 deletions(-) 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 } - From 39a72204e519976d020f5013e5bf035344e58241 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Fri, 29 Nov 2024 13:11:07 -0500 Subject: [PATCH 112/447] Staging: New PowerShell script to check and manage service statuses --- .../Win_Services_CheckforProblems.ps1 | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 scripts_staging/Win_Services_CheckforProblems.ps1 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 +} From 3a387383f804845b9c8c0ce89df86fc09e013a66 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:02:53 +0000 Subject: [PATCH 113/447] Update ./scripts/Backend/Uptime Kuma Monitoring For Tactical.py --- .../Uptime Kuma Monitoring For Tactical.py | 542 ++++++++++++++++++ 1 file changed, 542 insertions(+) create mode 100644 scripts_staging/Backend/Uptime Kuma Monitoring For Tactical.py 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 From ba7bbc1f56b6610ecdd29cf3ef23f129e199bfd1 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:02:55 +0000 Subject: [PATCH 114/447] Update ./scripts/Backend/Export TRMM Scripts to folder and git sync V2.py --- ... TRMM Scripts to folder and git sync V2.py | 456 ++++++++++++++++++ 1 file changed, 456 insertions(+) create mode 100644 scripts_staging/Backend/Export TRMM Scripts to folder and git sync V2.py diff --git a/scripts_staging/Backend/Export TRMM Scripts to folder and git sync V2.py b/scripts_staging/Backend/Export TRMM Scripts to folder and git sync V2.py new file mode 100644 index 00000000..0b073695 --- /dev/null +++ b/scripts_staging/Backend/Export TRMM Scripts to folder and git sync V2.py @@ -0,0 +1,456 @@ +#!/usr/bin/python3 +#public + +""" +.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. + Each part can be toggled with its own flag to help troubleshoot any issue. + 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 + +.WORKFLOW + 0. The mapped folder should already be configured with git + + 1. Pull all the modifications from the git repo 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 scripts out to 4 folders: + scripts: extracted script code from the API converted from json + scriptsraw: All json data from the API for later processing, currently used for hash comparison + snippets: extracted snippet code from the API converted from json + snippetsraw: All json data for later import/migration + + 4. Push all the modifications to the git repo configured for the folder via git commands + If there are no changes, no commit will be made. + +.EXEMPLE +DOMAIN=https://api-rmm +API_TOKEN={{global.rmm_key_for_git_script}} +SCRIPTPATH=/var/RMM-script-repo + +.CHANGELOG + v5.0 Y Exports functional, adds script ID to from as "id - " + v5.a Y "id - " for only raw folder. Fixed to use X-API-KEY + v5.1 Y Sanitizing script names when has / in it + v5.2 Y moving url and api token to .env file + v5.3 Y Making script folders be subfolders of where export.py file is + v5.4 Y making filenames utf-8 compliant + v5.5 7/11/2024 X Save PowerShell scripts with .ps1 and Python scripts with .py extensions + v5.6 7/11/2024 X Count the total number of scripts and print at the end + v5.7 7/11/2024 X Print a summary of all the different types of shells exported + v5.8 7/11/2024 X Add support for additional shell extension types + v5.9 7/11/2024 X Detect deleted scripts and delete them from both folders + v6 7/31/2024 SAN Add support for specifying the save folder via the SCRIPTPATH environment variable + v6.0.1 7/31/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 + + +.TODO + Add reporting support + add writeback for snippets + simplify the functions that does the writeback + Move raws from "scriptsraw" to scripts/subfolder/raws/ to group them with their scripts + add debug statements and debug flags + find edge-cases and add exit code for them + add logging + add counters and separators at the end of each function + investigate if the lines returns in the code causes issues in some case (theoretical issue) + send workflow flags to ENV default to true + make the commit message to be dynamic ex. "modified xxx.ps1, xxx.py" + +""" + +import subprocess +import sys +import os +import hashlib +import json +from collections import defaultdict +from pathlib import Path +import requests +from pathvalidate import sanitize_filename +import re + +# Toggle flags +ENABLE_GIT_PULL = True +ENABLE_GIT_PUSH = True +ENABLE_WRITEBACK = True +ENABLE_WRITETOFILE = True + +def delete_obsolete_files(folder, current_scripts): + print(f"Deleting obsolete files and directories in {folder}...") + all_files = set() + relevant_dirs = set() + + for item in folder.rglob('*'): + if item.is_file(): + all_files.add(item.relative_to(folder)) + elif item.is_dir() and any(item.glob('*')): + relevant_dirs.add(item.relative_to(folder)) + + obsolete_files = all_files - current_scripts + for item in folder.rglob('*'): + if item.is_file() and item.relative_to(folder) in obsolete_files: + try: + print(f"Deleting obsolete file: {item}") + item.unlink() + except Exception as e: + print(f"Error deleting file {item}: {e}") + + for dirpath in sorted(folder.rglob('*'), key=lambda p: len(p.parts), reverse=True): + if dirpath.is_dir() and not any(dirpath.glob('*')) and dirpath.relative_to(folder) not in relevant_dirs: + try: + dirpath.rmdir() + print(f"Deleting empty obsolete directory: {dirpath}") + except OSError as e: + print(f"Could not delete directory {dirpath}: {e}") + +def process_scripts(scripts, script_folder, script_raw_folder, shell_summary, is_snippet=False): + print(f"Processing {'snippets' if is_snippet else 'user-defined scripts'}...") + current_scripts = set() + + for script in scripts: + script_id = script.get('id') + script_name = sanitize_filename(script.get('name', 'Unnamed Script')) + category = script.get('category', '').strip() if script.get('category') else '' + category = sanitize_filename(category) + category_folder = script_folder / category if category else script_folder + category_raw_folder = script_raw_folder / category if category else script_raw_folder + + category_folder.mkdir(parents=True, exist_ok=True) + category_raw_folder.mkdir(parents=True, exist_ok=True) + + if not is_snippet: + download_url = f"{domain}/scripts/{script_id}/download/?with_snippets=false" + script_data = fetch_data(download_url, headers) + else: + script_data = script + + if script_data: + code = script_data.get('code') + shell = script.get('shell') + extension = { + 'powershell': '.ps1', + 'python': '.py', + 'cmd': '.bat', + 'shell': '.sh', + 'nushell': '.nu' + }.get(shell, '.txt') + + if not is_snippet: + shell_summary[shell] += 1 + + script_filename = f"{script_name}{extension}" + script_file_path = category_folder / script_filename + save_file(script_file_path, code) + + raw_filename = f"{script_id} - {script_name}.json" + raw_file_path = category_raw_folder / raw_filename + save_file(raw_file_path, {**script_data, **script}, is_json=True) + + current_scripts.add(script_file_path.relative_to(script_folder)) + current_scripts.add(raw_file_path.relative_to(script_raw_folder)) + + print(f"Processed {len(current_scripts)} {'snippets' if is_snippet else 'scripts'}.") + return current_scripts + + +def compute_hash(file_path): + """Compute SHA-256 hash of a file.""" + hash_sha256 = hashlib.sha256() + try: + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_sha256.update(chunk) + except FileNotFoundError: + return None + return hash_sha256.hexdigest() + +def save_file(path, content, is_json=False): + """Save the file unconditionally.""" + new_content = json.dumps(content, indent=4, ensure_ascii=False) if is_json else content + + if ENABLE_WRITETOFILE: + with open(path, 'w', encoding="utf-8") as file: + file.write(new_content) + print(f"File saved: {path}") + else: + print(f"File would be saved (simulation): {path}") + + +def fetch_data(url, headers): + print(f"Fetching data from {url}...") + response = requests.get(url, headers=headers) + if response.status_code == 200: + print(f"Data fetched successfully from {url}.") + return response.json() + else: + print(f"Error fetching data from {url}: {response.status_code}") + return [] + + +def compare_script_and_json(folders): + """Compare script files with their corresponding JSON files and return mismatches.""" + print("Comparing script files with JSON files...") + + mismatches = [] + existing_files = defaultdict(dict) + + for raw_file_path in folders['scriptsraw'].rglob('*.json'): + raw_filename = raw_file_path.stem # Get the filename without extension + raw_name_cleaned = re.sub(r'^\d+ - ', '', raw_filename).lower() # Clean filename + + matched_script_path = None + for script_file_path in folders['scripts'].rglob('*'): + if script_file_path.is_file(): + script_filename = script_file_path.stem.lower() + if script_filename == raw_name_cleaned: + matched_script_path = script_file_path + break + + if matched_script_path: + print(f"Matched script file: {matched_script_path} with raw file: {raw_file_path}") + + script_hash = compute_hash(matched_script_path) + + with open(raw_file_path, 'r', encoding='utf-8') as json_file: + raw_data = json.load(json_file) + json_script_content = raw_data.get('code', '') + + # Compare the hashes of the actual script content + json_script_hash = hashlib.sha256(json_script_content.encode('utf-8')).hexdigest() + + print(f"Script file hash: {script_hash}") + print(f"JSON 'code' field hash: {json_script_hash}") + + if script_hash != json_script_hash: + print("\n--- Script File Content (first 10 lines) ---") + with open(matched_script_path, 'r', encoding='utf-8') as script_file: + for i, line in enumerate(script_file): + if i < 10: + print(line.strip()) + else: + break + + print("\n--- JSON 'Code' Field Content (first 10 lines) ---") + json_lines = json_script_content.splitlines() + for i, line in enumerate(json_lines): + if i < 10: + print(line.strip()) + else: + break + + mismatches.append({ + 'script_path': matched_script_path, + 'raw_path': raw_file_path, + 'script_hash': script_hash, + 'json_script_hash': json_script_hash + }) + + existing_files[matched_script_path.relative_to(folders['scripts'])] = { + 'script_path': matched_script_path, + 'raw_path': raw_file_path, + 'script_hash': script_hash, + 'json_script_hash': json_script_hash + } + else: + print(f"No matching script file found for JSON: {raw_file_path}") + + return mismatches + +def write_modifications_to_api(base_dir, folders, api_token): + """Main function to compare files and send data to the API.""" + mismatches = compare_script_and_json(folders) + send_mismatched_data_to_api(mismatches, api_token) + + +def update_api(script_id, payload, api_token): + # Convert 'code' to 'script_body' + if 'code' in payload: + payload['script_body'] = payload.pop('code') + + url = f"{domain}/scripts/{script_id}/" + headers = { + 'X-API-KEY': api_token, + 'Content-Type': 'application/json', + 'Accept': 'application/json, text/plain, */*' + } + + # Log the script body length and truncated content + script_body_length = len(payload.get('script_body', '')) + truncated_body = (payload['script_body'][:1000] + '...') if script_body_length > 1000 else payload['script_body'] + print(f"Updating script {script_id}, length: {script_body_length}, payload: {json.dumps({**payload, 'script_body': truncated_body}, indent=2)}") + + # Make the request with a longer timeout + try: + response = requests.put(url, headers=headers, json=payload, timeout=120) + except requests.exceptions.RequestException as e: + print(f"Request failed: {e}") + return + + # Print response for debugging + print(f"Response status code: {response.status_code}, Response content: {response.text}") + + # Check response status + if response.status_code == 200: + print(f"Script {script_id} updated successfully.") + elif response.status_code == 401: + print(f"Failed to update script {script_id}: 401 Unauthorized. Check API token.") + elif response.status_code == 404: + print(f"Failed to update script {script_id}: 404 Not Found. The resource may not exist.") + else: + print(f"Failed to update script {script_id}: {response.status_code} {response.text}") + + + + +def send_mismatched_data_to_api(mismatches, api_token): + """Send mismatched script data to the API.""" + for mismatch in mismatches: + script_path = mismatch.get('script_path') + raw_path = mismatch.get('raw_path') + + with open(raw_path, 'r', encoding='utf-8') as f: + raw_data = json.load(f) + + updated_payload = {**raw_data, 'code': open(script_path, 'r').read()} + + # Convert 'code' to 'script_body' before updating the API + try: + if ENABLE_WRITEBACK: + print(f"Updating API with payload for {script_path}:") + # Call the update function with api_token + update_api(raw_data.get('id'), updated_payload, api_token) + else: + print(f"Payload that would be pushed for {script_path}:") + # Preview the payload with 'script_body' instead of 'code' + updated_payload['script_body'] = updated_payload.pop('code') + print(json.dumps(updated_payload, indent=4)) + sys.stdout.flush() # Explicitly flush stdout + except BrokenPipeError: + sys.stderr.close() + sys.stdout.close() + + +def git_pull(base_dir): + """Force pull the latest changes from the git repository, discarding local changes.""" + if ENABLE_GIT_PULL: + print("Starting force pull...") + try: + subprocess.check_call(['git', '-C', base_dir, 'fetch', 'origin']) + subprocess.check_call(['git', '-C', base_dir, 'reset', '--hard', 'origin/master']) + print("Successfully force-pulled the latest changes from the repository.") + except subprocess.CalledProcessError as e: + print(f"Failed to force-pull changes from Git: {e}") + sys.exit(1) + else: + print("Git pull is disabled.") + + +def git_push(base_dir): + """Push local changes to the git repository.""" + if ENABLE_GIT_PUSH: + print("Starting git push...") + 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("Rebase in progress. Please complete or abort the rebase manually.") + sys.exit(1) + + branch_result = subprocess.run(['git', '-C', base_dir, 'rev-parse', '--abbrev-ref', 'HEAD'], + capture_output=True, text=True) + branch_name = branch_result.stdout.strip() + + if branch_name == 'HEAD': + branch_name = "update-scripts" + subprocess.check_call(['git', '-C', base_dir, 'checkout', '-b', branch_name]) + print(f"Switched to new branch '{branch_name}'") + + 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', '.']) + subprocess.check_call(['git', '-C', base_dir, 'commit', '-m', 'Update scripts and raw data']) + print(f"Committed changes to branch '{branch_name}'") + else: + print("No changes to commit.") + + subprocess.check_call(['git', '-C', base_dir, 'push', 'origin', branch_name]) + print(f"Changes pushed to branch '{branch_name}'") + except subprocess.CalledProcessError as e: + print(f"Git operation failed: {e}") + else: + print("Git push is disabled.") + +def download_scripts(): + global domain, headers + + domain = os.getenv('DOMAIN') + api_token = os.getenv('API_TOKEN') + scriptpath = os.getenv('SCRIPTPATH') + + if not domain or not api_token or not scriptpath: + print("Error: DOMAIN, API_TOKEN, and SCRIPTPATH must be set in the environment.") + sys.exit(1) + + headers = {"X-API-KEY": api_token} + base_dir = Path(scriptpath).resolve() + folders = { + "scripts": base_dir / "scripts", + "scriptsraw": base_dir / "scriptsraw", + "snippets": base_dir / "snippets", + "snippetsraw": base_dir / "snippetsraw" + } + for folder in folders.values(): + folder.mkdir(parents=True, exist_ok=True) + + shell_summary = defaultdict(int) + current_scripts = set() + + if ENABLE_GIT_PULL: + git_pull(base_dir) + + write_modifications_to_api(base_dir, folders, api_token) + + print("Fetching user-defined scripts...") + user_defined_scripts = fetch_data(f"{domain}/scripts/?showHiddenScripts=true", headers) + 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)) + + print("Fetching snippets...") + snippets = fetch_data(f"{domain}/scripts/snippets/", headers) + current_scripts.update(process_scripts(snippets, folders['snippets'], folders['snippetsraw'], shell_summary, is_snippet=True)) + + for folder in folders.values(): + delete_obsolete_files(folder, current_scripts) + + if ENABLE_GIT_PUSH: + git_push(base_dir) + + print(f"Total number of scripts exported: {len(current_scripts)}") + print("Shell summary:") + for shell, count in shell_summary.items(): + print(f"{shell}: {count}") + + + +if __name__ == "__main__": + download_scripts() \ No newline at end of file From 6f9fbc5ed1dc8a8fa8dc427d37205f67fc7c24b6 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:02:58 +0000 Subject: [PATCH 115/447] Update ./scripts/Backend/Repo package updater.py --- .../Backend/Repo package updater.py | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 scripts_staging/Backend/Repo package updater.py diff --git a/scripts_staging/Backend/Repo package updater.py b/scripts_staging/Backend/Repo package updater.py new file mode 100644 index 00000000..5c25c91b --- /dev/null +++ b/scripts_staging/Backend/Repo package updater.py @@ -0,0 +1,178 @@ +#!/usr/bin/python3 +#public +import os +import requests +import subprocess +import re +import time + +""" +.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/ + +.NOTE + Author: SAN + +.TODO + Move packages to env + +""" + +# List of package names to download +package_names = [ + "Chocolatey", + "chocolatey-compatibility.extension", + "chocolatey-core.extension", + "chocolatey-windowsupdate.extension", + "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 variables from environment +outdir = os.getenv("CHOCOLATEY_OUTDIR") +local_choco_server = os.getenv("CHOCOLATEY_LOCAL_SERVER") +api_key = os.getenv("CHOCOLATEY_API_KEY") +base_url = os.getenv("CHOCOLATEY_BASE_URL", "https://community.chocolatey.org/api/v2/package/") + +# Check if all required environment variables are set +if not outdir: + print("Error: CHOCOLATEY_OUTDIR environment variable is not set.") + exit(1) +if not local_choco_server: + print("Error: CHOCOLATEY_LOCAL_SERVER environment variable is not set.") + exit(1) +if not api_key: + print("Error: CHOCOLATEY_API_KEY environment variable is not set.") + exit(1) + +# Ensure the output directory exists, if not, create it +if not os.path.exists(outdir): + print(f"Output directory '{outdir}' does not exist. Exiting.") + exit(1) + +# Variable to track if any failure occurred +error_occurred = False + +# Iterate through each package name +for package_name in package_names: + # Wait before downloading the next package + time.sleep(10) + + # Construct the full URL for the package + package_url = base_url + package_name + + # Retry logic for downloading the package + for attempt in range(3): + try: + # Send a GET request to download the package + response = requests.get(package_url) + + # Check if the request was successful + if response.status_code == 200: + # Get the default filename from the response headers + default_filename = os.path.basename(response.url) + filepath = os.path.join(outdir, default_filename) + + # Extract version from filename + version_match = re.search(r"(\d+\.\d+(\.\d+)?)", default_filename) + if version_match: + version = version_match.group(1) + + # Debug: Print filename and version + print(f"Package '{package_name}': Filename = {default_filename}, Version = {version}") + + # TODO: Query the local Chocolatey server to check if this package version already exists + # If version already exists on the server, skip downloading + if any(version in filename for filename in os.listdir(outdir)): + print(f"Package '{package_name}' with version {version} already exists in the directory. Skipping.") + break + + # Save the package to a file in the specified directory using the default filename + with open(filepath, "wb") as file: + file.write(response.content) + print(f"Package '{package_name}' downloaded successfully.") + + # Push the package to the local Chocolatey server + push_command = f"choco push \"{filepath}\" --source={local_choco_server} --api-key='{api_key}' --force" + subprocess.run(push_command, shell=True, check=True) + print(f"Package '{package_name}' pushed to the local Chocolatey server.") + break + + else: + print(f"Failed to download package '{package_name}'. Status code: {response.status_code}") + + except (requests.exceptions.RequestException, subprocess.CalledProcessError) as e: + print(f"An error occurred while processing package '{package_name}': {str(e)}") + + # Retry after 1 minute if an error occurred and this is not the final attempt + if attempt < 2: + print(f"Retrying download for '{package_name}' in 1 minute... (Attempt {attempt + 2}/3)") + time.sleep(60) + + # If all 3 attempts failed, mark error_occurred as True + else: + print(f"Failed to process package '{package_name}' after 3 attempts.") + error_occurred = True + +# Exit with status code 1 if any error occurred +if error_occurred: + exit(1) \ No newline at end of file From 7b61a77d9d067fc756e9a22053dfcade5f6ba378 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:00 +0000 Subject: [PATCH 116/447] Update ./scripts/Build/Forward HTTP Traffic To Company Website.ps1 --- ...orward HTTP Traffic To Company Website.ps1 | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 scripts_staging/Build/Forward HTTP Traffic To Company Website.ps1 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..9a6f6c2d --- /dev/null +++ b/scripts_staging/Build/Forward HTTP Traffic To Company Website.ps1 @@ -0,0 +1,70 @@ +<# +.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. + +.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." From a206a6623428e464dfd7f1121a6aad1ae16e3b83 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:02 +0000 Subject: [PATCH 117/447] Update ./scripts/Build/Change NTP target to company.ps1 --- .../Build/Change NTP target to company.ps1 | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 scripts_staging/Build/Change NTP target to company.ps1 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 From 3a72945e14bc6dfcb8232defb15fbde851848d02 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:05 +0000 Subject: [PATCH 118/447] Update ./scripts/Build/Update TRMM agent.ps1 --- scripts_staging/Build/Update TRMM agent.ps1 | 173 ++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 scripts_staging/Build/Update TRMM agent.ps1 diff --git a/scripts_staging/Build/Update TRMM agent.ps1 b/scripts_staging/Build/Update TRMM agent.ps1 new file mode 100644 index 00000000..a9c61896 --- /dev/null +++ b/scripts_staging/Build/Update TRMM agent.ps1 @@ -0,0 +1,173 @@ +<# +.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 can be specified through the environment variable `version`. + +.PARAMETER isSigned + Boolean flag to determine if the signed version should be downloaded. + Set to true in the environment variable `issigned` to download the signed version; otherwise, the unsigned version is downloaded. + +.PARAMETER signedDownloadToken + The token used for authenticated signed downloads. This should be set in the environment variable `trmm_sign_download_token` + and is required if `isSigned` is set to true. + +.EXEMPLE var + trmm_sign_download_token={{global.trmm_sign_download_token}} + version=latest + version=2.7.0 + issigned=true + +.NOTES + Author: SAN + Date: 29.10.24 + #public + +.CHANGELOG + - Initial version + - Added support for environment variable input + - Enhanced error handling and process execution + - Added local version check to skip download if versions match + +.TODO + integrate to monthly update runs +#> + +# Variables +$version = $env:version # Specify a version manually, or leave as "latest" to get the latest version from GitHub +$isSigned = $env:issigned -eq 'true' # Set to true to download the signed version +$signedDownloadToken = $env:trmm_sign_download_token # Token used for signed downloads only + +# Check for signed download token if isSigned is true +if ($isSigned -and -not $signedDownloadToken) { + Write-Output "Error: Missing signed download token. Exiting..." + exit 1 +} + +# 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 + $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 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..." + # List all installed software for debugging + $allInstalledSoftware = Get-CimInstance -ClassName Win32_Product + Write-Output "Currently installed software (Win32_Product):" + $allInstalledSoftware | ForEach-Object { Write-Output "- $($_.Name)" } + + # Check the uninstall registry key as well + $uninstallKeys = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" + ) + Write-Output "Currently installed software (Registry):" + foreach ($key in $uninstallKeys) { + $allInstalledSoftware = Get-ItemProperty $key + $allInstalledSoftware | ForEach-Object { Write-Output "- $($_.DisplayName)" } + } + } + + # 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 $isSigned variable + if ($isSigned) { + # Download the signed agent using the token + $downloadUrl = "https://agents.tacticalrmm.com/api/v2/agents?version=$version&arch=amd64&token=$signedDownloadToken&plat=windows&api=api-rmm-managed-services.vtx.ch" + } 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 +} From 5f9bf134836a789c5c84bb59f6dde3e16fd2e827 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:07 +0000 Subject: [PATCH 119/447] Update ./scripts/Build/Change default chocolatey repo to internal.ps1 --- ...ge default chocolatey repo to internal.ps1 | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 scripts_staging/Build/Change default chocolatey repo to internal.ps1 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..025602c7 --- /dev/null +++ b/scripts_staging/Build/Change default chocolatey repo to internal.ps1 @@ -0,0 +1,38 @@ +<# +.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. + +.EXAMPLE + NEW_URL="https://myrepo.com/chocolatey/" + NEW_NAME="myrepo" + +.NOTES + Author: SAN + Date: 01.01.2024 + #public + +.CHANGELOG + SAN 11.12.24 Moved new info to env + + +#> + +$newUrl = $env:NEW_URL +$newPriority = 5 +$newName = $env:NEW_NAME + +$defaultUrl = "https://chocolatey.org/api/v2/" +$defaultPriority = 10 +$defaultName = "chocolatey" + +# Remove settings +choco source remove -n $defaultName -y +choco source remove -n $newName -y + +# Add the new Chocolatey repository with the specified priority +choco source add -n $newName -s $newUrl --priority $newPriority +# Add the default Chocolatey repository with a low priority +choco source add -n $defaultName -s $defaultUrl --priority $defaultPriority From 729b1532c4499a835aaf75cf3e0de3fa999a0455 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:09 +0000 Subject: [PATCH 120/447] Update ./scripts/Checks/AD link health.ps1 --- scripts_staging/Checks/AD link health.ps1 | 147 ++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 scripts_staging/Checks/AD link health.ps1 diff --git a/scripts_staging/Checks/AD link health.ps1 b/scripts_staging/Checks/AD link health.ps1 new file mode 100644 index 00000000..6802bb99 --- /dev/null +++ b/scripts_staging/Checks/AD link health.ps1 @@ -0,0 +1,147 @@ +<# +.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 + +.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 + ) + $connection = Test-NetConnection -ComputerName $ADDomainController -Port $Port + $status = if ($connection.TcpTestSucceeded) { "OK" } else { "KO" } + + [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 + } +} + +# Main function to test AD connections +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 + } + + # Spacer + $results += "" + } + + + # Count and handle failures + $failedCount = ($results | Where-Object { $_.Status -eq "KO" }).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." +} From b180fd776672c6e7b43b602a7f98d12172fa86bf Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:11 +0000 Subject: [PATCH 121/447] Update ./scripts/Checks/Ping monitoring.ps1 --- scripts_staging/Checks/Ping monitoring.ps1 | 91 ++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 scripts_staging/Checks/Ping monitoring.ps1 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 From 3ffa3f4eddff789c5bbf9243d57420b266aa4142 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:13 +0000 Subject: [PATCH 122/447] Update ./scripts/Checks/is process running.ps1 --- scripts_staging/Checks/is process running.ps1 | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 scripts_staging/Checks/is process running.ps1 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 From 52fb1afb0d457a3a6561f0cccc452e0914645c1b Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:16 +0000 Subject: [PATCH 123/447] Update ./scripts/Checks/Activation status.ps1 --- scripts_staging/Checks/Activation status.ps1 | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 scripts_staging/Checks/Activation status.ps1 diff --git a/scripts_staging/Checks/Activation status.ps1 b/scripts_staging/Checks/Activation status.ps1 new file mode 100644 index 00000000..4c8fc8e1 --- /dev/null +++ b/scripts_staging/Checks/Activation status.ps1 @@ -0,0 +1,27 @@ +<# +.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 + +#> + +$activationStatus = Get-WmiObject -Query "SELECT * FROM SoftwareLicensingProduct WHERE PartialProductKey <> NULL" + +if ($activationStatus.LicenseStatus -eq 1) { + Write-Host "Windows is activated." + exit 0 +} else { + Write-Host "Windows is not activated." + exit 1 +} From 19a3c2efa61e339937233b1d6aa655a8cb5dd82e Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:18 +0000 Subject: [PATCH 124/447] Update ./scripts/Checks/Rdcms size.ps1 --- scripts_staging/Checks/Rdcms size.ps1 | 45 +++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 scripts_staging/Checks/Rdcms size.ps1 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 From 93c43d352f733562af553f0d8784ff10bbfc5243 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:20 +0000 Subject: [PATCH 125/447] Update ./scripts/Checks/Exchange Health.ps1 --- scripts_staging/Checks/Exchange Health.ps1 | 166 +++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 scripts_staging/Checks/Exchange Health.ps1 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 From a168e361e95fff0dbe819e926b5f53efc38c3990 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:22 +0000 Subject: [PATCH 126/447] Update ./scripts/Checks/is RDP port ok.ps1 --- scripts_staging/Checks/is RDP port ok.ps1 | 39 +++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 scripts_staging/Checks/is RDP port ok.ps1 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..dff54378 --- /dev/null +++ b/scripts_staging/Checks/is RDP port ok.ps1 @@ -0,0 +1,39 @@ +<# +.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 +#> + +$port = 3389 +$address = "localhost" + +# Try Test-NetConnection if available +if (Get-Command Test-NetConnection -ErrorAction SilentlyContinue) { + $tcpConnection = Test-NetConnection -ComputerName $address -Port $port + if ($tcpConnection.TcpTestSucceeded) { + Write-Output "Port $port is open." + } else { + Write-Output "Port $port is not open." + exit 1 + } +} else { + # Fallback using TcpClient + try { + $tcpClient = New-Object System.Net.Sockets.TcpClient + $tcpClient.Connect($address, $port) + Write-Output "Port $port is open." + $tcpClient.Close() + } catch { + Write-Output "Port $port is not open." + exit 1 + } +} \ No newline at end of file From 6ab62cdc9aa8b6093dc7eb30825628afccf34332 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:25 +0000 Subject: [PATCH 127/447] Update ./scripts/Checks/Eset Status.ps1 --- scripts_staging/Checks/Eset Status.ps1 | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 scripts_staging/Checks/Eset Status.ps1 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 From e8c31f1fba94b0ea5fe6039b5627612fb397daa9 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:27 +0000 Subject: [PATCH 128/447] Update ./scripts/Checks/Boot mode.ps1 --- scripts_staging/Checks/Boot mode.ps1 | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 scripts_staging/Checks/Boot mode.ps1 diff --git a/scripts_staging/Checks/Boot mode.ps1 b/scripts_staging/Checks/Boot mode.ps1 new file mode 100644 index 00000000..c5dbe18e --- /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 + + +#> + +# Check if the system is booted in Safe Mode +$regPath = "HKLM:\SYSTEM\CurrentControlSet\Control\SafeBoot\Option" +$safeModeKeyExists = Test-Path $regPath + +if ($safeModeKeyExists) { + Write-Host "System is booted in Safe Mode." + exit 1 +} else { + Write-Host "System is not booted in Safe Mode." + exit 0 +} \ No newline at end of file From 8ac07054a54b1e60fc9f479d3a358b3ee91caa4c Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:29 +0000 Subject: [PATCH 129/447] Update ./scripts/Checks/Windows Services.ps1 --- scripts_staging/Checks/Windows Services.ps1 | 124 ++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 scripts_staging/Checks/Windows Services.ps1 diff --git a/scripts_staging/Checks/Windows Services.ps1 b/scripts_staging/Checks/Windows Services.ps1 new file mode 100644 index 00000000..2f9fe3b1 --- /dev/null +++ b/scripts_staging/Checks/Windows Services.ps1 @@ -0,0 +1,124 @@ +<# +.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). + +.CHANGELOG + 28.10.24 SAN - Removed ignored output without the debug flag. + 28.10.24 SAN - cleanup documentation. + +#> + + + +# Define a generic list of services names to be ignored by 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 it should or could be fixed + "MSExchangeNotificationsBroker", + "tiledatamodelsvc", + "BITS", + "CDPSvc", + "AGSService", + "ShellHWDetection" # this one is strange it was failing on a lot of devices but no idea if it should or could be fixed + +) + +# 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))" + } + + Exit 1 + +} From d6efd755a8604cf963518f25c7e1e1c2d9dfb03b Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:31 +0000 Subject: [PATCH 130/447] Update ./scripts/Checks/AD Connect health.ps1 --- scripts_staging/Checks/AD Connect health.ps1 | 72 ++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 scripts_staging/Checks/AD Connect health.ps1 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) From 5d35e25208ae69f22bd5ade67ce5e92b4315cda2 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:34 +0000 Subject: [PATCH 131/447] Update ./scripts/Checks/Certificates expiry.ps1 --- .../Checks/Certificates expiry.ps1 | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 scripts_staging/Checks/Certificates expiry.ps1 diff --git a/scripts_staging/Checks/Certificates expiry.ps1 b/scripts_staging/Checks/Certificates expiry.ps1 new file mode 100644 index 00000000..3c47f793 --- /dev/null +++ b/scripts_staging/Checks/Certificates expiry.ps1 @@ -0,0 +1,198 @@ +<# +.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 + +.TODO + Make the output messages more readable + move all flags and var to env + +#> + +# Get values from environment variables, defaulting if unset +$warnThresholdDays = [int](if ($env:WARN_THRESHOLD_DAYS) { $env:WARN_THRESHOLD_DAYS } else { 20 }) +$errorThresholdDays = [int](if ($env:ERROR_THRESHOLD_DAYS) { $env:ERROR_THRESHOLD_DAYS } else { 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 +} From b9fd41e9cc0790fb251680ddc1e95d11706e2292 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:36 +0000 Subject: [PATCH 132/447] Update ./scripts/Checks/Windows Reliabilty Score.ps1 --- .../Checks/Windows Reliabilty Score.ps1 | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 scripts_staging/Checks/Windows Reliabilty Score.ps1 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 From 1abb459ca34aacf0c6e3412807cbbfe1824ebeda Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:39 +0000 Subject: [PATCH 133/447] Update ./scripts/Checks/Disk RW.ps1 --- scripts_staging/Checks/Disk RW.ps1 | 109 +++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 scripts_staging/Checks/Disk RW.ps1 diff --git a/scripts_staging/Checks/Disk RW.ps1 b/scripts_staging/Checks/Disk RW.ps1 new file mode 100644 index 00000000..a43e4eec --- /dev/null +++ b/scripts_staging/Checks/Disk RW.ps1 @@ -0,0 +1,109 @@ +<# +.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 +#> + +# Variables for thresholds with default values from environment or script +$ReadWarnThresholdMBps = [int]$env:READ_WARN_THRESHOLD_MBPS +$ReadErrorThresholdMBps = [int]$env:READ_ERROR_THRESHOLD_MBPS +$WriteWarnThresholdMBps = [int]$env:WRITE_WARN_THRESHOLD_MBPS +$WriteErrorThresholdMBps = [int]$env:WRITE_ERROR_THRESHOLD_MBPS + +# Set default values if environment variables are not set +if (-not $ReadWarnThresholdMBps) { $ReadWarnThresholdMBps = 2000 } +if (-not $ReadErrorThresholdMBps) { $ReadErrorThresholdMBps = 1500 } +if (-not $WriteWarnThresholdMBps) { $WriteWarnThresholdMBps = 80 } +if (-not $WriteErrorThresholdMBps) { $WriteErrorThresholdMBps = 50 } + +# Function to test disk speed using a test file +function Test-DiskSpeed { + param ( + [string]$TestFile = $env:target_file, # Get target file from environment variable + [int]$FileSizeInMB = 1024 + ) + + # Check if the environment variable is set + if (-not $TestFile) { + Write-Output "Error: Environment variable 'target_file' is not set or is empty." + exit 1 # Exit with warning code if the variable is not set or empty + } + + # Create a buffer for writing + $buffer = New-Object byte[] (1MB) + $rnd = New-Object Random + + # Write speed 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() + $writeEnd = Get-Date + $writeDuration = ($writeEnd - $writeStart).TotalSeconds + $writeSpeedMBps = $FileSizeInMB / $writeDuration + + # Read speed test + $readStart = Get-Date + $stream = [System.IO.File]::OpenRead($TestFile) + while ($stream.Read($buffer, 0, $buffer.Length)) { + # Reading the file + } + $stream.Close() + $readEnd = Get-Date + $readDuration = ($readEnd - $readStart).TotalSeconds + $readSpeedMBps = $FileSizeInMB / $readDuration + + # Cleanup + Remove-Item $TestFile + + return [pscustomobject]@{ + WriteSpeedMBps = [math]::Round($writeSpeedMBps, 2) + ReadSpeedMBps = [math]::Round($readSpeedMBps, 2) + } +} + +# Run the test +$speedResults = Test-DiskSpeed + +# Output the results +Write-Output "W: $($speedResults.WriteSpeedMBps) MB/s" +Write-Output "R: $($speedResults.ReadSpeedMBps) MB/s" +Write-Output "T: $env:target_file " + +# Check conditions for exit codes based on thresholds +if ($speedResults.WriteSpeedMBps -lt $WriteErrorThresholdMBps -or $speedResults.ReadSpeedMBps -lt $ReadErrorThresholdMBps) { + exit 2 # Error condition if below error thresholds +} elseif ($speedResults.WriteSpeedMBps -lt $WriteWarnThresholdMBps -or $speedResults.ReadSpeedMBps -lt $ReadWarnThresholdMBps) { + exit 1 # Warning condition if below warning thresholds +} else { + exit 0 # All good +} From 47fd6727465f078c70aa0d856c42e418f118c8c2 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:41 +0000 Subject: [PATCH 134/447] Update ./scripts/Checks/Swap health.ps1 --- scripts_staging/Checks/Swap health.ps1 | 81 ++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 scripts_staging/Checks/Swap health.ps1 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)) From b41dd69b564e6c688f3968b571ed7cc94e164ba2 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:43 +0000 Subject: [PATCH 135/447] Update ./scripts/Checks/Last errors logs.ps1 --- scripts_staging/Checks/Last errors logs.ps1 | 78 +++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 scripts_staging/Checks/Last errors logs.ps1 diff --git a/scripts_staging/Checks/Last errors logs.ps1 b/scripts_staging/Checks/Last errors logs.ps1 new file mode 100644 index 00000000..c9723791 --- /dev/null +++ b/scripts_staging/Checks/Last errors logs.ps1 @@ -0,0 +1,78 @@ +<# +.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). + +.NOTES + Author: SAN + Date: 24.10.2024 + #public + +.CHANGELOG + 04.12.24 SAN added id to ignore in comma separeted variable + +.TODO + Set 20 Error Events and 48 hours in vars same for 4 and 12 + +#> +# Define the time ranges +$start48h = (Get-Date).AddHours(-48) +$start12h = (Get-Date).AddHours(-12) + +# Define a list of event IDs to ignore +$ignoredEventIds = @(10016, 10016) + +#10016 safe to ignore https://learn.microsoft.com/en-us/troubleshoot/windows-client/application-management/event-10016-logged-when-accessing-dcom + +# Retrieve the last 20 error events in the last 48 hours +$errors48h = Get-WinEvent -FilterHashtable @{LogName='System'; Level=2; StartTime=$start48h} -MaxEvents 20 -ErrorAction SilentlyContinue + +# Filter out events with IDs in the ignored list +$filteredErrors48h = $errors48h | Where-Object { $ignoredEventIds -notcontains $_.Id } + +# Count of errors found in the last 48 hours (after ignoring specified event IDs) +$errorCount = if ($filteredErrors48h) { $filteredErrors48h.Count } else { 0 } + +if ($errorCount -gt 0) { + # Output the number of errors found + Write-Output "$errorCount error(s) found recently." +} + +# If errors are found, display them +if ($errorCount -gt 0) { + Write-Output "Last 20 Error Events in the last 48 hours (excluding ignored event IDs):" + $filteredErrors48h | ForEach-Object { + Write-Output "TimeCreated: $($_.TimeCreated)" + Write-Output "Event ID: $($_.Id)" + Write-Output "Message: $($_.Message)" + Write-Output "----------------------------------------" + } +} + +# Retrieve the error events from the last 12 hours +$errors12h = Get-WinEvent -FilterHashtable @{LogName='System'; Level=2; StartTime=$start12h} -ErrorAction SilentlyContinue + +# Filter out events with IDs in the ignored list for the 12-hour check +$filteredErrors12h = $errors12h | Where-Object { $ignoredEventIds -notcontains $_.Id } + +# Check if there are 4 or more errors in the last 12 hours (excluding ignored event IDs) +if ($filteredErrors12h -and $filteredErrors12h.Count -ge 4) { + Write-Output "Error: 4 or more error events found in the last 12 hours." + exit 1 +} else { + if (!$filteredErrors12h) { + Write-Output "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 +} From b1fbaf322d06d3278f836f7a90f8666f61a7ac39 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:45 +0000 Subject: [PATCH 136/447] Update ./snippets/CallPowerShell7.ps1 --- scripts_staging/snippets/CallPowerShell7.ps1 | 55 ++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 scripts_staging/snippets/CallPowerShell7.ps1 diff --git a/scripts_staging/snippets/CallPowerShell7.ps1 b/scripts_staging/snippets/CallPowerShell7.ps1 new file mode 100644 index 00000000..4c1793ed --- /dev/null +++ b/scripts_staging/snippets/CallPowerShell7.ps1 @@ -0,0 +1,55 @@ +#public + +<# +.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 + +#> + +# 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" \ No newline at end of file From c26576e439b424bfa0449955634ba82420622b01 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:48 +0000 Subject: [PATCH 137/447] Update ./snippets/Cleaner.ps1 --- scripts_staging/snippets/Cleaner.ps1 | 462 +++++++++++++++++++++++++++ 1 file changed, 462 insertions(+) create mode 100644 scripts_staging/snippets/Cleaner.ps1 diff --git a/scripts_staging/snippets/Cleaner.ps1 b/scripts_staging/snippets/Cleaner.ps1 new file mode 100644 index 00000000..489d2223 --- /dev/null +++ b/scripts_staging/snippets/Cleaner.ps1 @@ -0,0 +1,462 @@ +#public +<# +.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. + All deleted files will go into a log transcript in $env:TEMP. By default this + script leaves files that are newer than 7 days old however this variable can be edited. + + +.NOTES + This script will typically clean up anywhere from 1GB up to 15GB of space from a C: drive. + +.CHANGELOG + 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 + +.TODO + Full code refactoring, get everything out of function, remove logging, streamline opperation + +#> +Function Start-Cleanup { + + +## Allows the use of -WhatIf +[CmdletBinding(SupportsShouldProcess=$True)] + +param( + ## Delete data older then $daystodelete + [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=0)] + $DaysToDelete = 7, + + ## LogFile path for the transcript to be written to + [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=1)] + $LogFile = ("$env:TEMP\" + (get-date -format "MM-d-yy-HH-mm") + '.log'), + + ## All verbose outputs will get logged in the transcript($logFile) + [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=2)] + $VerbosePreference = "Continue", + + ## All errors should be withheld from the console + [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=3)] + $ErrorActionPreference = "SilentlyContinue" +) + + ## Begin the timer + $Starters = (Get-Date) + + ## Check $VerbosePreference variable, and turns -Verbose on + Function global:Write-Verbose ( [string]$Message ) { + if ( $VerbosePreference -ne 'SilentlyContinue' ) { + Write-Host "$Message" + } + } + + ## Tests if the log file already exists and renames the old file if it does exist + if(Test-Path $LogFile){ + ## Renames the log to be .old + Rename-Item $LogFile $LogFile.old -Verbose -Force + } else { + ## Starts a transcript in C:\temp so you can see which files were deleted + Write-Host (Start-Transcript -Path $LogFile) + } + + ## Writes a verbose output to the screen for user information + Write-Host "Retriving current disk percent free for comparison once the script has completed." + Write-Host "[DONE]" + + ## Gathers the amount of disk space used before running the script + $Before = Get-WmiObject Win32_LogicalDisk | Where-Object { $_.DriveType -eq "3" } | Select-Object SystemName, + @{ Name = "Drive" ; Expression = { ( $_.DeviceID ) } }, + @{ Name = "Size (GB)" ; Expression = {"{0:N1}" -f ( $_.Size / 1gb)}}, + @{ Name = "FreeSpace (GB)" ; Expression = {"{0:N1}" -f ( $_.Freespace / 1gb ) } }, + @{ Name = "PercentFree" ; Expression = {"{0:P1}" -f ( $_.FreeSpace / $_.Size ) } } | + Format-Table -AutoSize | + Out-String + + ## Stops the windows update service so that c:\windows\softwaredistribution can be cleaned up + Get-Service -Name wuauserv | Stop-Service -Force -ErrorAction SilentlyContinue -WarningAction SilentlyContinue -Verbose + + # Sets the SCCM cache size to 1 GB if it exists. + if ((Get-WmiObject -namespace root\ccm\SoftMgmtAgent -class CacheConfig) -ne "$null"){ + # if data is returned and sccm cache is configured it will shrink the size to 1024MB. + $cache = Get-WmiObject -namespace root\ccm\SoftMgmtAgent -class CacheConfig + $Cache.size = 1024 | Out-Null + $Cache.Put() | Out-Null + Restart-Service ccmexec -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + } + + ## Compaction of Windows.edb + # Check if the Windows.edb file exists + $windowsEdbPath = "$env:ALLUSERSPROFILE\Microsoft\Search\Data\Applications\Windows\Windows.edb" + if (Test-Path $windowsEdbPath) { + + # Disable the Windows Search service + Write-Host "Disabling the Windows Search service..." + Set-Service -Name wsearch -StartupType Disabled + + # Stop the Windows Search service + Write-Host "Stopping the Windows Search service..." + Stop-Service -Name wsearch -Force + Write-Host "Windows Search service stopped." + + # Perform offline compaction of the Windows.edb file + 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 the Windows Search service to manual start + Write-Host "Setting the Windows Search service to manual start..." + Set-Service -Name wsearch -StartupType Manual + + # Start the Windows Search service + Write-Host "Starting the Windows Search service..." + Start-Service -Name wsearch + Write-Host "Windows Search service started." + + } else { + Write-Host "Windows.edb file not found, skipping all actions." + } + + ## Delete old adobe updates + Get-ChildItem "C:\ProgramData\Adobe\ARM\*" -Recurse -Force -ErrorAction SilentlyContinue | Remove-Item -Recurse -ErrorAction SilentlyContinue -Verbose + Write-Host "The Contents of Adobe ARM have been removed successfully!" + Write-Host "[DONE]" + + ## Deletes the contents of the Windows Temp folder. + Get-ChildItem "C:\Windows\Temp\*" -Recurse -Force -Verbose -ErrorAction SilentlyContinue | + Where-Object { ($_.CreationTime -lt $(Get-Date).AddDays( - $DaysToDelete)) } | Remove-Item -force -recurse -ErrorAction SilentlyContinue -Verbose + Write-host "The Contents of Windows Temp have been removed successfully!" + Write-Host "[DONE]" + + ## Deletes the contents of Windows Software Distribution. + Get-ChildItem "C:\Windows\SoftwareDistribution\*" -Recurse -Force -ErrorAction SilentlyContinue | Remove-Item -recurse -ErrorAction SilentlyContinue -Verbose + Write-Host "The Contents of Windows SoftwareDistribution have been removed successfully!" + Write-Host "[DONE]" + + ## Deletes all files and folders in user's Temp folder older then $DaysToDelete + Get-ChildItem "C:\users\*\AppData\Local\Temp\*" -Recurse -Force -ErrorAction SilentlyContinue | + Where-Object { ($_.CreationTime -lt $(Get-Date).AddDays( - $DaysToDelete))} | + Remove-Item -force -recurse -ErrorAction SilentlyContinue -Verbose + Write-Host "The contents of `$env:TEMP have been removed successfully!" + Write-Host "[DONE]" + + ## Deletes all files and folders in CSBack folder older then $DaysToDelete + Get-ChildItem "C:\csback\*" -Recurse -Force -ErrorAction SilentlyContinue | + Where-Object { ($_.CreationTime -lt $(Get-Date).AddDays( - $DaysToDelete))} | + Remove-Item -force -recurse -ErrorAction SilentlyContinue -Verbose + Write-Host "The contents of csback have been removed successfully!" + Write-Host "[DONE]" + + ## Removes all files and folders in user's Temporary Internet Files older then $DaysToDelete + Get-ChildItem "C:\users\*\AppData\Local\Microsoft\Windows\Temporary Internet Files\*" ` + -Recurse -Force -Verbose -ErrorAction SilentlyContinue | + Where-Object {($_.CreationTime -lt $(Get-Date).AddDays( - $DaysToDelete))} | + Remove-Item -Force -Recurse -ErrorAction SilentlyContinue -Verbose + Write-Host "All Temporary Internet Files have been removed successfully!" + Write-Host "[DONE]" + + ## Removes *.log from C:\windows\CBS + if(Test-Path C:\Windows\logs\CBS\){ + Get-ChildItem "C:\Windows\logs\CBS\*.log" -Recurse -Force -ErrorAction SilentlyContinue | + remove-item -force -recurse -ErrorAction SilentlyContinue -Verbose + Write-Host "All CBS logs have been removed successfully!" + Write-Host "[DONE]" + } else { + Write-Host "C:\inetpub\logs\LogFiles\ does not exist, there is nothing to cleanup." + Write-Host "[WARNING]" + } + + ## Cleans IIS Logs older then $DaysToDelete + if (Test-Path C:\inetpub\logs\LogFiles\) { + Get-ChildItem "C:\inetpub\logs\LogFiles\*" -Recurse -Force -ErrorAction SilentlyContinue | + Where-Object { ($_.CreationTime -lt $(Get-Date).AddDays(-25)) } | Remove-Item -Force -Verbose -Recurse -ErrorAction SilentlyContinue + Write-Host "All IIS Logfiles over $DaysToDelete days old have been removed Successfully!" + Write-Host "[DONE]" + } + else { + Write-Host "C:\Windows\logs\CBS\ does not exist, there is nothing to cleanup." + Write-Host "[WARNING]" + } + + ## Removes C:\Config.Msi + if (test-path C:\Config.Msi){ + remove-item -Path C:\Config.Msi -force -recurse -Verbose -ErrorAction SilentlyContinue + } else { + Write-Host "C:\Config.Msi does not exist, there is nothing to cleanup." + Write-Host "[WARNING]" + } + + ## Removes c:\Intel + if (test-path c:\Intel){ + remove-item -Path c:\Intel -force -recurse -Verbose -ErrorAction SilentlyContinue + } else { + Write-Host "c:\Intel does not exist, there is nothing to cleanup." + Write-Host "[WARNING]" + } + + ## Removes c:\PerfLogs + if (test-path c:\PerfLogs){ + remove-item -Path c:\PerfLogs -force -recurse -Verbose -ErrorAction SilentlyContinue + } else { + Write-Host "c:\PerfLogs does not exist, there is nothing to cleanup." + Write-Host "[WARNING]" + } + + ## Removes $env:windir\memory.dmp + if (test-path $env:windir\memory.dmp){ + remove-item $env:windir\memory.dmp -force -Verbose -ErrorAction SilentlyContinue + } else { + Write-Host "C:\Windows\memory.dmp does not exist, there is nothing to cleanup." + Write-Host "[WARNING]" + } + + ## Removes rouge folders + Write-host "Deleting Rouge folders" + Write-Host "[DONE]" + + ## Removes Windows Error Reporting files + if (test-path C:\ProgramData\Microsoft\Windows\WER){ + Get-ChildItem -Path C:\ProgramData\Microsoft\Windows\WER -Recurse | Remove-Item -force -recurse -Verbose -ErrorAction SilentlyContinue + Write-host "Deleting Windows Error Reporting files" + Write-Host "[DONE]" + } else { + Write-Host "C:\ProgramData\Microsoft\Windows\WER does not exist, there is nothing to cleanup." + Write-Host "[WARNING]" + } + + ## Removes System and User Temp Files - lots of access denied will occur. + ## Cleans up c:\windows\temp + if (Test-Path $env:windir\Temp\) { + Remove-Item -Path "$env:windir\Temp\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue + } else { + Write-Host "C:\Windows\Temp does not exist, there is nothing to cleanup." + Write-Host "[WARNING]" + } + + ## Cleans up minidump + if (Test-Path $env:windir\minidump\) { + Remove-Item -Path "$env:windir\minidump\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue + } else { + Write-Host "$env:windir\minidump\ does not exist, there is nothing to cleanup." + Write-Host "[WARNING]" + } + + ## Cleans up prefetch + if (Test-Path $env:windir\Prefetch\) { + Remove-Item -Path "$env:windir\Prefetch\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue + } else { + Write-Host "$env:windir\Prefetch\ does not exist, there is nothing to cleanup." + Write-Host "[WARNING]" + } + + ## Cleans up each user's temp folder + if (Test-Path "C:\Users\*\AppData\Local\Temp\") { + Remove-Item -Path "C:\Users\*\AppData\Local\Temp\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue + } else { + Write-Host "C:\Users\*\AppData\Local\Temp\ does not exist, there is nothing to cleanup." + Write-Host "[WARNING]" + } + + ## Cleans up all user's Windows error reporting + if (Test-Path "C:\Users\*\AppData\Local\Microsoft\Windows\WER\") { + Remove-Item -Path "C:\Users\*\AppData\Local\Microsoft\Windows\WER\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue + } else { + Write-Host "C:\ProgramData\Microsoft\Windows\WER does not exist, there is nothing to cleanup." + Write-Host "[WARNING]" + } + + ## Cleans up user's temporary internet files + if (Test-Path "C:\Users\*\AppData\Local\Microsoft\Windows\Temporary Internet Files\") { + Remove-Item -Path "C:\Users\*\AppData\Local\Microsoft\Windows\Temporary Internet Files\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue + } else { + Write-Host "C:\Users\*\AppData\Local\Microsoft\Windows\Temporary Internet Files\ does not exist." + Write-Host "[WARNING]" + } + + ## Cleans up Internet Explorer cache + if (Test-Path "C:\Users\*\AppData\Local\Microsoft\Windows\IECompatCache\") { + Remove-Item -Path "C:\Users\*\AppData\Local\Microsoft\Windows\IECompatCache\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue + } else { + Write-Host "C:\Users\*\AppData\Local\Microsoft\Windows\IECompatCache\ does not exist." + Write-Host "[WARNING]" + } + + ## Cleans up Internet Explorer cache + if (Test-Path "C:\Users\*\AppData\Local\Microsoft\Windows\IECompatUaCache\") { + Remove-Item -Path "C:\Users\*\AppData\Local\Microsoft\Windows\IECompatUaCache\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue + } else { + Write-Host "C:\Users\*\AppData\Local\Microsoft\Windows\IECompatUaCache\ does not exist." + Write-Host "[WARNING]" + } + + ## Cleans up Internet Explorer download history + if (Test-Path "C:\Users\*\AppData\Local\Microsoft\Windows\IEDownloadHistory\") { + Remove-Item -Path "C:\Users\*\AppData\Local\Microsoft\Windows\IEDownloadHistory\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue + } else { + Write-Host "C:\Users\*\AppData\Local\Microsoft\Windows\IEDownloadHistory\ does not exist." + Write-Host "[WARNING]" + } + + ## Cleans up Internet Cache + if (Test-Path "C:\Users\*\AppData\Local\Microsoft\Windows\INetCache\") { + Remove-Item -Path "C:\Users\*\AppData\Local\Microsoft\Windows\INetCache\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue + } else { + Write-Host "C:\Users\*\AppData\Local\Microsoft\Windows\INetCache\ does not exist." + Write-Host "[WARNING]" + } + + ## Cleans up Internet Cookies + if (Test-Path "C:\Users\*\AppData\Local\Microsoft\Windows\INetCookies\") { + Remove-Item -Path "C:\Users\*\AppData\Local\Microsoft\Windows\INetCookies\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue + } else { + Write-Host "C:\Users\*\AppData\Local\Microsoft\Windows\INetCookies\ does not exist." + Write-Host "[WARNING]" + } + + ## Cleans up terminal server cache + if (Test-Path "C:\Users\*\AppData\Local\Microsoft\Terminal Server Client\Cache\") { + Remove-Item -Path "C:\Users\*\AppData\Local\Microsoft\Terminal Server Client\Cache\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue + } else { + Write-Host "C:\Users\*\AppData\Local\Microsoft\Terminal Server Client\Cache\ does not exist." + Write-Host "[WARNING]" + } + + Write-host "Removing System and User Temp Files" + Write-Host "[DONE]" + + ## Removes the hidden recycle bin. + if (Test-path 'C:\$Recycle.Bin'){ + Remove-Item 'C:\$Recycle.Bin' -Recurse -Force -Verbose -ErrorAction SilentlyContinue + } else { + Write-Host "C:\`$Recycle.Bin does not exist, there is nothing to cleanup." + Write-Host "[WARNING]" + } + + ## Turns errors back on + $ErrorActionPreference = "Continue" + + ## Checks the version of PowerShell + ## If PowerShell version 4 or below is installed the following will process + if ($PSVersionTable.PSVersion.Major -le 4) { + + ## Empties the recycle bin, the desktop recycle bin + $Recycler = (New-Object -ComObject Shell.Application).NameSpace(0xa) + $Recycler.items() | ForEach-Object { + ## If PowerShell version 4 or below is installed the following will process + Remove-Item -Include $_.path -Force -Recurse -Verbose + Write-Host "The recycling bin has been cleaned up successfully!" + Write-Host "[DONE]" + } + } elseif ($PSVersionTable.PSVersion.Major -ge 5) { + ## If PowerShell version 5 is running on the machine the following will process + Clear-RecycleBin -DriveLetter C:\ -Force -Verbose + Write-Host "The recycling bin has been cleaned up successfully!" + Write-Host "[DONE]" + } + + ## Starts cleanmgr.exe + # Function to add the registry keys + function Add-RegistryKeys { + # Define the base registry key path + $baseKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches\" + + # Get all subkeys under the base key + $subKeys = Get-ChildItem -Path $baseKey -ErrorAction SilentlyContinue | Where-Object { $_.PSChildName -ne "StateFlags0001" } + + # Loop through each subkey and create/update the DWORD value + foreach ($subKey in $subKeys) { + $keyPath = Join-Path -Path $baseKey -ChildPath $subKey.PSChildName + $valueName = "StateFlags0001" + $value = 2 + + # Check if the value already exists, if not create it + if (-not (Test-Path -Path "$keyPath\$valueName")) { + New-ItemProperty -Path $keyPath -Name $valueName -Value $value -PropertyType DWORD -Force | Out-Null + } else { + # Update the existing value + Set-ItemProperty -Path $keyPath -Name $valueName -Value $value + } + } + + Write-Host "StateFlags0001 DWORD value created/updated in all subkeys under $baseKey" + } + + + + # Add registry keys + Add-RegistryKeys + # Run Disk Cleanup with sagerun1 + Start-Process -FilePath "cleanmgr.exe" -ArgumentList "/sagerun:1" -Wait + + + ## 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 ) } }, + @{ Name = "Size (GB)" ; Expression = {"{0:N1}" -f ( $_.Size / 1gb)}}, + @{ Name = "FreeSpace (GB)" ; Expression = {"{0:N1}" -f ( $_.Freespace / 1gb ) } }, + @{ Name = "PercentFree" ; Expression = {"{0:P1}" -f ( $_.FreeSpace / $_.Size ) } } | + Format-Table -AutoSize | Out-String + + ## Restarts wuauserv + Get-Service -Name wuauserv | Start-Service -ErrorAction SilentlyContinue + + ## Stop timer + $Enders = (Get-Date) + + ## Calculate amount of seconds your code takes to complete. + Write-Verbose "Elapsed Time: $(($Enders - $Starters).totalseconds) seconds + +" + ## Sends hostname to the console for ticketing purposes. + Write-Host (Hostname) + + ## Sends the date and time to the console for ticketing purposes. + Write-Host (Get-Date | Select-Object -ExpandProperty DateTime) + + ## Sends the disk usage before running the cleanup script to the console for ticketing purposes. + Write-Verbose " +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"), + $message = (Write-Host "Would you like to scan $ScanPath for ISOs or VHD(X) files?") + ) + $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." + 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." + } + } + } + PromptforScan -ScanPath C:\ # end of function + + ## Completed Successfully! + Write-Host (Stop-Transcript) + + Write-host "Script finished" -NoNewline + Write-Host "[DONE]" + +} +Start-Cleanup \ No newline at end of file From ddfea57b789c921fcd983183b577c0eecabb10da Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:50 +0000 Subject: [PATCH 138/447] Update ./snippets/GeneratedPassphrase.ps1 --- .../snippets/GeneratedPassphrase.ps1 | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 scripts_staging/snippets/GeneratedPassphrase.ps1 diff --git a/scripts_staging/snippets/GeneratedPassphrase.ps1 b/scripts_staging/snippets/GeneratedPassphrase.ps1 new file mode 100644 index 00000000..240c16a6 --- /dev/null +++ b/scripts_staging/snippets/GeneratedPassphrase.ps1 @@ -0,0 +1,47 @@ +#public +<# +.SYNOPSIS + This script changes the password for the user to a randomly generated passphrase. + +.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 generated "$GeneratedPassphrase" variable. + +.NOTES + Author : SAN + https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases + +#> + + + +function GeneratedPassphrase { + param ( + [int]$NumWords = 3, # Number of words in the passphrase + [int]$MinWordLength = 4, # Minimum length of each word + [int]$MaxWordLength = 10 # Maximum length of each word + ) + + # Expanded built-in 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") + + # Filter out words not within the specified length range and without spaces + $FilteredWords = $WordList | Where-Object { $_.Length -ge $MinWordLength -and $_.Length -le $MaxWordLength -and $_ -notmatch '\s' } + + # Select random words from the filtered list + $PassphraseWords = Get-Random -InputObject $FilteredWords -Count $NumWords + + # Capitalize the first letter of each word + $PassphraseWordsCapitalized = $PassphraseWords | ForEach-Object { $_.Substring(0,1).ToUpper() + $_.Substring(1) } + + # Add a random number after one of the words + $RandomIndex = Get-Random -Minimum 0 -Maximum $NumWords + $PassphraseWordsCapitalized[$RandomIndex] += (Get-Random -Minimum 0 -Maximum 10) + + # Join the words into a passphrase + $password = $PassphraseWordsCapitalized -join '-' + + return $password +} + +$GeneratedPassphrase = GeneratedPassphrase \ No newline at end of file From 01950d4f1b708b3d3f61651839b2fec126081eef Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:09:14 +0000 Subject: [PATCH 139/447] Update ./scripts/Tools/azure AD Connect Certificate Cleanup.ps1 --- .../azure AD Connect Certificate Cleanup.ps1 | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 scripts_staging/Tools/azure AD Connect Certificate Cleanup.ps1 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." From 64145812be506cd691ff507593f4df71cf9a412b Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:15:17 +0000 Subject: [PATCH 140/447] Update ./scripts/Tools/Expand partitiondrivedisk size.ps1 --- .../Tools/Expand partitiondrivedisk size.ps1 | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 scripts_staging/Tools/Expand partitiondrivedisk size.ps1 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 From fc579edb56a2b2acbd5eb6baafaa9adf12f6d189 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 17:46:35 +0000 Subject: [PATCH 141/447] Update ./scripts/Tasks/Start Eset scan V2.ps1 --- scripts_staging/Tasks/Start Eset scan V2.ps1 | 117 +++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 scripts_staging/Tasks/Start Eset scan V2.ps1 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 From f4a433d0bdc1a6560fb0473060392549bb101e0c Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:10:48 +0000 Subject: [PATCH 142/447] Update ./scripts/Tasks/Import RD Gateway Cert From IIS.ps1 --- .../Tasks/Import RD Gateway Cert From IIS.ps1 | 302 ++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 scripts_staging/Tasks/Import RD Gateway Cert From IIS.ps1 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..a467b024 --- /dev/null +++ b/scripts_staging/Tasks/Import RD Gateway Cert From IIS.ps1 @@ -0,0 +1,302 @@ +<# +.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 + + +.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 +#> +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-OldCertificates { + param ( + [string]$OldThumbprint + ) + + if (-not $OldThumbprint) { + Write-Warning "Old thumbprint is not provided. Skipping certificate removal process." + return $false + } + + $stores = @( + "Cert:\LocalMachine\My", + "Cert:\LocalMachine\WebHosting", + "Cert:\LocalMachine\Remote Desktop" + ) + + $certsRemoved = $false + + foreach ($store in $stores) { + try { + $certs = Get-ChildItem -Path $store -Recurse | Where-Object {$_.Thumbprint -eq $OldThumbprint} + if ($certs.Count -eq 0) { + Write-Host "No certificates with thumbprint $OldThumbprint found in $store." + } else { + foreach ($cert in $certs) { + Remove-Item -Path $cert.PSPath -Confirm:$false + Write-Host "Removed certificate with thumbprint $OldThumbprint 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 "The RD Gateway SSL certificate is already the same as IIS. No replacement needed." + exit 0 + } + + # 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 + } +<# + # Validate RD Gateway certificate thumbprint + if (Is-ValidThumbprint -Thumbprint $RDSCertThumbprint) { + Write-Host "RD Gateway certificate thumbprint $RDSCertThumbprint is valid. Continuing." + } else { + Write-Error "Invalid RD Gateway certificate thumbprint: $RDSCertThumbprint. 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 (assumes function is defined elsewhere) + $certsRemoved = Remove-OldCertificates -OldThumbprint $RDSCertThumbprint + + # Check if old certificates were removed + if (-not $certsRemoved) { + Write-Error "No old certificates 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 From 74eba5d10fe129ffa230f00334aa67c388efcce0 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:23:10 +0000 Subject: [PATCH 143/447] Update ./scripts/Tasks/Kill Switch Manager.ps1 --- scripts_staging/Tasks/Kill Switch Manager.ps1 | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 scripts_staging/Tasks/Kill Switch Manager.ps1 diff --git a/scripts_staging/Tasks/Kill Switch Manager.ps1 b/scripts_staging/Tasks/Kill Switch Manager.ps1 new file mode 100644 index 00000000..03d0c99d --- /dev/null +++ b/scripts_staging/Tasks/Kill Switch Manager.ps1 @@ -0,0 +1,124 @@ +<# +.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 securely. + +.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 + $env:killswitchdomain="example.com" + $env:companyfolder="C:\CompanyFolder" + + Run the script to set up the kill switch for Tactical RMM. + +.NOTES + Author: SAN + Date: ??? + #public + +.CHANGELOG + + +.TODO + Integrate this script into the deployment process. + Add global var to var +#> + + + +# 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 + +# Script content to save in the file +$scriptContent = @" +# Function to execute the stop branch +function ExecuteStopBranch { + # Stop Service name: tacticalrmm + Stop-Service -Name "tacticalrmm" -Force + + # Kill all tacticalrmm.exe processes + Get-Process -Name "tacticalrmm" | Stop-Process -Force + + # Stop Service name: Mesh Agent + Stop-Service -Name "Mesh Agent" -Force + + # Kill all MeshAgent.exe processes + Get-Process -Name "MeshAgent" | Stop-Process -Force +} + +# Function to execute the uninstall branch +function ExecuteUninstallBranch { + # Execute the uninstall command silently + #Start-Process -FilePath "C:\Program Files\TacticalAgent\unins000.exe" -ArgumentList "/VERYSILENT" -Wait +} + +# Resolve the TXT record +\$record = Resolve-DnsName -Name "$domain" -Type "TXT" + +# Check if the record was found +if (\$record) { + \$txtData = \$record | Select-Object -ExpandProperty Strings + \$foundStop = \$txtData -match "stop=true" + \$foundUninstall = \$txtData -match "uninstall=true" + + if (-not \$foundStop -and -not \$foundUninstall) { + # Neither stop=true nor uninstall=true found + Write-Host "Neither 'stop=true' nor 'uninstall=true' found in the TXT record for $domain." + # Add your code for the default case here + } + elseif (\$foundStop) { + # Branch for stop=true + ExecuteStopBranch + } + elseif (\$foundUninstall) { + # Branch for uninstall=true + ExecuteUninstallBranch + } +} else { + Write-Host "TXT record for $domain not found." +} +"@ + +# Save the script content to the file +$scriptContent | Out-File -FilePath $scriptPath -Encoding UTF8 -Force + +# 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 From 08164737f16e1cb28a988b360101aca1ee559a0e Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:29:01 +0000 Subject: [PATCH 144/447] Update ./scripts/Checks/Internet uplink.ps1 --- scripts_staging/Checks/Internet uplink.ps1 | 88 ++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 scripts_staging/Checks/Internet uplink.ps1 diff --git a/scripts_staging/Checks/Internet uplink.ps1 b/scripts_staging/Checks/Internet uplink.ps1 new file mode 100644 index 00000000..c4eeef8e --- /dev/null +++ b/scripts_staging/Checks/Internet uplink.ps1 @@ -0,0 +1,88 @@ +<# +.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: ??? + #public + +.CHANGELOG + +.TODO + Include customizable input for the list of IP addresses. + Enhance error handling for unreachable hosts. + for test all to env + tnc has some relability issue maybe use normal ping +#> + + +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 "Ping to $ip ($owner) failed." + $pingFailed = $true + } else { + Write-Host "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 "Ping to $randomIp ($owner) failed." + exit 1 + } else { + Write-Host "Ping to $randomIp ($owner) succeeded." + } +} \ No newline at end of file From a4c1577b3f7a6136378f17f7bf40bd328c1f7c43 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:29:11 +0000 Subject: [PATCH 145/447] Update ./scripts/Tools/Activate windows with KMS.ps1 --- .../Tools/Activate windows with KMS.ps1 | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 scripts_staging/Tools/Activate windows with KMS.ps1 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..7f8de58c --- /dev/null +++ b/scripts_staging/Tools/Activate windows with KMS.ps1 @@ -0,0 +1,91 @@ +<# +.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: ??? + #public + +.CHANGELOG + 11.12.24 SAN Code Cleanup + +.TODO + Convert the script to use the PowerShell module as the future of vbs is uncertain. +#> + + + + +# 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 +} + +$kmsServer = $env:kms_server + +# Set KMS server +Write-Host "Setting KMS server to: $kmsServer..." +try { + Start-Process -FilePath "cscript.exe" -ArgumentList "$env:SystemRoot\System32\slmgr.vbs /skms $kmsServer" -NoNewWindow -Wait -ErrorAction Stop + Write-Host "Successfully set KMS server to: $kmsServer" +} 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 "Windows activation process complete." +} catch { + Write-Host "Windows activation failed. 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 From cd1bf87b3df55a490d4f1bc3bb05f8ec775b3908 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:35:19 +0000 Subject: [PATCH 146/447] Update ./scripts/Fixes/Bluescreen report.ps1 --- scripts_staging/Fixes/Bluescreen report.ps1 | 103 ++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 scripts_staging/Fixes/Bluescreen report.ps1 diff --git a/scripts_staging/Fixes/Bluescreen report.ps1 b/scripts_staging/Fixes/Bluescreen report.ps1 new file mode 100644 index 00000000..609307b6 --- /dev/null +++ b/scripts_staging/Fixes/Bluescreen report.ps1 @@ -0,0 +1,103 @@ +<# +.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 + +.NOTES + Author: SAN + Date: 02.12.24 + Dependencies: Chocolatey, Nextcloud public share + #PUBLIC + +.CHANGELOG + +#> + +# Step 1: Retrieve Nextcloud WebDAV URL and Token from environment variables +$nextcloudWebdavUrl = [System.Environment]::GetEnvironmentVariable("NEXTCLOUD_WEBDAV_URL") +$webdavUser = [System.Environment]::GetEnvironmentVariable("NEXTCLOUD_TOKEN") + +# 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 +} + +# Variables (defined at the top for easy configuration) +$minidumpPath = "C:\Windows\Minidump" # Path to Minidump folder +$hostname = (Get-WmiObject -Class Win32_ComputerSystem).Name # Get the system hostname + +# 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..." +Start-Process $bluescreenViewerPath -ArgumentList "/stext $bluescreenLogFile" -Wait +if ($LASTEXITCODE -ne 0) { + Write-Host "Failed to run Bluescreen Viewer. Continuing with script." +} + +# Step 4: Output the content of the crash log file to the terminal +Write-Host "Displaying crash logs..." +if (Test-Path $bluescreenLogFile) { + Get-Content $bluescreenLogFile +} else { + Write-Host "The crash log file does not exist." +} + +# Step 5: Check if the Minidump directory exists and process the files +if (Test-Path $minidumpPath) { + # Get all files in the Minidump directory, excluding those already marked as "_sent" + $files = Get-ChildItem -Path $minidumpPath | Where-Object { $_.Name -notlike "*_sent*" } + + # Step 6: Loop through each Minidump file and upload to Nextcloud WebDAV + foreach ($file in $files) { + # Construct a new file name with the hostname at the beginning + $newFileName = "$hostname" + "_" + $file.Name + $uploadUrl = $nextcloudWebdavUrl + $newFileName # Construct WebDAV URL for each file + + # 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..." + Invoke-WebRequest -Uri $uploadUrl -Method Put -InFile $file.FullName -Headers $headers -UseBasicParsing + 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" + } catch { + Write-Host "Failed to upload $($file.Name): $_" + } + } +} else { + Write-Host "Minidump folder not found!" +} From a36714f271ee33fc85fe4a39ea228a9e591a3a67 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:35:21 +0000 Subject: [PATCH 147/447] Update ./scripts/Fixes/Fix broken ESET installation.ps1 --- .../Fixes/Fix broken ESET installation.ps1 | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 scripts_staging/Fixes/Fix broken ESET installation.ps1 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..ac033a83 --- /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-Error "Error: The file 'ermm.exe' exists at the specified path. This script may not work as expected." + exit 1 + } 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 From 8179023cdb3b95561bade4e8a572c06212f67b92 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:43:17 +0000 Subject: [PATCH 148/447] Update ./scripts/Checks/Maximum UpTime.ps1 --- scripts_staging/Checks/Maximum UpTime.ps1 | 46 ++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 scripts_staging/Checks/Maximum UpTime.ps1 diff --git a/scripts_staging/Checks/Maximum UpTime.ps1 b/scripts_staging/Checks/Maximum UpTime.ps1 new file mode 100644 index 00000000..2cb527cc --- /dev/null +++ b/scripts_staging/Checks/Maximum UpTime.ps1 @@ -0,0 +1,46 @@ +<# +.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 + +#> + +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 "Uptime OK" + #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 From 4ef08b7c8a815603718936bb82360756c4b042f3 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:43:22 +0000 Subject: [PATCH 149/447] Update ./scripts/Fixes/RDS Fix taskbar.ps1 --- scripts_staging/Fixes/RDS Fix taskbar.ps1 | 39 +++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 scripts_staging/Fixes/RDS Fix taskbar.ps1 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 From c044c76cb67dd301eb1aa206d55f3327dd7bb721 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:43:24 +0000 Subject: [PATCH 150/447] Update ./scripts/Fixes/Resync time NTP.ps1 --- scripts_staging/Fixes/Resync time NTP.ps1 | 44 +++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 scripts_staging/Fixes/Resync time NTP.ps1 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 +} From 23599b28f572efff6b88a7c92435145d2075b892 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:43:35 +0000 Subject: [PATCH 151/447] Update ./scripts/Tools/SSL Certificate manager.ps1 --- .../Tools/SSL Certificate manager.ps1 | 283 ++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 scripts_staging/Tools/SSL Certificate manager.ps1 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 From 356b46db835a94a9ed29c8bd64b5541029625efc Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:56:00 +0000 Subject: [PATCH 152/447] Update ./scripts/Tasks/Start Eset update.ps1 --- scripts_staging/Tasks/Start Eset update.ps1 | 74 +++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 scripts_staging/Tasks/Start Eset update.ps1 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 From 50331e62e5c11e1fc0a4f783938f5bbda881a018 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:56:08 +0000 Subject: [PATCH 153/447] Update ./scripts/Tools/Reset permission of target folder.ps1 --- .../Reset permission of target folder.ps1 | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 scripts_staging/Tools/Reset permission of target folder.ps1 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 From eca3c15073c1ab75ebf27871b4e7dd2bd0086ccc Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 21:01:13 +0000 Subject: [PATCH 154/447] Update ./scripts/Build/Create generic admin account.ps1 --- .../Build/Create generic admin account.ps1 | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 scripts_staging/Build/Create generic admin account.ps1 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 From 0f0c82378773b4c5ef69bf5811b81ac816ee501d Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 21:01:40 +0000 Subject: [PATCH 155/447] Update ./scripts/Checks/Active Directory Health.ps1 --- .../Checks/Active Directory Health.ps1 | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 scripts_staging/Checks/Active Directory Health.ps1 diff --git a/scripts_staging/Checks/Active Directory Health.ps1 b/scripts_staging/Checks/Active Directory Health.ps1 new file mode 100644 index 00000000..a112caef --- /dev/null +++ b/scripts_staging/Checks/Active Directory Health.ps1 @@ -0,0 +1,126 @@ +<# +.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: ??? + #public + +.CHANGELOG + +#> + +# Initialize exit code +$exitCode = 0 + +# Function to perform Active Directory tests +function CheckAD { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] + [string[]]$Tests + ) + + process { + $results = @{} + + foreach ($test in $Tests) { + $output = dcdiag /test:$test + + if ($output -notmatch "chou") { + $results[$test] = "OK" + } else { + $results[$test] = "Failed!" + $global:exitCode++ + } + + # Output individual test result + Write-Host "DCDIAG Test: $test Result: $($results[$test])" + } + + $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)" -ForegroundColor Red + $global:exitCode++ + } else { + Write-Host "$GPOName : USER Versions identiques" -ForegroundColor Green + } + + # COMPUTER - Compare version numbers + if ($NumComputerSysvol -ne $NumComputerAD) { + Write-Host "$GPOName ($GPOId) : COMPUTER Versions différentes (Sysvol : $NumComputerSysvol | AD : $NumComputerAD)" -ForegroundColor Red + $global:exitCode++ + } else { + Write-Host "$GPOName : COMPUTER Versions identiques" -ForegroundColor Green + } + } + Write-Host "GPO USER/COMPUTER Version OK" -ForegroundColor Green + } +} + +# Check if Active Directory Domain Services feature is installed +try { + $adFeature = Get-WindowsFeature -Name AD-Domain-Services -ErrorAction Stop + + if ($adFeature.InstallState -eq "Installed") { + # Specify your AD tests + $tests = ("Advertising", "FrsSysVol", "MachineAccount", "Replications", "RidManager", "Services", "FsmoCheck", "SysVolCheck") + # Call the function with the AD tests + Write-Host "DCDIAG" + $testResults = CheckAD -Tests $tests + + $failedTests = $testResults.GetEnumerator() | Where-Object { $_.Value -eq "Failed!" } + + if ($failedTests) { + Write-Error "Some Active Directory tests failed." + $failedTests | ForEach-Object { Write-Error "$($_.Key) test failed." } + $global:exitCode += $failedTests.Count + } else { + Write-Host "All Active Directory tests passed successfully." + } + Write-Host "" + Write-Host "GPO Versions checks" + # Call the function to compare GPO versions + Compare-GPOVersions + } 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++ +} + +exit $exitCode \ No newline at end of file From 5f672d8a93c8cc65ca8ae6f22769ec7710226839 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 21:48:33 +0000 Subject: [PATCH 156/447] Update ./scripts/Checks/Is TCP port open.ps1 --- scripts_staging/Checks/Is TCP port open.ps1 | 87 +++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 scripts_staging/Checks/Is TCP port open.ps1 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 From f924415ee4a88c3c07620d0c7ee1b3a65e29ad5e Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 21:48:41 +0000 Subject: [PATCH 157/447] Update ./scripts/Fixes/Ensure all services with startup type Automatic are running.ps1 --- ...ith startup type Automatic are running.ps1 | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 scripts_staging/Fixes/Ensure all services with startup type Automatic are running.ps1 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." +} From a2fed5d994d7e0c9d795da97a5224c6423b2a829 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 21:54:43 +0000 Subject: [PATCH 158/447] Update ./scripts/Checks/Task Scheduler scanner.ps1 --- .../Checks/Task Scheduler scanner.ps1 | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 scripts_staging/Checks/Task Scheduler scanner.ps1 diff --git a/scripts_staging/Checks/Task Scheduler scanner.ps1 b/scripts_staging/Checks/Task Scheduler scanner.ps1 new file mode 100644 index 00000000..160fe2aa --- /dev/null +++ b/scripts_staging/Checks/Task Scheduler scanner.ps1 @@ -0,0 +1,135 @@ +<# +.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 + + +.NOTES + Author: SAN + Date: ??? + #public + +.CHANGELOG + + +.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\" +) +$ignoreNames = @( + "Optimize Start Menu Cache", + "DropboxUpdateTaskUserS", + "GoogleUpdate", + "User_Feed_Synchronization", + "Adobe Acrobat", + "RMM", + "edgeupdate", + "OneDrive Reporting Task", + "ZoomUpdateTaskUser", + "OneDrive Standalone Update 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 From 4b22d68ff2f7e7dcce59162ff5e5815cae1213d9 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 23:44:46 +0000 Subject: [PATCH 159/447] Update ./snippets/VHDXCleaner.ps1 --- scripts_staging/snippets/VHDXCleaner.ps1 | 192 +++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 scripts_staging/snippets/VHDXCleaner.ps1 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 + } + } + } +} From 3ab33299d2778e912a56bab7a1423c45010d959c Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Dec 2024 23:50:58 +0000 Subject: [PATCH 160/447] Update ./scripts/Tools/Windows update force install new updates.ps1 --- ...ndows update force install new updates.ps1 | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 scripts_staging/Tools/Windows update force install new updates.ps1 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..8ecd0dd1 --- /dev/null +++ b/scripts_staging/Tools/Windows update force install new updates.ps1 @@ -0,0 +1,67 @@ +<# +.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 + +#> + + + + +{{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 + } +} + +# Function to start the update process +function StartUpdateProcess { + Write-Host "Start updates:" + Get-WindowsUpdate -Verbose -Install -AcceptAll -AutoReboot +} + +# Check updates +Write-Host "Check for available updates:" +Get-WindowsUpdate + +# Start update process +Write-Host "Updating Windows" +StartUpdateProcess + +Write-Host "Output of the last updates results:" +$results = Get-WULastResults +$lastInstallationSuccessDate = $results | Select-Object -ExpandProperty LastInstallationSuccessDate +$lastInstallationSuccessDate \ No newline at end of file From 6560fb2661ad7f527bb08c1dc668639e3d0e76ba Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:19:41 +0000 Subject: [PATCH 161/447] Update ./scripts/Checks/Active Directory Health.ps1 --- scripts_staging/Checks/Active Directory Health.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts_staging/Checks/Active Directory Health.ps1 b/scripts_staging/Checks/Active Directory Health.ps1 index a112caef..c72bc466 100644 --- a/scripts_staging/Checks/Active Directory Health.ps1 +++ b/scripts_staging/Checks/Active Directory Health.ps1 @@ -9,7 +9,7 @@ .NOTES Author: SAN - Date: ??? + Date: 01.01.24 #public .CHANGELOG From 3ab9e46e4dcfd89767abb8a0d1289e7524b5e0d4 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:20:03 +0000 Subject: [PATCH 162/447] Update ./scripts/Tools/Activate windows with KMS.ps1 --- scripts_staging/Tools/Activate windows with KMS.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts_staging/Tools/Activate windows with KMS.ps1 b/scripts_staging/Tools/Activate windows with KMS.ps1 index 7f8de58c..49bf0f79 100644 --- a/scripts_staging/Tools/Activate windows with KMS.ps1 +++ b/scripts_staging/Tools/Activate windows with KMS.ps1 @@ -11,7 +11,7 @@ .NOTES Author: SAN - Date: ??? + Date: 14.11.24 #public .CHANGELOG @@ -19,6 +19,8 @@ .TODO Convert the script to use the PowerShell module as the future of vbs is uncertain. + see code bellow for prototype + #> From 5413b3de8f24fc2755d337ab30426001366a7dc7 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:26:15 +0000 Subject: [PATCH 163/447] Update ./scripts/Tasks/Auto-logoff users.ps1 --- scripts_staging/Tasks/Auto-logoff users.ps1 | 156 ++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 scripts_staging/Tasks/Auto-logoff users.ps1 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 From 2b9ddac6902c9516d57547f3a0b7e129686117f6 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:34:11 +0000 Subject: [PATCH 164/447] Update ./scripts/Tasks/Change user password.ps1 --- .../Tasks/Change user password.ps1 | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 scripts_staging/Tasks/Change user password.ps1 diff --git a/scripts_staging/Tasks/Change user password.ps1 b/scripts_staging/Tasks/Change user password.ps1 new file mode 100644 index 00000000..2638cb7a --- /dev/null +++ b/scripts_staging/Tasks/Change user password.ps1 @@ -0,0 +1,38 @@ +<# +.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 + +.TODO + Do not allow to change the password on non primary DC it causes conflicts + move param to env +#> + +param( + [Parameter(Mandatory=$true)] + [string]$username +) + +#Call snippet +{{GeneratedPassphrase}} +$newPassword = $GeneratedPassphrase + +# Set the new password for the user +net user $username $newPassword + +# Check if the password change was successful +if ($LASTEXITCODE -eq 0) { + Write-Host "$newPassword" +} else { + Write-Host "Password change for $username failed. Please check for errors." + exit 1 +} \ No newline at end of file From 43e7282c664bf4ae0e7ac6e88beb45fa443a6c10 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:46:20 +0000 Subject: [PATCH 165/447] Update ./scripts/Collectors/Collect Licensing 1 General.ps1 --- .../Collect Licensing 1 General.ps1 | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 scripts_staging/Collectors/Collect Licensing 1 General.ps1 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 From 5aa9e92df743bfdaf553eafe735cc2eff9bcda5c Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:46:22 +0000 Subject: [PATCH 166/447] Update ./scripts/Collectors/Collect Licensing 2 SQL.ps1 --- .../Collectors/Collect Licensing 2 SQL.ps1 | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 scripts_staging/Collectors/Collect Licensing 2 SQL.ps1 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 From f514812297d31d2a06ea291b56fdb2e35035993f Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:46:24 +0000 Subject: [PATCH 167/447] Update ./scripts/Collectors/Collect Licensing 3 Exchange.ps1 --- .../Collect Licensing 3 Exchange.ps1 | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 scripts_staging/Collectors/Collect Licensing 3 Exchange.ps1 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 From 4b9f23543d4978c8a633479fb8406e28e96942cc Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:52:15 +0000 Subject: [PATCH 168/447] Update ./scripts/Collectors/Collect Licensing 4 RDS.ps1 --- .../Collectors/Collect Licensing 4 RDS.ps1 | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 scripts_staging/Collectors/Collect Licensing 4 RDS.ps1 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 From df1a574e8fcaf464918126edccdd499db95df260 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:52:18 +0000 Subject: [PATCH 169/447] Update ./scripts/Collectors/Collect Licensing 5 Office.ps1 --- .../Collectors/Collect Licensing 5 Office.ps1 | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 scripts_staging/Collectors/Collect Licensing 5 Office.ps1 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 From 7a2d483b550bd5f85d71ce9bb904533d53c2d82d Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:12:41 +0000 Subject: [PATCH 170/447] Update ./scripts/Checks/DFS replication.ps1 --- scripts_staging/Checks/DFS replication.ps1 | 133 +++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 scripts_staging/Checks/DFS replication.ps1 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 +} From ace1448ccca8733271ed8cf51f4026c78c968cc4 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:12:43 +0000 Subject: [PATCH 171/447] Update ./scripts/Checks/Disk Free Space.ps1 --- scripts_staging/Checks/Disk Free Space.ps1 | 99 ++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 scripts_staging/Checks/Disk Free Space.ps1 diff --git a/scripts_staging/Checks/Disk Free Space.ps1 b/scripts_staging/Checks/Disk Free Space.ps1 new file mode 100644 index 00000000..2f71887b --- /dev/null +++ b/scripts_staging/Checks/Disk Free Space.ps1 @@ -0,0 +1,99 @@ +<# +.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 + +.TODO + Add debug flag + move flags to env + +#> + + +param( + [int]$warningThreshold = 10, + [int]$errorThreshold = 5, + [string[]]$ignoreDisks = @() +) + +function CheckDiskSpace { + [CmdletBinding()] + param() + + # Get all local drives excluding network drives and the ones specified to ignore + $drives = Get-WmiObject Win32_LogicalDisk | Where-Object { $_.DriveType -eq 3 -and $_.DeviceID -notin $ignoreDisks } + + $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 ($failedDrives.Count -gt 0) { + # Write-Host "ERROR: The following drives have less than $($errorThreshold)% free space:" + # $failedDrives | ForEach-Object { Write-Host "$($_.DeviceID): $([math]::Round(($_.FreeSpace / $_.Size) * 100, 2))%" } + # Write-Host "ERROR: exit 2" + $host.SetShouldExit(2) + } + elseif ($warningDrives.Count -gt 0) { + # Write-Host "WARNING: The following drives have less than $($warningThreshold)% free space:" + # $warningDrives | ForEach-Object { Write-Host "$($_.DeviceID): $([math]::Round(($_.FreeSpace / $_.Size) * 100, 2))%" } + # Write-Host "Warning: exit 1" + $host.SetShouldExit(1) + } + else { + # Write-Host "OK: All drives have sufficient free space." + $host.SetShouldExit(0) + } +} + +# Execute the function +CheckDiskSpace \ No newline at end of file From e340df3496cdd013a42485108264042ddcdd1ebb Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:19:15 +0000 Subject: [PATCH 172/447] Update ./scripts/Lab/Fake CheckRandom Alert 2.py --- .../Lab/Fake CheckRandom Alert 2.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 scripts_staging/Lab/Fake CheckRandom Alert 2.py 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 From 9954e127e72228ab75a9ec25b24db56d582ba549 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:19:18 +0000 Subject: [PATCH 173/447] Update ./scripts/Lab/Fake CheckRandom Alert.py --- scripts_staging/Lab/Fake CheckRandom Alert.py | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 scripts_staging/Lab/Fake CheckRandom Alert.py 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 From 67b3eb309133469355ea3b7f701e852e7677c470 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:25:33 +0000 Subject: [PATCH 174/447] Update ./scripts/Tools/Force Azureo365 AD sync.ps1 --- .../Tools/Force Azureo365 AD sync.ps1 | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 scripts_staging/Tools/Force Azureo365 AD sync.ps1 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." +} From a0f05e81c7a4f87c1f682141440b993bb022ce81 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:31:22 +0000 Subject: [PATCH 175/447] Update ./scripts/Collectors/get Domains or Workgroup name.ps1 --- .../get Domains or Workgroup name.ps1 | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 scripts_staging/Collectors/get Domains or Workgroup name.ps1 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 From f64f73f27b1c97cf12bd30eef1344ae071910a4d Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:31:52 +0000 Subject: [PATCH 176/447] Update ./scripts/Tools/Get last shutdown info.ps1 --- .../Tools/Get last shutdown info.ps1 | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 scripts_staging/Tools/Get last shutdown info.ps1 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..8749570c --- /dev/null +++ b/scripts_staging/Tools/Get last shutdown info.ps1 @@ -0,0 +1,41 @@ +<# +.SYNOPSIS + Retrieves and displays system uptime and shutdown event information. + +.DESCRIPTION + This script retrieves the last boot time of the system, calculates the uptime (in days, hours, and minutes), + and retrieves the most recent shutdown event from the system's event log (EventID 1074). + +.NOTES + Author: SAN + Date: 03.10.24 + #public + +.CHANGELOG + SAN 12.12.24 Code cleanup + +#> + +$lastBootTime = (Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime +$uptime = (Get-Date) - $lastBootTime +$shutdownEvent = Get-WinEvent -LogName System -FilterXPath "*[System/EventID=1074]" | Select-Object -First 1 + +Write-Output "===========================" +Write-Output " Last Reboot Information" +Write-Output "===========================" + +Write-Output "Last Boot Time : $($lastBootTime)" +Write-Output "Uptime (since last boot) : $($uptime.Days) days, $($uptime.Hours) hours, $($uptime.Minutes) minutes" +Write-Output "Event Log Time : $($shutdownEvent.TimeCreated)" + +Write-Output "===========================" +Write-Output " All Event Properties" +Write-Output "===========================" + +Write-Output "Initiating Process/Executable : $($shutdownEvent.Properties[0].Value)" +Write-Output "Initiating Machine : $($shutdownEvent.Properties[1].Value)" +Write-Output "Shutdown Reason : $($shutdownEvent.Properties[2].Value)" +Write-Output "Shutdown Code : $($shutdownEvent.Properties[3].Value)" +Write-Output "Shutdown Type : $($shutdownEvent.Properties[4].Value)" +Write-Output "Additional Info : $($shutdownEvent.Properties[5].Value)" +Write-Output "User Account : $($shutdownEvent.Properties[6].Value)" From ebcaa92a5192ca55663a9d520fe28a2d34daee30 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:39:13 +0000 Subject: [PATCH 177/447] Update ./scripts/Collectors/OS Install Date.ps1 --- .../Collectors/OS Install Date.ps1 | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 scripts_staging/Collectors/OS Install Date.ps1 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 From 7f0ace1e2a5e11d94c507470e07635e3f4104f60 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:39:46 +0000 Subject: [PATCH 178/447] Update ./scripts/Tools/Get logon events.ps1 --- scripts_staging/Tools/Get logon events.ps1 | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 scripts_staging/Tools/Get logon events.ps1 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 From 1685765f8d728e62c5f9a4fc4266a139858cf602 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:45:43 +0000 Subject: [PATCH 179/447] Update ./scripts/Collectors/Retrieve all IIS bindings.ps1 --- .../Collectors/Retrieve all IIS bindings.ps1 | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 scripts_staging/Collectors/Retrieve all IIS bindings.ps1 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 From a5d525f7b36c05daac3908cd5a1c23e245985c76 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:52:03 +0000 Subject: [PATCH 180/447] Update ./scripts/Lab/Send mail test.ps1 --- scripts_staging/Lab/Send mail test.ps1 | 73 ++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 scripts_staging/Lab/Send mail test.ps1 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!" From 6f7e4d9bf47af3bab90f9134bf2c54222d1e40d4 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:16:15 +0000 Subject: [PATCH 181/447] Update ./scripts/Checks/is RDP port ok.ps1 --- scripts_staging/Checks/is RDP port ok.ps1 | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts_staging/Checks/is RDP port ok.ps1 b/scripts_staging/Checks/is RDP port ok.ps1 index dff54378..6a1fa48b 100644 --- a/scripts_staging/Checks/is RDP port ok.ps1 +++ b/scripts_staging/Checks/is RDP port ok.ps1 @@ -11,6 +11,10 @@ Author: SAN Date: 26.09.2024 #public + +.CHANGELOG + 12.12.24 SAN Changed outputs + #> $port = 3389 @@ -20,9 +24,9 @@ $address = "localhost" if (Get-Command Test-NetConnection -ErrorAction SilentlyContinue) { $tcpConnection = Test-NetConnection -ComputerName $address -Port $port if ($tcpConnection.TcpTestSucceeded) { - Write-Output "Port $port is open." + Write-Output "RDP is open." } else { - Write-Output "Port $port is not open." + Write-Output "Port $port is not open RDP will not work." exit 1 } } else { @@ -30,10 +34,10 @@ if (Get-Command Test-NetConnection -ErrorAction SilentlyContinue) { try { $tcpClient = New-Object System.Net.Sockets.TcpClient $tcpClient.Connect($address, $port) - Write-Output "Port $port is open." + Write-Output "RDP is open but TNC does not work." $tcpClient.Close() } catch { - Write-Output "Port $port is not open." + Write-Output "Port $port is not open and TNC does not work." exit 1 } } \ No newline at end of file From bacb2d219933b2da9a7a80582f24df4f2a4685ac Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:28:15 +0000 Subject: [PATCH 182/447] Update ./scripts/Checks/Boot mode.ps1 --- scripts_staging/Checks/Boot mode.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts_staging/Checks/Boot mode.ps1 b/scripts_staging/Checks/Boot mode.ps1 index c5dbe18e..b77e31df 100644 --- a/scripts_staging/Checks/Boot mode.ps1 +++ b/scripts_staging/Checks/Boot mode.ps1 @@ -12,7 +12,7 @@ #public .CHANGELOG - + 12.12.24 SAN Changed outputs #> @@ -21,9 +21,9 @@ $regPath = "HKLM:\SYSTEM\CurrentControlSet\Control\SafeBoot\Option" $safeModeKeyExists = Test-Path $regPath if ($safeModeKeyExists) { - Write-Host "System is booted in Safe Mode." + Write-Host "KO: System is booted in Safe Mode." exit 1 } else { - Write-Host "System is not booted in Safe Mode." + Write-Host "OK: System is not booted in Safe Mode." exit 0 } \ No newline at end of file From c329a155a68dc0e14acec17d7fe8e9a2b639d9ca Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:28:17 +0000 Subject: [PATCH 183/447] Update ./scripts/Checks/Maximum UpTime.ps1 --- scripts_staging/Checks/Maximum UpTime.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts_staging/Checks/Maximum UpTime.ps1 b/scripts_staging/Checks/Maximum UpTime.ps1 index 2cb527cc..f24dce87 100644 --- a/scripts_staging/Checks/Maximum UpTime.ps1 +++ b/scripts_staging/Checks/Maximum UpTime.ps1 @@ -19,6 +19,7 @@ move var to env .CHANGELOG + 12.12.24 SAN Changed outputs #> @@ -39,7 +40,7 @@ if ($uptimeDays -gt $MaxTime) { Write-Output "The computer has an uptime greater than $MaxTime days." exit 1 } else { - Write-Output "Uptime OK" + 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 From c7ca46519fec7f3b51dd26f3239fcd9682db2042 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:28:19 +0000 Subject: [PATCH 184/447] Update ./scripts/Checks/SQL Health.ps1 --- scripts_staging/Checks/SQL Health.ps1 | 172 ++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 scripts_staging/Checks/SQL Health.ps1 diff --git a/scripts_staging/Checks/SQL Health.ps1 b/scripts_staging/Checks/SQL Health.ps1 new file mode 100644 index 00000000..254ba924 --- /dev/null +++ b/scripts_staging/Checks/SQL Health.ps1 @@ -0,0 +1,172 @@ +<# +.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 and blocked requests. + It provides a modular approach with separate functions for each check. + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.TODO + Need to go back to version 1 and implement the missing functions of this check + handle Master-slave monitoring + +.CHANGELOG + SAN 12.12.24 Changed outputs +#> + +function Get-SqlServerVersion { + Write-Host "function Get-SqlServerVersion" + + $computername = $env:computername + $instances = (Get-ItemProperty -Path "HKLM:\Software\Microsoft\Microsoft SQL Server" -Name "InstalledInstances").InstalledInstances + + $errorEncountered = $false # Initialize flag for tracking errors + + foreach ($instance in $instances) { + if ($instance -eq "MSSQLSERVER") { + $serverInstance = "localhost" + } else { + $portsql = (Get-ItemProperty -Path "HKLM:\Software\Microsoft\Microsoft SQL Server\$instance\MSSQLServer\SuperSocketNetLib\Tcp" -Name "TcpPort").TcpPort + $serverInstance = "$computername\$instance,$portsql" + } + + $cmdName = 'Invoke-Sqlcmd' + + if (-not (Get-Command $cmdName -ErrorAction SilentlyContinue)) { + Add-PSSnapin SqlServerCmdletSnapin100 + Add-PSSnapin SqlServerProviderSnapin100 + } + + try { + $version = Invoke-Sqlcmd -ServerInstance $serverInstance -Query "SELECT @@VERSION;" -QueryTimeout 3 -ErrorAction Stop + + if (-not $version) { + Write-Host "Error: SQL Check Failed for $serverInstance" + $errorEncountered = $true # Set flag to indicate error + } else { + Write-Host "OK: $($serverInstance) $($version[0].replace("`n`t"," "))" + } + } catch { + Write-Host "Error: $($_.Exception.Message) for $serverInstance" + $errorEncountered = $true # Set flag to indicate error + } + } + + if ($errorEncountered) { + return "Error" # Return "Error" if any error occurred during the process + } else { + return "OK" # Return "OK" if no errors occurred + } +} + + + + +function Get-BlockedSqlRequests { + Write-Host "function Get-BlockedSqlRequests" + + $computername = $env:computername + $instances = (Get-ItemProperty -Path "HKLM:\Software\Microsoft\Microsoft SQL Server" -Name "InstalledInstances").InstalledInstances + + $errorEncountered = $false # Initialize flag for tracking errors + + foreach ($instance in $instances) { + if ($instance -eq "MSSQLSERVER") { + $serverInstance = "localhost" + } else { + $portsql = (Get-ItemProperty -Path "HKLM:\Software\Microsoft\Microsoft SQL Server\$instance\MSSQLServer\SuperSocketNetLib\Tcp" -Name "TcpPort").TcpPort + $serverInstance = "$computername\$instance,$portsql" + } + + $mysqlrequest = @" + USE master + SELECT db.name DBName, + tl.request_session_id, + wt.blocking_session_id, + OBJECT_NAME(p.OBJECT_ID) BlockedObjectName, + tl.resource_type, + h1.TEXT AS RequestingText, + h2.TEXT AS BlockingTest, + tl.request_mode + FROM sys.dm_tran_locks AS tl + INNER JOIN sys.databases db ON db.database_id = tl.resource_database_id + INNER JOIN sys.dm_os_waiting_tasks AS wt ON tl.lock_owner_address = wt.resource_address + INNER JOIN sys.partitions AS p ON p.hobt_id = tl.resource_associated_entity_id + INNER JOIN sys.dm_exec_connections ec1 ON ec1.session_id = tl.request_session_id + INNER 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 +"@ + + $cmdName = 'Invoke-Sqlcmd' + + if (-not (Get-Command $cmdName -ErrorAction SilentlyContinue)) { + Add-PSSnapin SqlServerCmdletSnapin100 + Add-PSSnapin SqlServerProviderSnapin100 + } + + try { + $result = Invoke-Sqlcmd -ServerInstance $serverInstance -Query $mysqlrequest -QueryTimeout 60 -ErrorAction Stop + + if (-not $result) { + Write-Host "OK: $($serverInstance) No Blocking Requests" + } else { + Write-Host "Error: Unable to retrieve blocked requests for $($serverInstance). $($result[0].Exception.Message)" + $errorEncountered = $true # Set flag to indicate error + } + } catch { + Write-Host "Error: Unable to retrieve blocked requests for $($serverInstance). $($Error[0].Exception.Message)" + $errorEncountered = $true # Set flag to indicate error + } + } + + if ($errorEncountered) { + return "Error" # Return "Error" if any error occurred during the process + } else { + return "OK" # Return "OK" if no errors occurred + } +} + + +function Check-SqlServerInstallation { + # Check if Invoke-Sqlcmd is available + if (Get-Command Invoke-Sqlcmd -ErrorAction SilentlyContinue) { + return $true + } else { + Write-Host "SQL Server is not installed on this device" + return $false + } +} + +# Check if SQL Server is installed before proceeding with health checks +if (Check-SqlServerInstallation) { + $cmdName = 'Invoke-Sqlcmd' + + # Run each function and report the result + $result1 = Get-SqlServerVersion + $result2 = Get-BlockedSqlRequests + + # Check the results and provide the overall status + if ($result1 -eq "OK" -and $result2 -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" + } + + $errorList = $errorComponents -join ", " + Write-Host "Overall Status: Some components encountered errors. Errors in: $errorList" + Exit 1 + } +} \ No newline at end of file From fe829ea58c074a01db897cadb917971115fdf482 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 14:54:16 +0000 Subject: [PATCH 185/447] Update ./scripts/Checks/Last errors logs.ps1 --- scripts_staging/Checks/Last errors logs.ps1 | 129 ++++++++++++++++---- 1 file changed, 103 insertions(+), 26 deletions(-) diff --git a/scripts_staging/Checks/Last errors logs.ps1 b/scripts_staging/Checks/Last errors logs.ps1 index c9723791..8424b4b2 100644 --- a/scripts_staging/Checks/Last errors logs.ps1 +++ b/scripts_staging/Checks/Last errors logs.ps1 @@ -12,45 +12,120 @@ 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 + + .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 #> -# Define the time ranges + +$defaultEventIds = @(10016) #add value comma separeted +$defaultKeywords = @("gupdate") #add value comma separeted + +$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) -# Define a list of event IDs to ignore -$ignoredEventIds = @(10016, 10016) +$allErrors48h = Get-WinEvent -FilterHashtable @{LogName='System'; Level=2; StartTime=$start48h} -ErrorAction SilentlyContinue -#10016 safe to ignore https://learn.microsoft.com/en-us/troubleshoot/windows-client/application-management/event-10016-logged-when-accessing-dcom +$eventsWithIdFilter = $allErrors48h | Where-Object { $ignoredEventIds -contains $_.Id.ToString() } -# Retrieve the last 20 error events in the last 48 hours -$errors48h = Get-WinEvent -FilterHashtable @{LogName='System'; Level=2; StartTime=$start48h} -MaxEvents 20 -ErrorAction SilentlyContinue +$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 +} -# Filter out events with IDs in the ignored list -$filteredErrors48h = $errors48h | Where-Object { $ignoredEventIds -notcontains $_.Id } +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." + } -# Count of errors found in the last 48 hours (after ignoring specified event IDs) -$errorCount = if ($filteredErrors48h) { $filteredErrors48h.Count } else { 0 } + 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." + } +} -if ($errorCount -gt 0) { - # Output the number of errors found - Write-Output "$errorCount error(s) found recently." +$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 errors are found, display them -if ($errorCount -gt 0) { - Write-Output "Last 20 Error Events in the last 48 hours (excluding ignored event IDs):" - $filteredErrors48h | ForEach-Object { +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)" @@ -58,19 +133,21 @@ if ($errorCount -gt 0) { } } -# Retrieve the error events from the last 12 hours -$errors12h = Get-WinEvent -FilterHashtable @{LogName='System'; Level=2; StartTime=$start12h} -ErrorAction SilentlyContinue +$errors12h = $remainingErrors48h | Where-Object { $_.TimeCreated -gt $start12h } -# Filter out events with IDs in the ignored list for the 12-hour check -$filteredErrors12h = $errors12h | Where-Object { $ignoredEventIds -notcontains $_.Id } - -# Check if there are 4 or more errors in the last 12 hours (excluding ignored event IDs) -if ($filteredErrors12h -and $filteredErrors12h.Count -ge 4) { +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 (!$filteredErrors12h) { - Write-Output "No error events found in the last 12 hours." + 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." } From c3620ba3af7048c55eddccfffcda5ecb13bc3380 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 15:14:48 +0000 Subject: [PATCH 186/447] Update ./scripts/TasksUpdater/Updater P1 Template WU, SU, PS and Cleaner.ps1 --- ...ter P1 Template WU, SU, PS and Cleaner.ps1 | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 scripts_staging/TasksUpdater/Updater P1 Template WU, SU, PS and Cleaner.ps1 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..8256db08 --- /dev/null +++ b/scripts_staging/TasksUpdater/Updater P1 Template WU, SU, PS and Cleaner.ps1 @@ -0,0 +1,259 @@ +<# +.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 + +#> + + +$Debug = $false + +# Check if CurrentSchedules contains "skip" or forcechange is not "true" +if ($Env:CurrentSchedules -match "skip" -or $Env:forcechange -ne "true") { + # Check environment variable + if ($Env:CurrentSchedules -ne $null -and $Env:CurrentSchedules -notmatch "Collected") { + # Split the variable into lines, filter out lines containing "CurrentSchedules", and join them back into a single string + $filteredLines = $Env:CurrentSchedules -split "`n" | Where-Object { $_ -notmatch "CurrentSchedules" } + + # Join the lines and trim any extra spaces and line breaks + $cleanedOutput = ($filteredLines -join "`n").Trim() + + # Remove any remaining consecutive line breaks (more than one in a row) + $cleanedOutput = $cleanedOutput -replace "(\r?\n){2,}", "`n" + + Write-Host $cleanedOutput + exit + } +} + + + +# 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 + +# 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" # Week 3 + $softwareUpdateDay = "1st Tuesday" # Week 1 + $tempFileCleanupDay = "4th Tuesday" # Week 4 + $powershellUpdateDay = "2nd Tuesday" # Week 2 + } + "DCOdd" { + $windowsUpdateDay = "2nd Tuesday" # Week 2 + $softwareUpdateDay = "4th Tuesday" # Week 4 + $tempFileCleanupDay = "3rd Tuesday" # Week 3 + $powershellUpdateDay = "1st Tuesday" # Week 1 + } + "DBEven" { + $windowsUpdateDay = "1st Wednesday" # Week 1 + $softwareUpdateDay = "3rd Wednesday" # Week 3 + $tempFileCleanupDay = "4th Wednesday" # Week 4 + $powershellUpdateDay = "2nd Wednesday" # Week 2 + } + "DBOdd" { + $windowsUpdateDay = "2nd Wednesday" # Week 2 + $softwareUpdateDay = "4th Wednesday" # Week 4 + $tempFileCleanupDay = "1st Wednesday" # Week 1 + $powershellUpdateDay = "3rd Wednesday" # Week 3 + } + "APPEven" { + $windowsUpdateDay = "3rd Thursday" # Week 3 + $softwareUpdateDay = "1st Thursday" # Week 1 + $tempFileCleanupDay = "4th Thursday" # Week 4 + $powershellUpdateDay = "2nd Thursday" # Week 2 + } + "APPOdd" { + $windowsUpdateDay = "2nd Thursday" # Week 2 + $softwareUpdateDay = "4th Thursday" # Week 4 + $tempFileCleanupDay = "1st Thursday" # Week 1 + $powershellUpdateDay = "3rd Thursday" # Week 3 + } + "RDSEven" { + $windowsUpdateDay = "4th Tuesday" # Week 4 + $softwareUpdateDay = "1st Tuesday" # Week 1 + $tempFileCleanupDay = "2nd Tuesday" # Week 2 + $powershellUpdateDay = "3rd Tuesday" # Week 3 + } + "RDSOdd" { + $windowsUpdateDay = "3rd Tuesday" # Week 3 + $softwareUpdateDay = "2nd Tuesday" # Week 2 + $tempFileCleanupDay = "4th Tuesday" # Week 4 + $powershellUpdateDay = "1st Tuesday" # Week 1 + } + "ExchangeEven" { + $windowsUpdateDay = "4th Wednesday" # Week 4 + $softwareUpdateDay = "2nd Wednesday" # Week 2 + $tempFileCleanupDay = "3rd Wednesday" # Week 3 + $powershellUpdateDay = "1st Wednesday" # Week 1 + } + "ExchangeOdd" { + $windowsUpdateDay = "3rd Wednesday" # Week 3 + $softwareUpdateDay = "1st Wednesday" # Week 1 + $tempFileCleanupDay = "4th Wednesday" # Week 4 + $powershellUpdateDay = "2nd Wednesday" # Week 2 + } + default { + $windowsUpdateDay = "4th Thursday" # Week 4 + $softwareUpdateDay = "2nd Thursday" # Week 2 + $tempFileCleanupDay = "3rd Thursday" # Week 3 + $powershellUpdateDay = "1st Thursday" # Week 1 + } +} + + +# 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 + +$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 From 6113a41b55cf59afe9ef6a19b45e8311274e30ee Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 12 Dec 2024 15:41:04 +0000 Subject: [PATCH 187/447] Update ./scripts/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 --- ...er P2 Scheduler WU, SU, PS and Cleaner.ps1 | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 scripts_staging/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 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..5db5a083 --- /dev/null +++ b/scripts_staging/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 @@ -0,0 +1,113 @@ +<# +.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 + + +.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 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 From 66347ac303795b27f2d64aa4ff9327e380355044 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 13 Dec 2024 07:57:53 +0000 Subject: [PATCH 188/447] Update ./scripts/TasksUpdater/Updater P1 Template WU, SU, PS and Cleaner.ps1 --- .../Updater P1 Template WU, SU, PS and Cleaner.ps1 | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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 index 8256db08..88c337f3 100644 --- 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 @@ -32,9 +32,11 @@ .CHANGELOG 04.09.24 SAN Refactored to determine device categories and odd/even status for scheduling purposes. - + 13.12.24 SAN Removed useless check .TODO add debug flag to env + rename env CurrentSchedules to env CurrentTemplate and CurrentSchedules to ExistingTemplate + #> @@ -42,10 +44,10 @@ $Debug = $false # Check if CurrentSchedules contains "skip" or forcechange is not "true" -if ($Env:CurrentSchedules -match "skip" -or $Env:forcechange -ne "true") { +if ($Env:forcechange -ne "true") { # Check environment variable if ($Env:CurrentSchedules -ne $null -and $Env:CurrentSchedules -notmatch "Collected") { - # Split the variable into lines, filter out lines containing "CurrentSchedules", and join them back into a single string + # Split the variable into lines, filter out lines containing "CurrentSchedules", and join them back into a single string to fix any empty space that could be found $filteredLines = $Env:CurrentSchedules -split "`n" | Where-Object { $_ -notmatch "CurrentSchedules" } # Join the lines and trim any extra spaces and line breaks @@ -125,6 +127,8 @@ function Get-DigitSum($inputString) { # 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" @@ -248,6 +252,9 @@ $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') From 1348fc3723efde88aac0b0bb075dc78cc9785ea9 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 13 Dec 2024 09:26:33 +0000 Subject: [PATCH 189/447] Update ./snippets/Logging.ps1 --- scripts_staging/snippets/Logging.ps1 | 120 +++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 scripts_staging/snippets/Logging.ps1 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 +} + From e18e6a601e216b4435dcd74bf890e9fb24c87569 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:37:46 +0000 Subject: [PATCH 190/447] Update ./scripts/TasksUpdater/Updater P3 Run Cleaner.ps1 --- .../TasksUpdater/Updater P3 Run Cleaner.ps1 | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 scripts_staging/TasksUpdater/Updater P3 Run Cleaner.ps1 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..762e51cd --- /dev/null +++ b/scripts_staging/TasksUpdater/Updater P3 Run Cleaner.ps1 @@ -0,0 +1,54 @@ +<# +.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 + #public + + +.CHANGELOG + 28.11.24 SAN Incorporated VHDX cleaner. + 13.12.24 SAN Split logging from parser. +#> + + + +# 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 From 82b2db78056bad1b7c7cd8b85984ee07e3621c08 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:37:48 +0000 Subject: [PATCH 191/447] Update ./scripts/TasksUpdater/Updater P3 Run PS.ps1 --- .../TasksUpdater/Updater P3 Run PS.ps1 | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 scripts_staging/TasksUpdater/Updater P3 Run PS.ps1 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..71c67fcc --- /dev/null +++ b/scripts_staging/TasksUpdater/Updater P3 Run PS.ps1 @@ -0,0 +1,58 @@ +<# +.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. + +#> + + +# 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}} + +# Call the pwsh snippet +{{CallPowerShell7}} + +# 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 From 7ace1b40423cc13dd25e66c6a45f5da5fbb38b24 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:37:50 +0000 Subject: [PATCH 192/447] Update ./scripts/TasksUpdater/Updater P3 Run SU.ps1 --- .../TasksUpdater/Updater P3 Run SU.ps1 | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 scripts_staging/TasksUpdater/Updater P3 Run SU.ps1 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..38947a11 --- /dev/null +++ b/scripts_staging/TasksUpdater/Updater P3 Run SU.ps1 @@ -0,0 +1,166 @@ +<# +.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}} + +.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 + #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. + +.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 +# List outdated packages and capture output +$outdatedPackages = choco outdated | Out-String +# Upgrade all packages and capture output +$upgradeResult = choco upgrade all -y | Out-String + +Write-Host "" +Write-Host "------------------------------------------------------------" +Write-Host "" +Write-Host "Outdated Packages:" +Write-Host $outdatedPackages +Write-Host "------------------------------------------------------------" +Write-Host "Upgrade Result:" +Write-Host $upgradeResult +Write-Host "" +Write-Host "------------------------------------------------------------" +Write-Host "" + + +# 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 From 3447f0df66946b3b768ed4285f0d35640e73313f Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:37:52 +0000 Subject: [PATCH 193/447] Update ./scripts/TasksUpdater/Updater P3 Run WU.ps1 --- .../TasksUpdater/Updater P3 Run WU.ps1 | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 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..f5482408 --- /dev/null +++ b/scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 @@ -0,0 +1,77 @@ +<# +.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 reboots the system if required. + 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 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. + + The script validates the availability of the `PSWindowsUpdate` module, installing it if necessary. + It then schedules or executes Windows updates at the parsed time, ensuring compliance with the predefined schedule. + +.EXAMPLE + Schedules={{agent.Schedules}} + Company_folder_path={{global.Company_folder_path}} + +.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. + +#> + + +# 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}} + +# Call the pwsh snippet +{{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 + } +} + +# Run Windows update with PSWindowsUpdate and rebooting at time found in parser +Get-WindowsUpdate -Verbose -Install -AcceptAll -AutoReboot -ScheduleReboot $scheduledTime From 4f452f555c1d16293ad90d1f407aa5781a18099b Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:44:20 +0000 Subject: [PATCH 194/447] Update ./snippets/Updater P3.5 Schedules parser.ps1 --- .../Updater P3.5 Schedules parser.ps1 | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 scripts_staging/snippets/Updater P3.5 Schedules parser.ps1 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..fc713fec --- /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 "Exit due to skip detected" + 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 From c244ac0266f14a2105cc8609e055ce35dc2a05fd Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:52:01 +0000 Subject: [PATCH 195/447] Update ./scripts/TasksUpdater/Updater P1 Template WU, SU, PS and Cleaner.ps1 --- ...ter P1 Template WU, SU, PS and Cleaner.ps1 | 118 +++++++++--------- 1 file changed, 62 insertions(+), 56 deletions(-) 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 index 88c337f3..ae7f9fbb 100644 --- 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 @@ -32,7 +32,7 @@ .CHANGELOG 04.09.24 SAN Refactored to determine device categories and odd/even status for scheduling purposes. - 13.12.24 SAN Removed useless check + .TODO add debug flag to env rename env CurrentSchedules to env CurrentTemplate and CurrentSchedules to ExistingTemplate @@ -43,26 +43,32 @@ $Debug = $false -# Check if CurrentSchedules contains "skip" or forcechange is not "true" -if ($Env:forcechange -ne "true") { - # Check environment variable +# 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") { - # Split the variable into lines, filter out lines containing "CurrentSchedules", and join them back into a single string to fix any empty space that could be found + # 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 and trim any extra spaces and line breaks - $cleanedOutput = ($filteredLines -join "`n").Trim() - - # Remove any remaining consecutive line breaks (more than one in a row) - $cleanedOutput = $cleanedOutput -replace "(\r?\n){2,}", "`n" - + # 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 + + # Exit the script + exit 0 } } + # Get the hostname of the device $hostname = [System.Net.Dns]::GetHostName() @@ -143,70 +149,70 @@ Write-Output "$deviceCategory $oddEven" switch -Regex ($deviceCategory + $oddEven) { "DCEven" { - $windowsUpdateDay = "3rd Tuesday" # Week 3 - $softwareUpdateDay = "1st Tuesday" # Week 1 - $tempFileCleanupDay = "4th Tuesday" # Week 4 - $powershellUpdateDay = "2nd Tuesday" # Week 2 + $windowsUpdateDay = "3rd Tuesday" + $softwareUpdateDay = "1st Tuesday" + $tempFileCleanupDay = "4th Tuesday" + $powershellUpdateDay = "2nd Tuesday" } "DCOdd" { - $windowsUpdateDay = "2nd Tuesday" # Week 2 - $softwareUpdateDay = "4th Tuesday" # Week 4 - $tempFileCleanupDay = "3rd Tuesday" # Week 3 - $powershellUpdateDay = "1st Tuesday" # Week 1 + $windowsUpdateDay = "2nd Tuesday" + $softwareUpdateDay = "4th Tuesday" + $tempFileCleanupDay = "3rd Tuesday" + $powershellUpdateDay = "1st Tuesday" } "DBEven" { - $windowsUpdateDay = "1st Wednesday" # Week 1 - $softwareUpdateDay = "3rd Wednesday" # Week 3 - $tempFileCleanupDay = "4th Wednesday" # Week 4 - $powershellUpdateDay = "2nd Wednesday" # Week 2 + $windowsUpdateDay = "1st Wednesday" + $softwareUpdateDay = "3rd Wednesday" + $tempFileCleanupDay = "4th Wednesday" + $powershellUpdateDay = "2nd Wednesday" } "DBOdd" { - $windowsUpdateDay = "2nd Wednesday" # Week 2 - $softwareUpdateDay = "4th Wednesday" # Week 4 - $tempFileCleanupDay = "1st Wednesday" # Week 1 - $powershellUpdateDay = "3rd Wednesday" # Week 3 + $windowsUpdateDay = "2nd Wednesday" + $softwareUpdateDay = "4th Wednesday" + $tempFileCleanupDay = "1st Wednesday" + $powershellUpdateDay = "3rd Wednesday" } "APPEven" { - $windowsUpdateDay = "3rd Thursday" # Week 3 - $softwareUpdateDay = "1st Thursday" # Week 1 - $tempFileCleanupDay = "4th Thursday" # Week 4 - $powershellUpdateDay = "2nd Thursday" # Week 2 + $windowsUpdateDay = "3rd Thursday" + $softwareUpdateDay = "1st Thursday" + $tempFileCleanupDay = "4th Thursday" + $powershellUpdateDay = "2nd Thursday" } "APPOdd" { - $windowsUpdateDay = "2nd Thursday" # Week 2 - $softwareUpdateDay = "4th Thursday" # Week 4 - $tempFileCleanupDay = "1st Thursday" # Week 1 - $powershellUpdateDay = "3rd Thursday" # Week 3 + $windowsUpdateDay = "2nd Thursday" + $softwareUpdateDay = "4th Thursday" + $tempFileCleanupDay = "1st Thursday" + $powershellUpdateDay = "3rd Thursday" } "RDSEven" { - $windowsUpdateDay = "4th Tuesday" # Week 4 - $softwareUpdateDay = "1st Tuesday" # Week 1 - $tempFileCleanupDay = "2nd Tuesday" # Week 2 - $powershellUpdateDay = "3rd Tuesday" # Week 3 + $windowsUpdateDay = "4th Tuesday" + $softwareUpdateDay = "1st Tuesday" + $tempFileCleanupDay = "2nd Tuesday" + $powershellUpdateDay = "3rd Tuesday" } "RDSOdd" { - $windowsUpdateDay = "3rd Tuesday" # Week 3 - $softwareUpdateDay = "2nd Tuesday" # Week 2 - $tempFileCleanupDay = "4th Tuesday" # Week 4 - $powershellUpdateDay = "1st Tuesday" # Week 1 + $windowsUpdateDay = "3rd Tuesday" + $softwareUpdateDay = "2nd Tuesday" + $tempFileCleanupDay = "4th Tuesday" + $powershellUpdateDay = "1st Tuesday" } "ExchangeEven" { - $windowsUpdateDay = "4th Wednesday" # Week 4 - $softwareUpdateDay = "2nd Wednesday" # Week 2 - $tempFileCleanupDay = "3rd Wednesday" # Week 3 - $powershellUpdateDay = "1st Wednesday" # Week 1 + $windowsUpdateDay = "4th Wednesday" + $softwareUpdateDay = "2nd Wednesday" + $tempFileCleanupDay = "3rd Wednesday" + $powershellUpdateDay = "1st Wednesday" } "ExchangeOdd" { - $windowsUpdateDay = "3rd Wednesday" # Week 3 - $softwareUpdateDay = "1st Wednesday" # Week 1 - $tempFileCleanupDay = "4th Wednesday" # Week 4 - $powershellUpdateDay = "2nd Wednesday" # Week 2 + $windowsUpdateDay = "3rd Wednesday" + $softwareUpdateDay = "1st Wednesday" + $tempFileCleanupDay = "4th Wednesday" + $powershellUpdateDay = "2nd Wednesday" } default { - $windowsUpdateDay = "4th Thursday" # Week 4 - $softwareUpdateDay = "2nd Thursday" # Week 2 - $tempFileCleanupDay = "3rd Thursday" # Week 3 - $powershellUpdateDay = "1st Thursday" # Week 1 + $windowsUpdateDay = "4th Thursday" + $softwareUpdateDay = "2nd Thursday" + $tempFileCleanupDay = "3rd Thursday" + $powershellUpdateDay = "1st Thursday" } } From 9801b0229aeb92976a140a9b27028f610b054bcf Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:27:21 +0000 Subject: [PATCH 196/447] Update ./snippets/Updater P3.5 Schedules parser.ps1 --- scripts_staging/snippets/Updater P3.5 Schedules parser.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts_staging/snippets/Updater P3.5 Schedules parser.ps1 b/scripts_staging/snippets/Updater P3.5 Schedules parser.ps1 index fc713fec..d6a2db3d 100644 --- a/scripts_staging/snippets/Updater P3.5 Schedules parser.ps1 +++ b/scripts_staging/snippets/Updater P3.5 Schedules parser.ps1 @@ -65,7 +65,7 @@ function Get-CheckSchedule { if ($Debug -eq 1) { Write-Host "Debug: Skip pattern detected. Exiting function." } - Write-Host "Exit due to skip detected" + Write-Host "$PartName lines contains skip exiting as per requirement" exit 0 } From 4e5b061a3cfc74089bee834163ac6f71ba0bc879 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 16 Dec 2024 08:29:50 +0000 Subject: [PATCH 197/447] Update ./scripts/Fixes/Fix broken ESET installation.ps1 --- scripts_staging/Fixes/Fix broken ESET installation.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts_staging/Fixes/Fix broken ESET installation.ps1 b/scripts_staging/Fixes/Fix broken ESET installation.ps1 index ac033a83..fc98a687 100644 --- a/scripts_staging/Fixes/Fix broken ESET installation.ps1 +++ b/scripts_staging/Fixes/Fix broken ESET installation.ps1 @@ -28,8 +28,8 @@ if ($env:force_execution -eq 'true') { } 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-Error "Error: The file 'ermm.exe' exists at the specified path. This script may not work as expected." - exit 1 + 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." } From a5be9a651447f8b89b46a30ca4da8862ba2ea2c1 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 17 Dec 2024 08:47:53 +0000 Subject: [PATCH 198/447] Update ./snippets/Cleaner.ps1 --- scripts_staging/snippets/Cleaner.ps1 | 578 ++++++++------------------- 1 file changed, 160 insertions(+), 418 deletions(-) diff --git a/scripts_staging/snippets/Cleaner.ps1 b/scripts_staging/snippets/Cleaner.ps1 index 489d2223..a5b6629c 100644 --- a/scripts_staging/snippets/Cleaner.ps1 +++ b/scripts_staging/snippets/Cleaner.ps1 @@ -1,462 +1,204 @@ -#public <# .SYNOPSIS - Automate cleaning up the C:\ drive with low disk space warning. + 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. - All deleted files will go into a log transcript in $env:TEMP. By default this - script leaves files that are newer than 7 days old however this variable can be edited. + 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 - This script will typically clean up anywhere from 1GB up to 15GB of space from a C: drive. + Author: SAN + Date: 01.01.24 + #public + +.EXEMPLE + DaysToDelete=25 .CHANGELOG - Changed to 25 day of IIS logs + 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 - Full code refactoring, get everything out of function, remove logging, streamline opperation - -#> -Function Start-Cleanup { - - -## Allows the use of -WhatIf -[CmdletBinding(SupportsShouldProcess=$True)] - -param( - ## Delete data older then $daystodelete - [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=0)] - $DaysToDelete = 7, - - ## LogFile path for the transcript to be written to - [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=1)] - $LogFile = ("$env:TEMP\" + (get-date -format "MM-d-yy-HH-mm") + '.log'), - - ## All verbose outputs will get logged in the transcript($logFile) - [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=2)] - $VerbosePreference = "Continue", - - ## All errors should be withheld from the console - [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=3)] - $ErrorActionPreference = "SilentlyContinue" -) - - ## Begin the timer - $Starters = (Get-Date) + Integrate bleachbit this would help avoid having to update this script too often. - ## Check $VerbosePreference variable, and turns -Verbose on - Function global:Write-Verbose ( [string]$Message ) { - if ( $VerbosePreference -ne 'SilentlyContinue' ) { - Write-Host "$Message" - } - } - - ## Tests if the log file already exists and renames the old file if it does exist - if(Test-Path $LogFile){ - ## Renames the log to be .old - Rename-Item $LogFile $LogFile.old -Verbose -Force - } else { - ## Starts a transcript in C:\temp so you can see which files were deleted - Write-Host (Start-Transcript -Path $LogFile) - } - - ## Writes a verbose output to the screen for user information - Write-Host "Retriving current disk percent free for comparison once the script has completed." - Write-Host "[DONE]" - - ## Gathers the amount of disk space used before running the script - $Before = Get-WmiObject Win32_LogicalDisk | Where-Object { $_.DriveType -eq "3" } | Select-Object SystemName, - @{ Name = "Drive" ; Expression = { ( $_.DeviceID ) } }, - @{ Name = "Size (GB)" ; Expression = {"{0:N1}" -f ( $_.Size / 1gb)}}, - @{ Name = "FreeSpace (GB)" ; Expression = {"{0:N1}" -f ( $_.Freespace / 1gb ) } }, - @{ Name = "PercentFree" ; Expression = {"{0:P1}" -f ( $_.FreeSpace / $_.Size ) } } | - Format-Table -AutoSize | - Out-String - - ## Stops the windows update service so that c:\windows\softwaredistribution can be cleaned up - Get-Service -Name wuauserv | Stop-Service -Force -ErrorAction SilentlyContinue -WarningAction SilentlyContinue -Verbose - - # Sets the SCCM cache size to 1 GB if it exists. - if ((Get-WmiObject -namespace root\ccm\SoftMgmtAgent -class CacheConfig) -ne "$null"){ - # if data is returned and sccm cache is configured it will shrink the size to 1024MB. - $cache = Get-WmiObject -namespace root\ccm\SoftMgmtAgent -class CacheConfig - $Cache.size = 1024 | Out-Null - $Cache.Put() | Out-Null - Restart-Service ccmexec -ErrorAction SilentlyContinue -WarningAction SilentlyContinue - } - - ## Compaction of Windows.edb - # Check if the Windows.edb file exists - $windowsEdbPath = "$env:ALLUSERSPROFILE\Microsoft\Search\Data\Applications\Windows\Windows.edb" - if (Test-Path $windowsEdbPath) { - - # Disable the Windows Search service - Write-Host "Disabling the Windows Search service..." - Set-Service -Name wsearch -StartupType Disabled - - # Stop the Windows Search service - Write-Host "Stopping the Windows Search service..." - Stop-Service -Name wsearch -Force - Write-Host "Windows Search service stopped." - - # Perform offline compaction of the Windows.edb file - 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 the Windows Search service to manual start - Write-Host "Setting the Windows Search service to manual start..." - Set-Service -Name wsearch -StartupType Manual - - # Start the Windows Search service - Write-Host "Starting the Windows Search service..." - Start-Service -Name wsearch - Write-Host "Windows Search service started." - - } else { - Write-Host "Windows.edb file not found, skipping all actions." - } - - ## Delete old adobe updates - Get-ChildItem "C:\ProgramData\Adobe\ARM\*" -Recurse -Force -ErrorAction SilentlyContinue | Remove-Item -Recurse -ErrorAction SilentlyContinue -Verbose - Write-Host "The Contents of Adobe ARM have been removed successfully!" - Write-Host "[DONE]" - - ## Deletes the contents of the Windows Temp folder. - Get-ChildItem "C:\Windows\Temp\*" -Recurse -Force -Verbose -ErrorAction SilentlyContinue | - Where-Object { ($_.CreationTime -lt $(Get-Date).AddDays( - $DaysToDelete)) } | Remove-Item -force -recurse -ErrorAction SilentlyContinue -Verbose - Write-host "The Contents of Windows Temp have been removed successfully!" - Write-Host "[DONE]" - - ## Deletes the contents of Windows Software Distribution. - Get-ChildItem "C:\Windows\SoftwareDistribution\*" -Recurse -Force -ErrorAction SilentlyContinue | Remove-Item -recurse -ErrorAction SilentlyContinue -Verbose - Write-Host "The Contents of Windows SoftwareDistribution have been removed successfully!" - Write-Host "[DONE]" - - ## Deletes all files and folders in user's Temp folder older then $DaysToDelete - Get-ChildItem "C:\users\*\AppData\Local\Temp\*" -Recurse -Force -ErrorAction SilentlyContinue | - Where-Object { ($_.CreationTime -lt $(Get-Date).AddDays( - $DaysToDelete))} | - Remove-Item -force -recurse -ErrorAction SilentlyContinue -Verbose - Write-Host "The contents of `$env:TEMP have been removed successfully!" - Write-Host "[DONE]" +#> - ## Deletes all files and folders in CSBack folder older then $DaysToDelete - Get-ChildItem "C:\csback\*" -Recurse -Force -ErrorAction SilentlyContinue | - Where-Object { ($_.CreationTime -lt $(Get-Date).AddDays( - $DaysToDelete))} | - Remove-Item -force -recurse -ErrorAction SilentlyContinue -Verbose - Write-Host "The contents of csback have been removed successfully!" - Write-Host "[DONE]" +# Check environment variable and set default if not defined +$DaysToDelete = if ([string]::IsNullOrEmpty($env:DaysToDelete)) { 30 } else { [int]$env:DaysToDelete } - ## Removes all files and folders in user's Temporary Internet Files older then $DaysToDelete - Get-ChildItem "C:\users\*\AppData\Local\Microsoft\Windows\Temporary Internet Files\*" ` - -Recurse -Force -Verbose -ErrorAction SilentlyContinue | - Where-Object {($_.CreationTime -lt $(Get-Date).AddDays( - $DaysToDelete))} | - Remove-Item -Force -Recurse -ErrorAction SilentlyContinue -Verbose - Write-Host "All Temporary Internet Files have been removed successfully!" - Write-Host "[DONE]" +Write-Host "Days to delete set to: $DaysToDelete" - ## Removes *.log from C:\windows\CBS - if(Test-Path C:\Windows\logs\CBS\){ - Get-ChildItem "C:\Windows\logs\CBS\*.log" -Recurse -Force -ErrorAction SilentlyContinue | - remove-item -force -recurse -ErrorAction SilentlyContinue -Verbose - Write-Host "All CBS logs have been removed successfully!" - Write-Host "[DONE]" - } else { - Write-Host "C:\inetpub\logs\LogFiles\ does not exist, there is nothing to cleanup." - Write-Host "[WARNING]" - } +$VerbosePreference = "Continue" +$ErrorActionPreference = "SilentlyContinue" +$Starters = Get-Date - ## Cleans IIS Logs older then $DaysToDelete - if (Test-Path C:\inetpub\logs\LogFiles\) { - Get-ChildItem "C:\inetpub\logs\LogFiles\*" -Recurse -Force -ErrorAction SilentlyContinue | - Where-Object { ($_.CreationTime -lt $(Get-Date).AddDays(-25)) } | Remove-Item -Force -Verbose -Recurse -ErrorAction SilentlyContinue - Write-Host "All IIS Logfiles over $DaysToDelete days old have been removed Successfully!" - Write-Host "[DONE]" - } - else { - Write-Host "C:\Windows\logs\CBS\ does not exist, there is nothing to cleanup." - Write-Host "[WARNING]" - } +# Log initial information +Write-Host "[DONE] Retrieving current disk percent free for comparison after script completion." - ## Removes C:\Config.Msi - if (test-path C:\Config.Msi){ - remove-item -Path C:\Config.Msi -force -recurse -Verbose -ErrorAction SilentlyContinue - } else { - Write-Host "C:\Config.Msi does not exist, there is nothing to cleanup." - Write-Host "[WARNING]" - } - - ## Removes c:\Intel - if (test-path c:\Intel){ - remove-item -Path c:\Intel -force -recurse -Verbose -ErrorAction SilentlyContinue - } else { - Write-Host "c:\Intel does not exist, there is nothing to cleanup." - Write-Host "[WARNING]" - } - - ## Removes c:\PerfLogs - if (test-path c:\PerfLogs){ - remove-item -Path c:\PerfLogs -force -recurse -Verbose -ErrorAction SilentlyContinue - } else { - Write-Host "c:\PerfLogs does not exist, there is nothing to cleanup." - Write-Host "[WARNING]" - } - - ## Removes $env:windir\memory.dmp - if (test-path $env:windir\memory.dmp){ - remove-item $env:windir\memory.dmp -force -Verbose -ErrorAction SilentlyContinue - } else { - Write-Host "C:\Windows\memory.dmp does not exist, there is nothing to cleanup." - Write-Host "[WARNING]" - } - - ## Removes rouge folders - Write-host "Deleting Rouge folders" - Write-Host "[DONE]" +# Function to retrieve and display disk space info +function Get-DiskInfo { + $DiskInfo = Get-WmiObject Win32_LogicalDisk | Where-Object { $_.DriveType -eq 3 } | + Select-Object SystemName, + @{ Name = "Drive"; Expression = { $_.DeviceID } }, + @{ Name = "Size (GB)"; Expression = { "{0:N1}" -f ($_.Size / 1GB) } }, + @{ Name = "FreeSpace (GB)"; Expression = { "{0:N1}" -f ($_.FreeSpace / 1GB) } }, + @{ Name = "PercentFree"; Expression = { "{0:P1}" -f ($_.FreeSpace / $_.Size) } } + return $DiskInfo +} - ## Removes Windows Error Reporting files - if (test-path C:\ProgramData\Microsoft\Windows\WER){ - Get-ChildItem -Path C:\ProgramData\Microsoft\Windows\WER -Recurse | Remove-Item -force -recurse -Verbose -ErrorAction SilentlyContinue - Write-host "Deleting Windows Error Reporting files" - Write-Host "[DONE]" +# Function to remove items +function Remove-Items { + param ( + [string]$Path, + [int]$Days + ) + + if (Test-Path $Path) { + # Check if the Path is a file + if ((Get-Item $Path).PSIsContainer -eq $false) { + # Remove the single file if it meets the age condition + if ((Get-Item $Path).CreationTime -lt (Get-Date).AddDays(-$Days)) { + Remove-Item -Path $Path -Force -Verbose + Write-Host "[DONE] Removed single item: $Path" + } else { + Write-Host "[INFO] $Path does not meet the age condition, skipping removal." + } } else { - Write-Host "C:\ProgramData\Microsoft\Windows\WER does not exist, there is nothing to cleanup." - Write-Host "[WARNING]" - } - - ## Removes System and User Temp Files - lots of access denied will occur. - ## Cleans up c:\windows\temp - if (Test-Path $env:windir\Temp\) { - Remove-Item -Path "$env:windir\Temp\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue - } else { - Write-Host "C:\Windows\Temp does not exist, there is nothing to cleanup." - Write-Host "[WARNING]" - } - - ## Cleans up minidump - if (Test-Path $env:windir\minidump\) { - Remove-Item -Path "$env:windir\minidump\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue - } else { - Write-Host "$env:windir\minidump\ does not exist, there is nothing to cleanup." - Write-Host "[WARNING]" - } - - ## Cleans up prefetch - if (Test-Path $env:windir\Prefetch\) { - Remove-Item -Path "$env:windir\Prefetch\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue - } else { - Write-Host "$env:windir\Prefetch\ does not exist, there is nothing to cleanup." - Write-Host "[WARNING]" - } - - ## Cleans up each user's temp folder - if (Test-Path "C:\Users\*\AppData\Local\Temp\") { - Remove-Item -Path "C:\Users\*\AppData\Local\Temp\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue - } else { - Write-Host "C:\Users\*\AppData\Local\Temp\ does not exist, there is nothing to cleanup." - Write-Host "[WARNING]" - } - - ## Cleans up all user's Windows error reporting - if (Test-Path "C:\Users\*\AppData\Local\Microsoft\Windows\WER\") { - Remove-Item -Path "C:\Users\*\AppData\Local\Microsoft\Windows\WER\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue - } else { - Write-Host "C:\ProgramData\Microsoft\Windows\WER does not exist, there is nothing to cleanup." - Write-Host "[WARNING]" - } - - ## Cleans up user's temporary internet files - if (Test-Path "C:\Users\*\AppData\Local\Microsoft\Windows\Temporary Internet Files\") { - Remove-Item -Path "C:\Users\*\AppData\Local\Microsoft\Windows\Temporary Internet Files\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue - } else { - Write-Host "C:\Users\*\AppData\Local\Microsoft\Windows\Temporary Internet Files\ does not exist." - Write-Host "[WARNING]" - } - - ## Cleans up Internet Explorer cache - if (Test-Path "C:\Users\*\AppData\Local\Microsoft\Windows\IECompatCache\") { - Remove-Item -Path "C:\Users\*\AppData\Local\Microsoft\Windows\IECompatCache\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue - } else { - Write-Host "C:\Users\*\AppData\Local\Microsoft\Windows\IECompatCache\ does not exist." - Write-Host "[WARNING]" - } - - ## Cleans up Internet Explorer cache - if (Test-Path "C:\Users\*\AppData\Local\Microsoft\Windows\IECompatUaCache\") { - Remove-Item -Path "C:\Users\*\AppData\Local\Microsoft\Windows\IECompatUaCache\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue - } else { - Write-Host "C:\Users\*\AppData\Local\Microsoft\Windows\IECompatUaCache\ does not exist." - Write-Host "[WARNING]" - } - - ## Cleans up Internet Explorer download history - if (Test-Path "C:\Users\*\AppData\Local\Microsoft\Windows\IEDownloadHistory\") { - Remove-Item -Path "C:\Users\*\AppData\Local\Microsoft\Windows\IEDownloadHistory\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue - } else { - Write-Host "C:\Users\*\AppData\Local\Microsoft\Windows\IEDownloadHistory\ does not exist." - Write-Host "[WARNING]" - } - - ## Cleans up Internet Cache - if (Test-Path "C:\Users\*\AppData\Local\Microsoft\Windows\INetCache\") { - Remove-Item -Path "C:\Users\*\AppData\Local\Microsoft\Windows\INetCache\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue + # If it's a directory, remove its sub-items based on the age condition + Get-ChildItem -Path $Path -Recurse -Force | + Where-Object { $_.CreationTime -lt (Get-Date).AddDays(-$Days) } | + Remove-Item -Force -Recurse -Verbose + Write-Host "[DONE] Cleaned up directory: $Path" + } } else { - Write-Host "C:\Users\*\AppData\Local\Microsoft\Windows\INetCache\ does not exist." - Write-Host "[WARNING]" + Write-Host "[WARNING] $Path does not exist, skipping cleanup." } +} - ## Cleans up Internet Cookies - if (Test-Path "C:\Users\*\AppData\Local\Microsoft\Windows\INetCookies\") { - Remove-Item -Path "C:\Users\*\AppData\Local\Microsoft\Windows\INetCookies\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue - } else { - Write-Host "C:\Users\*\AppData\Local\Microsoft\Windows\INetCookies\ does not exist." - Write-Host "[WARNING]" - } +# 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 - ## Cleans up terminal server cache - if (Test-Path "C:\Users\*\AppData\Local\Microsoft\Terminal Server Client\Cache\") { - Remove-Item -Path "C:\Users\*\AppData\Local\Microsoft\Terminal Server Client\Cache\*" -Force -Recurse -Verbose -ErrorAction SilentlyContinue - } else { - Write-Host "C:\Users\*\AppData\Local\Microsoft\Terminal Server Client\Cache\ does not exist." - Write-Host "[WARNING]" - } + # Get all subkeys except the one named "StateFlags0001" + $subKeys = Get-ChildItem -Path $baseKey -ErrorAction SilentlyContinue | Where-Object { $_.PSChildName -ne $valueName } - Write-host "Removing System and User Temp Files" - Write-Host "[DONE]" + foreach ($subKey in $subKeys) { + $keyPath = $subKey.PSPath - ## Removes the hidden recycle bin. - if (Test-path 'C:\$Recycle.Bin'){ - Remove-Item 'C:\$Recycle.Bin' -Recurse -Force -Verbose -ErrorAction SilentlyContinue - } else { - Write-Host "C:\`$Recycle.Bin does not exist, there is nothing to cleanup." - Write-Host "[WARNING]" + # 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." +} - ## Turns errors back on - $ErrorActionPreference = "Continue" - - ## Checks the version of PowerShell - ## If PowerShell version 4 or below is installed the following will process - if ($PSVersionTable.PSVersion.Major -le 4) { +# 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" +} - ## Empties the recycle bin, the desktop recycle bin - $Recycler = (New-Object -ComObject Shell.Application).NameSpace(0xa) - $Recycler.items() | ForEach-Object { - ## If PowerShell version 4 or below is installed the following will process - Remove-Item -Include $_.path -Force -Recurse -Verbose - Write-Host "The recycling bin has been cleaned up successfully!" - Write-Host "[DONE]" - } - } elseif ($PSVersionTable.PSVersion.Major -ge 5) { - ## If PowerShell version 5 is running on the machine the following will process - Clear-RecycleBin -DriveLetter C:\ -Force -Verbose - Write-Host "The recycling bin has been cleaned up successfully!" - Write-Host "[DONE]" - } +# 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\*" +} - ## Starts cleanmgr.exe - # Function to add the registry keys - function Add-RegistryKeys { - # Define the base registry key path - $baseKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches\" +# Display disk space before cleanup +$Before = Get-DiskInfo | Format-Table -AutoSize | Out-String - # Get all subkeys under the base key - $subKeys = Get-ChildItem -Path $baseKey -ErrorAction SilentlyContinue | Where-Object { $_.PSChildName -ne "StateFlags0001" } +# Stop Windows Update service +Stop-Service -Name wuauserv -Force -ErrorAction SilentlyContinue -Verbose - # Loop through each subkey and create/update the DWORD value - foreach ($subKey in $subKeys) { - $keyPath = Join-Path -Path $baseKey -ChildPath $subKey.PSChildName - $valueName = "StateFlags0001" - $value = 2 +# Adjust SCCM cache size if configured +$cache = Get-WmiObject -Namespace root\ccm\SoftMgmtAgent -Class CacheConfig +if ($cache) { + $cache.size = 1024 + $cache.Put() | Out-Null + Restart-Service ccmexec -ErrorAction SilentlyContinue +} - # Check if the value already exists, if not create it - if (-not (Test-Path -Path "$keyPath\$valueName")) { - New-ItemProperty -Path $keyPath -Name $valueName -Value $value -PropertyType DWORD -Force | Out-Null - } else { - # Update the existing value - Set-ItemProperty -Path $keyPath -Name $valueName -Value $value - } - } +# 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." +} - Write-Host "StateFlags0001 DWORD value created/updated in all subkeys under $baseKey" +# 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 - Add-RegistryKeys - # Run Disk Cleanup with sagerun1 - Start-Process -FilePath "cleanmgr.exe" -ArgumentList "/sagerun:1" -Wait - - - ## 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 ) } }, - @{ Name = "Size (GB)" ; Expression = {"{0:N1}" -f ( $_.Size / 1gb)}}, - @{ Name = "FreeSpace (GB)" ; Expression = {"{0:N1}" -f ( $_.Freespace / 1gb ) } }, - @{ Name = "PercentFree" ; Expression = {"{0:P1}" -f ( $_.FreeSpace / $_.Size ) } } | - Format-Table -AutoSize | Out-String - - ## Restarts wuauserv - Get-Service -Name wuauserv | Start-Service -ErrorAction SilentlyContinue - - ## Stop timer - $Enders = (Get-Date) - - ## Calculate amount of seconds your code takes to complete. - Write-Verbose "Elapsed Time: $(($Enders - $Starters).totalseconds) seconds - -" - ## Sends hostname to the console for ticketing purposes. - Write-Host (Hostname) - - ## Sends the date and time to the console for ticketing purposes. - Write-Host (Get-Date | Select-Object -ExpandProperty DateTime) - - ## Sends the disk usage before running the cleanup script to the console for ticketing purposes. - Write-Verbose " -Before: $Before" - - ## Sends the disk usage after running the cleanup script to the console for ticketing purposes. - Write-Verbose "After: $After" +# Add registry keys for Disk Cleanup +Add-RegistryKeys-CleanMGR - ## Prompt to scan for large ISO, VHD, VHDX files. - Function PromptforScan { - param( - $ScanPath, - $title = (Write-Host "Search for large files"), - $message = (Write-Host "Would you like to scan $ScanPath for ISOs or VHD(X) files?") - ) - $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." - 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." - } - } - } - PromptforScan -ScanPath C:\ # end of function +# Run Disk Cleanup with custom settings +Start-Process -FilePath "cleanmgr.exe" -ArgumentList "/sagerun:1" -Wait - ## Completed Successfully! - Write-Host (Stop-Transcript) +# Gather disk usage after cleanup +$After = Get-DiskInfo | Format-Table -AutoSize | Out-String - Write-host "Script finished" -NoNewline - Write-Host "[DONE]" +# Restart Windows Update service +Start-Service -Name wuauserv -ErrorAction SilentlyContinue -} -Start-Cleanup \ No newline at end of file +# 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" From 3a3fc8927529f98d22130c3e1fe97bd506f4e1bb Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 17 Dec 2024 09:47:27 +0000 Subject: [PATCH 199/447] Update ./scripts/Tools/Cleanup temp files.ps1 --- scripts_staging/Tools/Cleanup temp files.ps1 | 35 ++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 scripts_staging/Tools/Cleanup temp files.ps1 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 From bd1a7d4b86778383d5129a5a3a8d903703a4a383 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 17 Dec 2024 09:47:49 +0000 Subject: [PATCH 200/447] Update ./snippets/Cleaner.ps1 --- scripts_staging/snippets/Cleaner.ps1 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts_staging/snippets/Cleaner.ps1 b/scripts_staging/snippets/Cleaner.ps1 index a5b6629c..d06d7413 100644 --- a/scripts_staging/snippets/Cleaner.ps1 +++ b/scripts_staging/snippets/Cleaner.ps1 @@ -38,9 +38,6 @@ $VerbosePreference = "Continue" $ErrorActionPreference = "SilentlyContinue" $Starters = Get-Date -# Log initial information -Write-Host "[DONE] Retrieving current disk percent free for comparison after script completion." - # Function to retrieve and display disk space info function Get-DiskInfo { $DiskInfo = Get-WmiObject Win32_LogicalDisk | Where-Object { $_.DriveType -eq 3 } | @@ -131,6 +128,7 @@ $UserPathsToClean = @{ } # 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 From 0cb141b0ef8ac327371d245a5aed3b795688f26c Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 17 Dec 2024 12:26:34 +0000 Subject: [PATCH 201/447] Update ./scripts/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 --- .../Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 index 5db5a083..fff4d4f5 100644 --- 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 @@ -29,7 +29,7 @@ .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. @@ -95,7 +95,8 @@ foreach ($schedule in $rawSchedules) { $updatedTime = $timeWithOffset.ToString("HH:mm:ss") # Format the date as MM/dd/yyyy - $formattedTaskDate = $taskDate.ToString("MM/dd/yyyy").Replace('.', '/') + $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" From 3e6acd55a27ac83e1a58a3071f67c6ecb6529a5c Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:00:56 +0000 Subject: [PATCH 202/447] Update ./scripts/Checks/AD link health.ps1 --- scripts_staging/Checks/AD link health.ps1 | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts_staging/Checks/AD link health.ps1 b/scripts_staging/Checks/AD link health.ps1 index 6802bb99..408f4d95 100644 --- a/scripts_staging/Checks/AD link health.ps1 +++ b/scripts_staging/Checks/AD link health.ps1 @@ -16,6 +16,7 @@ .CHANGELOG 26.11.24 SAN big code cleanup, bug fix, removal of debug to help with cleanup + 17.12.24 SAN Fixed counting issue .TODO Make ldap rpc smb followup querries to test that the protocol works @@ -115,14 +116,11 @@ function Test-ADConnection { foreach ($service in $PortsToCheck.GetEnumerator()) { $results += Test-PortConnection -ADDomainController $ADDomainController -Port $service.Value -ServiceName $service.Key } - - # Spacer - $results += "" } - # Count and handle failures - $failedCount = ($results | Where-Object { $_.Status -eq "KO" }).Count + $failedCount = ($results | Where-Object { $_.Status -eq "KO" -and $_.Status }) | Measure-Object | Select-Object -ExpandProperty Count + Write-Host "$failedCount tests failed." Write-Host "" @@ -134,6 +132,7 @@ function Test-ADConnection { } } + # Discover all domain controllers in the current domain $domain = (Get-WmiObject Win32_ComputerSystem).Domain if ($domain -and $domain -ne 'WORKGROUP') { From 7a99baea7b3382c8d9364c9176b0c177b6d24d41 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:03:02 +0000 Subject: [PATCH 203/447] Update ./scripts/Checks/AD link health.ps1 --- scripts_staging/Checks/AD link health.ps1 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts_staging/Checks/AD link health.ps1 b/scripts_staging/Checks/AD link health.ps1 index 408f4d95..65d7480c 100644 --- a/scripts_staging/Checks/AD link health.ps1 +++ b/scripts_staging/Checks/AD link health.ps1 @@ -16,7 +16,6 @@ .CHANGELOG 26.11.24 SAN big code cleanup, bug fix, removal of debug to help with cleanup - 17.12.24 SAN Fixed counting issue .TODO Make ldap rpc smb followup querries to test that the protocol works @@ -97,7 +96,6 @@ function Test-KerberosAuthentication { } } -# Main function to test AD connections function Test-ADConnection { param ( [string[]]$ADDomainControllers, @@ -116,6 +114,13 @@ function Test-ADConnection { 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 From 40f7c219eb3f45c794623ef7dc67331bac66e856 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:15:30 +0000 Subject: [PATCH 204/447] Update ./scripts/Checks/AD link health.ps1 --- scripts_staging/Checks/AD link health.ps1 | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/scripts_staging/Checks/AD link health.ps1 b/scripts_staging/Checks/AD link health.ps1 index 65d7480c..a114b44e 100644 --- a/scripts_staging/Checks/AD link health.ps1 +++ b/scripts_staging/Checks/AD link health.ps1 @@ -16,6 +16,7 @@ .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 @@ -71,9 +72,25 @@ function Test-PortConnection { [int]$Port, [string]$ServiceName ) - $connection = Test-NetConnection -ComputerName $ADDomainController -Port $Port - $status = if ($connection.TcpTestSucceeded) { "OK" } else { "KO" } + # 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 @@ -81,6 +98,7 @@ function Test-PortConnection { } } + # Function to perform Kerberos authentication test function Test-KerberosAuthentication { param ( From c99f3325d9e756ffebdecb25525d4ed006e9fcea Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:56:52 +0000 Subject: [PATCH 205/447] Update ./scripts/Checks/AD link health.ps1 --- scripts_staging/Checks/AD link health.ps1 | 36 +++++------------------ 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/scripts_staging/Checks/AD link health.ps1 b/scripts_staging/Checks/AD link health.ps1 index a114b44e..6802bb99 100644 --- a/scripts_staging/Checks/AD link health.ps1 +++ b/scripts_staging/Checks/AD link health.ps1 @@ -16,7 +16,6 @@ .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 @@ -72,25 +71,9 @@ function Test-PortConnection { [int]$Port, [string]$ServiceName ) + $connection = Test-NetConnection -ComputerName $ADDomainController -Port $Port + $status = if ($connection.TcpTestSucceeded) { "OK" } else { "KO" } - # 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 @@ -98,7 +81,6 @@ function Test-PortConnection { } } - # Function to perform Kerberos authentication test function Test-KerberosAuthentication { param ( @@ -114,6 +96,7 @@ function Test-KerberosAuthentication { } } +# Main function to test AD connections function Test-ADConnection { param ( [string[]]$ADDomainControllers, @@ -133,17 +116,13 @@ function Test-ADConnection { $results += Test-PortConnection -ADDomainController $ADDomainController -Port $service.Value -ServiceName $service.Key } - # Add a separator - $results += [PSCustomObject]@{ - TestName = "--------" - Status = "" - TargetDC = "--------" - } + # Spacer + $results += "" } - # Count and handle failures - $failedCount = ($results | Where-Object { $_.Status -eq "KO" -and $_.Status }) | Measure-Object | Select-Object -ExpandProperty Count + # Count and handle failures + $failedCount = ($results | Where-Object { $_.Status -eq "KO" }).Count Write-Host "$failedCount tests failed." Write-Host "" @@ -155,7 +134,6 @@ function Test-ADConnection { } } - # Discover all domain controllers in the current domain $domain = (Get-WmiObject Win32_ComputerSystem).Domain if ($domain -and $domain -ne 'WORKGROUP') { From 083a00943fc096de8e4edb4cd551648728cbdf73 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:58:28 +0000 Subject: [PATCH 206/447] Update ./scripts/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 --- .../Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 index fff4d4f5..5db5a083 100644 --- 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 @@ -29,7 +29,7 @@ .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. @@ -95,8 +95,7 @@ foreach ($schedule in $rawSchedules) { $updatedTime = $timeWithOffset.ToString("HH:mm:ss") # Format the date as MM/dd/yyyy - $formattedTaskDate = $taskDate.ToString("MM/dd/yyyy").Replace('.', '/').Replace('-', '/') - + $formattedTaskDate = $taskDate.ToString("MM/dd/yyyy").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" From 965a0964aabf4296d635c94ac9d8b1f5a1f42551 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:59:11 +0000 Subject: [PATCH 207/447] Update ./scripts/Checks/AD link health.ps1 --- scripts_staging/Checks/AD link health.ps1 | 36 ++++++++++++++++++----- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/scripts_staging/Checks/AD link health.ps1 b/scripts_staging/Checks/AD link health.ps1 index 6802bb99..a114b44e 100644 --- a/scripts_staging/Checks/AD link health.ps1 +++ b/scripts_staging/Checks/AD link health.ps1 @@ -16,6 +16,7 @@ .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 @@ -71,9 +72,25 @@ function Test-PortConnection { [int]$Port, [string]$ServiceName ) - $connection = Test-NetConnection -ComputerName $ADDomainController -Port $Port - $status = if ($connection.TcpTestSucceeded) { "OK" } else { "KO" } + # 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 @@ -81,6 +98,7 @@ function Test-PortConnection { } } + # Function to perform Kerberos authentication test function Test-KerberosAuthentication { param ( @@ -96,7 +114,6 @@ function Test-KerberosAuthentication { } } -# Main function to test AD connections function Test-ADConnection { param ( [string[]]$ADDomainControllers, @@ -116,13 +133,17 @@ function Test-ADConnection { $results += Test-PortConnection -ADDomainController $ADDomainController -Port $service.Value -ServiceName $service.Key } - # Spacer - $results += "" + # Add a separator + $results += [PSCustomObject]@{ + TestName = "--------" + Status = "" + TargetDC = "--------" + } } - # Count and handle failures - $failedCount = ($results | Where-Object { $_.Status -eq "KO" }).Count + $failedCount = ($results | Where-Object { $_.Status -eq "KO" -and $_.Status }) | Measure-Object | Select-Object -ExpandProperty Count + Write-Host "$failedCount tests failed." Write-Host "" @@ -134,6 +155,7 @@ function Test-ADConnection { } } + # Discover all domain controllers in the current domain $domain = (Get-WmiObject Win32_ComputerSystem).Domain if ($domain -and $domain -ne 'WORKGROUP') { From 31ae3cb805208261cff7300c1f302f3d5af90f91 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:00:31 +0000 Subject: [PATCH 208/447] Update ./scripts/Checks/AD link health.ps1 --- scripts_staging/Checks/AD link health.ps1 | 338 +++++++++++----------- 1 file changed, 169 insertions(+), 169 deletions(-) diff --git a/scripts_staging/Checks/AD link health.ps1 b/scripts_staging/Checks/AD link health.ps1 index a114b44e..f5f3f738 100644 --- a/scripts_staging/Checks/AD link health.ps1 +++ b/scripts_staging/Checks/AD link health.ps1 @@ -1,169 +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." -} +<# +.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." +} From d0aed6e4005d0212c9d645bc9beb436411dc890e Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:02:07 +0000 Subject: [PATCH 209/447] Update ./scripts/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 --- .../Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 index 5db5a083..fff4d4f5 100644 --- 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 @@ -29,7 +29,7 @@ .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. @@ -95,7 +95,8 @@ foreach ($schedule in $rawSchedules) { $updatedTime = $timeWithOffset.ToString("HH:mm:ss") # Format the date as MM/dd/yyyy - $formattedTaskDate = $taskDate.ToString("MM/dd/yyyy").Replace('.', '/') + $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" From 6dbac42aa5fd7d0b5709bdbfe095dbecf735726b Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 18 Dec 2024 08:44:11 +0000 Subject: [PATCH 210/447] Update ./scripts/Fixes/Bluescreen report.ps1 --- scripts_staging/Fixes/Bluescreen report.ps1 | 60 ++++++++++++++------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/scripts_staging/Fixes/Bluescreen report.ps1 b/scripts_staging/Fixes/Bluescreen report.ps1 index 609307b6..79f94778 100644 --- a/scripts_staging/Fixes/Bluescreen report.ps1 +++ b/scripts_staging/Fixes/Bluescreen report.ps1 @@ -9,6 +9,8 @@ .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 @@ -17,12 +19,15 @@ #PUBLIC .CHANGELOG + 18.12.24 SAN Added site & client name to the uploaded file #> -# Step 1: Retrieve Nextcloud WebDAV URL and Token from environment variables +# 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) { @@ -30,10 +35,21 @@ if (-not $nextcloudWebdavUrl -or -not $webdavUser) { 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" # Path to Minidump folder $hostname = (Get-WmiObject -Class Win32_ComputerSystem).Name # Get the system hostname +# Sanitize Client Name and Site Name to keep only a-z, 0-9, and spaces, then replace spaces with underscores +$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 @@ -50,9 +66,13 @@ if ($LASTEXITCODE -ne 0) { # 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..." -Start-Process $bluescreenViewerPath -ArgumentList "/stext $bluescreenLogFile" -Wait -if ($LASTEXITCODE -ne 0) { - Write-Host "Failed to run Bluescreen Viewer. Continuing with script." +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 @@ -70,9 +90,9 @@ if (Test-Path $minidumpPath) { # Step 6: Loop through each Minidump file and upload to Nextcloud WebDAV foreach ($file in $files) { - # Construct a new file name with the hostname at the beginning - $newFileName = "$hostname" + "_" + $file.Name - $uploadUrl = $nextcloudWebdavUrl + $newFileName # Construct WebDAV URL for each file + + $newFileName = "$clientName`_$siteName`_$hostname`_$($file.Name)" + $uploadUrl = $nextcloudWebdavUrl + $newFileName # Step 7: Prepare the authorization header for WebDAV (no password) $headers = @{ @@ -83,17 +103,21 @@ if (Test-Path $minidumpPath) { # Step 8: Upload the file to Nextcloud WebDAV try { Write-Host "Uploading $($file.Name) to $uploadUrl..." - Invoke-WebRequest -Uri $uploadUrl -Method Put -InFile $file.FullName -Headers $headers -UseBasicParsing - 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" + $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)" + } } catch { Write-Host "Failed to upload $($file.Name): $_" } From f3ae21ade0ecaef5f906eff82d74bcd812c20ddc Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 18 Dec 2024 09:34:12 +0000 Subject: [PATCH 211/447] Update ./scripts/Checks/Last errors logs.ps1 --- scripts_staging/Checks/Last errors logs.ps1 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts_staging/Checks/Last errors logs.ps1 b/scripts_staging/Checks/Last errors logs.ps1 index 8424b4b2..55e1f306 100644 --- a/scripts_staging/Checks/Last errors logs.ps1 +++ b/scripts_staging/Checks/Last errors logs.ps1 @@ -24,7 +24,8 @@ 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 @@ -36,8 +37,8 @@ #> -$defaultEventIds = @(10016) #add value comma separeted -$defaultKeywords = @("gupdate") #add value comma separeted +$defaultEventIds = @(10016,36874) +$defaultKeywords = @("gupdate","anotherkeyword") $debug = [System.Environment]::GetEnvironmentVariable("DEBUG") $filterIdEnv = [System.Environment]::GetEnvironmentVariable("FILTER_ID") From f818d85e81d6cab961b4ae5cf81cc4e2aadd3efb Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 18 Dec 2024 12:43:56 +0000 Subject: [PATCH 212/447] Update ./scripts/Fixes/Bluescreen report.ps1 --- scripts_staging/Fixes/Bluescreen report.ps1 | 100 +++++++++++--------- 1 file changed, 55 insertions(+), 45 deletions(-) diff --git a/scripts_staging/Fixes/Bluescreen report.ps1 b/scripts_staging/Fixes/Bluescreen report.ps1 index 79f94778..e74ca1a7 100644 --- a/scripts_staging/Fixes/Bluescreen report.ps1 +++ b/scripts_staging/Fixes/Bluescreen report.ps1 @@ -19,10 +19,9 @@ #PUBLIC .CHANGELOG - 18.12.24 SAN Added site & client name to the uploaded file + 18.12.24 SAN Added site & client name to the uploaded file, added boot time to the report, moved dmp check #> - # 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") @@ -41,14 +40,23 @@ if (-not $nextcloudWebdavUrl.EndsWith("/")) { } # Variables (defined at the top for easy configuration) -$minidumpPath = "C:\Windows\Minidump" # Path to Minidump folder -$hostname = (Get-WmiObject -Class Win32_ComputerSystem).Name # Get the system hostname +$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-Host "Minidump folder not found!" + exit 1 +} elseif (-not (Get-ChildItem -Path $minidumpPath -Filter "*.dmp")) { + Write-Host "No dump files found in Minidump folder!" + exit 1 +} -# Sanitize Client Name and Site Name to keep only a-z, 0-9, and spaces, then replace spaces with underscores +# 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 +$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" @@ -76,52 +84,54 @@ if (Test-Path $bluescreenViewerPath) { } # Step 4: Output the content of the crash log file to the terminal -Write-Host "Displaying crash logs..." 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: Check if the Minidump directory exists and process the files -if (Test-Path $minidumpPath) { - # Get all files in the Minidump directory, excluding those already marked as "_sent" - $files = Get-ChildItem -Path $minidumpPath | Where-Object { $_.Name -notlike "*_sent*" } - - # Step 6: Loop through each Minidump file and upload to Nextcloud WebDAV - foreach ($file in $files) { +# 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*" } - $newFileName = "$clientName`_$siteName`_$hostname`_$($file.Name)" - $uploadUrl = $nextcloudWebdavUrl + $newFileName +# 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 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)" - } - } catch { - Write-Host "Failed to upload $($file.Name): $_" + # 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)" } + } catch { + Write-Host "Failed to upload $($file.Name): $_" } -} else { - Write-Host "Minidump folder not found!" } From ac8f36f4c7027fabd7bac90564a78b571d4a1a3c Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:00:01 +0000 Subject: [PATCH 213/447] =?UTF-8?q?Update=20./scripts/Lab/Generate=20Blue?= =?UTF-8?q?=20screen=20of=20death=20=E2=98=A2=EF=B8=8F.ps1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...een of death \342\230\242\357\270\217.ps1" | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 "scripts_staging/Lab/Generate Blue screen of death \342\230\242\357\270\217.ps1" diff --git "a/scripts_staging/Lab/Generate Blue screen of death \342\230\242\357\270\217.ps1" "b/scripts_staging/Lab/Generate Blue screen of death \342\230\242\357\270\217.ps1" new file mode 100644 index 00000000..14d160a3 --- /dev/null +++ "b/scripts_staging/Lab/Generate Blue screen of death \342\230\242\357\270\217.ps1" @@ -0,0 +1,70 @@ +<# +.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); + NtRaiseHardError(0xc0000022, 0, 0, IntPtr.Zero, 6, out response); + } +} +"@ + + # 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 From 7630a738b4a503baa512165f7ba1e841f020c83a Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:06:17 +0000 Subject: [PATCH 214/447] =?UTF-8?q?Update=20./scripts/Lab/=E2=98=A2?= =?UTF-8?q?=EF=B8=8F=20Generate=20Blue=20screen=20of=20death.ps1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...270\217 Generate Blue screen of death.ps1" | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 "scripts_staging/Lab/\342\230\242\357\270\217 Generate Blue screen of death.ps1" 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..14d160a3 --- /dev/null +++ "b/scripts_staging/Lab/\342\230\242\357\270\217 Generate Blue screen of death.ps1" @@ -0,0 +1,70 @@ +<# +.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); + NtRaiseHardError(0xc0000022, 0, 0, IntPtr.Zero, 6, out response); + } +} +"@ + + # 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 From c879a58e7f5663779131b9b0b1b4731775a0f2f2 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:14:39 +0000 Subject: [PATCH 215/447] =?UTF-8?q?Update=20./scripts/Lab/=E2=98=A2?= =?UTF-8?q?=EF=B8=8F=20Generate=20Blue=20screen=20of=20death.ps1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...42\357\270\217 Generate Blue screen of death.ps1" | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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" index 14d160a3..898364ca 100644 --- "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" @@ -32,7 +32,6 @@ if (-not [System.Diagnostics.EventLog]::SourceExists($eventSource)) { [System.Diagnostics.EventLog]::CreateEventSource($eventSource, $eventLog) } - function Invoke-Death { $source = @" using System; @@ -48,10 +47,18 @@ public static class CS { public static void InvokeDeath() { bool previousValue; uint response; + RtlAdjustPrivilege(19, true, false, out previousValue); - NtRaiseHardError(0xc0000022, 0, 0, IntPtr.Zero, 6, out response); + + 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 @@ -64,6 +71,7 @@ public static class CS { } + Write-EventLog -LogName $eventLog -Source $eventSource -EventId 1 -EntryType Error -Message "Now I am become Death, the destroyer of worlds." From 245d3dd0ced7f1ccbad5a19af803b78d69354382 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 19 Dec 2024 11:58:17 +0100 Subject: [PATCH 216/447] =?UTF-8?q?Delete=20scripts=5Fstaging/Lab/Generate?= =?UTF-8?q?=20Blue=20screen=20of=20death=20=E2=98=A2=EF=B8=8F.ps1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...een of death \342\230\242\357\270\217.ps1" | 70 ------------------- 1 file changed, 70 deletions(-) delete mode 100644 "scripts_staging/Lab/Generate Blue screen of death \342\230\242\357\270\217.ps1" diff --git "a/scripts_staging/Lab/Generate Blue screen of death \342\230\242\357\270\217.ps1" "b/scripts_staging/Lab/Generate Blue screen of death \342\230\242\357\270\217.ps1" deleted file mode 100644 index 14d160a3..00000000 --- "a/scripts_staging/Lab/Generate Blue screen of death \342\230\242\357\270\217.ps1" +++ /dev/null @@ -1,70 +0,0 @@ -<# -.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); - NtRaiseHardError(0xc0000022, 0, 0, IntPtr.Zero, 6, out response); - } -} -"@ - - # 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 From 5df751869e3972d404480cb7b1ae01b2eb9719c1 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 20 Dec 2024 00:09:04 +0000 Subject: [PATCH 217/447] Update ./scripts/Build/Forward HTTP Traffic To Company Website.ps1 --- .../Build/Forward HTTP Traffic To Company Website.ps1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts_staging/Build/Forward HTTP Traffic To Company Website.ps1 b/scripts_staging/Build/Forward HTTP Traffic To Company Website.ps1 index 9a6f6c2d..7f4d087a 100644 --- a/scripts_staging/Build/Forward HTTP Traffic To Company Website.ps1 +++ b/scripts_staging/Build/Forward HTTP Traffic To Company Website.ps1 @@ -15,6 +15,9 @@ 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 From 70673dc837b5f9f4f77210b00ad783f34b9a9811 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 20 Dec 2024 09:17:54 +0000 Subject: [PATCH 218/447] Update ./scripts/Checks/is RDP port ok.ps1 --- scripts_staging/Checks/is RDP port ok.ps1 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts_staging/Checks/is RDP port ok.ps1 b/scripts_staging/Checks/is RDP port ok.ps1 index 6a1fa48b..aac6ebbd 100644 --- a/scripts_staging/Checks/is RDP port ok.ps1 +++ b/scripts_staging/Checks/is RDP port ok.ps1 @@ -14,7 +14,7 @@ .CHANGELOG 12.12.24 SAN Changed outputs - + 20.12.24 SAN Changed outputs #> $port = 3389 @@ -24,9 +24,9 @@ $address = "localhost" if (Get-Command Test-NetConnection -ErrorAction SilentlyContinue) { $tcpConnection = Test-NetConnection -ComputerName $address -Port $port if ($tcpConnection.TcpTestSucceeded) { - Write-Output "RDP is open." + Write-Output "OK: RDP is open." } else { - Write-Output "Port $port is not open RDP will not work." + Write-Output "KO: Port $port is not open RDP will not work." exit 1 } } else { @@ -34,10 +34,10 @@ if (Get-Command Test-NetConnection -ErrorAction SilentlyContinue) { try { $tcpClient = New-Object System.Net.Sockets.TcpClient $tcpClient.Connect($address, $port) - Write-Output "RDP is open but TNC does not work." + Write-Output "OK: RDP is open but TNC does not work." $tcpClient.Close() } catch { - Write-Output "Port $port is not open and TNC does not work." + Write-Output "KO: Port $port is not open and TNC does not work." exit 1 } } \ No newline at end of file From d86572d982701da262cc683c37ec2f124bc29cda Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Sat, 21 Dec 2024 21:37:06 +0000 Subject: [PATCH 219/447] Update ./scripts/Build/Update TRMM agent.ps1 Update ./scripts/Build/Update TRMM agent.ps1 --- e -i HEAD~3 | 257 ++++++++++++++++++++ scripts_staging/Build/Update TRMM agent.ps1 | 65 ++--- 2 files changed, 282 insertions(+), 40 deletions(-) create mode 100644 e -i HEAD~3 diff --git a/e -i HEAD~3 b/e -i HEAD~3 new file mode 100644 index 00000000..488c765a --- /dev/null +++ b/e -i HEAD~3 @@ -0,0 +1,257 @@ +cf2d1a7 (HEAD -> main, origin/main, origin/HEAD) Update ./scripts/Build/Update TRMM agent.ps1 +b8f19df Update ./scripts/Build/Update TRMM agent.ps1 +70673dc Update ./scripts/Checks/is RDP port ok.ps1 +5df7518 Update ./scripts/Build/Forward HTTP Traffic To Company Website.ps1 +245d3dd Delete scripts_staging/Lab/Generate Blue screen of death ☢️.ps1 +c879a58 Update ./scripts/Lab/☢️ Generate Blue screen of death.ps1 +7630a73 Update ./scripts/Lab/☢️ Generate Blue screen of death.ps1 +ac8f36f Update ./scripts/Lab/Generate Blue screen of death ☢️.ps1 +f818d85 Update ./scripts/Fixes/Bluescreen report.ps1 +f3ae21a Update ./scripts/Checks/Last errors logs.ps1 +6dbac42 Update ./scripts/Fixes/Bluescreen report.ps1 +d0aed6e Update ./scripts/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 +31ae3cb Update ./scripts/Checks/AD link health.ps1 +965a096 Update ./scripts/Checks/AD link health.ps1 +083a009 Update ./scripts/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 +c99f332 Update ./scripts/Checks/AD link health.ps1 +40f7c21 Update ./scripts/Checks/AD link health.ps1 +7a99bae Update ./scripts/Checks/AD link health.ps1 +3e6acd5 Update ./scripts/Checks/AD link health.ps1 +0cb141b Update ./scripts/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 +bd1a7d4 Update ./snippets/Cleaner.ps1 +3a3fc89 Update ./scripts/Tools/Cleanup temp files.ps1 +a5be9a6 Update ./snippets/Cleaner.ps1 +9271682 Merge branch 'amidaware:main' into main +4e5b061 Update ./scripts/Fixes/Fix broken ESET installation.ps1 +42227b0 Merge pull request #264 from P6g9YHK6/main +9801b02 Update ./snippets/Updater P3.5 Schedules parser.ps1 +c244ac0 Update ./scripts/TasksUpdater/Updater P1 Template WU, SU, PS and Cleaner.ps1 +4f452f5 Update ./snippets/Updater P3.5 Schedules parser.ps1 +3447f0d Update ./scripts/TasksUpdater/Updater P3 Run WU.ps1 +7ace1b4 Update ./scripts/TasksUpdater/Updater P3 Run SU.ps1 +82b2db7 Update ./scripts/TasksUpdater/Updater P3 Run PS.ps1 +e18e6a6 Update ./scripts/TasksUpdater/Updater P3 Run Cleaner.ps1 +1348fc3 Update ./snippets/Logging.ps1 +66347ac Update ./scripts/TasksUpdater/Updater P1 Template WU, SU, PS and Cleaner.ps1 +6113a41 Update ./scripts/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 +c3620ba Update ./scripts/TasksUpdater/Updater P1 Template WU, SU, PS and Cleaner.ps1 +af2afd3 Merge branch 'amidaware:main' into main +fe829ea Update ./scripts/Checks/Last errors logs.ps1 +e860053 Merge pull request #263 from P6g9YHK6/main +c7ca465 Update ./scripts/Checks/SQL Health.ps1 +c329a15 Update ./scripts/Checks/Maximum UpTime.ps1 +bacb2d2 Update ./scripts/Checks/Boot mode.ps1 +6f7e4d9 Update ./scripts/Checks/is RDP port ok.ps1 +a5d525f Update ./scripts/Lab/Send mail test.ps1 +1685765 Update ./scripts/Collectors/Retrieve all IIS bindings.ps1 +7f0ace1 Update ./scripts/Tools/Get logon events.ps1 +ebcaa92 Update ./scripts/Collectors/OS Install Date.ps1 +f64f73f Update ./scripts/Tools/Get last shutdown info.ps1 +a0f05e8 Update ./scripts/Collectors/get Domains or Workgroup name.ps1 +67b3eb3 Update ./scripts/Tools/Force Azureo365 AD sync.ps1 +9954e12 Update ./scripts/Lab/Fake CheckRandom Alert.py +e340df3 Update ./scripts/Lab/Fake CheckRandom Alert 2.py +ace1448 Update ./scripts/Checks/Disk Free Space.ps1 +7a2d483 Update ./scripts/Checks/DFS replication.ps1 +df1a574 Update ./scripts/Collectors/Collect Licensing 5 Office.ps1 +4b9f235 Update ./scripts/Collectors/Collect Licensing 4 RDS.ps1 +f514812 Update ./scripts/Collectors/Collect Licensing 3 Exchange.ps1 +5aa9e92 Update ./scripts/Collectors/Collect Licensing 2 SQL.ps1 +43e7282 Update ./scripts/Collectors/Collect Licensing 1 General.ps1 +2b9ddac Update ./scripts/Tasks/Change user password.ps1 +5413b3d Update ./scripts/Tasks/Auto-logoff users.ps1 +3ab9e46 Update ./scripts/Tools/Activate windows with KMS.ps1 +6560fb2 Update ./scripts/Checks/Active Directory Health.ps1 +3ab3329 Update ./scripts/Tools/Windows update force install new updates.ps1 +4b22d68 Update ./snippets/VHDXCleaner.ps1 +a2fed5d Update ./scripts/Checks/Task Scheduler scanner.ps1 +f924415 Update ./scripts/Fixes/Ensure all services with startup type Automatic are running.ps1 +5f672d8 Update ./scripts/Checks/Is TCP port open.ps1 +0f0c823 Update ./scripts/Checks/Active Directory Health.ps1 +eca3c15 Update ./scripts/Build/Create generic admin account.ps1 +50331e6 Update ./scripts/Tools/Reset permission of target folder.ps1 +356b46d Update ./scripts/Tasks/Start Eset update.ps1 +23599b2 Update ./scripts/Tools/SSL Certificate manager.ps1 +c044c76 Update ./scripts/Fixes/Resync time NTP.ps1 +4ef08b7 Update ./scripts/Fixes/RDS Fix taskbar.ps1 +8179023 Update ./scripts/Checks/Maximum UpTime.ps1 +a36714f Update ./scripts/Fixes/Fix broken ESET installation.ps1 +cd1bf87 Update ./scripts/Fixes/Bluescreen report.ps1 +a4c1577 Update ./scripts/Tools/Activate windows with KMS.ps1 +0816473 Update ./scripts/Checks/Internet uplink.ps1 +74eba5d Update ./scripts/Tasks/Kill Switch Manager.ps1 +f4a433d Update ./scripts/Tasks/Import RD Gateway Cert From IIS.ps1 +7456fc2 Merge pull request #262 from P6g9YHK6/main +fc579ed Update ./scripts/Tasks/Start Eset scan V2.ps1 +6414581 Update ./scripts/Tools/Expand partitiondrivedisk size.ps1 +01950d4 Update ./scripts/Tools/azure AD Connect Certificate Cleanup.ps1 +ddfea57 Update ./snippets/GeneratedPassphrase.ps1 +c26576e Update ./snippets/Cleaner.ps1 +b1fbaf3 Update ./snippets/CallPowerShell7.ps1 +b41dd69 Update ./scripts/Checks/Last errors logs.ps1 +47fd672 Update ./scripts/Checks/Swap health.ps1 +1abb459 Update ./scripts/Checks/Disk RW.ps1 +b9fd41e Update ./scripts/Checks/Windows Reliabilty Score.ps1 +5d35e25 Update ./scripts/Checks/Certificates expiry.ps1 +d6efd75 Update ./scripts/Checks/AD Connect health.ps1 +8ac0705 Update ./scripts/Checks/Windows Services.ps1 +e8c31f1 Update ./scripts/Checks/Boot mode.ps1 +6ab62cd Update ./scripts/Checks/Eset Status.ps1 +a168e36 Update ./scripts/Checks/is RDP port ok.ps1 +93c43d3 Update ./scripts/Checks/Exchange Health.ps1 +19a3c2e Update ./scripts/Checks/Rdcms size.ps1 +52fb1af Update ./scripts/Checks/Activation status.ps1 +3ffa3f4 Update ./scripts/Checks/is process running.ps1 +b180fd7 Update ./scripts/Checks/Ping monitoring.ps1 +729b153 Update ./scripts/Checks/AD link health.ps1 +5f9bf13 Update ./scripts/Build/Change default chocolatey repo to internal.ps1 +3a72945 Update ./scripts/Build/Update TRMM agent.ps1 +a206a66 Update ./scripts/Build/Change NTP target to company.ps1 +7b61a77 Update ./scripts/Build/Forward HTTP Traffic To Company Website.ps1 +6f9fbc5 Update ./scripts/Backend/Repo package updater.py +ba7bbc1 Update ./scripts/Backend/Export TRMM Scripts to folder and git sync V2.py +3a38738 Update ./scripts/Backend/Uptime Kuma Monitoring For Tactical.py +82427ca Merge pull request #260 from silversword411/main +39a7220 Staging: New PowerShell script to check and manage service statuses +32d101d Merge pull request #259 from dinger1986/main +9be69b5 Update Win_Bluescreen_Report.ps1 +7028b3f Merge pull request #258 from silversword411/main +13e8ad3 refactor: Replace PowerShell login audit script with Python version +3dfffa3 wip: Add script to retrieve cellular data using WMI +ed3fe18 wip: Add Disk Speed Multitest script with cross-platform support +e3757cb wip: DISM and SFC checker and fixer +7927b04 Staging: Rename troubleshooting script +26e9257 Merge pull request #257 from silversword411/main +cf5f18f Staging: Windows Agent Troubleshooting script +d9a91bc Merge pull request #254 from silversword411/main +3c50009 PROD Update: Refactor choco bulk to add support for listing upgradeable packages +dd29b84 Staging: Add script to enable exclusions for specific applications and processes in Windows Defender. Thx @dinger1986 for giving us a starting point +b0e632d Dell PERC add keyword +00fe75f Merge pull request #251 from silversword411/main +930c833 WIP: Add reboot via toast requests +2f15cf5 WIP add: Win_Reboot_usingIdleandUptime.ps1 script for rebooting device +f801441 Merge pull request #250 from silversword411/main +98fa5b5 add WIP: Add Win_Screenconnect_Detectothers.ps1 script for detecting other remote access systems +374b5f0 Merge pull request #249 from silversword411/main +9e193e8 chore: Refactor Win_Wifi_SSID_and_Password_Retrieval.ps1 to add autoconnect column +f19c419 Merge pull request #248 from silversword411/main +e44c7a7 add WIP: TRMM agent installer and lock/unlock scripts. Thx CBG_ITSUP +b0a0b28 Merge pull request #247 from cdp1337/community-scripts-245 +513a529 Proposed work for amidaware/community-scripts#245 +008a6dd Merge pull request #244 from silversword411/main +9ff865a chore: Refactor Win_TRMM_ScheduledTasks_List.ps1 to improve task information retrieval +cce3206 Merge pull request #243 from silversword411/main +116e51d Merge pull request #241 from styx-tdo/main +c97842f Fix variable name certfileMY Add check for friendly name, use Subject if not present +cb39181 chore: Refactor add folder creation function +f287fbd WIP: ASUS debloater +fe843cd WIP: Add Dell RAID monitoring script +6e314ce Merge pull request #240 from silversword411/main +b81e750 chore: Refactor Win_RunAsUser_Example.ps1 to simplify CaptureOutput +c6a8e1b WIP: Adding urbackup scripts +40ac664 Rename MDM for keyword searching +76c58b2 Updating RunAsUser template +8ee1fe4 Merge pull request #235 from silversword411/main +87cff55 Merge pull request #239 from bbrendon/patch-1 +484743a remove dead url +a42aaf1 Update Win_Speedtest.ps1 +83b637b Fix reboot script +4d65341 Merge pull request #237 from redanthrax/LocalAdminFixes +054a214 fixed issue with account existing +79af8f0 Fixing reboot script and timeouts +c426a5a derp...forgot a new guid +f338fab New Community Script: Reboot +e4071f1 Merge pull request #234 from silversword411/main +efca24c Move TRMM tasks +e114b3d Windows Network scanner via python v1.5 +43e8c88 Staged: Windows backup Monitor +068f675 TRMM tasks +ab32372 Merge pull request #233 from silversword411/main +a32be81 fix spywarekiller version comment headers +9431faa Merge pull request #232 from redanthrax/Bitlocker +3e35640 Merge pull request #231 from redanthrax/WinREFix +71e0b70 Updated Bitlocker Script Name +e72330f WinRE Fix Script +019977b Merge pull request #230 from dinger1986/main +401548b Update Win_Duplicati_Status.ps1 +baff79e Merge pull request #229 from dinger1986/main +76e10a5 Update Win_Duplicati_Status.ps1 +527471a Merge pull request #228 from dinger1986/main +5e17c71 Update Win_Duplicati_Status.ps1 +d4989ba Merge pull request #225 from redanthrax/Crowdstrike +a284173 Merge pull request #226 from redanthrax/LogShipperUpdate +98380b3 Merge pull request #227 from redanthrax/AutoStoreUpdate +3ae85a0 MS Store Update Setting +da9d1e3 added logic for upgrade, updated params +5830509 crowdstrike script +0c0fc0d Merge pull request #224 from silversword411/main +401197e Screenconnect AIO Update json for syntax +b4e00d7 Merge branch 'main' of https://github.com/silversword411/community-scripts +4fad311 Merge pull request #223 from redanthrax/UninstallerImprovements +804732f Merge pull request #222 from redanthrax/AutoElevateUninstallFix +d5e3297 Merge pull request #220 from redanthrax/Bitlocker +6c456c1 improved uninstall, added ability for multiple apps, improved output +39f1a05 updated for better uninstall functionality +9b22691 Merge pull request #221 from redanthrax/DuoUninstallFixes +35c44ab updated script to handle uninstall scenarios better +aa88e2c removed end output +aa99076 added fix scenario for an issue preventing key backup, end output +637a579 Screenconnect AIO - adding debug and formatting +36c57da added check for tpm +27c365f added recovery password set +a637392 Added initial script that is WIP +e4b698a Merge pull request #216 from redanthrax/DuoRemoveUpgradeUninstall +436259f Merge pull request #213 from derfladi/mode-upgrade-only-installed +147ab78 Merge branch 'main' into mode-upgrade-only-installed +a1dd608 Rename Windows_Clear_cookies.ps1 to Win_Clear_cookies.ps1 +64bbb8a Merge pull request #218 from dinger1986/main +0c845d5 Create Windows_Clear_cookies.ps1 +37da664 Merge pull request #217 from dinger1986/main +00a373b Create linux_os_update_check.sh +0ee108e Update linux_os_update.sh +20c9eaa Update linux_os_update.sh +20e3755 changed upgrade to not uninstall first +63177e9 Merge pull request #215 from silversword411/main +f6210fb Updating choco bulk to use --no-progress +5ba2263 Merge pull request #189 from lcsnetworks:fix_disk_cleanup_script +6e810a2 Merge pull request #204 from Aidan-abss/main +933e97e Moving to staging and renaming for further testing +ddd5081 Merge pull request #211 from redanthrax/PrinterInstaller +e4a8be8 Merge pull request #214 from silversword411/main +5528749 staged new speed test script - Test pls +6c885be WIP - try and kill Firefox full screening for Tech Support scammer +54f022c Updating official defender exclusions +477d18a Veeam Collection +d8d3d51 fix wording +94756d0 Add new mode +408accb Update Win_Chocolatey_Manage_Apps_Bulk.ps1 +809de6f Merge pull request #212 from silversword411/main +0fb38c2 WIP: Spywarekiller v1.3 +2b8876a Fixing Recycle bin empty +5668448 created printer installer script +d14350c Merge pull request #210 from silversword411/main +a7d403d NIC enable/disable script. Thx https://github.com/orbitturner ! +ddc2fb9 Merge pull request #209 from MalteKiefer/patch-1 +78420c0 Update Win_WinGet_Manage_Apps.ps1 +ed7ca32 Merge pull request #203 from silversword411/main +193631a Choco List Converting PS to bat +39ee66d WIP: Vuln scanner +dc7adf3 Merge pull request #208 from dinger1986/main +274a4cd Update Win_Win11_Ready.ps1 +6fec3b6 Merge pull request #207 from redanthrax/NewTeams +dc53bf6 WIP SMB1 checker +a344f93 Added script to upgrade Teams to New Teams +54db857 Merge pull request #193 from brisksystems-us/main +59f04fc Moving script to wip folder +f7a51d8 Merge pull request #206 from redanthrax/ClearOfficeCache +0dce5b0 added script for setting office to clear cache +63fb68d Merge pull request #205 from redanthrax/main +a3739c3 Updated for CyberCNS v4 +ff52c50 Rename Win_Failed_Logon_Check to Win_Failed_Logon_Check.ps1 +19f9a15 Create Win_Failed_Logon_Check +3a128d8 choco fixing comment headers +cc6d9a4 choco bulk - adding seconds debug info +dc1ea6e Refactor choco and add static choco paths +5890cf8 Merge pull request #202 from ConvexSERV/main +a11f760 Update Win_StorageCraftImageManager_Status.ps1 +b119cdc Update Win_StorageCraftImageManager_Status.ps1 diff --git a/scripts_staging/Build/Update TRMM agent.ps1 b/scripts_staging/Build/Update TRMM agent.ps1 index a9c61896..bb1a14f9 100644 --- a/scripts_staging/Build/Update TRMM agent.ps1 +++ b/scripts_staging/Build/Update TRMM agent.ps1 @@ -10,21 +10,21 @@ .PARAMETER version Specifies the version to download. If set to "latest," the script retrieves the latest version available on GitHub. - This can be specified through the environment variable `version`. - -.PARAMETER isSigned - Boolean flag to determine if the signed version should be downloaded. - Set to true in the environment variable `issigned` to download the signed version; otherwise, the unsigned version is downloaded. + 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` - and is required if `isSigned` is set to true. + 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 var +.EXEMPLE trmm_sign_download_token={{global.trmm_sign_download_token}} version=latest version=2.7.0 - issigned=true + trmm_api_target=api.exemple.com .NOTES Author: SAN @@ -32,25 +32,20 @@ #public .CHANGELOG - - Initial version - - Added support for environment variable input - - Enhanced error handling and process execution - - Added local version check to skip download if versions match + 29.10.24 SAN Initial script with signed and unsigned download support. + 21.12.24 SAN updated the script to not require "issigned" .TODO - integrate to monthly update runs + integrate to our monthly update runs + test if api target is really needed + default to latest when nothing is set #> + # Variables $version = $env:version # Specify a version manually, or leave as "latest" to get the latest version from GitHub -$isSigned = $env:issigned -eq 'true' # Set to true to download the signed version $signedDownloadToken = $env:trmm_sign_download_token # Token used for signed downloads only - -# Check for signed download token if isSigned is true -if ($isSigned -and -not $signedDownloadToken) { - Write-Output "Error: Missing signed download token. Exiting..." - exit 1 -} +$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" @@ -108,31 +103,21 @@ try { } } else { Write-Output "'Tactical RMM Agent' is not installed on this system. Checking installed software..." - # List all installed software for debugging - $allInstalledSoftware = Get-CimInstance -ClassName Win32_Product - Write-Output "Currently installed software (Win32_Product):" - $allInstalledSoftware | ForEach-Object { Write-Output "- $($_.Name)" } - - # Check the uninstall registry key as well - $uninstallKeys = @( - "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", - "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" - ) - Write-Output "Currently installed software (Registry):" - foreach ($key in $uninstallKeys) { - $allInstalledSoftware = Get-ItemProperty $key - $allInstalledSoftware | ForEach-Object { Write-Output "- $($_.DisplayName)" } - } } - + # 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 $isSigned variable - if ($isSigned) { + # 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=api-rmm-managed-services.vtx.ch" + $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" From 9acefa593b64c64a2b4ff0e3cdce24d1db19064c Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Sun, 22 Dec 2024 11:08:08 +0000 Subject: [PATCH 220/447] Update ./scripts/Build/Update TRMM agent.ps1 --- scripts_staging/Build/Update TRMM agent.ps1 | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts_staging/Build/Update TRMM agent.ps1 b/scripts_staging/Build/Update TRMM agent.ps1 index bb1a14f9..e098b1ad 100644 --- a/scripts_staging/Build/Update TRMM agent.ps1 +++ b/scripts_staging/Build/Update TRMM agent.ps1 @@ -40,8 +40,6 @@ test if api target is really needed default to latest when nothing is set #> - - # 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 @@ -52,7 +50,7 @@ $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 + $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) { @@ -81,6 +79,11 @@ try { "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..." From e185f97a702904f722338db2f3476f2603c3dd67 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Sun, 22 Dec 2024 11:14:48 +0000 Subject: [PATCH 221/447] Update ./scripts/Build/Update TRMM agent.ps1 --- scripts_staging/Build/Update TRMM agent.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts_staging/Build/Update TRMM agent.ps1 b/scripts_staging/Build/Update TRMM agent.ps1 index e098b1ad..ca264829 100644 --- a/scripts_staging/Build/Update TRMM agent.ps1 +++ b/scripts_staging/Build/Update TRMM agent.ps1 @@ -34,11 +34,12 @@ .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 - default to latest when nothing is set + #> # Variables $version = $env:version # Specify a version manually, or leave as "latest" to get the latest version from GitHub From 0197130535df9fd6bf88c6df565637cf4e5ceacd Mon Sep 17 00:00:00 2001 From: P6g9YHK6 Date: Sun, 22 Dec 2024 14:28:50 +0100 Subject: [PATCH 222/447] new file: e -i HEAD~3 modified: scripts_staging/Build/Update TRMM agent.ps1 --- e -i HEAD~3 | 257 ++++++++++++++++++++ scripts_staging/Build/Update TRMM agent.ps1 | 73 +++--- 2 files changed, 288 insertions(+), 42 deletions(-) create mode 100644 e -i HEAD~3 diff --git a/e -i HEAD~3 b/e -i HEAD~3 new file mode 100644 index 00000000..488c765a --- /dev/null +++ b/e -i HEAD~3 @@ -0,0 +1,257 @@ +cf2d1a7 (HEAD -> main, origin/main, origin/HEAD) Update ./scripts/Build/Update TRMM agent.ps1 +b8f19df Update ./scripts/Build/Update TRMM agent.ps1 +70673dc Update ./scripts/Checks/is RDP port ok.ps1 +5df7518 Update ./scripts/Build/Forward HTTP Traffic To Company Website.ps1 +245d3dd Delete scripts_staging/Lab/Generate Blue screen of death ☢️.ps1 +c879a58 Update ./scripts/Lab/☢️ Generate Blue screen of death.ps1 +7630a73 Update ./scripts/Lab/☢️ Generate Blue screen of death.ps1 +ac8f36f Update ./scripts/Lab/Generate Blue screen of death ☢️.ps1 +f818d85 Update ./scripts/Fixes/Bluescreen report.ps1 +f3ae21a Update ./scripts/Checks/Last errors logs.ps1 +6dbac42 Update ./scripts/Fixes/Bluescreen report.ps1 +d0aed6e Update ./scripts/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 +31ae3cb Update ./scripts/Checks/AD link health.ps1 +965a096 Update ./scripts/Checks/AD link health.ps1 +083a009 Update ./scripts/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 +c99f332 Update ./scripts/Checks/AD link health.ps1 +40f7c21 Update ./scripts/Checks/AD link health.ps1 +7a99bae Update ./scripts/Checks/AD link health.ps1 +3e6acd5 Update ./scripts/Checks/AD link health.ps1 +0cb141b Update ./scripts/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 +bd1a7d4 Update ./snippets/Cleaner.ps1 +3a3fc89 Update ./scripts/Tools/Cleanup temp files.ps1 +a5be9a6 Update ./snippets/Cleaner.ps1 +9271682 Merge branch 'amidaware:main' into main +4e5b061 Update ./scripts/Fixes/Fix broken ESET installation.ps1 +42227b0 Merge pull request #264 from P6g9YHK6/main +9801b02 Update ./snippets/Updater P3.5 Schedules parser.ps1 +c244ac0 Update ./scripts/TasksUpdater/Updater P1 Template WU, SU, PS and Cleaner.ps1 +4f452f5 Update ./snippets/Updater P3.5 Schedules parser.ps1 +3447f0d Update ./scripts/TasksUpdater/Updater P3 Run WU.ps1 +7ace1b4 Update ./scripts/TasksUpdater/Updater P3 Run SU.ps1 +82b2db7 Update ./scripts/TasksUpdater/Updater P3 Run PS.ps1 +e18e6a6 Update ./scripts/TasksUpdater/Updater P3 Run Cleaner.ps1 +1348fc3 Update ./snippets/Logging.ps1 +66347ac Update ./scripts/TasksUpdater/Updater P1 Template WU, SU, PS and Cleaner.ps1 +6113a41 Update ./scripts/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 +c3620ba Update ./scripts/TasksUpdater/Updater P1 Template WU, SU, PS and Cleaner.ps1 +af2afd3 Merge branch 'amidaware:main' into main +fe829ea Update ./scripts/Checks/Last errors logs.ps1 +e860053 Merge pull request #263 from P6g9YHK6/main +c7ca465 Update ./scripts/Checks/SQL Health.ps1 +c329a15 Update ./scripts/Checks/Maximum UpTime.ps1 +bacb2d2 Update ./scripts/Checks/Boot mode.ps1 +6f7e4d9 Update ./scripts/Checks/is RDP port ok.ps1 +a5d525f Update ./scripts/Lab/Send mail test.ps1 +1685765 Update ./scripts/Collectors/Retrieve all IIS bindings.ps1 +7f0ace1 Update ./scripts/Tools/Get logon events.ps1 +ebcaa92 Update ./scripts/Collectors/OS Install Date.ps1 +f64f73f Update ./scripts/Tools/Get last shutdown info.ps1 +a0f05e8 Update ./scripts/Collectors/get Domains or Workgroup name.ps1 +67b3eb3 Update ./scripts/Tools/Force Azureo365 AD sync.ps1 +9954e12 Update ./scripts/Lab/Fake CheckRandom Alert.py +e340df3 Update ./scripts/Lab/Fake CheckRandom Alert 2.py +ace1448 Update ./scripts/Checks/Disk Free Space.ps1 +7a2d483 Update ./scripts/Checks/DFS replication.ps1 +df1a574 Update ./scripts/Collectors/Collect Licensing 5 Office.ps1 +4b9f235 Update ./scripts/Collectors/Collect Licensing 4 RDS.ps1 +f514812 Update ./scripts/Collectors/Collect Licensing 3 Exchange.ps1 +5aa9e92 Update ./scripts/Collectors/Collect Licensing 2 SQL.ps1 +43e7282 Update ./scripts/Collectors/Collect Licensing 1 General.ps1 +2b9ddac Update ./scripts/Tasks/Change user password.ps1 +5413b3d Update ./scripts/Tasks/Auto-logoff users.ps1 +3ab9e46 Update ./scripts/Tools/Activate windows with KMS.ps1 +6560fb2 Update ./scripts/Checks/Active Directory Health.ps1 +3ab3329 Update ./scripts/Tools/Windows update force install new updates.ps1 +4b22d68 Update ./snippets/VHDXCleaner.ps1 +a2fed5d Update ./scripts/Checks/Task Scheduler scanner.ps1 +f924415 Update ./scripts/Fixes/Ensure all services with startup type Automatic are running.ps1 +5f672d8 Update ./scripts/Checks/Is TCP port open.ps1 +0f0c823 Update ./scripts/Checks/Active Directory Health.ps1 +eca3c15 Update ./scripts/Build/Create generic admin account.ps1 +50331e6 Update ./scripts/Tools/Reset permission of target folder.ps1 +356b46d Update ./scripts/Tasks/Start Eset update.ps1 +23599b2 Update ./scripts/Tools/SSL Certificate manager.ps1 +c044c76 Update ./scripts/Fixes/Resync time NTP.ps1 +4ef08b7 Update ./scripts/Fixes/RDS Fix taskbar.ps1 +8179023 Update ./scripts/Checks/Maximum UpTime.ps1 +a36714f Update ./scripts/Fixes/Fix broken ESET installation.ps1 +cd1bf87 Update ./scripts/Fixes/Bluescreen report.ps1 +a4c1577 Update ./scripts/Tools/Activate windows with KMS.ps1 +0816473 Update ./scripts/Checks/Internet uplink.ps1 +74eba5d Update ./scripts/Tasks/Kill Switch Manager.ps1 +f4a433d Update ./scripts/Tasks/Import RD Gateway Cert From IIS.ps1 +7456fc2 Merge pull request #262 from P6g9YHK6/main +fc579ed Update ./scripts/Tasks/Start Eset scan V2.ps1 +6414581 Update ./scripts/Tools/Expand partitiondrivedisk size.ps1 +01950d4 Update ./scripts/Tools/azure AD Connect Certificate Cleanup.ps1 +ddfea57 Update ./snippets/GeneratedPassphrase.ps1 +c26576e Update ./snippets/Cleaner.ps1 +b1fbaf3 Update ./snippets/CallPowerShell7.ps1 +b41dd69 Update ./scripts/Checks/Last errors logs.ps1 +47fd672 Update ./scripts/Checks/Swap health.ps1 +1abb459 Update ./scripts/Checks/Disk RW.ps1 +b9fd41e Update ./scripts/Checks/Windows Reliabilty Score.ps1 +5d35e25 Update ./scripts/Checks/Certificates expiry.ps1 +d6efd75 Update ./scripts/Checks/AD Connect health.ps1 +8ac0705 Update ./scripts/Checks/Windows Services.ps1 +e8c31f1 Update ./scripts/Checks/Boot mode.ps1 +6ab62cd Update ./scripts/Checks/Eset Status.ps1 +a168e36 Update ./scripts/Checks/is RDP port ok.ps1 +93c43d3 Update ./scripts/Checks/Exchange Health.ps1 +19a3c2e Update ./scripts/Checks/Rdcms size.ps1 +52fb1af Update ./scripts/Checks/Activation status.ps1 +3ffa3f4 Update ./scripts/Checks/is process running.ps1 +b180fd7 Update ./scripts/Checks/Ping monitoring.ps1 +729b153 Update ./scripts/Checks/AD link health.ps1 +5f9bf13 Update ./scripts/Build/Change default chocolatey repo to internal.ps1 +3a72945 Update ./scripts/Build/Update TRMM agent.ps1 +a206a66 Update ./scripts/Build/Change NTP target to company.ps1 +7b61a77 Update ./scripts/Build/Forward HTTP Traffic To Company Website.ps1 +6f9fbc5 Update ./scripts/Backend/Repo package updater.py +ba7bbc1 Update ./scripts/Backend/Export TRMM Scripts to folder and git sync V2.py +3a38738 Update ./scripts/Backend/Uptime Kuma Monitoring For Tactical.py +82427ca Merge pull request #260 from silversword411/main +39a7220 Staging: New PowerShell script to check and manage service statuses +32d101d Merge pull request #259 from dinger1986/main +9be69b5 Update Win_Bluescreen_Report.ps1 +7028b3f Merge pull request #258 from silversword411/main +13e8ad3 refactor: Replace PowerShell login audit script with Python version +3dfffa3 wip: Add script to retrieve cellular data using WMI +ed3fe18 wip: Add Disk Speed Multitest script with cross-platform support +e3757cb wip: DISM and SFC checker and fixer +7927b04 Staging: Rename troubleshooting script +26e9257 Merge pull request #257 from silversword411/main +cf5f18f Staging: Windows Agent Troubleshooting script +d9a91bc Merge pull request #254 from silversword411/main +3c50009 PROD Update: Refactor choco bulk to add support for listing upgradeable packages +dd29b84 Staging: Add script to enable exclusions for specific applications and processes in Windows Defender. Thx @dinger1986 for giving us a starting point +b0e632d Dell PERC add keyword +00fe75f Merge pull request #251 from silversword411/main +930c833 WIP: Add reboot via toast requests +2f15cf5 WIP add: Win_Reboot_usingIdleandUptime.ps1 script for rebooting device +f801441 Merge pull request #250 from silversword411/main +98fa5b5 add WIP: Add Win_Screenconnect_Detectothers.ps1 script for detecting other remote access systems +374b5f0 Merge pull request #249 from silversword411/main +9e193e8 chore: Refactor Win_Wifi_SSID_and_Password_Retrieval.ps1 to add autoconnect column +f19c419 Merge pull request #248 from silversword411/main +e44c7a7 add WIP: TRMM agent installer and lock/unlock scripts. Thx CBG_ITSUP +b0a0b28 Merge pull request #247 from cdp1337/community-scripts-245 +513a529 Proposed work for amidaware/community-scripts#245 +008a6dd Merge pull request #244 from silversword411/main +9ff865a chore: Refactor Win_TRMM_ScheduledTasks_List.ps1 to improve task information retrieval +cce3206 Merge pull request #243 from silversword411/main +116e51d Merge pull request #241 from styx-tdo/main +c97842f Fix variable name certfileMY Add check for friendly name, use Subject if not present +cb39181 chore: Refactor add folder creation function +f287fbd WIP: ASUS debloater +fe843cd WIP: Add Dell RAID monitoring script +6e314ce Merge pull request #240 from silversword411/main +b81e750 chore: Refactor Win_RunAsUser_Example.ps1 to simplify CaptureOutput +c6a8e1b WIP: Adding urbackup scripts +40ac664 Rename MDM for keyword searching +76c58b2 Updating RunAsUser template +8ee1fe4 Merge pull request #235 from silversword411/main +87cff55 Merge pull request #239 from bbrendon/patch-1 +484743a remove dead url +a42aaf1 Update Win_Speedtest.ps1 +83b637b Fix reboot script +4d65341 Merge pull request #237 from redanthrax/LocalAdminFixes +054a214 fixed issue with account existing +79af8f0 Fixing reboot script and timeouts +c426a5a derp...forgot a new guid +f338fab New Community Script: Reboot +e4071f1 Merge pull request #234 from silversword411/main +efca24c Move TRMM tasks +e114b3d Windows Network scanner via python v1.5 +43e8c88 Staged: Windows backup Monitor +068f675 TRMM tasks +ab32372 Merge pull request #233 from silversword411/main +a32be81 fix spywarekiller version comment headers +9431faa Merge pull request #232 from redanthrax/Bitlocker +3e35640 Merge pull request #231 from redanthrax/WinREFix +71e0b70 Updated Bitlocker Script Name +e72330f WinRE Fix Script +019977b Merge pull request #230 from dinger1986/main +401548b Update Win_Duplicati_Status.ps1 +baff79e Merge pull request #229 from dinger1986/main +76e10a5 Update Win_Duplicati_Status.ps1 +527471a Merge pull request #228 from dinger1986/main +5e17c71 Update Win_Duplicati_Status.ps1 +d4989ba Merge pull request #225 from redanthrax/Crowdstrike +a284173 Merge pull request #226 from redanthrax/LogShipperUpdate +98380b3 Merge pull request #227 from redanthrax/AutoStoreUpdate +3ae85a0 MS Store Update Setting +da9d1e3 added logic for upgrade, updated params +5830509 crowdstrike script +0c0fc0d Merge pull request #224 from silversword411/main +401197e Screenconnect AIO Update json for syntax +b4e00d7 Merge branch 'main' of https://github.com/silversword411/community-scripts +4fad311 Merge pull request #223 from redanthrax/UninstallerImprovements +804732f Merge pull request #222 from redanthrax/AutoElevateUninstallFix +d5e3297 Merge pull request #220 from redanthrax/Bitlocker +6c456c1 improved uninstall, added ability for multiple apps, improved output +39f1a05 updated for better uninstall functionality +9b22691 Merge pull request #221 from redanthrax/DuoUninstallFixes +35c44ab updated script to handle uninstall scenarios better +aa88e2c removed end output +aa99076 added fix scenario for an issue preventing key backup, end output +637a579 Screenconnect AIO - adding debug and formatting +36c57da added check for tpm +27c365f added recovery password set +a637392 Added initial script that is WIP +e4b698a Merge pull request #216 from redanthrax/DuoRemoveUpgradeUninstall +436259f Merge pull request #213 from derfladi/mode-upgrade-only-installed +147ab78 Merge branch 'main' into mode-upgrade-only-installed +a1dd608 Rename Windows_Clear_cookies.ps1 to Win_Clear_cookies.ps1 +64bbb8a Merge pull request #218 from dinger1986/main +0c845d5 Create Windows_Clear_cookies.ps1 +37da664 Merge pull request #217 from dinger1986/main +00a373b Create linux_os_update_check.sh +0ee108e Update linux_os_update.sh +20c9eaa Update linux_os_update.sh +20e3755 changed upgrade to not uninstall first +63177e9 Merge pull request #215 from silversword411/main +f6210fb Updating choco bulk to use --no-progress +5ba2263 Merge pull request #189 from lcsnetworks:fix_disk_cleanup_script +6e810a2 Merge pull request #204 from Aidan-abss/main +933e97e Moving to staging and renaming for further testing +ddd5081 Merge pull request #211 from redanthrax/PrinterInstaller +e4a8be8 Merge pull request #214 from silversword411/main +5528749 staged new speed test script - Test pls +6c885be WIP - try and kill Firefox full screening for Tech Support scammer +54f022c Updating official defender exclusions +477d18a Veeam Collection +d8d3d51 fix wording +94756d0 Add new mode +408accb Update Win_Chocolatey_Manage_Apps_Bulk.ps1 +809de6f Merge pull request #212 from silversword411/main +0fb38c2 WIP: Spywarekiller v1.3 +2b8876a Fixing Recycle bin empty +5668448 created printer installer script +d14350c Merge pull request #210 from silversword411/main +a7d403d NIC enable/disable script. Thx https://github.com/orbitturner ! +ddc2fb9 Merge pull request #209 from MalteKiefer/patch-1 +78420c0 Update Win_WinGet_Manage_Apps.ps1 +ed7ca32 Merge pull request #203 from silversword411/main +193631a Choco List Converting PS to bat +39ee66d WIP: Vuln scanner +dc7adf3 Merge pull request #208 from dinger1986/main +274a4cd Update Win_Win11_Ready.ps1 +6fec3b6 Merge pull request #207 from redanthrax/NewTeams +dc53bf6 WIP SMB1 checker +a344f93 Added script to upgrade Teams to New Teams +54db857 Merge pull request #193 from brisksystems-us/main +59f04fc Moving script to wip folder +f7a51d8 Merge pull request #206 from redanthrax/ClearOfficeCache +0dce5b0 added script for setting office to clear cache +63fb68d Merge pull request #205 from redanthrax/main +a3739c3 Updated for CyberCNS v4 +ff52c50 Rename Win_Failed_Logon_Check to Win_Failed_Logon_Check.ps1 +19f9a15 Create Win_Failed_Logon_Check +3a128d8 choco fixing comment headers +cc6d9a4 choco bulk - adding seconds debug info +dc1ea6e Refactor choco and add static choco paths +5890cf8 Merge pull request #202 from ConvexSERV/main +a11f760 Update Win_StorageCraftImageManager_Status.ps1 +b119cdc Update Win_StorageCraftImageManager_Status.ps1 diff --git a/scripts_staging/Build/Update TRMM agent.ps1 b/scripts_staging/Build/Update TRMM agent.ps1 index a9c61896..ca264829 100644 --- a/scripts_staging/Build/Update TRMM agent.ps1 +++ b/scripts_staging/Build/Update TRMM agent.ps1 @@ -10,21 +10,21 @@ .PARAMETER version Specifies the version to download. If set to "latest," the script retrieves the latest version available on GitHub. - This can be specified through the environment variable `version`. - -.PARAMETER isSigned - Boolean flag to determine if the signed version should be downloaded. - Set to true in the environment variable `issigned` to download the signed version; otherwise, the unsigned version is downloaded. + 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` - and is required if `isSigned` is set to true. + 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 var +.EXEMPLE trmm_sign_download_token={{global.trmm_sign_download_token}} version=latest version=2.7.0 - issigned=true + trmm_api_target=api.exemple.com .NOTES Author: SAN @@ -32,32 +32,26 @@ #public .CHANGELOG - - Initial version - - Added support for environment variable input - - Enhanced error handling and process execution - - Added local version check to skip download if versions match + 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 monthly update runs + 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 -$isSigned = $env:issigned -eq 'true' # Set to true to download the signed version $signedDownloadToken = $env:trmm_sign_download_token # Token used for signed downloads only - -# Check for signed download token if isSigned is true -if ($isSigned -and -not $signedDownloadToken) { - Write-Output "Error: Missing signed download token. Exiting..." - exit 1 -} +$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 + $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) { @@ -86,6 +80,11 @@ try { "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..." @@ -108,31 +107,21 @@ try { } } else { Write-Output "'Tactical RMM Agent' is not installed on this system. Checking installed software..." - # List all installed software for debugging - $allInstalledSoftware = Get-CimInstance -ClassName Win32_Product - Write-Output "Currently installed software (Win32_Product):" - $allInstalledSoftware | ForEach-Object { Write-Output "- $($_.Name)" } - - # Check the uninstall registry key as well - $uninstallKeys = @( - "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", - "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" - ) - Write-Output "Currently installed software (Registry):" - foreach ($key in $uninstallKeys) { - $allInstalledSoftware = Get-ItemProperty $key - $allInstalledSoftware | ForEach-Object { Write-Output "- $($_.DisplayName)" } - } } - + # 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 $isSigned variable - if ($isSigned) { + # 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=api-rmm-managed-services.vtx.ch" + $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" From 91cd18779351766aa6f93424b7a72f3b4abff661 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 Date: Mon, 23 Dec 2024 23:54:11 +0100 Subject: [PATCH 223/447] deleted: e -i HEAD~3 --- e -i HEAD~3 | 257 ---------------------------------------------------- 1 file changed, 257 deletions(-) delete mode 100644 e -i HEAD~3 diff --git a/e -i HEAD~3 b/e -i HEAD~3 deleted file mode 100644 index 488c765a..00000000 --- a/e -i HEAD~3 +++ /dev/null @@ -1,257 +0,0 @@ -cf2d1a7 (HEAD -> main, origin/main, origin/HEAD) Update ./scripts/Build/Update TRMM agent.ps1 -b8f19df Update ./scripts/Build/Update TRMM agent.ps1 -70673dc Update ./scripts/Checks/is RDP port ok.ps1 -5df7518 Update ./scripts/Build/Forward HTTP Traffic To Company Website.ps1 -245d3dd Delete scripts_staging/Lab/Generate Blue screen of death ☢️.ps1 -c879a58 Update ./scripts/Lab/☢️ Generate Blue screen of death.ps1 -7630a73 Update ./scripts/Lab/☢️ Generate Blue screen of death.ps1 -ac8f36f Update ./scripts/Lab/Generate Blue screen of death ☢️.ps1 -f818d85 Update ./scripts/Fixes/Bluescreen report.ps1 -f3ae21a Update ./scripts/Checks/Last errors logs.ps1 -6dbac42 Update ./scripts/Fixes/Bluescreen report.ps1 -d0aed6e Update ./scripts/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 -31ae3cb Update ./scripts/Checks/AD link health.ps1 -965a096 Update ./scripts/Checks/AD link health.ps1 -083a009 Update ./scripts/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 -c99f332 Update ./scripts/Checks/AD link health.ps1 -40f7c21 Update ./scripts/Checks/AD link health.ps1 -7a99bae Update ./scripts/Checks/AD link health.ps1 -3e6acd5 Update ./scripts/Checks/AD link health.ps1 -0cb141b Update ./scripts/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 -bd1a7d4 Update ./snippets/Cleaner.ps1 -3a3fc89 Update ./scripts/Tools/Cleanup temp files.ps1 -a5be9a6 Update ./snippets/Cleaner.ps1 -9271682 Merge branch 'amidaware:main' into main -4e5b061 Update ./scripts/Fixes/Fix broken ESET installation.ps1 -42227b0 Merge pull request #264 from P6g9YHK6/main -9801b02 Update ./snippets/Updater P3.5 Schedules parser.ps1 -c244ac0 Update ./scripts/TasksUpdater/Updater P1 Template WU, SU, PS and Cleaner.ps1 -4f452f5 Update ./snippets/Updater P3.5 Schedules parser.ps1 -3447f0d Update ./scripts/TasksUpdater/Updater P3 Run WU.ps1 -7ace1b4 Update ./scripts/TasksUpdater/Updater P3 Run SU.ps1 -82b2db7 Update ./scripts/TasksUpdater/Updater P3 Run PS.ps1 -e18e6a6 Update ./scripts/TasksUpdater/Updater P3 Run Cleaner.ps1 -1348fc3 Update ./snippets/Logging.ps1 -66347ac Update ./scripts/TasksUpdater/Updater P1 Template WU, SU, PS and Cleaner.ps1 -6113a41 Update ./scripts/TasksUpdater/Updater P2 Scheduler WU, SU, PS and Cleaner.ps1 -c3620ba Update ./scripts/TasksUpdater/Updater P1 Template WU, SU, PS and Cleaner.ps1 -af2afd3 Merge branch 'amidaware:main' into main -fe829ea Update ./scripts/Checks/Last errors logs.ps1 -e860053 Merge pull request #263 from P6g9YHK6/main -c7ca465 Update ./scripts/Checks/SQL Health.ps1 -c329a15 Update ./scripts/Checks/Maximum UpTime.ps1 -bacb2d2 Update ./scripts/Checks/Boot mode.ps1 -6f7e4d9 Update ./scripts/Checks/is RDP port ok.ps1 -a5d525f Update ./scripts/Lab/Send mail test.ps1 -1685765 Update ./scripts/Collectors/Retrieve all IIS bindings.ps1 -7f0ace1 Update ./scripts/Tools/Get logon events.ps1 -ebcaa92 Update ./scripts/Collectors/OS Install Date.ps1 -f64f73f Update ./scripts/Tools/Get last shutdown info.ps1 -a0f05e8 Update ./scripts/Collectors/get Domains or Workgroup name.ps1 -67b3eb3 Update ./scripts/Tools/Force Azureo365 AD sync.ps1 -9954e12 Update ./scripts/Lab/Fake CheckRandom Alert.py -e340df3 Update ./scripts/Lab/Fake CheckRandom Alert 2.py -ace1448 Update ./scripts/Checks/Disk Free Space.ps1 -7a2d483 Update ./scripts/Checks/DFS replication.ps1 -df1a574 Update ./scripts/Collectors/Collect Licensing 5 Office.ps1 -4b9f235 Update ./scripts/Collectors/Collect Licensing 4 RDS.ps1 -f514812 Update ./scripts/Collectors/Collect Licensing 3 Exchange.ps1 -5aa9e92 Update ./scripts/Collectors/Collect Licensing 2 SQL.ps1 -43e7282 Update ./scripts/Collectors/Collect Licensing 1 General.ps1 -2b9ddac Update ./scripts/Tasks/Change user password.ps1 -5413b3d Update ./scripts/Tasks/Auto-logoff users.ps1 -3ab9e46 Update ./scripts/Tools/Activate windows with KMS.ps1 -6560fb2 Update ./scripts/Checks/Active Directory Health.ps1 -3ab3329 Update ./scripts/Tools/Windows update force install new updates.ps1 -4b22d68 Update ./snippets/VHDXCleaner.ps1 -a2fed5d Update ./scripts/Checks/Task Scheduler scanner.ps1 -f924415 Update ./scripts/Fixes/Ensure all services with startup type Automatic are running.ps1 -5f672d8 Update ./scripts/Checks/Is TCP port open.ps1 -0f0c823 Update ./scripts/Checks/Active Directory Health.ps1 -eca3c15 Update ./scripts/Build/Create generic admin account.ps1 -50331e6 Update ./scripts/Tools/Reset permission of target folder.ps1 -356b46d Update ./scripts/Tasks/Start Eset update.ps1 -23599b2 Update ./scripts/Tools/SSL Certificate manager.ps1 -c044c76 Update ./scripts/Fixes/Resync time NTP.ps1 -4ef08b7 Update ./scripts/Fixes/RDS Fix taskbar.ps1 -8179023 Update ./scripts/Checks/Maximum UpTime.ps1 -a36714f Update ./scripts/Fixes/Fix broken ESET installation.ps1 -cd1bf87 Update ./scripts/Fixes/Bluescreen report.ps1 -a4c1577 Update ./scripts/Tools/Activate windows with KMS.ps1 -0816473 Update ./scripts/Checks/Internet uplink.ps1 -74eba5d Update ./scripts/Tasks/Kill Switch Manager.ps1 -f4a433d Update ./scripts/Tasks/Import RD Gateway Cert From IIS.ps1 -7456fc2 Merge pull request #262 from P6g9YHK6/main -fc579ed Update ./scripts/Tasks/Start Eset scan V2.ps1 -6414581 Update ./scripts/Tools/Expand partitiondrivedisk size.ps1 -01950d4 Update ./scripts/Tools/azure AD Connect Certificate Cleanup.ps1 -ddfea57 Update ./snippets/GeneratedPassphrase.ps1 -c26576e Update ./snippets/Cleaner.ps1 -b1fbaf3 Update ./snippets/CallPowerShell7.ps1 -b41dd69 Update ./scripts/Checks/Last errors logs.ps1 -47fd672 Update ./scripts/Checks/Swap health.ps1 -1abb459 Update ./scripts/Checks/Disk RW.ps1 -b9fd41e Update ./scripts/Checks/Windows Reliabilty Score.ps1 -5d35e25 Update ./scripts/Checks/Certificates expiry.ps1 -d6efd75 Update ./scripts/Checks/AD Connect health.ps1 -8ac0705 Update ./scripts/Checks/Windows Services.ps1 -e8c31f1 Update ./scripts/Checks/Boot mode.ps1 -6ab62cd Update ./scripts/Checks/Eset Status.ps1 -a168e36 Update ./scripts/Checks/is RDP port ok.ps1 -93c43d3 Update ./scripts/Checks/Exchange Health.ps1 -19a3c2e Update ./scripts/Checks/Rdcms size.ps1 -52fb1af Update ./scripts/Checks/Activation status.ps1 -3ffa3f4 Update ./scripts/Checks/is process running.ps1 -b180fd7 Update ./scripts/Checks/Ping monitoring.ps1 -729b153 Update ./scripts/Checks/AD link health.ps1 -5f9bf13 Update ./scripts/Build/Change default chocolatey repo to internal.ps1 -3a72945 Update ./scripts/Build/Update TRMM agent.ps1 -a206a66 Update ./scripts/Build/Change NTP target to company.ps1 -7b61a77 Update ./scripts/Build/Forward HTTP Traffic To Company Website.ps1 -6f9fbc5 Update ./scripts/Backend/Repo package updater.py -ba7bbc1 Update ./scripts/Backend/Export TRMM Scripts to folder and git sync V2.py -3a38738 Update ./scripts/Backend/Uptime Kuma Monitoring For Tactical.py -82427ca Merge pull request #260 from silversword411/main -39a7220 Staging: New PowerShell script to check and manage service statuses -32d101d Merge pull request #259 from dinger1986/main -9be69b5 Update Win_Bluescreen_Report.ps1 -7028b3f Merge pull request #258 from silversword411/main -13e8ad3 refactor: Replace PowerShell login audit script with Python version -3dfffa3 wip: Add script to retrieve cellular data using WMI -ed3fe18 wip: Add Disk Speed Multitest script with cross-platform support -e3757cb wip: DISM and SFC checker and fixer -7927b04 Staging: Rename troubleshooting script -26e9257 Merge pull request #257 from silversword411/main -cf5f18f Staging: Windows Agent Troubleshooting script -d9a91bc Merge pull request #254 from silversword411/main -3c50009 PROD Update: Refactor choco bulk to add support for listing upgradeable packages -dd29b84 Staging: Add script to enable exclusions for specific applications and processes in Windows Defender. Thx @dinger1986 for giving us a starting point -b0e632d Dell PERC add keyword -00fe75f Merge pull request #251 from silversword411/main -930c833 WIP: Add reboot via toast requests -2f15cf5 WIP add: Win_Reboot_usingIdleandUptime.ps1 script for rebooting device -f801441 Merge pull request #250 from silversword411/main -98fa5b5 add WIP: Add Win_Screenconnect_Detectothers.ps1 script for detecting other remote access systems -374b5f0 Merge pull request #249 from silversword411/main -9e193e8 chore: Refactor Win_Wifi_SSID_and_Password_Retrieval.ps1 to add autoconnect column -f19c419 Merge pull request #248 from silversword411/main -e44c7a7 add WIP: TRMM agent installer and lock/unlock scripts. Thx CBG_ITSUP -b0a0b28 Merge pull request #247 from cdp1337/community-scripts-245 -513a529 Proposed work for amidaware/community-scripts#245 -008a6dd Merge pull request #244 from silversword411/main -9ff865a chore: Refactor Win_TRMM_ScheduledTasks_List.ps1 to improve task information retrieval -cce3206 Merge pull request #243 from silversword411/main -116e51d Merge pull request #241 from styx-tdo/main -c97842f Fix variable name certfileMY Add check for friendly name, use Subject if not present -cb39181 chore: Refactor add folder creation function -f287fbd WIP: ASUS debloater -fe843cd WIP: Add Dell RAID monitoring script -6e314ce Merge pull request #240 from silversword411/main -b81e750 chore: Refactor Win_RunAsUser_Example.ps1 to simplify CaptureOutput -c6a8e1b WIP: Adding urbackup scripts -40ac664 Rename MDM for keyword searching -76c58b2 Updating RunAsUser template -8ee1fe4 Merge pull request #235 from silversword411/main -87cff55 Merge pull request #239 from bbrendon/patch-1 -484743a remove dead url -a42aaf1 Update Win_Speedtest.ps1 -83b637b Fix reboot script -4d65341 Merge pull request #237 from redanthrax/LocalAdminFixes -054a214 fixed issue with account existing -79af8f0 Fixing reboot script and timeouts -c426a5a derp...forgot a new guid -f338fab New Community Script: Reboot -e4071f1 Merge pull request #234 from silversword411/main -efca24c Move TRMM tasks -e114b3d Windows Network scanner via python v1.5 -43e8c88 Staged: Windows backup Monitor -068f675 TRMM tasks -ab32372 Merge pull request #233 from silversword411/main -a32be81 fix spywarekiller version comment headers -9431faa Merge pull request #232 from redanthrax/Bitlocker -3e35640 Merge pull request #231 from redanthrax/WinREFix -71e0b70 Updated Bitlocker Script Name -e72330f WinRE Fix Script -019977b Merge pull request #230 from dinger1986/main -401548b Update Win_Duplicati_Status.ps1 -baff79e Merge pull request #229 from dinger1986/main -76e10a5 Update Win_Duplicati_Status.ps1 -527471a Merge pull request #228 from dinger1986/main -5e17c71 Update Win_Duplicati_Status.ps1 -d4989ba Merge pull request #225 from redanthrax/Crowdstrike -a284173 Merge pull request #226 from redanthrax/LogShipperUpdate -98380b3 Merge pull request #227 from redanthrax/AutoStoreUpdate -3ae85a0 MS Store Update Setting -da9d1e3 added logic for upgrade, updated params -5830509 crowdstrike script -0c0fc0d Merge pull request #224 from silversword411/main -401197e Screenconnect AIO Update json for syntax -b4e00d7 Merge branch 'main' of https://github.com/silversword411/community-scripts -4fad311 Merge pull request #223 from redanthrax/UninstallerImprovements -804732f Merge pull request #222 from redanthrax/AutoElevateUninstallFix -d5e3297 Merge pull request #220 from redanthrax/Bitlocker -6c456c1 improved uninstall, added ability for multiple apps, improved output -39f1a05 updated for better uninstall functionality -9b22691 Merge pull request #221 from redanthrax/DuoUninstallFixes -35c44ab updated script to handle uninstall scenarios better -aa88e2c removed end output -aa99076 added fix scenario for an issue preventing key backup, end output -637a579 Screenconnect AIO - adding debug and formatting -36c57da added check for tpm -27c365f added recovery password set -a637392 Added initial script that is WIP -e4b698a Merge pull request #216 from redanthrax/DuoRemoveUpgradeUninstall -436259f Merge pull request #213 from derfladi/mode-upgrade-only-installed -147ab78 Merge branch 'main' into mode-upgrade-only-installed -a1dd608 Rename Windows_Clear_cookies.ps1 to Win_Clear_cookies.ps1 -64bbb8a Merge pull request #218 from dinger1986/main -0c845d5 Create Windows_Clear_cookies.ps1 -37da664 Merge pull request #217 from dinger1986/main -00a373b Create linux_os_update_check.sh -0ee108e Update linux_os_update.sh -20c9eaa Update linux_os_update.sh -20e3755 changed upgrade to not uninstall first -63177e9 Merge pull request #215 from silversword411/main -f6210fb Updating choco bulk to use --no-progress -5ba2263 Merge pull request #189 from lcsnetworks:fix_disk_cleanup_script -6e810a2 Merge pull request #204 from Aidan-abss/main -933e97e Moving to staging and renaming for further testing -ddd5081 Merge pull request #211 from redanthrax/PrinterInstaller -e4a8be8 Merge pull request #214 from silversword411/main -5528749 staged new speed test script - Test pls -6c885be WIP - try and kill Firefox full screening for Tech Support scammer -54f022c Updating official defender exclusions -477d18a Veeam Collection -d8d3d51 fix wording -94756d0 Add new mode -408accb Update Win_Chocolatey_Manage_Apps_Bulk.ps1 -809de6f Merge pull request #212 from silversword411/main -0fb38c2 WIP: Spywarekiller v1.3 -2b8876a Fixing Recycle bin empty -5668448 created printer installer script -d14350c Merge pull request #210 from silversword411/main -a7d403d NIC enable/disable script. Thx https://github.com/orbitturner ! -ddc2fb9 Merge pull request #209 from MalteKiefer/patch-1 -78420c0 Update Win_WinGet_Manage_Apps.ps1 -ed7ca32 Merge pull request #203 from silversword411/main -193631a Choco List Converting PS to bat -39ee66d WIP: Vuln scanner -dc7adf3 Merge pull request #208 from dinger1986/main -274a4cd Update Win_Win11_Ready.ps1 -6fec3b6 Merge pull request #207 from redanthrax/NewTeams -dc53bf6 WIP SMB1 checker -a344f93 Added script to upgrade Teams to New Teams -54db857 Merge pull request #193 from brisksystems-us/main -59f04fc Moving script to wip folder -f7a51d8 Merge pull request #206 from redanthrax/ClearOfficeCache -0dce5b0 added script for setting office to clear cache -63fb68d Merge pull request #205 from redanthrax/main -a3739c3 Updated for CyberCNS v4 -ff52c50 Rename Win_Failed_Logon_Check to Win_Failed_Logon_Check.ps1 -19f9a15 Create Win_Failed_Logon_Check -3a128d8 choco fixing comment headers -cc6d9a4 choco bulk - adding seconds debug info -dc1ea6e Refactor choco and add static choco paths -5890cf8 Merge pull request #202 from ConvexSERV/main -a11f760 Update Win_StorageCraftImageManager_Status.ps1 -b119cdc Update Win_StorageCraftImageManager_Status.ps1 From 0e43e0d7b35c7c6b5729494997a439f35a9d3788 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Sun, 5 Jan 2025 23:38:28 +0000 Subject: [PATCH 224/447] Update ./scripts/Lab/TRMM Variable exemples.ps1 --- .../Lab/TRMM Variable exemples.ps1 | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 scripts_staging/Lab/TRMM Variable exemples.ps1 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 From a2bb5d895af90f4cc61bff199355a47935e5ce0a Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:58:14 +0000 Subject: [PATCH 225/447] Update ./scripts/Checks/Certificates expiry.ps1 --- scripts_staging/Checks/Certificates expiry.ps1 | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/scripts_staging/Checks/Certificates expiry.ps1 b/scripts_staging/Checks/Certificates expiry.ps1 index 3c47f793..41a5b4f4 100644 --- a/scripts_staging/Checks/Certificates expiry.ps1 +++ b/scripts_staging/Checks/Certificates expiry.ps1 @@ -28,16 +28,27 @@ 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 -$warnThresholdDays = [int](if ($env:WARN_THRESHOLD_DAYS) { $env:WARN_THRESHOLD_DAYS } else { 20 }) -$errorThresholdDays = [int](if ($env:ERROR_THRESHOLD_DAYS) { $env:ERROR_THRESHOLD_DAYS } else { 5 }) +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") From 66ea663d09b9abcfea1796f70be25aba4fe73aca Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:33:15 +0000 Subject: [PATCH 226/447] Update ./scripts/Tasks/Import RD Gateway Cert From IIS.ps1 --- .../Tasks/Import RD Gateway Cert From IIS.ps1 | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/scripts_staging/Tasks/Import RD Gateway Cert From IIS.ps1 b/scripts_staging/Tasks/Import RD Gateway Cert From IIS.ps1 index a467b024..07c406db 100644 --- a/scripts_staging/Tasks/Import RD Gateway Cert From IIS.ps1 +++ b/scripts_staging/Tasks/Import RD Gateway Cert From IIS.ps1 @@ -38,6 +38,7 @@ ignore the fail-safe checks and force the replacement of rds certs and restart t 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 @@ -104,15 +105,7 @@ function Is-ValidThumbprint { return $Thumbprint -and $Thumbprint.Length -eq 40 -and $Thumbprint -match '^[0-9A-Fa-f]+$' } -Function Remove-OldCertificates { - param ( - [string]$OldThumbprint - ) - - if (-not $OldThumbprint) { - Write-Warning "Old thumbprint is not provided. Skipping certificate removal process." - return $false - } +Function Remove-OldLECertificates { $stores = @( "Cert:\LocalMachine\My", @@ -124,13 +117,20 @@ Function Remove-OldCertificates { foreach ($store in $stores) { try { - $certs = Get-ChildItem -Path $store -Recurse | Where-Object {$_.Thumbprint -eq $OldThumbprint} - if ($certs.Count -eq 0) { - Write-Host "No certificates with thumbprint $OldThumbprint found in $store." + # 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 { - foreach ($cert in $certs) { - Remove-Item -Path $cert.PSPath -Confirm:$false - Write-Host "Removed certificate with thumbprint $OldThumbprint from $store." + # 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 } } @@ -142,6 +142,7 @@ Function Remove-OldCertificates { return $certsRemoved } + # Check if Get-RDUserSession is available, if not exit with code 0 try { $null = Get-RDUserSession -ErrorAction Stop @@ -205,9 +206,13 @@ if ($gatewayService -and $winAcmeTask) { 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) { @@ -216,14 +221,6 @@ if ($gatewayService -and $winAcmeTask) { Write-Error "Invalid IIS certificate thumbprint: $IISCertThumbprint. Exiting script." exit 1 } -<# - # Validate RD Gateway certificate thumbprint - if (Is-ValidThumbprint -Thumbprint $RDSCertThumbprint) { - Write-Host "RD Gateway certificate thumbprint $RDSCertThumbprint is valid. Continuing." - } else { - Write-Error "Invalid RD Gateway certificate thumbprint: $RDSCertThumbprint. Exiting script." - exit 1 - }#> } # Retrieve the certificate from the local machine store that matches the specified thumbprint @@ -274,12 +271,12 @@ if ($gatewayService -and $winAcmeTask) { Restart-Service TSGateway -Force -ErrorAction Stop Write-Host "TSGateway service restarted successfully." - # Call function to remove old certificates (assumes function is defined elsewhere) - $certsRemoved = Remove-OldCertificates -OldThumbprint $RDSCertThumbprint + # Call function to remove old certificates + $certsRemoved = Remove-OldLECertificates # Check if old certificates were removed if (-not $certsRemoved) { - Write-Error "No old certificates were removed. Exiting script." + Write-Error "No old certificates $RDSCertThumbprint were removed. Exiting script." exit 1 } else { Write-Host "Old certificates removed successfully." From d5b82b4121124d1d17c713459329e14bedbe1b6b Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 8 Jan 2025 07:56:59 +0000 Subject: [PATCH 227/447] Update ./scripts/Fixes/Bluescreen report.ps1 --- scripts_staging/Fixes/Bluescreen report.ps1 | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts_staging/Fixes/Bluescreen report.ps1 b/scripts_staging/Fixes/Bluescreen report.ps1 index e74ca1a7..31bc86a1 100644 --- a/scripts_staging/Fixes/Bluescreen report.ps1 +++ b/scripts_staging/Fixes/Bluescreen report.ps1 @@ -20,6 +20,7 @@ .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 @@ -45,11 +46,11 @@ $hostname = (Get-WmiObject -Class Win32_ComputerSystem).Name # Check if the Minidump directory exists and contains any .dmp if (-not (Test-Path $minidumpPath)) { - Write-Host "Minidump folder not found!" - exit 1 + Write-Error "Minidump folder not found!" + exit } elseif (-not (Get-ChildItem -Path $minidumpPath -Filter "*.dmp")) { - Write-Host "No dump files found in Minidump folder!" - exit 1 + 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 From 8561266ba3bb326845864e52f62e6a75e828ac67 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:01:23 +0000 Subject: [PATCH 228/447] Update ./snippets/Cleaner.ps1 --- scripts_staging/snippets/Cleaner.ps1 | 47 ++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/scripts_staging/snippets/Cleaner.ps1 b/scripts_staging/snippets/Cleaner.ps1 index d06d7413..7d24c419 100644 --- a/scripts_staging/snippets/Cleaner.ps1 +++ b/scripts_staging/snippets/Cleaner.ps1 @@ -23,10 +23,11 @@ 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 .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 @@ -49,7 +50,6 @@ function Get-DiskInfo { return $DiskInfo } -# Function to remove items function Remove-Items { param ( [string]$Path, @@ -59,25 +59,46 @@ function Remove-Items { if (Test-Path $Path) { # Check if the Path is a file if ((Get-Item $Path).PSIsContainer -eq $false) { - # Remove the single file if it meets the age condition - if ((Get-Item $Path).CreationTime -lt (Get-Date).AddDays(-$Days)) { - Remove-Item -Path $Path -Force -Verbose - Write-Host "[DONE] Removed single item: $Path" - } else { - Write-Host "[INFO] $Path does not meet the age condition, skipping removal." + try { + # Remove the single file if it meets the age condition + if ((Get-Item $Path).CreationTime -lt (Get-Date).AddDays(-$Days)) { + Remove-Item -Path $Path -Force -Verbose -Confirm:$false + Write-Host "[DONE] Removed single item: $Path" + } else { + Write-Host "[INFO] $Path does not meet the age condition, skipping removal." + } + } catch { + Write-Host "[ERROR] Failed to remove item: $Path. $_" } } else { - # If it's a directory, remove its sub-items based on the age condition - Get-ChildItem -Path $Path -Recurse -Force | - Where-Object { $_.CreationTime -lt (Get-Date).AddDays(-$Days) } | - Remove-Item -Force -Recurse -Verbose - Write-Host "[DONE] Cleaned up directory: $Path" + try { + # Get all items in the folder + $items = Get-ChildItem -Path $Path -Recurse -Force | + Where-Object { $_.CreationTime -lt (Get-Date).AddDays(-$Days) } | + Sort-Object { $_.Name.Length } -Descending + + if ($items.Count -gt 0) { + Write-Host "[INFO] Listing items for removal in order of name length:" + foreach ($item in $items) { + Write-Host " - $($item.FullName)" + } + + # Remove items from longest name to shortest + $items | Remove-Item -Force -Recurse -Verbose -Confirm:$false + Write-Host "[DONE] Cleaned up directory: $Path" + } else { + Write-Host "[INFO] No items met the age condition in directory: $Path" + } + } catch { + Write-Host "[ERROR] Failed to clean up directory: $Path. $_" + } } } else { Write-Host "[WARNING] $Path does not exist, skipping cleanup." } } + # Function to add or update registry keys for Disk Cleanup function Add-RegistryKeys-CleanMGR { $baseKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches" From 38e776db4fbf6d2b0303b34af5c216c3f6ab5ccd Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 21 Jan 2025 07:55:41 +0000 Subject: [PATCH 229/447] Update ./scripts/Checks/Windows Services.ps1 --- scripts_staging/Checks/Windows Services.ps1 | 47 ++++++++++----------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/scripts_staging/Checks/Windows Services.ps1 b/scripts_staging/Checks/Windows Services.ps1 index 2f9fe3b1..b1145eaa 100644 --- a/scripts_staging/Checks/Windows Services.ps1 +++ b/scripts_staging/Checks/Windows Services.ps1 @@ -23,13 +23,14 @@ .CHANGELOG 28.10.24 SAN - Removed ignored output without the debug flag. 28.10.24 SAN - cleanup documentation. + 21.01.25 SAN - Code cleanup -#> +#> -# Define a generic list of services names to be ignored by the check -$ignoredPartialDisplayNames = @( +# Define a generic list of service names to be ignored by default +$ignoredByDefault = @( "Software Protection", "Remote Registry", "State Repository Service", @@ -37,7 +38,7 @@ $ignoredPartialDisplayNames = @( "Clipboard User Service", "Service Brave Update", "Google Update Service", - "Windows Modules Installer", # not sure about this one if we should monitor it or not + "Windows Modules Installer", # Unsure if this one should be monitored "Downloaded Maps Manager", "Windows Biometric Service", "RemoteRegistry", @@ -51,31 +52,30 @@ $ignoredPartialDisplayNames = @( "sppsvc", "SharePoint Migration Service", "dbupdate", - "TrustedInstaller", # this one is strange it was failing on a lot of devices but no idea if it should or could be fixed + "TrustedInstaller", # Frequently failing; unclear if actionable "MSExchangeNotificationsBroker", "tiledatamodelsvc", "BITS", "CDPSvc", "AGSService", - "ShellHWDetection" # this one is strange it was failing on a lot of devices but no idea if it should or could be fixed - + "ShellHWDetection" # Frequently failing; unclear if actionable ) -# 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 +# Check if the "IgnoredServices" environment variable exists and add those services to the ignore list +$addonsToIgnoredList = [Environment]::GetEnvironmentVariable('IgnoredServices') +if (-not [string]::IsNullOrEmpty($addonsToIgnoredList)) { + $additionalServices = $addonsToIgnoredList -split ',' + $ignoredByDefault += $additionalServices } -# Convert ignored partial display names to a regular expression pattern -$ignoredPattern = ($ignoredPartialDisplayNames | ForEach-Object { [regex]::Escape($_) }) -join '|' +# Convert ignored services to a regular expression pattern +$ignoredPattern = ($ignoredByDefault | ForEach-Object { [regex]::Escape($_) }) -join '|' -# Get services with automatic start type or Automatic (Delayed Start) that are not running +# 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 = @() +$servicesNeedingAttention = @() $ignoredStoppedServices = @() # Check the status of each service @@ -83,29 +83,29 @@ 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 + $servicesNeedingAttention += $service } else { # Add the service to the list of ignored stopped services $ignoredStoppedServices += $service } } -# Check if enabledebug environment variable is set to true +# Check if the "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" + Write-Host "Debug mode is enabled." } + # Display the results -if ($servicesToStart.Count -eq 0) { +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 ignored:" + Write-Host "The following services were stopped but are ignored:" foreach ($service in $ignoredStoppedServices) { Write-Host "$($service.DisplayName) ($($service.ServiceName))" } @@ -115,10 +115,9 @@ if ($servicesToStart.Count -eq 0) { } else { Write-Host "The following services need attention:" - foreach ($service in $servicesToStart) { + foreach ($service in $servicesNeedingAttention) { Write-Host "$($service.DisplayName) ($($service.ServiceName))" } Exit 1 - } From 578bf6ca57509357ee38790a5ff086c2e311a6d5 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 30 Jan 2025 10:32:46 +0000 Subject: [PATCH 230/447] Update ./scripts/Lab/IP block lists for specified countries.ps1 --- ...IP block lists for specified countries.ps1 | 212 ++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 scripts_staging/Lab/IP block lists for specified countries.ps1 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..1c0e4e33 --- /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. + + 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/) + V2 Author: Vinahost release + V3 Author: SAN + #public + +.CHANGELOG + 28.01.25 Default to inbound only to reduce the load, 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 From d94e1d798dafcf34ca3e901b540e0b684e5680d4 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 30 Jan 2025 10:38:52 +0000 Subject: [PATCH 231/447] Update ./scripts/Lab/IP block lists for specified countries.ps1 --- .../Lab/IP block lists for specified countries.ps1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts_staging/Lab/IP block lists for specified countries.ps1 b/scripts_staging/Lab/IP block lists for specified countries.ps1 index 1c0e4e33..5dc6307c 100644 --- a/scripts_staging/Lab/IP block lists for specified countries.ps1 +++ b/scripts_staging/Lab/IP block lists for specified countries.ps1 @@ -13,7 +13,7 @@ This script can be used to block IPs from countries with high levels of unwanted traffic or suspected malicious activity. - Problematic Countries (often associated with cyberattacks, fraud, or high-risk traffic): + Sample of problematic Countries (often associated with cyberattacks, fraud, or high-risk traffic): - CN (China) - RU (Russia) - IN (India) @@ -60,7 +60,7 @@ If set, deletes all firewall rules matching "*xx.zone*". .EXAMPLE - -Countries "ru,cn" + -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 @@ -70,13 +70,13 @@ -DeleteOnly .NOTE - V1 Author: Jason Fossen (http://www.sans.org/windows-security/) - V2 Author: Vinahost release - V3 Author: SAN + 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 Default to inbound only to reduce the load, added feature to add countries in bulk, fixed deleteonly to remove all rules created, upgrade to PowerShell cmdlets for fw rules + 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 From a767b1d51f57651ed893cb4592264d5aece5173d Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 30 Jan 2025 10:46:28 +0000 Subject: [PATCH 232/447] Update ./scripts/TasksUpdater/Updater P3 Run WU.ps1 --- scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 b/scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 index f5482408..ca813b67 100644 --- a/scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 +++ b/scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 @@ -74,4 +74,6 @@ if (Get-Module -ListAvailable -Name PSWindowsUpdate) { } # Run Windows update with PSWindowsUpdate and rebooting at time found in parser +Write-Output "Running windows updates:" +Write-Output "Get-WindowsUpdate -Verbose -Install -AcceptAll -AutoReboot -ScheduleReboot $scheduledTime" Get-WindowsUpdate -Verbose -Install -AcceptAll -AutoReboot -ScheduleReboot $scheduledTime From c72e55d8cef39cba5ea9d69a9be044795c8e2161 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 30 Jan 2025 10:46:44 +0000 Subject: [PATCH 233/447] Update ./scripts/Tools/IP block lists for specified countries.ps1 --- ...IP block lists for specified countries.ps1 | 212 ++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 scripts_staging/Tools/IP block lists for specified countries.ps1 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 From 28e8b3ecc9672c37f73b747f3d0f6ff8f2c5085f Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 30 Jan 2025 15:45:03 +0000 Subject: [PATCH 234/447] Update ./scripts/TasksUpdater/Updater P3 Run WU.ps1 --- scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 b/scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 index ca813b67..df4a8227 100644 --- a/scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 +++ b/scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 @@ -32,6 +32,7 @@ .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 #> @@ -74,6 +75,6 @@ if (Get-Module -ListAvailable -Name PSWindowsUpdate) { } # Run Windows update with PSWindowsUpdate and rebooting at time found in parser -Write-Output "Running windows updates:" -Write-Output "Get-WindowsUpdate -Verbose -Install -AcceptAll -AutoReboot -ScheduleReboot $scheduledTime" +Write-Host "Running windows updates:" +Write-Host "Get-WindowsUpdate -Verbose -Install -AcceptAll -AutoReboot -ScheduleReboot $scheduledTime" Get-WindowsUpdate -Verbose -Install -AcceptAll -AutoReboot -ScheduleReboot $scheduledTime From 6a01b878b1af3bcab7a77f40f157d2d60c7cd41a Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 11 Feb 2025 10:44:22 +0000 Subject: [PATCH 235/447] Update ./scripts/Checks/is Remote TCP port open.ps1 --- .../Checks/is Remote TCP port open.ps1 | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 scripts_staging/Checks/is Remote TCP port open.ps1 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 + } +} From be5c38eda09c7dff9a7e5dda87e4f1b806cc2f83 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 11 Feb 2025 10:44:26 +0000 Subject: [PATCH 236/447] Update ./scripts/Checks/is TCP port open.ps1 --- scripts_staging/Checks/is TCP port open.ps1 | 87 +++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 scripts_staging/Checks/is TCP port open.ps1 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 From 193d14bd0186da9bcbac0f0990b9f2c675029708 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 17 Feb 2025 08:14:58 +0000 Subject: [PATCH 237/447] Update ./scripts/Checks/Windows Services.ps1 --- scripts_staging/Checks/Windows Services.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts_staging/Checks/Windows Services.ps1 b/scripts_staging/Checks/Windows Services.ps1 index b1145eaa..685e61ce 100644 --- a/scripts_staging/Checks/Windows Services.ps1 +++ b/scripts_staging/Checks/Windows Services.ps1 @@ -58,7 +58,8 @@ $ignoredByDefault = @( "BITS", "CDPSvc", "AGSService", - "ShellHWDetection" # Frequently failing; unclear if actionable + "ShellHWDetection", # Frequently failing; unclear if actionable + "DropboxUpdater" ) # Check if the "IgnoredServices" environment variable exists and add those services to the ignore list From 5f3eeffd6c4184fddfb7bc5ef30430dfb93588d5 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Tue, 18 Feb 2025 12:33:18 -0500 Subject: [PATCH 238/447] Enhance Win_NetworkScanner.py: add response time measurement, improve output formatting --- scripts_staging/Win_NetworkScanner.py | 79 +++++++++++++++++++++------ 1 file changed, 62 insertions(+), 17 deletions(-) diff --git a/scripts_staging/Win_NetworkScanner.py b/scripts_staging/Win_NetworkScanner.py index 5c67ade4..bc3d9634 100644 --- a/scripts_staging/Win_NetworkScanner.py +++ b/scripts_staging/Win_NetworkScanner.py @@ -3,39 +3,57 @@ """ 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 + v1.1 2/2024 silversword411 v1.4 added open port checker v1.5 5/2/2024 integrated reverse DNS lookup into the ping function with 1-second timeout +v1.6 5/31/2024 align output to columns and ports low to high +v1.7 2/18/2025 fix columns with long host names and added response time -TODO: Make subnet get automatically detected instead of assuming /24 -TODO: Compatible with Linux as well +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 + # Function to get the IP address of the primary network interface def get_host_ip(): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: - s.connect(('10.255.255.255', 1)) + s.connect(("10.255.255.255", 1)) IP = s.getsockname()[0] except Exception: - IP = '127.0.0.1' + IP = "127.0.0.1" finally: s.close() return IP -# Function to ping an IP address, check if it is alive, and optionally perform a reverse DNS lookup + +# Function to ping an IP address, check if it is alive, measure response time, and optionally perform a reverse DNS lookup 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) + 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 + ) # If no time found, use -1 + hostname = "NA" if do_reverse_dns: try: @@ -46,10 +64,12 @@ def ping_ip(ip, alive_hosts, do_reverse_dns): hostname = "unknown" finally: s.close() - alive_hosts.append((alive_ip, hostname)) + + alive_hosts.append((alive_ip, hostname, response_time)) except Exception: pass + # Function to check for open ports def check_ports(ip, port, open_ports): try: @@ -60,19 +80,25 @@ def check_ports(ip, port, open_ports): except Exception: pass + # Parse command-line arguments def parse_arguments(): - parser = argparse.ArgumentParser(description="Scan network subnet for alive hosts, open ports, and optionally perform reverse DNS lookup.") - parser.add_argument("--hostname", help="Perform reverse DNS lookup", action="store_true") + parser = argparse.ArgumentParser( + description="Scan network subnet for alive hosts, open ports, and optionally perform reverse DNS lookup." + ) + parser.add_argument( + "--hostname", help="Perform reverse DNS lookup", action="store_true" + ) return parser.parse_args() + # Main function to detect the subnet and scan it 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) + subnet = ipaddress.ip_network(f"{host_ip}/24", strict=False) alive_hosts = [] open_ports = defaultdict(list) @@ -90,21 +116,40 @@ def main(): # Launch port checks port_check_threads = [] - for host, _ in alive_hosts: + 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() - print(f"Alive hosts in the subnet {subnet}:") - for host, hostname in alive_hosts: - ports = ', '.join(str(port) for port in open_ports[str(host)]) - print(f"IP: {host}, {hostname}, Open Ports: {ports}") + # Determine column widths dynamically + 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 # Minimum width of 12 + response_time_column_width = 8 + ports_column_width = 50 # Static width for ports + + # Print header + header = f"{'IP':<{ip_column_width}}{'(ms)':<{response_time_column_width}}{'Hostname':<{hostname_column_width}}{'Open Ports':<{ports_column_width}}" + print(header) + print("-" * len(header)) + + # Print results + for host, hostname, response_time 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" + print( + f"{str(host):<{ip_column_width}}{response_time_str:<{response_time_column_width}}{hostname:<{hostname_column_width}}{ports_str:<{ports_column_width}}" + ) print(f"\nTotal count of alive hosts: {len(alive_hosts)}") + if __name__ == "__main__": - main() \ No newline at end of file + main() From 5c7810823f59529e592b9d9d7b4ea1622099e6f1 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 27 Feb 2025 09:56:43 +0000 Subject: [PATCH 239/447] Update ./scripts/Checks/Active Directory Health.ps1 --- .../Checks/Active Directory Health.ps1 | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/scripts_staging/Checks/Active Directory Health.ps1 b/scripts_staging/Checks/Active Directory Health.ps1 index c72bc466..1be837a2 100644 --- a/scripts_staging/Checks/Active Directory Health.ps1 +++ b/scripts_staging/Checks/Active Directory Health.ps1 @@ -90,6 +90,21 @@ function Compare-GPOVersions { } } +# 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-Output "OK: Recycle Bin enabled" + } else { + Write-Output "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 @@ -114,6 +129,12 @@ try { Write-Host "GPO Versions checks" # Call the function to compare GPO versions Compare-GPOVersions + + Write-Host "" + Write-Host "Recycle Bin checks" + # Call the function to check the Recycle Bin + Check-ADRecycleBin + } else { Write-Host "Active Directory Domain Services feature is not installed or not in the 'Installed' state." exit From f3d01a7cbf977c44f6903190c80309a6eb2efbfb Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 6 Mar 2025 10:02:56 +0000 Subject: [PATCH 240/447] Update ./snippets/Update TRMM agent.ps1 --- .../snippets/Update TRMM agent.ps1 | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 scripts_staging/snippets/Update TRMM agent.ps1 diff --git a/scripts_staging/snippets/Update TRMM agent.ps1 b/scripts_staging/snippets/Update TRMM agent.ps1 new file mode 100644 index 00000000..52791fd9 --- /dev/null +++ b/scripts_staging/snippets/Update TRMM agent.ps1 @@ -0,0 +1,157 @@ +<# +.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 + +#> +# 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 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 +} From 80d9bb0afa65d56e35726c6fa39b3f41a66f2439 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 6 Mar 2025 10:09:02 +0000 Subject: [PATCH 241/447] Update ./snippets/Update TRMM agent.ps1 --- scripts_staging/snippets/Update TRMM agent.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts_staging/snippets/Update TRMM agent.ps1 b/scripts_staging/snippets/Update TRMM agent.ps1 index 52791fd9..659ecfdb 100644 --- a/scripts_staging/snippets/Update TRMM agent.ps1 +++ b/scripts_staging/snippets/Update TRMM agent.ps1 @@ -83,7 +83,7 @@ try { $version = "latest" } if ($version -eq "latest") { - Write-Output "Fetching the latest version information from GitHub..." + 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" @@ -96,7 +96,7 @@ try { 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." + 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." From 451e22c4e8a78ffc8d3571ffca3048b427902cce Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 6 Mar 2025 10:15:14 +0000 Subject: [PATCH 242/447] Update ./snippets/Update TRMM agent.ps1 --- scripts_staging/snippets/Update TRMM agent.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts_staging/snippets/Update TRMM agent.ps1 b/scripts_staging/snippets/Update TRMM agent.ps1 index 659ecfdb..6b3f7527 100644 --- a/scripts_staging/snippets/Update TRMM agent.ps1 +++ b/scripts_staging/snippets/Update TRMM agent.ps1 @@ -25,6 +25,7 @@ version=latest version=2.7.0 trmm_api_target=api.exemple.com + trmm_api_target={{global.RMM_API_URL}} .NOTES Author: SAN From af823d27baea4f115a8f260419373a4444f03e40 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 6 Mar 2025 10:20:40 +0000 Subject: [PATCH 243/447] Update ./scripts/TasksUpdater/Updater P3 Run SU.ps1 --- scripts_staging/TasksUpdater/Updater P3 Run SU.ps1 | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts_staging/TasksUpdater/Updater P3 Run SU.ps1 b/scripts_staging/TasksUpdater/Updater P3 Run SU.ps1 index 38947a11..955cc548 100644 --- a/scripts_staging/TasksUpdater/Updater P3 Run SU.ps1 +++ b/scripts_staging/TasksUpdater/Updater P3 Run SU.ps1 @@ -17,6 +17,9 @@ 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 @@ -24,6 +27,7 @@ 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 @@ -33,6 +37,7 @@ 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. .TODO Fix rename? @@ -120,7 +125,7 @@ if ($result.RebootRequired) { 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 +# 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 @@ -129,14 +134,17 @@ $upgradeResult = choco upgrade all -y | Out-String Write-Host "" Write-Host "------------------------------------------------------------" Write-Host "" -Write-Host "Outdated Packages:" +Write-Host "Chocolatey Outdated Packages before upgrade:" Write-Host $outdatedPackages Write-Host "------------------------------------------------------------" -Write-Host "Upgrade Result:" +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 From caf39a9a6b440101d5646362dbf88ea6eccc9d2a Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 6 Mar 2025 10:21:42 +0000 Subject: [PATCH 244/447] Update ./snippets/Update TRMM agent.ps1 --- scripts_staging/snippets/Update TRMM agent.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts_staging/snippets/Update TRMM agent.ps1 b/scripts_staging/snippets/Update TRMM agent.ps1 index 6b3f7527..6b32487e 100644 --- a/scripts_staging/snippets/Update TRMM agent.ps1 +++ b/scripts_staging/snippets/Update TRMM agent.ps1 @@ -38,6 +38,7 @@ 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 From 174274c16b5374494e144981bcd80b71921c6700 Mon Sep 17 00:00:00 2001 From: officialJCReyes Date: Fri, 7 Mar 2025 15:50:15 -0500 Subject: [PATCH 245/447] Update Win_Win11_Ready.ps1 Added a check to determine if Windows 11 is already installed and skip the rest of the check. Added some spacing for error reporting as to why a device is not compatible --- scripts/Win_Win11_Ready.ps1 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/Win_Win11_Ready.ps1 b/scripts/Win_Win11_Ready.ps1 index abed6448..1befaef8 100644 --- a/scripts/Win_Win11_Ready.ps1 +++ b/scripts/Win_Win11_Ready.ps1 @@ -7,6 +7,17 @@ # #============================================================================================================================= +# Check Windows Version +$osInfo = Get-WmiObject -Class Win32_OperatingSystem +$winVersion = [System.Version]$osInfo.Version + +if ($winVersion -ge [System.Version]::new(10, 0, 22000)) { + Write-Output "Already running Windows 11." + Exit 0 +} + +# Continue with Windows 11 readiness check + $exitCode = 0 [int]$MinOSDiskSizeGB = 64 @@ -480,6 +491,6 @@ if (0 -eq $outObject.returncode) { "Windows 11 Ready" } else { - "Not Windows 11 Ready" + "Not Windows 11 Ready | " Write-Output $outObject.returnReason } From 490fcb53f342f11236b8d2a272c10913d8f5c2e0 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Wed, 12 Mar 2025 21:47:13 -0400 Subject: [PATCH 246/447] Update Win_Win11_Ready.ps1 Shorten for report use later --- scripts/Win_Win11_Ready.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/Win_Win11_Ready.ps1 b/scripts/Win_Win11_Ready.ps1 index 1befaef8..2193398f 100644 --- a/scripts/Win_Win11_Ready.ps1 +++ b/scripts/Win_Win11_Ready.ps1 @@ -12,7 +12,7 @@ $osInfo = Get-WmiObject -Class Win32_OperatingSystem $winVersion = [System.Version]$osInfo.Version if ($winVersion -ge [System.Version]::new(10, 0, 22000)) { - Write-Output "Already running Windows 11." + Write-Output "Already Windows 11." Exit 0 } From e06db652c6cd052e4ea5cf92d611102f5a62885c Mon Sep 17 00:00:00 2001 From: silversword411 Date: Wed, 12 Mar 2025 21:52:13 -0400 Subject: [PATCH 247/447] Update Win_Win11_Ready.ps1 no period to match others --- scripts/Win_Win11_Ready.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/Win_Win11_Ready.ps1 b/scripts/Win_Win11_Ready.ps1 index 2193398f..45bf5245 100644 --- a/scripts/Win_Win11_Ready.ps1 +++ b/scripts/Win_Win11_Ready.ps1 @@ -12,7 +12,7 @@ $osInfo = Get-WmiObject -Class Win32_OperatingSystem $winVersion = [System.Version]$osInfo.Version if ($winVersion -ge [System.Version]::new(10, 0, 22000)) { - Write-Output "Already Windows 11." + Write-Output "Already Windows 11" Exit 0 } From c29ad0b68914fe037fe970a6f0a0b85bdccd45b0 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 14 Mar 2025 09:45:37 +0000 Subject: [PATCH 248/447] Update ./scripts/Checks/SQL Health.ps1 --- scripts_staging/Checks/SQL Health.ps1 | 84 +++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 6 deletions(-) diff --git a/scripts_staging/Checks/SQL Health.ps1 b/scripts_staging/Checks/SQL Health.ps1 index 254ba924..4ef570b1 100644 --- a/scripts_staging/Checks/SQL Health.ps1 +++ b/scripts_staging/Checks/SQL Health.ps1 @@ -12,11 +12,11 @@ #public .TODO - Need to go back to version 1 and implement the missing functions of this check - handle Master-slave monitoring + .CHANGELOG SAN 12.12.24 Changed outputs + SAN 14.03.24 Added availability groups checks #> function Get-SqlServerVersion { @@ -126,12 +126,78 @@ function Get-BlockedSqlRequests { } if ($errorEncountered) { - return "Error" # Return "Error" if any error occurred during the process + return "Error" } else { - return "OK" # Return "OK" if no errors occurred + return "OK" } } +Function Get-SqlAgSyncStatus { + Write-Host "Function Get-SqlAgSyncStatus" + + $server = "$( $env:COMPUTERNAME)\$( (Get-Item 'HKLM:\Software\Microsoft\Microsoft SQL Server\Instance Names\SQL').Property[0])" + Write-Host "Detected SQL Server Instance: $server" + + Function RunQuery($query) { + Try { + $conn = New-Object System.Data.SqlClient.SqlConnection + $conn.ConnectionString = "Server=$server;Database=master;Integrated Security=True" + $conn.Open() + + $userQuery = "SELECT SUSER_NAME(), USER_NAME();" + $cmdUser = New-Object System.Data.SqlClient.SqlCommand($userQuery, $conn) + $userReader = $cmdUser.ExecuteReader() + If ($userReader.Read()) { + Write-Host "Connected as: $($userReader.GetString(0)) ($($userReader.GetString(1)))" + } + $userReader.Close() + + Write-Host "SQL Connection Successful: $server" + + $cmd = New-Object System.Data.SqlClient.SqlCommand($query, $conn) + $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd) + $dataSet = New-Object System.Data.DataSet + $adapter.Fill($dataSet) | Out-Null + $conn.Close() + + return $dataSet.Tables[0] + } Catch { + Write-Host "SQL Error: $_" + return $null + } + } + + $query = "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);" + + $result = RunQuery $query + + if ($null -eq $result -or $result.Rows.Count -eq 0) { + Write-Host "OK : $server AG SYNCHRO : No Availability Groups found." + return "OK" + } + + Write-Host "Query Result Count: $($result.Rows.Count)" + Write-Host "Query Result: $($result | Out-String)" + + $description = "" + $statusLevel = 0 + foreach ($row in $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 { # Check if Invoke-Sqlcmd is available @@ -150,9 +216,10 @@ if (Check-SqlServerInstallation) { # Run each function and report the result $result1 = Get-SqlServerVersion $result2 = Get-BlockedSqlRequests + $result3 = Get-SqlAgSyncStatus # Check the results and provide the overall status - if ($result1 -eq "OK" -and $result2 -eq "OK") { + if ($result1 -eq "OK" -and $result2 -eq "OK" -and $result3 -eq "OK") { Write-Host "OK: All components are functioning properly" } else { $errorComponents = @() @@ -164,9 +231,14 @@ if (Check-SqlServerInstallation) { if ($result2 -ne "OK") { $errorComponents += "Blocked SQL Requests Check" } + + if ($result3 -ne "OK") { + $errorComponents += "AG Synchronization Check" + } $errorList = $errorComponents -join ", " Write-Host "Overall Status: Some components encountered errors. Errors in: $errorList" Exit 1 } -} \ No newline at end of file +} + From a0946d592a2c1428c151827916a304ae3ef5aec4 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 14 Mar 2025 09:51:11 +0000 Subject: [PATCH 249/447] Update ./scripts/Checks/SQL Health.ps1 --- scripts_staging/Checks/SQL Health.ps1 | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/scripts_staging/Checks/SQL Health.ps1 b/scripts_staging/Checks/SQL Health.ps1 index 4ef570b1..ffb5c8bb 100644 --- a/scripts_staging/Checks/SQL Health.ps1 +++ b/scripts_staging/Checks/SQL Health.ps1 @@ -3,20 +3,24 @@ This script performs health checks on a machine with SQL Server installed. .DESCRIPTION - The script checks various aspects of SQL Server health, including version and blocked requests. - It provides a modular approach with separate functions for each check. + 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.24 + Date: 01.01.2024 #public + +.CHANGELOG + SAN 12.12.2023 Changed outputs + SAN 14.03.2024 Added availability group checks .TODO + Optimize query execution + Improve error handling -.CHANGELOG - SAN 12.12.24 Changed outputs - SAN 14.03.24 Added availability groups checks #> function Get-SqlServerVersion { From 742a3ecd3a64b17893b32a9324b078b04a2d368e Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 19 Mar 2025 09:42:52 +0000 Subject: [PATCH 250/447] Update ./scripts/Fixes/Bluescreen report.ps1 --- scripts_staging/Fixes/Bluescreen report.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts_staging/Fixes/Bluescreen report.ps1 b/scripts_staging/Fixes/Bluescreen report.ps1 index 31bc86a1..8d2fcbab 100644 --- a/scripts_staging/Fixes/Bluescreen report.ps1 +++ b/scripts_staging/Fixes/Bluescreen report.ps1 @@ -131,8 +131,10 @@ foreach ($file in $files) { 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 } } From 40e93865d1a73d8220b3d2ab9ecfed044f15f08b Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 25 Mar 2025 09:52:11 +0000 Subject: [PATCH 251/447] Update ./scripts/Checks/Windows Update Health.ps1 --- .../Checks/Windows Update Health.ps1 | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 scripts_staging/Checks/Windows Update Health.ps1 diff --git a/scripts_staging/Checks/Windows Update Health.ps1 b/scripts_staging/Checks/Windows Update Health.ps1 new file mode 100644 index 00000000..e1ddaf29 --- /dev/null +++ b/scripts_staging/Checks/Windows Update Health.ps1 @@ -0,0 +1,53 @@ +<# +.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 + +.CHANGELOG + SAN 25.03.2025 Initial version of the script to check updates older than a specified threshold. + +#> + +$ThresholdDays = $env:ThresholdDays +if (-not $ThresholdDays) { + $ThresholdDays = 90 +} + +$CurrentDate = Get-Date +$AgeLimit = $CurrentDate.AddDays(-$ThresholdDays) + +try { + $updates = Get-WindowsUpdate +} 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." + } +} From fff3630c69a73a700d8a2170f6b00a8e1835fdfa Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 25 Mar 2025 09:57:50 +0000 Subject: [PATCH 252/447] Update ./scripts/Checks/Windows Update Health.ps1 --- scripts_staging/Checks/Windows Update Health.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts_staging/Checks/Windows Update Health.ps1 b/scripts_staging/Checks/Windows Update Health.ps1 index e1ddaf29..90eade74 100644 --- a/scripts_staging/Checks/Windows Update Health.ps1 +++ b/scripts_staging/Checks/Windows Update Health.ps1 @@ -16,7 +16,6 @@ .CHANGELOG SAN 25.03.2025 Initial version of the script to check updates older than a specified threshold. - #> $ThresholdDays = $env:ThresholdDays @@ -28,7 +27,7 @@ $CurrentDate = Get-Date $AgeLimit = $CurrentDate.AddDays(-$ThresholdDays) try { - $updates = Get-WindowsUpdate + $updates = Get-WindowsUpdate -ErrorAction Stop } catch { Write-Host "KO: An error occurred while fetching the updates: $_" exit 1 From 1025263d5402db0b89703cfce48a6776315b0e52 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 25 Mar 2025 10:27:13 +0000 Subject: [PATCH 253/447] Update ./scripts/Checks/Windows Update Health.ps1 --- scripts_staging/Checks/Windows Update Health.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts_staging/Checks/Windows Update Health.ps1 b/scripts_staging/Checks/Windows Update Health.ps1 index 90eade74..c9802b42 100644 --- a/scripts_staging/Checks/Windows Update Health.ps1 +++ b/scripts_staging/Checks/Windows Update Health.ps1 @@ -16,6 +16,10 @@ .CHANGELOG SAN 25.03.2025 Initial version of the script to check updates older than a specified threshold. + +.TODO + Add filters to ignore updates in env + #> $ThresholdDays = $env:ThresholdDays From 6e92d0f5a598c1e07788b92b10c4364998e88db1 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 25 Mar 2025 12:56:47 +0000 Subject: [PATCH 254/447] Update ./scripts/Tools/Troubleshoot windows update.ps1 --- .../Tools/Troubleshoot windows update.ps1 | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 scripts_staging/Tools/Troubleshoot windows update.ps1 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." From 9ec99a5c4613d3c456485fc45bb692fcabcfa74e Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 25 Mar 2025 13:07:51 +0000 Subject: [PATCH 255/447] Update ./scripts/Checks/Internet uplink.ps1 --- scripts_staging/Checks/Internet uplink.ps1 | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/scripts_staging/Checks/Internet uplink.ps1 b/scripts_staging/Checks/Internet uplink.ps1 index c4eeef8e..891d59bf 100644 --- a/scripts_staging/Checks/Internet uplink.ps1 +++ b/scripts_staging/Checks/Internet uplink.ps1 @@ -16,16 +16,17 @@ .NOTES Author: SAN - Date: ??? + 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. - for test all to env - tnc has some relability issue maybe use normal ping + move test all to env + tnc has some relability issue maybe use normal ping as fallback + #> @@ -59,10 +60,10 @@ if ($TestAll) { $pingResult = Test-Connection -ComputerName $ip -Count 1 -Quiet if (-not $pingResult) { - Write-Host "Ping to $ip ($owner) failed." + Write-Host "KO: Ping to $ip ($owner) failed." $pingFailed = $true } else { - Write-Host "Ping to $ip ($owner) succeeded." + Write-Host "OK: Ping to $ip ($owner) succeeded." } } @@ -80,9 +81,9 @@ if ($TestAll) { # Check the result of the ping and exit with status code 1 if it fails if (-not $pingResult) { - Write-Host "Ping to $randomIp ($owner) failed." + Write-Host "KO: Ping to $randomIp ($owner) failed." exit 1 } else { - Write-Host "Ping to $randomIp ($owner) succeeded." + Write-Host "OK: Ping to $randomIp ($owner) succeeded." } } \ No newline at end of file From 140dcf7a039f65e322b514b3e7287a5e4a6cfc53 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Wed, 26 Mar 2025 14:20:27 -0400 Subject: [PATCH 256/447] Update AV exclusions --- scripts/Win_TRMM_AV_Update_Exclusion.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/Win_TRMM_AV_Update_Exclusion.ps1 b/scripts/Win_TRMM_AV_Update_Exclusion.ps1 index fba0740d..5d659ea0 100644 --- a/scripts/Win_TRMM_AV_Update_Exclusion.ps1 +++ b/scripts/Win_TRMM_AV_Update_Exclusion.ps1 @@ -2,5 +2,4 @@ Add-MpPreference -ExclusionPath "C:\Program Files\Mesh Agent\*" Add-MpPreference -ExclusionPath "C:\Program Files\TacticalAgent\*" Add-MpPreference -ExclusionPath "C:\ProgramData\TacticalRMM\*" -# For agent updates. Inno setup temp directory -Add-MpPreference -ExclusionPath "%TEMPDIR%\is-*.tmp\tacticalagent*" +Add-MpPreference -ExclusionProcess "C:\Windows\Temp\is-*.tmp\tacticalagent*" From 7bc0a8084714f72cbd4dd205c5353cd486c5056b Mon Sep 17 00:00:00 2001 From: silversword411 Date: Wed, 26 Mar 2025 14:22:38 -0400 Subject: [PATCH 257/447] Add TacticalRMM Agent Troubleshooting Script for Windows --- community_scripts.json | 12 + scripts/Win_TRMM_Troubleshooting_Agent.ps1 | 458 +++++++++++++++++++++ 2 files changed, 470 insertions(+) create mode 100644 scripts/Win_TRMM_Troubleshooting_Agent.ps1 diff --git a/community_scripts.json b/community_scripts.json index af94a372..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", 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 From 6e7f6c5c0e73df271a248c31e203d6c18c88ad48 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 27 Mar 2025 07:33:42 +0000 Subject: [PATCH 258/447] Update ./snippets/Cleaner.ps1 --- scripts_staging/snippets/Cleaner.ps1 | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/scripts_staging/snippets/Cleaner.ps1 b/scripts_staging/snippets/Cleaner.ps1 index 7d24c419..f3c6bd6e 100644 --- a/scripts_staging/snippets/Cleaner.ps1 +++ b/scripts_staging/snippets/Cleaner.ps1 @@ -119,20 +119,21 @@ function Add-RegistryKeys-CleanMGR { # 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" + "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" + "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 From fa458f740aa7832233d034f3a6be869193bd528d Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 27 Mar 2025 08:51:49 +0000 Subject: [PATCH 259/447] Update ./scripts/Checks/Windows Services.ps1 --- scripts_staging/Checks/Windows Services.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts_staging/Checks/Windows Services.ps1 b/scripts_staging/Checks/Windows Services.ps1 index 685e61ce..ffd5162a 100644 --- a/scripts_staging/Checks/Windows Services.ps1 +++ b/scripts_staging/Checks/Windows Services.ps1 @@ -24,7 +24,7 @@ 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 #> @@ -59,7 +59,8 @@ $ignoredByDefault = @( "CDPSvc", "AGSService", "ShellHWDetection", # Frequently failing; unclear if actionable - "DropboxUpdater" + "DropboxUpdater", + "LocalKDC" # https://learn.microsoft.com/en-us/answers/questions/2136070/windows-server-2025-kerberos-local-key-distributio ) # Check if the "IgnoredServices" environment variable exists and add those services to the ignore list From 99d483705dbfbe6a914da699d5e37f5860e1aee9 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 27 Mar 2025 10:11:49 +0000 Subject: [PATCH 260/447] Update ./scripts/Build/Upgrade OS to Windows Server X Standard.ps1 --- ...pgrade OS to Windows Server X Standard.ps1 | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 scripts_staging/Build/Upgrade OS to Windows Server X Standard.ps1 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..e3e7671e --- /dev/null +++ b/scripts_staging/Build/Upgrade OS to Windows Server X Standard.ps1 @@ -0,0 +1,187 @@ +<# +.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= + + +.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 + +.TODO + more testing + find solutions for automated DC server upgrades + Add password on the nextcloud repo and make the script use it + +#> + + +# Windows Server Versions Metadata +$serverVersions = @{ + "2016" = @{ + "en" = @{ + "file" = "en_windows_server_2016_vl_x64_dvd_11636701.iso" + "checksum" = "47919CE8B4993F531CA1FA3F85941F4A72B47EBAA4D3A321FECF83CA9D17E6B8" # pragma: allowlist-secret + "licenseKey" = "WC2BQ-8NRM3-FDDYY-2BFGV-KHKQY" + } + "fr" = @{ + "file" = "fr_windows_server_2016_vl_x64_dvd_11636729.iso" + "checksum" = "81B809A9782C046A48D461AAEBFCD33D07A566C5A990373D0A36CDA1E08EA6F0" # pragma: allowlist-secret + "licenseKey" = "WC2BQ-8NRM3-FDDYY-2BFGV-KHKQY" + } + } + "2019" = @{ + "en" = @{ + "file" = "en-us_windows_server_2019_x64_dvd_f9475476.iso" + "checksum" = "EA247E5CF4DF3E5829BFAAF45D899933A2A67B1C700A02EE8141287A8520261C" # pragma: allowlist-secret + "licenseKey" = "N69G4-B89J2-4G8F4-WWYCC-J464C" + } + "fr" = @{ + "file" = "fr-fr_windows_server_2019_x64_dvd_f6f6acf6.iso" + "checksum" = "E0C6958E94F41163AA1EA9500825B8523136E1B8C5FC03CB7E3900858C7134AD" # pragma: allowlist-secret + "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 + "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 + "licenseKey" = "VDYBN-27WPP-V4HQT-9VMD4-VMK7H" + } + } + "2025" = @{ + "en" = @{ + "file" = "en-us_windows_server_2025_x64_dvd_b7ec10f3.iso" + "checksum" = "854109E1F215A29FC3541188297A6CA97C8A8F0F8C4DD6236B78DFDF845BF75E" # pragma: allowlist-secret + "licenseKey" = "TVRH6-WHNXV-R9WG3-9XRFY-MY832" + } + "fr" = @{ + "file" = "fr-fr_windows_server_2025_x64_dvd_bd6be507.iso" + "checksum" = "45384960A3F430D26454955D1198A6E38E7AA98C9E3906AC1AE9367229C103D0" # pragma: allowlist-secret + "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 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-Host "Starting in-place upgrade..." + Start-Process -FilePath $setupPath -ArgumentList $upgradeArgs -Wait -NoNewWindow + Write-Host "Upgrade process initiated." +} + +# Function to check requirements +function Check-Requirements { + param ([string]$targetedVersion, [string]$baseUrl) + + if (-not $targetedVersion) { Write-Host "TARGETED_VERSION is not set. Exiting."; exit 1 } + if (-not $baseUrl) { Write-Host "Download_Source is not set. Exiting."; exit 1 } + + # Detect system language (first two letters) + $systemLocale = (Get-WinSystemLocale).Name.Substring(0,2).ToLower() + + # Validate language availability + if (-not $serverVersions[$targetedVersion].ContainsKey($systemLocale)) { + Write-Host "Unsupported language: $systemLocale. Exiting." + exit 1 + } + + # Check for 7-Zip + $sevenZipPath = (Get-Command 7z.exe -ErrorAction SilentlyContinue).Source + if (-not $sevenZipPath) { + $sevenZipPath = "C:\\Program Files\\7-Zip\\7z.exe" + if (-not (Test-Path $sevenZipPath)) { + Write-Host "7-Zip not found in PATH or default location. Exiting." + exit 1 + } + } + + # Check available disk space + $freeSpace = (Get-PSDrive C).Free + if ($freeSpace -lt 12GB) { + Write-Host "Not enough disk space. Exiting." + exit 1 + } + + return $systemLocale, $sevenZipPath +} + +# 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-Host "Downloading ISO..." + Invoke-WebRequest -Uri "$baseUrl$($metadata.file)" -OutFile $isoFile + if (!(Verify-Checksum -filePath $isoFile -expectedChecksum $metadata.checksum)) { + Write-Host "Checksum verification failed. Exiting."; exit 1 + } +} + +# Clean and extract ISO +if (Test-Path $extractFolder) { Remove-Item -Recurse -Force $extractFolder } +Write-Host "Extracting ISO..." +Start-Process -FilePath $sevenZipPath -ArgumentList "x `"$isoFile`" -o`"$extractFolder`" -y" -Wait + +# Delete ISO file to free up space +Write-Host "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-Host "setup.exe not found. Exiting."; exit 1 +} + From bb3eb5485329da4e9762087a1b5ee14c0ac40c0c Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 27 Mar 2025 10:27:18 +0000 Subject: [PATCH 261/447] Update ./scripts/Build/Upgrade OS to Windows Server X Standard.ps1 --- .../Build/Upgrade OS to Windows Server X Standard.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index e3e7671e..8f71eedf 100644 --- a/scripts_staging/Build/Upgrade OS to Windows Server X Standard.ps1 +++ b/scripts_staging/Build/Upgrade OS to Windows Server X Standard.ps1 @@ -6,7 +6,7 @@ This script performs an in-place upgrade of a Windows Server machine by download 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 From f5557b85f34c392e71a7925ba74ee8a77c55224a Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 27 Mar 2025 14:05:17 +0000 Subject: [PATCH 262/447] Update ./scripts/Build/Upgrade OS to Windows Server X Standard.ps1 --- .../Build/Upgrade OS to Windows Server X Standard.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 8f71eedf..8bc47fe3 100644 --- a/scripts_staging/Build/Upgrade OS to Windows Server X Standard.ps1 +++ b/scripts_staging/Build/Upgrade OS to Windows Server X Standard.ps1 @@ -26,7 +26,7 @@ All keys are valid for initial installation and from https://learn.microsoft.com more testing 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 #> From ebced2054c549580a59bf85e10f216a3cb57bb54 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 28 Mar 2025 08:54:43 +0000 Subject: [PATCH 263/447] Update ./scripts/Tools/List Non-Standard Service Accounts.ps1 --- .../List Non-Standard Service Accounts.ps1 | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 scripts_staging/Tools/List Non-Standard Service Accounts.ps1 diff --git a/scripts_staging/Tools/List Non-Standard Service Accounts.ps1 b/scripts_staging/Tools/List Non-Standard Service Accounts.ps1 new file mode 100644 index 00000000..6a84174c --- /dev/null +++ b/scripts_staging/Tools/List Non-Standard Service Accounts.ps1 @@ -0,0 +1,50 @@ +<# +.SYNOPSIS + This script checks for Windows services running under unexpected system accounts. + +.DESCRIPTION + The script retrieves all Windows services and identifies any that are running under system accounts other than 'NT AUTHORITY\LOCAL SERVICE' or 'NT AUTHORITY\NETWORK SERVICE'. + It also filters services that have an automatic start mode. If any such services are found, they are displayed in a formatted table, including the username running the service, and the script exits with code 1. + +.NOTES + Author: SAN + Date: 01.01.24 + #public + +.CHANGELOG + +#> + +$allowedAccounts = @( + "NT AUTHORITY\\LOCAL SERVICE", + "NT AUTHORITY\\NETWORK SERVICE", + "LocalSystem", + "NT AUTHORITY\LocalService", + "NT AUTHORITY\NETWORKSERVICE" + + +) + +$found = $false +$services = @() + +# Get services +Get-WmiObject Win32_Service | ForEach-Object { + $service = $_ + if ($service.StartMode -eq "Auto" -and $service.StartName -notin $allowedAccounts) { + $services += [PSCustomObject]@{ + Name = $service.Name + DisplayName = $service.DisplayName + StartName = $service.StartName + State = $service.State + Username = $service.StartName + } + $found = $true + } +} + +# If any unexpected services are found, display them in one table and exit with code 1 +if ($found) { + $services | Format-Table -AutoSize + exit 1 +} From a3993d299fa50346f483c9fed27576e19e06cb5a Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 28 Mar 2025 10:24:39 +0000 Subject: [PATCH 264/447] Update ./scripts/Checks/Windows Update Health.ps1 --- scripts_staging/Checks/Windows Update Health.ps1 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts_staging/Checks/Windows Update Health.ps1 b/scripts_staging/Checks/Windows Update Health.ps1 index c9802b42..838f0a82 100644 --- a/scripts_staging/Checks/Windows Update Health.ps1 +++ b/scripts_staging/Checks/Windows Update Health.ps1 @@ -15,13 +15,24 @@ PSWindowsUpdate module .CHANGELOG - SAN 25.03.2025 Initial version of the script to check updates older than a specified threshold. + 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. .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 +} + +# Continue with the rest of the script +Write-Host "Proceeding with the script." $ThresholdDays = $env:ThresholdDays if (-not $ThresholdDays) { $ThresholdDays = 90 From df8edf52f1bd78fe77a963d7d9341614283f4719 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 28 Mar 2025 10:29:48 +0000 Subject: [PATCH 265/447] Update ./scripts/Checks/Windows Update Health.ps1 --- scripts_staging/Checks/Windows Update Health.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts_staging/Checks/Windows Update Health.ps1 b/scripts_staging/Checks/Windows Update Health.ps1 index 838f0a82..ab29c163 100644 --- a/scripts_staging/Checks/Windows Update Health.ps1 +++ b/scripts_staging/Checks/Windows Update Health.ps1 @@ -28,11 +28,10 @@ $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" + $host.SetShouldExit(15) exit 15 } -# Continue with the rest of the script -Write-Host "Proceeding with the script." $ThresholdDays = $env:ThresholdDays if (-not $ThresholdDays) { $ThresholdDays = 90 From 0f40ca6640438cc520766b4453117ae3e90061bb Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:27:21 +0000 Subject: [PATCH 266/447] Update ./scripts/Checks/Windows Update Health.ps1 --- scripts_staging/Checks/Windows Update Health.ps1 | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts_staging/Checks/Windows Update Health.ps1 b/scripts_staging/Checks/Windows Update Health.ps1 index ab29c163..53f85669 100644 --- a/scripts_staging/Checks/Windows Update Health.ps1 +++ b/scripts_staging/Checks/Windows Update Health.ps1 @@ -13,10 +13,10 @@ #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. + 28.03.2025 SAN added skip for windows 2012 & pwsh support .TODO Add filters to ignore updates in env @@ -26,12 +26,15 @@ $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) { +if ($osVersion.Major -eq 6 -and $osVersion.Minor -eq 2 -and $osVersion.Build -lt 9200) { Write-Host "Not supported on Server 2012" $host.SetShouldExit(15) exit 15 } + +{{CallPowerShell7}} + $ThresholdDays = $env:ThresholdDays if (-not $ThresholdDays) { $ThresholdDays = 90 From f165f78a928dd1e400b3f329b2565597daed4323 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:59:49 +0000 Subject: [PATCH 267/447] Update ./scripts/Checks/Windows Update Health.ps1 --- scripts_staging/Checks/Windows Update Health.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts_staging/Checks/Windows Update Health.ps1 b/scripts_staging/Checks/Windows Update Health.ps1 index 53f85669..cf763572 100644 --- a/scripts_staging/Checks/Windows Update Health.ps1 +++ b/scripts_staging/Checks/Windows Update Health.ps1 @@ -51,7 +51,7 @@ try { } if ($updates.Count -eq 0) { - Write-Host "OK: No updates found." + Write-Host "OK: No outdated updates found." } else { $updates | ForEach-Object { Write-Host "$($_.LastDeploymentChangeTime) | KB: $($_.KBArticleIDs) | $($_.Title)" From 004d652a33ec24b864808f9c5c36686830ebbba4 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:06:11 +0000 Subject: [PATCH 268/447] Update ./scripts/Checks/Windows Update Health.ps1 --- scripts_staging/Checks/Windows Update Health.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts_staging/Checks/Windows Update Health.ps1 b/scripts_staging/Checks/Windows Update Health.ps1 index cf763572..b2718b72 100644 --- a/scripts_staging/Checks/Windows Update Health.ps1 +++ b/scripts_staging/Checks/Windows Update Health.ps1 @@ -14,6 +14,7 @@ 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 @@ -51,7 +52,7 @@ try { } if ($updates.Count -eq 0) { - Write-Host "OK: No outdated updates found." + Write-Host "OK: No updates found." } else { $updates | ForEach-Object { Write-Host "$($_.LastDeploymentChangeTime) | KB: $($_.KBArticleIDs) | $($_.Title)" From 59d637e99a6ac48e2ad4c60607e5d7fae251adfd Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 31 Mar 2025 13:52:02 +0200 Subject: [PATCH 269/447] Delete scripts_staging/Tools/List Non-Standard Service Accounts.ps1 trashed idea --- .../List Non-Standard Service Accounts.ps1 | 50 ------------------- 1 file changed, 50 deletions(-) delete mode 100644 scripts_staging/Tools/List Non-Standard Service Accounts.ps1 diff --git a/scripts_staging/Tools/List Non-Standard Service Accounts.ps1 b/scripts_staging/Tools/List Non-Standard Service Accounts.ps1 deleted file mode 100644 index 6a84174c..00000000 --- a/scripts_staging/Tools/List Non-Standard Service Accounts.ps1 +++ /dev/null @@ -1,50 +0,0 @@ -<# -.SYNOPSIS - This script checks for Windows services running under unexpected system accounts. - -.DESCRIPTION - The script retrieves all Windows services and identifies any that are running under system accounts other than 'NT AUTHORITY\LOCAL SERVICE' or 'NT AUTHORITY\NETWORK SERVICE'. - It also filters services that have an automatic start mode. If any such services are found, they are displayed in a formatted table, including the username running the service, and the script exits with code 1. - -.NOTES - Author: SAN - Date: 01.01.24 - #public - -.CHANGELOG - -#> - -$allowedAccounts = @( - "NT AUTHORITY\\LOCAL SERVICE", - "NT AUTHORITY\\NETWORK SERVICE", - "LocalSystem", - "NT AUTHORITY\LocalService", - "NT AUTHORITY\NETWORKSERVICE" - - -) - -$found = $false -$services = @() - -# Get services -Get-WmiObject Win32_Service | ForEach-Object { - $service = $_ - if ($service.StartMode -eq "Auto" -and $service.StartName -notin $allowedAccounts) { - $services += [PSCustomObject]@{ - Name = $service.Name - DisplayName = $service.DisplayName - StartName = $service.StartName - State = $service.State - Username = $service.StartName - } - $found = $true - } -} - -# If any unexpected services are found, display them in one table and exit with code 1 -if ($found) { - $services | Format-Table -AutoSize - exit 1 -} From bcaff03973ba4fcf9e81c2bb6e842e5868ebed80 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 31 Mar 2025 11:58:19 +0000 Subject: [PATCH 270/447] Update ./scripts/Checks/Windows Services.ps1 --- scripts_staging/Checks/Windows Services.ps1 | 38 +++++++++++++-------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/scripts_staging/Checks/Windows Services.ps1 b/scripts_staging/Checks/Windows Services.ps1 index ffd5162a..1b5c145a 100644 --- a/scripts_staging/Checks/Windows Services.ps1 +++ b/scripts_staging/Checks/Windows Services.ps1 @@ -18,13 +18,16 @@ #public .TODO - - Recheck the list of services for any that should be monitored (e.g., ShellHWDetection). + 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 + 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 #> @@ -63,15 +66,23 @@ $ignoredByDefault = @( "LocalKDC" # https://learn.microsoft.com/en-us/answers/questions/2136070/windows-server-2025-kerberos-local-key-distributio ) -# Check if the "IgnoredServices" environment variable exists and add those services to the ignore list +# 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 } -# Convert ignored services to a regular expression pattern +# 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' } @@ -80,27 +91,26 @@ $servicesToCheck = Get-Service | Where-Object { ($_.StartType -eq 'Automatic' -o $servicesNeedingAttention = @() $ignoredStoppedServices = @() -# Check the status of each service +# Check the status of each service and categorize based on ignore patterns 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 + # 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 { - # Add the service to the list of ignored stopped services $ignoredStoppedServices += $service } } -# Check if the "enabledebug" environment variable is set to true +# 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 +# Display the results based on the service statuses if ($servicesNeedingAttention.Count -eq 0) { if (-not $debugEnabled) { Write-Host "All required services are running." From 4b6f5b792f966a06d1245d8c200e07979ca1e44d Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 2 Apr 2025 07:30:30 +0000 Subject: [PATCH 271/447] Update ./scripts/Checks/Windows Update Health.ps1 --- scripts_staging/Checks/Windows Update Health.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts_staging/Checks/Windows Update Health.ps1 b/scripts_staging/Checks/Windows Update Health.ps1 index b2718b72..81f986a9 100644 --- a/scripts_staging/Checks/Windows Update Health.ps1 +++ b/scripts_staging/Checks/Windows Update Health.ps1 @@ -18,6 +18,7 @@ .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 @@ -27,9 +28,8 @@ $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 -and $osVersion.Build -lt 9200) { +if ($osVersion.Major -eq 6 -and $osVersion.Minor -eq 2) { Write-Host "Not supported on Server 2012" - $host.SetShouldExit(15) exit 15 } From 9c099cde170f225b23c43610154d45f09b1f6ac0 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 2 Apr 2025 08:17:51 +0000 Subject: [PATCH 272/447] Update ./scripts/Backend/Export TRMM Scripts to folder and git sync V2.py --- .../Backend/Export TRMM Scripts to folder and git sync V2.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts_staging/Backend/Export TRMM Scripts to folder and git sync V2.py b/scripts_staging/Backend/Export TRMM Scripts to folder and git sync V2.py index 0b073695..669dda47 100644 --- a/scripts_staging/Backend/Export TRMM Scripts to folder and git sync V2.py +++ b/scripts_staging/Backend/Export TRMM Scripts to folder and git sync V2.py @@ -68,10 +68,8 @@ simplify the functions that does the writeback Move raws from "scriptsraw" to scripts/subfolder/raws/ to group them with their scripts add debug statements and debug flags - find edge-cases and add exit code for them add logging add counters and separators at the end of each function - investigate if the lines returns in the code causes issues in some case (theoretical issue) send workflow flags to ENV default to true make the commit message to be dynamic ex. "modified xxx.ps1, xxx.py" From 3a5efbfdb7fd0b4aefb0f536b839aaf477457db1 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 2 Apr 2025 11:02:59 +0200 Subject: [PATCH 273/447] renamed git sync script --- ... TRMM Scripts to folder and git sync V2.py | 454 ------------------ 1 file changed, 454 deletions(-) delete mode 100644 scripts_staging/Backend/Export TRMM Scripts to folder and git sync V2.py diff --git a/scripts_staging/Backend/Export TRMM Scripts to folder and git sync V2.py b/scripts_staging/Backend/Export TRMM Scripts to folder and git sync V2.py deleted file mode 100644 index 669dda47..00000000 --- a/scripts_staging/Backend/Export TRMM Scripts to folder and git sync V2.py +++ /dev/null @@ -1,454 +0,0 @@ -#!/usr/bin/python3 -#public - -""" -.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. - Each part can be toggled with its own flag to help troubleshoot any issue. - 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 - -.WORKFLOW - 0. The mapped folder should already be configured with git - - 1. Pull all the modifications from the git repo 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 scripts out to 4 folders: - scripts: extracted script code from the API converted from json - scriptsraw: All json data from the API for later processing, currently used for hash comparison - snippets: extracted snippet code from the API converted from json - snippetsraw: All json data for later import/migration - - 4. Push all the modifications to the git repo configured for the folder via git commands - If there are no changes, no commit will be made. - -.EXEMPLE -DOMAIN=https://api-rmm -API_TOKEN={{global.rmm_key_for_git_script}} -SCRIPTPATH=/var/RMM-script-repo - -.CHANGELOG - v5.0 Y Exports functional, adds script ID to from as "id - " - v5.a Y "id - " for only raw folder. Fixed to use X-API-KEY - v5.1 Y Sanitizing script names when has / in it - v5.2 Y moving url and api token to .env file - v5.3 Y Making script folders be subfolders of where export.py file is - v5.4 Y making filenames utf-8 compliant - v5.5 7/11/2024 X Save PowerShell scripts with .ps1 and Python scripts with .py extensions - v5.6 7/11/2024 X Count the total number of scripts and print at the end - v5.7 7/11/2024 X Print a summary of all the different types of shells exported - v5.8 7/11/2024 X Add support for additional shell extension types - v5.9 7/11/2024 X Detect deleted scripts and delete them from both folders - v6 7/31/2024 SAN Add support for specifying the save folder via the SCRIPTPATH environment variable - v6.0.1 7/31/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 - - -.TODO - Add reporting support - add writeback for snippets - simplify the functions that does the writeback - Move raws from "scriptsraw" to scripts/subfolder/raws/ to group them with their scripts - add debug statements and debug flags - add logging - add counters and separators at the end of each function - send workflow flags to ENV default to true - make the commit message to be dynamic ex. "modified xxx.ps1, xxx.py" - -""" - -import subprocess -import sys -import os -import hashlib -import json -from collections import defaultdict -from pathlib import Path -import requests -from pathvalidate import sanitize_filename -import re - -# Toggle flags -ENABLE_GIT_PULL = True -ENABLE_GIT_PUSH = True -ENABLE_WRITEBACK = True -ENABLE_WRITETOFILE = True - -def delete_obsolete_files(folder, current_scripts): - print(f"Deleting obsolete files and directories in {folder}...") - all_files = set() - relevant_dirs = set() - - for item in folder.rglob('*'): - if item.is_file(): - all_files.add(item.relative_to(folder)) - elif item.is_dir() and any(item.glob('*')): - relevant_dirs.add(item.relative_to(folder)) - - obsolete_files = all_files - current_scripts - for item in folder.rglob('*'): - if item.is_file() and item.relative_to(folder) in obsolete_files: - try: - print(f"Deleting obsolete file: {item}") - item.unlink() - except Exception as e: - print(f"Error deleting file {item}: {e}") - - for dirpath in sorted(folder.rglob('*'), key=lambda p: len(p.parts), reverse=True): - if dirpath.is_dir() and not any(dirpath.glob('*')) and dirpath.relative_to(folder) not in relevant_dirs: - try: - dirpath.rmdir() - print(f"Deleting empty obsolete directory: {dirpath}") - except OSError as e: - print(f"Could not delete directory {dirpath}: {e}") - -def process_scripts(scripts, script_folder, script_raw_folder, shell_summary, is_snippet=False): - print(f"Processing {'snippets' if is_snippet else 'user-defined scripts'}...") - current_scripts = set() - - for script in scripts: - script_id = script.get('id') - script_name = sanitize_filename(script.get('name', 'Unnamed Script')) - category = script.get('category', '').strip() if script.get('category') else '' - category = sanitize_filename(category) - category_folder = script_folder / category if category else script_folder - category_raw_folder = script_raw_folder / category if category else script_raw_folder - - category_folder.mkdir(parents=True, exist_ok=True) - category_raw_folder.mkdir(parents=True, exist_ok=True) - - if not is_snippet: - download_url = f"{domain}/scripts/{script_id}/download/?with_snippets=false" - script_data = fetch_data(download_url, headers) - else: - script_data = script - - if script_data: - code = script_data.get('code') - shell = script.get('shell') - extension = { - 'powershell': '.ps1', - 'python': '.py', - 'cmd': '.bat', - 'shell': '.sh', - 'nushell': '.nu' - }.get(shell, '.txt') - - if not is_snippet: - shell_summary[shell] += 1 - - script_filename = f"{script_name}{extension}" - script_file_path = category_folder / script_filename - save_file(script_file_path, code) - - raw_filename = f"{script_id} - {script_name}.json" - raw_file_path = category_raw_folder / raw_filename - save_file(raw_file_path, {**script_data, **script}, is_json=True) - - current_scripts.add(script_file_path.relative_to(script_folder)) - current_scripts.add(raw_file_path.relative_to(script_raw_folder)) - - print(f"Processed {len(current_scripts)} {'snippets' if is_snippet else 'scripts'}.") - return current_scripts - - -def compute_hash(file_path): - """Compute SHA-256 hash of a file.""" - hash_sha256 = hashlib.sha256() - try: - with open(file_path, 'rb') as f: - for chunk in iter(lambda: f.read(4096), b""): - hash_sha256.update(chunk) - except FileNotFoundError: - return None - return hash_sha256.hexdigest() - -def save_file(path, content, is_json=False): - """Save the file unconditionally.""" - new_content = json.dumps(content, indent=4, ensure_ascii=False) if is_json else content - - if ENABLE_WRITETOFILE: - with open(path, 'w', encoding="utf-8") as file: - file.write(new_content) - print(f"File saved: {path}") - else: - print(f"File would be saved (simulation): {path}") - - -def fetch_data(url, headers): - print(f"Fetching data from {url}...") - response = requests.get(url, headers=headers) - if response.status_code == 200: - print(f"Data fetched successfully from {url}.") - return response.json() - else: - print(f"Error fetching data from {url}: {response.status_code}") - return [] - - -def compare_script_and_json(folders): - """Compare script files with their corresponding JSON files and return mismatches.""" - print("Comparing script files with JSON files...") - - mismatches = [] - existing_files = defaultdict(dict) - - for raw_file_path in folders['scriptsraw'].rglob('*.json'): - raw_filename = raw_file_path.stem # Get the filename without extension - raw_name_cleaned = re.sub(r'^\d+ - ', '', raw_filename).lower() # Clean filename - - matched_script_path = None - for script_file_path in folders['scripts'].rglob('*'): - if script_file_path.is_file(): - script_filename = script_file_path.stem.lower() - if script_filename == raw_name_cleaned: - matched_script_path = script_file_path - break - - if matched_script_path: - print(f"Matched script file: {matched_script_path} with raw file: {raw_file_path}") - - script_hash = compute_hash(matched_script_path) - - with open(raw_file_path, 'r', encoding='utf-8') as json_file: - raw_data = json.load(json_file) - json_script_content = raw_data.get('code', '') - - # Compare the hashes of the actual script content - json_script_hash = hashlib.sha256(json_script_content.encode('utf-8')).hexdigest() - - print(f"Script file hash: {script_hash}") - print(f"JSON 'code' field hash: {json_script_hash}") - - if script_hash != json_script_hash: - print("\n--- Script File Content (first 10 lines) ---") - with open(matched_script_path, 'r', encoding='utf-8') as script_file: - for i, line in enumerate(script_file): - if i < 10: - print(line.strip()) - else: - break - - print("\n--- JSON 'Code' Field Content (first 10 lines) ---") - json_lines = json_script_content.splitlines() - for i, line in enumerate(json_lines): - if i < 10: - print(line.strip()) - else: - break - - mismatches.append({ - 'script_path': matched_script_path, - 'raw_path': raw_file_path, - 'script_hash': script_hash, - 'json_script_hash': json_script_hash - }) - - existing_files[matched_script_path.relative_to(folders['scripts'])] = { - 'script_path': matched_script_path, - 'raw_path': raw_file_path, - 'script_hash': script_hash, - 'json_script_hash': json_script_hash - } - else: - print(f"No matching script file found for JSON: {raw_file_path}") - - return mismatches - -def write_modifications_to_api(base_dir, folders, api_token): - """Main function to compare files and send data to the API.""" - mismatches = compare_script_and_json(folders) - send_mismatched_data_to_api(mismatches, api_token) - - -def update_api(script_id, payload, api_token): - # Convert 'code' to 'script_body' - if 'code' in payload: - payload['script_body'] = payload.pop('code') - - url = f"{domain}/scripts/{script_id}/" - headers = { - 'X-API-KEY': api_token, - 'Content-Type': 'application/json', - 'Accept': 'application/json, text/plain, */*' - } - - # Log the script body length and truncated content - script_body_length = len(payload.get('script_body', '')) - truncated_body = (payload['script_body'][:1000] + '...') if script_body_length > 1000 else payload['script_body'] - print(f"Updating script {script_id}, length: {script_body_length}, payload: {json.dumps({**payload, 'script_body': truncated_body}, indent=2)}") - - # Make the request with a longer timeout - try: - response = requests.put(url, headers=headers, json=payload, timeout=120) - except requests.exceptions.RequestException as e: - print(f"Request failed: {e}") - return - - # Print response for debugging - print(f"Response status code: {response.status_code}, Response content: {response.text}") - - # Check response status - if response.status_code == 200: - print(f"Script {script_id} updated successfully.") - elif response.status_code == 401: - print(f"Failed to update script {script_id}: 401 Unauthorized. Check API token.") - elif response.status_code == 404: - print(f"Failed to update script {script_id}: 404 Not Found. The resource may not exist.") - else: - print(f"Failed to update script {script_id}: {response.status_code} {response.text}") - - - - -def send_mismatched_data_to_api(mismatches, api_token): - """Send mismatched script data to the API.""" - for mismatch in mismatches: - script_path = mismatch.get('script_path') - raw_path = mismatch.get('raw_path') - - with open(raw_path, 'r', encoding='utf-8') as f: - raw_data = json.load(f) - - updated_payload = {**raw_data, 'code': open(script_path, 'r').read()} - - # Convert 'code' to 'script_body' before updating the API - try: - if ENABLE_WRITEBACK: - print(f"Updating API with payload for {script_path}:") - # Call the update function with api_token - update_api(raw_data.get('id'), updated_payload, api_token) - else: - print(f"Payload that would be pushed for {script_path}:") - # Preview the payload with 'script_body' instead of 'code' - updated_payload['script_body'] = updated_payload.pop('code') - print(json.dumps(updated_payload, indent=4)) - sys.stdout.flush() # Explicitly flush stdout - except BrokenPipeError: - sys.stderr.close() - sys.stdout.close() - - -def git_pull(base_dir): - """Force pull the latest changes from the git repository, discarding local changes.""" - if ENABLE_GIT_PULL: - print("Starting force pull...") - try: - subprocess.check_call(['git', '-C', base_dir, 'fetch', 'origin']) - subprocess.check_call(['git', '-C', base_dir, 'reset', '--hard', 'origin/master']) - print("Successfully force-pulled the latest changes from the repository.") - except subprocess.CalledProcessError as e: - print(f"Failed to force-pull changes from Git: {e}") - sys.exit(1) - else: - print("Git pull is disabled.") - - -def git_push(base_dir): - """Push local changes to the git repository.""" - if ENABLE_GIT_PUSH: - print("Starting git push...") - 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("Rebase in progress. Please complete or abort the rebase manually.") - sys.exit(1) - - branch_result = subprocess.run(['git', '-C', base_dir, 'rev-parse', '--abbrev-ref', 'HEAD'], - capture_output=True, text=True) - branch_name = branch_result.stdout.strip() - - if branch_name == 'HEAD': - branch_name = "update-scripts" - subprocess.check_call(['git', '-C', base_dir, 'checkout', '-b', branch_name]) - print(f"Switched to new branch '{branch_name}'") - - 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', '.']) - subprocess.check_call(['git', '-C', base_dir, 'commit', '-m', 'Update scripts and raw data']) - print(f"Committed changes to branch '{branch_name}'") - else: - print("No changes to commit.") - - subprocess.check_call(['git', '-C', base_dir, 'push', 'origin', branch_name]) - print(f"Changes pushed to branch '{branch_name}'") - except subprocess.CalledProcessError as e: - print(f"Git operation failed: {e}") - else: - print("Git push is disabled.") - -def download_scripts(): - global domain, headers - - domain = os.getenv('DOMAIN') - api_token = os.getenv('API_TOKEN') - scriptpath = os.getenv('SCRIPTPATH') - - if not domain or not api_token or not scriptpath: - print("Error: DOMAIN, API_TOKEN, and SCRIPTPATH must be set in the environment.") - sys.exit(1) - - headers = {"X-API-KEY": api_token} - base_dir = Path(scriptpath).resolve() - folders = { - "scripts": base_dir / "scripts", - "scriptsraw": base_dir / "scriptsraw", - "snippets": base_dir / "snippets", - "snippetsraw": base_dir / "snippetsraw" - } - for folder in folders.values(): - folder.mkdir(parents=True, exist_ok=True) - - shell_summary = defaultdict(int) - current_scripts = set() - - if ENABLE_GIT_PULL: - git_pull(base_dir) - - write_modifications_to_api(base_dir, folders, api_token) - - print("Fetching user-defined scripts...") - user_defined_scripts = fetch_data(f"{domain}/scripts/?showHiddenScripts=true", headers) - 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)) - - print("Fetching snippets...") - snippets = fetch_data(f"{domain}/scripts/snippets/", headers) - current_scripts.update(process_scripts(snippets, folders['snippets'], folders['snippetsraw'], shell_summary, is_snippet=True)) - - for folder in folders.values(): - delete_obsolete_files(folder, current_scripts) - - if ENABLE_GIT_PUSH: - git_push(base_dir) - - print(f"Total number of scripts exported: {len(current_scripts)}") - print("Shell summary:") - for shell, count in shell_summary.items(): - print(f"{shell}: {count}") - - - -if __name__ == "__main__": - download_scripts() \ No newline at end of file From af616ede3346bbce4b0dee179e320a70cef766c0 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 2 Apr 2025 09:04:59 +0000 Subject: [PATCH 274/447] Update ./scripts/Sync TRMM with GIT.py --- scripts_staging/scripts/Sync TRMM with GIT.py | 509 ++++++++++++++++++ 1 file changed, 509 insertions(+) create mode 100644 scripts_staging/scripts/Sync TRMM with GIT.py diff --git a/scripts_staging/scripts/Sync TRMM with GIT.py b/scripts_staging/scripts/Sync TRMM with GIT.py new file mode 100644 index 00000000..dd010333 --- /dev/null +++ b/scripts_staging/scripts/Sync TRMM with GIT.py @@ -0,0 +1,509 @@ +#!/usr/bin/python3 +#public + +""" +.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. The mapped folder should already be configured with git + + 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 scripts out to 4 folders: + scripts: extracted script code from the API converted from json + scriptsraw: All json data from the API for later processing, currently used for hash comparison + snippets: extracted snippet code from the API converted from json + snippetsraw: All json data for later import/migration + + 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 +DOMAIN=https://api-rmm +DOMAIN=https://{{global.RMM_API_URL}} +API_TOKEN={{global.rmm_key_for_git_script}} +API_TOKEN=asdf1234 +SCRIPTPATH=/var/RMM-script-repo + +.CHANGELOG + v5.0 Y Exports functional, adds script ID to from as "id - " + v5.a Y "id - " for only raw folder. Fixed to use X-API-KEY + v5.1 Y Sanitizing script names when has / in it + v5.2 Y moving url and api token to .env file + v5.3 Y Making script folders be subfolders of where export.py file is + v5.4 Y making filenames utf-8 compliant + v5.5 7/11/2024 X Save PowerShell scripts with .ps1 and Python scripts with .py extensions + v5.6 7/11/2024 X Count the total number of scripts and print at the end + v5.7 7/11/2024 X Print a summary of all the different types of shells exported + v5.8 7/11/2024 X Add support for additional shell extension types + v5.9 7/11/2024 X Detect deleted scripts and delete them from both folders + v6 7/31/2024 SAN Add support for specifying the save folder via the SCRIPTPATH environment variable + v6.0.1 7/31/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 + + +.TODO + Add reporting support + add writeback support for snippets + simplify the functions that does the writeback + Move raws from "scriptsraw" to scripts/subfolder/raws/ to group them with their scripts + add logging + add counters and separators at the end of each function + send workflow flags to ENV default to true + +""" + +import subprocess +import sys +import os +import hashlib +import json +from collections import defaultdict +from pathlib import Path +import requests +from pathvalidate import sanitize_filename +import re + +# Toggle flags +ENABLE_GIT_PULL = True +ENABLE_GIT_PUSH = True +ENABLE_WRITEBACK = True +ENABLE_WRITETOFILE = True + +def delete_obsolete_files(folder, current_scripts): + print(f"Deleting obsolete files and directories in {folder}...") + all_files = set() + relevant_dirs = set() + + for item in folder.rglob('*'): + if item.is_file(): + all_files.add(item.relative_to(folder)) + elif item.is_dir() and any(item.glob('*')): + relevant_dirs.add(item.relative_to(folder)) + + obsolete_files = all_files - current_scripts + for item in folder.rglob('*'): + if item.is_file() and item.relative_to(folder) in obsolete_files: + try: + print(f"Deleting obsolete file: {item}") + item.unlink() + except Exception as e: + print(f"Error deleting file {item}: {e}") + + for dirpath in sorted(folder.rglob('*'), key=lambda p: len(p.parts), reverse=True): + if dirpath.is_dir() and not any(dirpath.glob('*')) and dirpath.relative_to(folder) not in relevant_dirs: + try: + dirpath.rmdir() + print(f"Deleting empty obsolete directory: {dirpath}") + except OSError as e: + print(f"Could not delete directory {dirpath}: {e}") + +def process_scripts(scripts, script_folder, script_raw_folder, shell_summary, is_snippet=False): + print(f"Processing {'snippets' if is_snippet else 'user-defined scripts'}...") + current_scripts = set() + + for script in scripts: + script_id = script.get('id') + script_name = sanitize_filename(script.get('name', 'Unnamed Script')) + category = script.get('category', '').strip() if script.get('category') else '' + category = sanitize_filename(category) + category_folder = script_folder / category if category else script_folder + category_raw_folder = script_raw_folder / category if category else script_raw_folder + + category_folder.mkdir(parents=True, exist_ok=True) + category_raw_folder.mkdir(parents=True, exist_ok=True) + + if not is_snippet: + download_url = f"{domain}/scripts/{script_id}/download/?with_snippets=false" + script_data = fetch_data(download_url, headers) + else: + script_data = script + + if script_data: + code = script_data.get('code') + shell = script.get('shell') + extension = { + 'powershell': '.ps1', + 'python': '.py', + 'cmd': '.bat', + 'shell': '.sh', + 'nushell': '.nu' + }.get(shell, '.txt') + + if not is_snippet: + shell_summary[shell] += 1 + + script_filename = f"{script_name}{extension}" + script_file_path = category_folder / script_filename + save_file(script_file_path, code) + + raw_filename = f"{script_id} - {script_name}.json" + raw_file_path = category_raw_folder / raw_filename + save_file(raw_file_path, {**script_data, **script}, is_json=True) + + current_scripts.add(script_file_path.relative_to(script_folder)) + current_scripts.add(raw_file_path.relative_to(script_raw_folder)) + + print(f"Processed {len(current_scripts)} {'snippets' if is_snippet else 'scripts'}.") + return current_scripts + + +def compute_hash(file_path): + """Compute SHA-256 hash of a file.""" + hash_sha256 = hashlib.sha256() + try: + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_sha256.update(chunk) + except FileNotFoundError: + return None + return hash_sha256.hexdigest() + +def save_file(path, content, is_json=False): + """Save the file unconditionally.""" + new_content = json.dumps(content, indent=4, ensure_ascii=False) if is_json else content + + if ENABLE_WRITETOFILE: + with open(path, 'w', encoding="utf-8") as file: + file.write(new_content) + print(f"File saved: {path}") + else: + print(f"File would be saved (simulation): {path}") + + +def fetch_data(url, headers): + print(f"Fetching data from {url}...") + response = requests.get(url, headers=headers) + if response.status_code == 200: + print(f"Data fetched successfully from {url}.") + return response.json() + else: + print(f"Error fetching data from {url}: {response.status_code}") + return [] + + +def compare_script_and_json(folders): + """Compare script files with their corresponding JSON files and return mismatches.""" + print("Comparing script files with JSON files...") + + mismatches = [] + existing_files = defaultdict(dict) + + for raw_file_path in folders['scriptsraw'].rglob('*.json'): + raw_filename = raw_file_path.stem # Get the filename without extension + raw_name_cleaned = re.sub(r'^\d+ - ', '', raw_filename).lower() # Clean filename + + matched_script_path = None + for script_file_path in folders['scripts'].rglob('*'): + if script_file_path.is_file(): + script_filename = script_file_path.stem.lower() + if script_filename == raw_name_cleaned: + matched_script_path = script_file_path + break + + if matched_script_path: + print(f"Matched script file: {matched_script_path} with raw file: {raw_file_path}") + + script_hash = compute_hash(matched_script_path) + + with open(raw_file_path, 'r', encoding='utf-8') as json_file: + raw_data = json.load(json_file) + json_script_content = raw_data.get('code', '') + + # Compare the hashes of the actual script content + json_script_hash = hashlib.sha256(json_script_content.encode('utf-8')).hexdigest() + + print(f"Script file hash: {script_hash}") + print(f"JSON 'code' field hash: {json_script_hash}") + + if script_hash != json_script_hash: + print("\n--- Script File Content (first 10 lines) ---") + with open(matched_script_path, 'r', encoding='utf-8') as script_file: + for i, line in enumerate(script_file): + if i < 10: + print(line.strip()) + else: + break + + print("\n--- JSON 'Code' Field Content (first 10 lines) ---") + json_lines = json_script_content.splitlines() + for i, line in enumerate(json_lines): + if i < 10: + print(line.strip()) + else: + break + + mismatches.append({ + 'script_path': matched_script_path, + 'raw_path': raw_file_path, + 'script_hash': script_hash, + 'json_script_hash': json_script_hash + }) + + existing_files[matched_script_path.relative_to(folders['scripts'])] = { + 'script_path': matched_script_path, + 'raw_path': raw_file_path, + 'script_hash': script_hash, + 'json_script_hash': json_script_hash + } + else: + print(f"No matching script file found for JSON: {raw_file_path}") + + return mismatches + +def write_modifications_to_api(base_dir, folders, api_token): + """Main function to compare files and send data to the API.""" + mismatches = compare_script_and_json(folders) + send_mismatched_data_to_api(mismatches, api_token) + + +def update_api(script_id, payload, api_token): + # Convert 'code' to 'script_body' + if 'code' in payload: + payload['script_body'] = payload.pop('code') + + url = f"{domain}/scripts/{script_id}/" + headers = { + 'X-API-KEY': api_token, + 'Content-Type': 'application/json', + 'Accept': 'application/json, text/plain, */*' + } + + # Log the script body length and truncated content + script_body_length = len(payload.get('script_body', '')) + truncated_body = (payload['script_body'][:1000] + '...') if script_body_length > 1000 else payload['script_body'] + print(f"Updating script {script_id}, length: {script_body_length}, payload: {json.dumps({**payload, 'script_body': truncated_body}, indent=2)}") + + # Make the request with a longer timeout + try: + response = requests.put(url, headers=headers, json=payload, timeout=120) + except requests.exceptions.RequestException as e: + print(f"Request failed: {e}") + return + + # Print response for debugging + print(f"Response status code: {response.status_code}, Response content: {response.text}") + + # Check response status + if response.status_code == 200: + print(f"Script {script_id} updated successfully.") + elif response.status_code == 401: + print(f"Failed to update script {script_id}: 401 Unauthorized. Check API token.") + elif response.status_code == 404: + print(f"Failed to update script {script_id}: 404 Not Found. The resource may not exist.") + else: + print(f"Failed to update script {script_id}: {response.status_code} {response.text}") + + + + +def send_mismatched_data_to_api(mismatches, api_token): + """Send mismatched script data to the API.""" + for mismatch in mismatches: + script_path = mismatch.get('script_path') + raw_path = mismatch.get('raw_path') + + with open(raw_path, 'r', encoding='utf-8') as f: + raw_data = json.load(f) + + updated_payload = {**raw_data, 'code': open(script_path, 'r').read()} + + # Convert 'code' to 'script_body' before updating the API + try: + if ENABLE_WRITEBACK: + print(f"Updating API with payload for {script_path}:") + # Call the update function with api_token + update_api(raw_data.get('id'), updated_payload, api_token) + else: + print(f"Payload that would be pushed for {script_path}:") + # Preview the payload with 'script_body' instead of 'code' + updated_payload['script_body'] = updated_payload.pop('code') + print(json.dumps(updated_payload, indent=4)) + sys.stdout.flush() # Explicitly flush stdout + except BrokenPipeError: + sys.stderr.close() + sys.stdout.close() + + +def git_pull(base_dir): + """Force pull the latest changes from the git repository, discarding local changes.""" + if ENABLE_GIT_PULL: + print("Starting force pull...") + try: + subprocess.check_call(['git', '-C', base_dir, 'fetch', 'origin']) + subprocess.check_call(['git', '-C', base_dir, 'reset', '--hard', 'origin/master']) + print("Successfully force-pulled the latest changes from the repository.") + except subprocess.CalledProcessError as e: + print(f"Failed to force-pull changes from Git: {e}") + sys.exit(1) + else: + print("Git pull is disabled.") + + + +def get_staged_changes(base_dir): + """Get a list of staged files along with their status (created, modified, deleted)""" + result = subprocess.run( + ['git', '-C', base_dir, 'diff', '--cached', '--name-status'], + capture_output=True, text=True, check=True + ) + + changes = {"created": [], "modified": [], "deleted": []} + + for line in result.stdout.strip().split("\n"): + if not line: + continue + status, file = line.split("\t", 1) + + # Exclude files in the scriptsraw/ folder + if file.startswith("scriptsraw/"): + continue + + if status == "A": + changes["created"].append(file) + elif status == "M": + changes["modified"].append(file) + elif status == "D": + changes["deleted"].append(file) + + return changes + +def generate_commit_message(changes): + """Generate a meaningful commit message with filenames.""" + if not any(changes.values()): + return "Minor update" + + parts = [] + + if changes["created"]: + parts.append(f"Created {len(changes['created'])}: {', '.join(changes['created'])}") + if changes["modified"]: + parts.append(f"Modified {len(changes['modified'])}: {', '.join(changes['modified'])}") + if changes["deleted"]: + parts.append(f"Deleted {len(changes['deleted'])}: {', '.join(changes['deleted'])}") + + return "; ".join(parts) + +def git_push(base_dir): + """Push local changes to the git repository.""" + if ENABLE_GIT_PUSH: + print("Starting git push...") + try: + # Check if a rebase is in progress + 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("Rebase in progress. Please complete or abort the rebase manually.") + sys.exit(1) + + # Get the current branch + branch_result = subprocess.run(['git', '-C', base_dir, 'rev-parse', '--abbrev-ref', 'HEAD'], + capture_output=True, text=True) + branch_name = branch_result.stdout.strip() + + if branch_name == 'HEAD': + branch_name = "update-scripts" + subprocess.check_call(['git', '-C', base_dir, 'checkout', '-b', branch_name]) + print(f"Switched to new branch '{branch_name}'") + + 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', '.']) + + staged_changes = get_staged_changes(base_dir) + commit_message = generate_commit_message(staged_changes) + + subprocess.check_call(['git', '-C', base_dir, 'commit', '-m', commit_message]) + print(f"Committed changes to branch '{branch_name}': {commit_message}") + else: + print("No changes to commit.") + + subprocess.check_call(['git', '-C', base_dir, 'push', 'origin', branch_name]) + print(f"Changes pushed to branch '{branch_name}'") + except subprocess.CalledProcessError as e: + print(f"Git operation failed: {e}") + else: + print("Git push is disabled.") + +def download_scripts(): + global domain, headers + + domain = os.getenv('DOMAIN') + api_token = os.getenv('API_TOKEN') + scriptpath = os.getenv('SCRIPTPATH') + + if not domain or not api_token or not scriptpath: + print("Error: DOMAIN, API_TOKEN, and SCRIPTPATH must be set in the environment.") + sys.exit(1) + + headers = {"X-API-KEY": api_token} + base_dir = Path(scriptpath).resolve() + folders = { + "scripts": base_dir / "scripts", + "scriptsraw": base_dir / "scriptsraw", + "snippets": base_dir / "snippets", + "snippetsraw": base_dir / "snippetsraw" + } + for folder in folders.values(): + folder.mkdir(parents=True, exist_ok=True) + + shell_summary = defaultdict(int) + current_scripts = set() + + if ENABLE_GIT_PULL: + git_pull(base_dir) + + write_modifications_to_api(base_dir, folders, api_token) + + print("Fetching user-defined scripts...") + user_defined_scripts = fetch_data(f"{domain}/scripts/?showHiddenScripts=true", headers) + 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)) + + print("Fetching snippets...") + snippets = fetch_data(f"{domain}/scripts/snippets/", headers) + current_scripts.update(process_scripts(snippets, folders['snippets'], folders['snippetsraw'], shell_summary, is_snippet=True)) + + for folder in folders.values(): + delete_obsolete_files(folder, current_scripts) + + if ENABLE_GIT_PUSH: + git_push(base_dir) + + print(f"Total number of scripts exported: {len(current_scripts)}") + print("Shell summary:") + for shell, count in shell_summary.items(): + print(f"{shell}: {count}") + + + +if __name__ == "__main__": + download_scripts() \ No newline at end of file From 11b592630f20feb315dffa9d144e18355b4ea0e8 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 2 Apr 2025 11:11:41 +0200 Subject: [PATCH 275/447] wrong folder ._. --- scripts_staging/scripts/Sync TRMM with GIT.py | 509 ------------------ 1 file changed, 509 deletions(-) delete mode 100644 scripts_staging/scripts/Sync TRMM with GIT.py diff --git a/scripts_staging/scripts/Sync TRMM with GIT.py b/scripts_staging/scripts/Sync TRMM with GIT.py deleted file mode 100644 index dd010333..00000000 --- a/scripts_staging/scripts/Sync TRMM with GIT.py +++ /dev/null @@ -1,509 +0,0 @@ -#!/usr/bin/python3 -#public - -""" -.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. The mapped folder should already be configured with git - - 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 scripts out to 4 folders: - scripts: extracted script code from the API converted from json - scriptsraw: All json data from the API for later processing, currently used for hash comparison - snippets: extracted snippet code from the API converted from json - snippetsraw: All json data for later import/migration - - 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 -DOMAIN=https://api-rmm -DOMAIN=https://{{global.RMM_API_URL}} -API_TOKEN={{global.rmm_key_for_git_script}} -API_TOKEN=asdf1234 -SCRIPTPATH=/var/RMM-script-repo - -.CHANGELOG - v5.0 Y Exports functional, adds script ID to from as "id - " - v5.a Y "id - " for only raw folder. Fixed to use X-API-KEY - v5.1 Y Sanitizing script names when has / in it - v5.2 Y moving url and api token to .env file - v5.3 Y Making script folders be subfolders of where export.py file is - v5.4 Y making filenames utf-8 compliant - v5.5 7/11/2024 X Save PowerShell scripts with .ps1 and Python scripts with .py extensions - v5.6 7/11/2024 X Count the total number of scripts and print at the end - v5.7 7/11/2024 X Print a summary of all the different types of shells exported - v5.8 7/11/2024 X Add support for additional shell extension types - v5.9 7/11/2024 X Detect deleted scripts and delete them from both folders - v6 7/31/2024 SAN Add support for specifying the save folder via the SCRIPTPATH environment variable - v6.0.1 7/31/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 - - -.TODO - Add reporting support - add writeback support for snippets - simplify the functions that does the writeback - Move raws from "scriptsraw" to scripts/subfolder/raws/ to group them with their scripts - add logging - add counters and separators at the end of each function - send workflow flags to ENV default to true - -""" - -import subprocess -import sys -import os -import hashlib -import json -from collections import defaultdict -from pathlib import Path -import requests -from pathvalidate import sanitize_filename -import re - -# Toggle flags -ENABLE_GIT_PULL = True -ENABLE_GIT_PUSH = True -ENABLE_WRITEBACK = True -ENABLE_WRITETOFILE = True - -def delete_obsolete_files(folder, current_scripts): - print(f"Deleting obsolete files and directories in {folder}...") - all_files = set() - relevant_dirs = set() - - for item in folder.rglob('*'): - if item.is_file(): - all_files.add(item.relative_to(folder)) - elif item.is_dir() and any(item.glob('*')): - relevant_dirs.add(item.relative_to(folder)) - - obsolete_files = all_files - current_scripts - for item in folder.rglob('*'): - if item.is_file() and item.relative_to(folder) in obsolete_files: - try: - print(f"Deleting obsolete file: {item}") - item.unlink() - except Exception as e: - print(f"Error deleting file {item}: {e}") - - for dirpath in sorted(folder.rglob('*'), key=lambda p: len(p.parts), reverse=True): - if dirpath.is_dir() and not any(dirpath.glob('*')) and dirpath.relative_to(folder) not in relevant_dirs: - try: - dirpath.rmdir() - print(f"Deleting empty obsolete directory: {dirpath}") - except OSError as e: - print(f"Could not delete directory {dirpath}: {e}") - -def process_scripts(scripts, script_folder, script_raw_folder, shell_summary, is_snippet=False): - print(f"Processing {'snippets' if is_snippet else 'user-defined scripts'}...") - current_scripts = set() - - for script in scripts: - script_id = script.get('id') - script_name = sanitize_filename(script.get('name', 'Unnamed Script')) - category = script.get('category', '').strip() if script.get('category') else '' - category = sanitize_filename(category) - category_folder = script_folder / category if category else script_folder - category_raw_folder = script_raw_folder / category if category else script_raw_folder - - category_folder.mkdir(parents=True, exist_ok=True) - category_raw_folder.mkdir(parents=True, exist_ok=True) - - if not is_snippet: - download_url = f"{domain}/scripts/{script_id}/download/?with_snippets=false" - script_data = fetch_data(download_url, headers) - else: - script_data = script - - if script_data: - code = script_data.get('code') - shell = script.get('shell') - extension = { - 'powershell': '.ps1', - 'python': '.py', - 'cmd': '.bat', - 'shell': '.sh', - 'nushell': '.nu' - }.get(shell, '.txt') - - if not is_snippet: - shell_summary[shell] += 1 - - script_filename = f"{script_name}{extension}" - script_file_path = category_folder / script_filename - save_file(script_file_path, code) - - raw_filename = f"{script_id} - {script_name}.json" - raw_file_path = category_raw_folder / raw_filename - save_file(raw_file_path, {**script_data, **script}, is_json=True) - - current_scripts.add(script_file_path.relative_to(script_folder)) - current_scripts.add(raw_file_path.relative_to(script_raw_folder)) - - print(f"Processed {len(current_scripts)} {'snippets' if is_snippet else 'scripts'}.") - return current_scripts - - -def compute_hash(file_path): - """Compute SHA-256 hash of a file.""" - hash_sha256 = hashlib.sha256() - try: - with open(file_path, 'rb') as f: - for chunk in iter(lambda: f.read(4096), b""): - hash_sha256.update(chunk) - except FileNotFoundError: - return None - return hash_sha256.hexdigest() - -def save_file(path, content, is_json=False): - """Save the file unconditionally.""" - new_content = json.dumps(content, indent=4, ensure_ascii=False) if is_json else content - - if ENABLE_WRITETOFILE: - with open(path, 'w', encoding="utf-8") as file: - file.write(new_content) - print(f"File saved: {path}") - else: - print(f"File would be saved (simulation): {path}") - - -def fetch_data(url, headers): - print(f"Fetching data from {url}...") - response = requests.get(url, headers=headers) - if response.status_code == 200: - print(f"Data fetched successfully from {url}.") - return response.json() - else: - print(f"Error fetching data from {url}: {response.status_code}") - return [] - - -def compare_script_and_json(folders): - """Compare script files with their corresponding JSON files and return mismatches.""" - print("Comparing script files with JSON files...") - - mismatches = [] - existing_files = defaultdict(dict) - - for raw_file_path in folders['scriptsraw'].rglob('*.json'): - raw_filename = raw_file_path.stem # Get the filename without extension - raw_name_cleaned = re.sub(r'^\d+ - ', '', raw_filename).lower() # Clean filename - - matched_script_path = None - for script_file_path in folders['scripts'].rglob('*'): - if script_file_path.is_file(): - script_filename = script_file_path.stem.lower() - if script_filename == raw_name_cleaned: - matched_script_path = script_file_path - break - - if matched_script_path: - print(f"Matched script file: {matched_script_path} with raw file: {raw_file_path}") - - script_hash = compute_hash(matched_script_path) - - with open(raw_file_path, 'r', encoding='utf-8') as json_file: - raw_data = json.load(json_file) - json_script_content = raw_data.get('code', '') - - # Compare the hashes of the actual script content - json_script_hash = hashlib.sha256(json_script_content.encode('utf-8')).hexdigest() - - print(f"Script file hash: {script_hash}") - print(f"JSON 'code' field hash: {json_script_hash}") - - if script_hash != json_script_hash: - print("\n--- Script File Content (first 10 lines) ---") - with open(matched_script_path, 'r', encoding='utf-8') as script_file: - for i, line in enumerate(script_file): - if i < 10: - print(line.strip()) - else: - break - - print("\n--- JSON 'Code' Field Content (first 10 lines) ---") - json_lines = json_script_content.splitlines() - for i, line in enumerate(json_lines): - if i < 10: - print(line.strip()) - else: - break - - mismatches.append({ - 'script_path': matched_script_path, - 'raw_path': raw_file_path, - 'script_hash': script_hash, - 'json_script_hash': json_script_hash - }) - - existing_files[matched_script_path.relative_to(folders['scripts'])] = { - 'script_path': matched_script_path, - 'raw_path': raw_file_path, - 'script_hash': script_hash, - 'json_script_hash': json_script_hash - } - else: - print(f"No matching script file found for JSON: {raw_file_path}") - - return mismatches - -def write_modifications_to_api(base_dir, folders, api_token): - """Main function to compare files and send data to the API.""" - mismatches = compare_script_and_json(folders) - send_mismatched_data_to_api(mismatches, api_token) - - -def update_api(script_id, payload, api_token): - # Convert 'code' to 'script_body' - if 'code' in payload: - payload['script_body'] = payload.pop('code') - - url = f"{domain}/scripts/{script_id}/" - headers = { - 'X-API-KEY': api_token, - 'Content-Type': 'application/json', - 'Accept': 'application/json, text/plain, */*' - } - - # Log the script body length and truncated content - script_body_length = len(payload.get('script_body', '')) - truncated_body = (payload['script_body'][:1000] + '...') if script_body_length > 1000 else payload['script_body'] - print(f"Updating script {script_id}, length: {script_body_length}, payload: {json.dumps({**payload, 'script_body': truncated_body}, indent=2)}") - - # Make the request with a longer timeout - try: - response = requests.put(url, headers=headers, json=payload, timeout=120) - except requests.exceptions.RequestException as e: - print(f"Request failed: {e}") - return - - # Print response for debugging - print(f"Response status code: {response.status_code}, Response content: {response.text}") - - # Check response status - if response.status_code == 200: - print(f"Script {script_id} updated successfully.") - elif response.status_code == 401: - print(f"Failed to update script {script_id}: 401 Unauthorized. Check API token.") - elif response.status_code == 404: - print(f"Failed to update script {script_id}: 404 Not Found. The resource may not exist.") - else: - print(f"Failed to update script {script_id}: {response.status_code} {response.text}") - - - - -def send_mismatched_data_to_api(mismatches, api_token): - """Send mismatched script data to the API.""" - for mismatch in mismatches: - script_path = mismatch.get('script_path') - raw_path = mismatch.get('raw_path') - - with open(raw_path, 'r', encoding='utf-8') as f: - raw_data = json.load(f) - - updated_payload = {**raw_data, 'code': open(script_path, 'r').read()} - - # Convert 'code' to 'script_body' before updating the API - try: - if ENABLE_WRITEBACK: - print(f"Updating API with payload for {script_path}:") - # Call the update function with api_token - update_api(raw_data.get('id'), updated_payload, api_token) - else: - print(f"Payload that would be pushed for {script_path}:") - # Preview the payload with 'script_body' instead of 'code' - updated_payload['script_body'] = updated_payload.pop('code') - print(json.dumps(updated_payload, indent=4)) - sys.stdout.flush() # Explicitly flush stdout - except BrokenPipeError: - sys.stderr.close() - sys.stdout.close() - - -def git_pull(base_dir): - """Force pull the latest changes from the git repository, discarding local changes.""" - if ENABLE_GIT_PULL: - print("Starting force pull...") - try: - subprocess.check_call(['git', '-C', base_dir, 'fetch', 'origin']) - subprocess.check_call(['git', '-C', base_dir, 'reset', '--hard', 'origin/master']) - print("Successfully force-pulled the latest changes from the repository.") - except subprocess.CalledProcessError as e: - print(f"Failed to force-pull changes from Git: {e}") - sys.exit(1) - else: - print("Git pull is disabled.") - - - -def get_staged_changes(base_dir): - """Get a list of staged files along with their status (created, modified, deleted)""" - result = subprocess.run( - ['git', '-C', base_dir, 'diff', '--cached', '--name-status'], - capture_output=True, text=True, check=True - ) - - changes = {"created": [], "modified": [], "deleted": []} - - for line in result.stdout.strip().split("\n"): - if not line: - continue - status, file = line.split("\t", 1) - - # Exclude files in the scriptsraw/ folder - if file.startswith("scriptsraw/"): - continue - - if status == "A": - changes["created"].append(file) - elif status == "M": - changes["modified"].append(file) - elif status == "D": - changes["deleted"].append(file) - - return changes - -def generate_commit_message(changes): - """Generate a meaningful commit message with filenames.""" - if not any(changes.values()): - return "Minor update" - - parts = [] - - if changes["created"]: - parts.append(f"Created {len(changes['created'])}: {', '.join(changes['created'])}") - if changes["modified"]: - parts.append(f"Modified {len(changes['modified'])}: {', '.join(changes['modified'])}") - if changes["deleted"]: - parts.append(f"Deleted {len(changes['deleted'])}: {', '.join(changes['deleted'])}") - - return "; ".join(parts) - -def git_push(base_dir): - """Push local changes to the git repository.""" - if ENABLE_GIT_PUSH: - print("Starting git push...") - try: - # Check if a rebase is in progress - 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("Rebase in progress. Please complete or abort the rebase manually.") - sys.exit(1) - - # Get the current branch - branch_result = subprocess.run(['git', '-C', base_dir, 'rev-parse', '--abbrev-ref', 'HEAD'], - capture_output=True, text=True) - branch_name = branch_result.stdout.strip() - - if branch_name == 'HEAD': - branch_name = "update-scripts" - subprocess.check_call(['git', '-C', base_dir, 'checkout', '-b', branch_name]) - print(f"Switched to new branch '{branch_name}'") - - 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', '.']) - - staged_changes = get_staged_changes(base_dir) - commit_message = generate_commit_message(staged_changes) - - subprocess.check_call(['git', '-C', base_dir, 'commit', '-m', commit_message]) - print(f"Committed changes to branch '{branch_name}': {commit_message}") - else: - print("No changes to commit.") - - subprocess.check_call(['git', '-C', base_dir, 'push', 'origin', branch_name]) - print(f"Changes pushed to branch '{branch_name}'") - except subprocess.CalledProcessError as e: - print(f"Git operation failed: {e}") - else: - print("Git push is disabled.") - -def download_scripts(): - global domain, headers - - domain = os.getenv('DOMAIN') - api_token = os.getenv('API_TOKEN') - scriptpath = os.getenv('SCRIPTPATH') - - if not domain or not api_token or not scriptpath: - print("Error: DOMAIN, API_TOKEN, and SCRIPTPATH must be set in the environment.") - sys.exit(1) - - headers = {"X-API-KEY": api_token} - base_dir = Path(scriptpath).resolve() - folders = { - "scripts": base_dir / "scripts", - "scriptsraw": base_dir / "scriptsraw", - "snippets": base_dir / "snippets", - "snippetsraw": base_dir / "snippetsraw" - } - for folder in folders.values(): - folder.mkdir(parents=True, exist_ok=True) - - shell_summary = defaultdict(int) - current_scripts = set() - - if ENABLE_GIT_PULL: - git_pull(base_dir) - - write_modifications_to_api(base_dir, folders, api_token) - - print("Fetching user-defined scripts...") - user_defined_scripts = fetch_data(f"{domain}/scripts/?showHiddenScripts=true", headers) - 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)) - - print("Fetching snippets...") - snippets = fetch_data(f"{domain}/scripts/snippets/", headers) - current_scripts.update(process_scripts(snippets, folders['snippets'], folders['snippetsraw'], shell_summary, is_snippet=True)) - - for folder in folders.values(): - delete_obsolete_files(folder, current_scripts) - - if ENABLE_GIT_PUSH: - git_push(base_dir) - - print(f"Total number of scripts exported: {len(current_scripts)}") - print("Shell summary:") - for shell, count in shell_summary.items(): - print(f"{shell}: {count}") - - - -if __name__ == "__main__": - download_scripts() \ No newline at end of file From 3920ccf5fab02471ae623d5346b9ca6d9da73d9e Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 2 Apr 2025 09:12:17 +0000 Subject: [PATCH 276/447] Update ./scripts/Backend/Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 520 ++++++++++++++++++ 1 file changed, 520 insertions(+) create mode 100644 scripts_staging/Backend/Sync TRMM with GIT.py 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..58dffa2d --- /dev/null +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -0,0 +1,520 @@ +#!/usr/bin/python3 +#public + +""" +.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. The mapped folder should already be configured with git + + 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 scripts out to 4 folders: + scripts: extracted script code from the API converted from json + scriptsraw: All json data from the API for later processing, currently used for hash comparison + snippets: extracted snippet code from the API converted from json + snippetsraw: All json data for later import/migration + + 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 +DOMAIN=https://api-rmm +DOMAIN=https://{{global.RMM_API_URL}} +API_TOKEN={{global.rmm_key_for_git_script}} +API_TOKEN=asdf1234 +SCRIPTPATH=/var/RMM-script-repo + +.CHANGELOG + v5.0 Y Exports functional, adds script ID to from as "id - " + v5.a Y "id - " for only raw folder. Fixed to use X-API-KEY + v5.1 Y Sanitizing script names when has / in it + v5.2 Y moving url and api token to .env file + v5.3 Y Making script folders be subfolders of where export.py file is + v5.4 Y making filenames utf-8 compliant + v5.5 7/11/2024 X Save PowerShell scripts with .ps1 and Python scripts with .py extensions + v5.6 7/11/2024 X Count the total number of scripts and print at the end + v5.7 7/11/2024 X Print a summary of all the different types of shells exported + v5.8 7/11/2024 X Add support for additional shell extension types + v5.9 7/11/2024 X Detect deleted scripts and delete them from both folders + v6 7/31/2024 SAN Add support for specifying the save folder via the SCRIPTPATH environment variable + v6.0.1 7/31/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 + + +.TODO + Add reporting support + add writeback support for snippets + simplify the functions that does the writeback + Move raws from "scriptsraw" to scripts/subfolder/raws/ to group them with their scripts + add logging + add counters and separators at the end of each function + send workflow flags to ENV default to true + +""" + +import subprocess +import sys +import os +import hashlib +import json +from collections import defaultdict +from pathlib import Path +import requests +from pathvalidate import sanitize_filename +import re + +# Toggle flags +ENABLE_GIT_PULL = True +ENABLE_GIT_PUSH = True +ENABLE_WRITEBACK = True +ENABLE_WRITETOFILE = True + +def delete_obsolete_files(folder, current_scripts): + print(f"Deleting obsolete files and directories in {folder}...") + all_files = set() + relevant_dirs = set() + + for item in folder.rglob('*'): + if item.is_file(): + all_files.add(item.relative_to(folder)) + elif item.is_dir() and any(item.glob('*')): + relevant_dirs.add(item.relative_to(folder)) + + obsolete_files = all_files - current_scripts + for item in folder.rglob('*'): + if item.is_file() and item.relative_to(folder) in obsolete_files: + try: + print(f"Deleting obsolete file: {item}") + item.unlink() + except Exception as e: + print(f"Error deleting file {item}: {e}") + + for dirpath in sorted(folder.rglob('*'), key=lambda p: len(p.parts), reverse=True): + if dirpath.is_dir() and not any(dirpath.glob('*')) and dirpath.relative_to(folder) not in relevant_dirs: + try: + dirpath.rmdir() + print(f"Deleting empty obsolete directory: {dirpath}") + except OSError as e: + print(f"Could not delete directory {dirpath}: {e}") + +def process_scripts(scripts, script_folder, script_raw_folder, shell_summary, is_snippet=False): + print(f"Processing {'snippets' if is_snippet else 'user-defined scripts'}...") + current_scripts = set() + + for script in scripts: + script_id = script.get('id') + script_name = sanitize_filename(script.get('name', 'Unnamed Script')) + category = script.get('category', '').strip() if script.get('category') else '' + category = sanitize_filename(category) + category_folder = script_folder / category if category else script_folder + category_raw_folder = script_raw_folder / category if category else script_raw_folder + + category_folder.mkdir(parents=True, exist_ok=True) + category_raw_folder.mkdir(parents=True, exist_ok=True) + + if not is_snippet: + download_url = f"{domain}/scripts/{script_id}/download/?with_snippets=false" + script_data = fetch_data(download_url, headers) + else: + script_data = script + + if script_data: + code = script_data.get('code') + shell = script.get('shell') + extension = { + 'powershell': '.ps1', + 'python': '.py', + 'cmd': '.bat', + 'shell': '.sh', + 'nushell': '.nu' + }.get(shell, '.txt') + + if not is_snippet: + shell_summary[shell] += 1 + + script_filename = f"{script_name}{extension}" + script_file_path = category_folder / script_filename + save_file(script_file_path, code) + + raw_filename = f"{script_id} - {script_name}.json" + raw_file_path = category_raw_folder / raw_filename + save_file(raw_file_path, {**script_data, **script}, is_json=True) + + current_scripts.add(script_file_path.relative_to(script_folder)) + current_scripts.add(raw_file_path.relative_to(script_raw_folder)) + + print(f"Processed {len(current_scripts)} {'snippets' if is_snippet else 'scripts'}.") + return current_scripts + + +def compute_hash(file_path): + """Compute SHA-256 hash of a file.""" + hash_sha256 = hashlib.sha256() + try: + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_sha256.update(chunk) + except FileNotFoundError: + return None + return hash_sha256.hexdigest() + +def save_file(path, content, is_json=False): + """Save the file unconditionally.""" + new_content = json.dumps(content, indent=4, ensure_ascii=False) if is_json else content + + if ENABLE_WRITETOFILE: + with open(path, 'w', encoding="utf-8") as file: + file.write(new_content) + print(f"File saved: {path}") + else: + print(f"File would be saved (simulation): {path}") + + +def fetch_data(url, headers): + print(f"Fetching data from {url}...") + response = requests.get(url, headers=headers) + if response.status_code == 200: + print(f"Data fetched successfully from {url}.") + return response.json() + else: + print(f"Error fetching data from {url}: {response.status_code}") + return [] + + +def compare_script_and_json(folders): + """Compare script files with their corresponding JSON files and return mismatches.""" + print("Comparing script files with JSON files...") + + mismatches = [] + existing_files = defaultdict(dict) + + for raw_file_path in folders['scriptsraw'].rglob('*.json'): + raw_filename = raw_file_path.stem # Get the filename without extension + raw_name_cleaned = re.sub(r'^\d+ - ', '', raw_filename).lower() # Clean filename + + matched_script_path = None + for script_file_path in folders['scripts'].rglob('*'): + if script_file_path.is_file(): + script_filename = script_file_path.stem.lower() + if script_filename == raw_name_cleaned: + matched_script_path = script_file_path + break + + if matched_script_path: + print(f"Matched script file: {matched_script_path} with raw file: {raw_file_path}") + + script_hash = compute_hash(matched_script_path) + + with open(raw_file_path, 'r', encoding='utf-8') as json_file: + raw_data = json.load(json_file) + json_script_content = raw_data.get('code', '') + + # Compare the hashes of the actual script content + json_script_hash = hashlib.sha256(json_script_content.encode('utf-8')).hexdigest() + + print(f"Script file hash: {script_hash}") + print(f"JSON 'code' field hash: {json_script_hash}") + + if script_hash != json_script_hash: + print("\n--- Script File Content (first 10 lines) ---") + with open(matched_script_path, 'r', encoding='utf-8') as script_file: + for i, line in enumerate(script_file): + if i < 10: + print(line.strip()) + else: + break + + print("\n--- JSON 'Code' Field Content (first 10 lines) ---") + json_lines = json_script_content.splitlines() + for i, line in enumerate(json_lines): + if i < 10: + print(line.strip()) + else: + break + + mismatches.append({ + 'script_path': matched_script_path, + 'raw_path': raw_file_path, + 'script_hash': script_hash, + 'json_script_hash': json_script_hash + }) + + existing_files[matched_script_path.relative_to(folders['scripts'])] = { + 'script_path': matched_script_path, + 'raw_path': raw_file_path, + 'script_hash': script_hash, + 'json_script_hash': json_script_hash + } + else: + print(f"No matching script file found for JSON: {raw_file_path}") + + return mismatches + +def write_modifications_to_api(base_dir, folders, api_token): + """Main function to compare files and send data to the API.""" + mismatches = compare_script_and_json(folders) + send_mismatched_data_to_api(mismatches, api_token) + + +def update_api(script_id, payload, api_token): + # Convert 'code' to 'script_body' + if 'code' in payload: + payload['script_body'] = payload.pop('code') + + url = f"{domain}/scripts/{script_id}/" + headers = { + 'X-API-KEY': api_token, + 'Content-Type': 'application/json', + 'Accept': 'application/json, text/plain, */*' + } + + # Log the script body length and truncated content + script_body_length = len(payload.get('script_body', '')) + truncated_body = (payload['script_body'][:1000] + '...') if script_body_length > 1000 else payload['script_body'] + print(f"Updating script {script_id}, length: {script_body_length}, payload: {json.dumps({**payload, 'script_body': truncated_body}, indent=2)}") + + # Make the request with a longer timeout + try: + response = requests.put(url, headers=headers, json=payload, timeout=120) + except requests.exceptions.RequestException as e: + print(f"Request failed: {e}") + return + + # Print response for debugging + print(f"Response status code: {response.status_code}, Response content: {response.text}") + + # Check response status + if response.status_code == 200: + print(f"Script {script_id} updated successfully.") + elif response.status_code == 401: + print(f"Failed to update script {script_id}: 401 Unauthorized. Check API token.") + elif response.status_code == 404: + print(f"Failed to update script {script_id}: 404 Not Found. The resource may not exist.") + else: + print(f"Failed to update script {script_id}: {response.status_code} {response.text}") + + + + +def send_mismatched_data_to_api(mismatches, api_token): + """Send mismatched script data to the API.""" + for mismatch in mismatches: + script_path = mismatch.get('script_path') + raw_path = mismatch.get('raw_path') + + with open(raw_path, 'r', encoding='utf-8') as f: + raw_data = json.load(f) + + updated_payload = {**raw_data, 'code': open(script_path, 'r').read()} + + # Convert 'code' to 'script_body' before updating the API + try: + if ENABLE_WRITEBACK: + print(f"Updating API with payload for {script_path}:") + # Call the update function with api_token + update_api(raw_data.get('id'), updated_payload, api_token) + else: + print(f"Payload that would be pushed for {script_path}:") + # Preview the payload with 'script_body' instead of 'code' + updated_payload['script_body'] = updated_payload.pop('code') + print(json.dumps(updated_payload, indent=4)) + sys.stdout.flush() # Explicitly flush stdout + except BrokenPipeError: + sys.stderr.close() + sys.stdout.close() + + +def git_pull(base_dir): + """Force pull the latest changes from the git repository, discarding local changes.""" + if ENABLE_GIT_PULL: + print("Starting force pull...") + try: + subprocess.check_call(['git', '-C', base_dir, 'fetch', 'origin']) + subprocess.check_call(['git', '-C', base_dir, 'reset', '--hard', 'origin/master']) + print("Successfully force-pulled the latest changes from the repository.") + except subprocess.CalledProcessError as e: + print(f"Failed to force-pull changes from Git: {e}") + sys.exit(1) + else: + print("Git pull is disabled.") + + + +def get_staged_changes(base_dir): + """Get a list of staged files along with their status (created, modified, deleted, renamed).""" + result = subprocess.run( + ['git', '-C', base_dir, 'diff', '--cached', '--name-status'], + capture_output=True, text=True, check=True + ) + + changes = {"created": [], "modified": [], "deleted": [], "renamed": []} + + for line in result.stdout.strip().split("\n"): + if not line: + continue + + parts = line.split("\t") + status, file = parts[0], parts[-1] + + # Exclude files in the scriptsraw/ folder + if file.startswith("scriptsraw/"): + 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) + elif status.startswith("R"): + changes["renamed"].append(f"{parts[1]} -> {parts[2]}") + + return changes + +def generate_commit_message(changes, max_files=5): + """Generate a meaningful commit message with filenames, truncating if needed.""" + if not any(changes.values()): + return "Minor update" + + parts = [] + + def format_files(change_type, files): + return f"{change_type} {len(files)}: {', '.join(files[:max_files])}" + ("..." if len(files) > max_files else "") + + if changes["created"]: + parts.append(format_files("Created", changes["created"])) + if changes["modified"]: + parts.append(format_files("Modified", changes["modified"])) + if changes["deleted"]: + parts.append(format_files("Deleted", changes["deleted"])) + if changes["renamed"]: + parts.append(format_files("Renamed", changes["renamed"])) + + return "; ".join(parts) + + +def git_push(base_dir): + """Push local changes to the git repository.""" + if ENABLE_GIT_PUSH: + print("Starting git push...") + try: + # Check if a rebase is in progress + 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("Rebase in progress. Please complete or abort the rebase manually.") + sys.exit(1) + + # Get the current branch + branch_result = subprocess.run(['git', '-C', base_dir, 'rev-parse', '--abbrev-ref', 'HEAD'], + capture_output=True, text=True) + branch_name = branch_result.stdout.strip() + + if branch_name == 'HEAD': + branch_name = "update-scripts" + subprocess.check_call(['git', '-C', base_dir, 'checkout', '-b', branch_name]) + print(f"Switched to new branch '{branch_name}'") + + 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', '.']) + + staged_changes = get_staged_changes(base_dir) + commit_message = generate_commit_message(staged_changes) + + subprocess.check_call(['git', '-C', base_dir, 'commit', '-m', commit_message]) + print(f"Committed changes to branch '{branch_name}': {commit_message}") + else: + print("No changes to commit.") + + subprocess.check_call(['git', '-C', base_dir, 'push', 'origin', branch_name]) + print(f"Changes pushed to branch '{branch_name}'") + except subprocess.CalledProcessError as e: + print(f"Git operation failed: {e}") + else: + print("Git push is disabled.") + +def download_scripts(): + global domain, headers + + domain = os.getenv('DOMAIN') + api_token = os.getenv('API_TOKEN') + scriptpath = os.getenv('SCRIPTPATH') + + if not domain or not api_token or not scriptpath: + print("Error: DOMAIN, API_TOKEN, and SCRIPTPATH must be set in the environment.") + sys.exit(1) + + headers = {"X-API-KEY": api_token} + base_dir = Path(scriptpath).resolve() + folders = { + "scripts": base_dir / "scripts", + "scriptsraw": base_dir / "scriptsraw", + "snippets": base_dir / "snippets", + "snippetsraw": base_dir / "snippetsraw" + } + for folder in folders.values(): + folder.mkdir(parents=True, exist_ok=True) + + shell_summary = defaultdict(int) + current_scripts = set() + + if ENABLE_GIT_PULL: + git_pull(base_dir) + + write_modifications_to_api(base_dir, folders, api_token) + + print("Fetching user-defined scripts...") + user_defined_scripts = fetch_data(f"{domain}/scripts/?showHiddenScripts=true", headers) + 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)) + + print("Fetching snippets...") + snippets = fetch_data(f"{domain}/scripts/snippets/", headers) + current_scripts.update(process_scripts(snippets, folders['snippets'], folders['snippetsraw'], shell_summary, is_snippet=True)) + + for folder in folders.values(): + delete_obsolete_files(folder, current_scripts) + + if ENABLE_GIT_PUSH: + git_push(base_dir) + + print(f"Total number of scripts exported: {len(current_scripts)}") + print("Shell summary:") + for shell, count in shell_summary.items(): + print(f"{shell}: {count}") + + + +if __name__ == "__main__": + download_scripts() \ No newline at end of file From 8bbaf1e6a33907db3c519d06a9f5de9c8b75dc5c Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 2 Apr 2025 09:17:57 +0000 Subject: [PATCH 277/447] Update ./scripts/Backend/Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index 58dffa2d..459d0b1a 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -1,6 +1,4 @@ #!/usr/bin/python3 -#public - """ .TITLE Tactical RMM Script Sync with GIT Integration @@ -34,12 +32,16 @@ If there are no changes, no commit will be made. .EXEMPLE -DOMAIN=https://api-rmm -DOMAIN=https://{{global.RMM_API_URL}} -API_TOKEN={{global.rmm_key_for_git_script}} -API_TOKEN=asdf1234 -SCRIPTPATH=/var/RMM-script-repo - + DOMAIN=https://api-rmm + DOMAIN=https://{{global.RMM_API_URL}} + API_TOKEN={{global.rmm_key_for_git_script}} + API_TOKEN=asdf1234 + SCRIPTPATH=/var/RMM-script-repo + +.NOTES + #public + Original source not disclosed + .CHANGELOG v5.0 Y Exports functional, adds script ID to from as "id - " v5.a Y "id - " for only raw folder. Fixed to use X-API-KEY From 18fecaee246c31f4f23bb2efef6f4ec33a8a28be Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 2 Apr 2025 10:15:07 +0000 Subject: [PATCH 278/447] Update ./scripts/Checks/is RDP port ok.ps1 --- scripts_staging/Checks/is RDP port ok.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts_staging/Checks/is RDP port ok.ps1 b/scripts_staging/Checks/is RDP port ok.ps1 index aac6ebbd..85ac3819 100644 --- a/scripts_staging/Checks/is RDP port ok.ps1 +++ b/scripts_staging/Checks/is RDP port ok.ps1 @@ -15,6 +15,7 @@ .CHANGELOG 12.12.24 SAN Changed outputs 20.12.24 SAN Changed outputs + 02.04.25 SAN Fixed Warn output #> $port = 3389 @@ -22,7 +23,7 @@ $address = "localhost" # Try Test-NetConnection if available if (Get-Command Test-NetConnection -ErrorAction SilentlyContinue) { - $tcpConnection = Test-NetConnection -ComputerName $address -Port $port + $tcpConnection = Test-NetConnection -ComputerName 127.0.0.1 -Port $port 2>$null -WarningAction SilentlyContinue if ($tcpConnection.TcpTestSucceeded) { Write-Output "OK: RDP is open." } else { From 39a08477bccbb6826e65331ceeb6dedf6be832f3 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 3 Apr 2025 07:43:46 +0000 Subject: [PATCH 279/447] Update ./scripts/Build/Upgrade OS to Windows Server X Standard.ps1 --- .../Build/Upgrade OS to Windows Server X Standard.ps1 | 1 - 1 file changed, 1 deletion(-) 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 index 8bc47fe3..1a41fd49 100644 --- a/scripts_staging/Build/Upgrade OS to Windows Server X Standard.ps1 +++ b/scripts_staging/Build/Upgrade OS to Windows Server X Standard.ps1 @@ -23,7 +23,6 @@ All keys are valid for initial installation and from https://learn.microsoft.com 27.03.25 SAN Full code refactorisation for more locale support & checksum verification & transfer repo to a single NC share .TODO - more testing 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 From 0e31d099deec19a153788fee661d2bdd985ca4b8 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 3 Apr 2025 08:02:25 +0000 Subject: [PATCH 280/447] Update ./scripts/Build/Upgrade OS to Windows Server X Standard.ps1 --- ...pgrade OS to Windows Server X Standard.ps1 | 40 ++++++------------- 1 file changed, 12 insertions(+), 28 deletions(-) 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 index 1a41fd49..47ca0d6c 100644 --- a/scripts_staging/Build/Upgrade OS to Windows Server X Standard.ps1 +++ b/scripts_staging/Build/Upgrade OS to Windows Server X Standard.ps1 @@ -21,11 +21,13 @@ All keys are valid for initial installation and from https://learn.microsoft.com .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 .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 + #> @@ -111,36 +113,18 @@ function Perform-InPlaceUpgrade { # Function to check requirements function Check-Requirements { param ([string]$targetedVersion, [string]$baseUrl) - - if (-not $targetedVersion) { Write-Host "TARGETED_VERSION is not set. Exiting."; exit 1 } - if (-not $baseUrl) { Write-Host "Download_Source is not set. Exiting."; exit 1 } - - # Detect system language (first two letters) + + if (-not $targetedVersion -or -not $baseUrl) { Write-Host "Missing parameters. Exiting."; exit 1 } + if (-not $serverVersions.ContainsKey($targetedVersion)) { Write-Host "Invalid version: $targetedVersion. Exiting."; exit 1 } + $systemLocale = (Get-WinSystemLocale).Name.Substring(0,2).ToLower() - - # Validate language availability - if (-not $serverVersions[$targetedVersion].ContainsKey($systemLocale)) { - Write-Host "Unsupported language: $systemLocale. Exiting." - exit 1 - } - - # Check for 7-Zip + if (-not $serverVersions[$targetedVersion].ContainsKey($systemLocale)) { Write-Host "Unsupported language: $systemLocale. Exiting."; exit 1 } + $sevenZipPath = (Get-Command 7z.exe -ErrorAction SilentlyContinue).Source - if (-not $sevenZipPath) { - $sevenZipPath = "C:\\Program Files\\7-Zip\\7z.exe" - if (-not (Test-Path $sevenZipPath)) { - Write-Host "7-Zip not found in PATH or default location. Exiting." - exit 1 - } - } - - # Check available disk space - $freeSpace = (Get-PSDrive C).Free - if ($freeSpace -lt 12GB) { - Write-Host "Not enough disk space. Exiting." - exit 1 - } - + if (-not $sevenZipPath -and -not (Test-Path ($sevenZipPath = "C:\Program Files\7-Zip\7z.exe"))) { Write-Host "7-Zip not found. Exiting."; exit 1 } + + if ((Get-PSDrive C).Free -lt 12GB) { Write-Host "Not enough disk space. Exiting."; exit 1 } + return $systemLocale, $sevenZipPath } From f5e4564597ffbc9e9d658a828dbb5a3049ccf04d Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 7 Apr 2025 09:18:01 +0000 Subject: [PATCH 281/447] Update ./scripts/Backend/Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 490 +++++++----------- 1 file changed, 175 insertions(+), 315 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index 459d0b1a..1d468bf2 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -67,6 +67,8 @@ 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 + .TODO @@ -98,259 +100,142 @@ ENABLE_WRITETOFILE = True def delete_obsolete_files(folder, current_scripts): - print(f"Deleting obsolete files and directories in {folder}...") - all_files = set() - relevant_dirs = set() - - for item in folder.rglob('*'): - if item.is_file(): - all_files.add(item.relative_to(folder)) - elif item.is_dir() and any(item.glob('*')): - relevant_dirs.add(item.relative_to(folder)) - - obsolete_files = all_files - current_scripts - for item in folder.rglob('*'): - if item.is_file() and item.relative_to(folder) in obsolete_files: - try: - print(f"Deleting obsolete file: {item}") - item.unlink() - except Exception as e: - print(f"Error deleting file {item}: {e}") + print(f"Cleaning {folder}...") + obsolete = {f for f in folder.rglob('*') if f.is_file() and f.relative_to(folder) not in current_scripts} + for f in obsolete: + try: f.unlink(); print(f"Deleted: {f}") + except Exception as e: print(f"Error deleting {f}: {e}") + + for d in sorted(folder.rglob('*'), key=lambda p: -len(p.parts)): + if d.is_dir() and not any(d.iterdir()): + try: d.rmdir(); print(f"Removed empty dir: {d}") + except Exception as e: print(f"Could not delete dir {d}: {e}") - for dirpath in sorted(folder.rglob('*'), key=lambda p: len(p.parts), reverse=True): - if dirpath.is_dir() and not any(dirpath.glob('*')) and dirpath.relative_to(folder) not in relevant_dirs: - try: - dirpath.rmdir() - print(f"Deleting empty obsolete directory: {dirpath}") - except OSError as e: - print(f"Could not delete directory {dirpath}: {e}") def process_scripts(scripts, script_folder, script_raw_folder, shell_summary, is_snippet=False): print(f"Processing {'snippets' if is_snippet else 'user-defined scripts'}...") - current_scripts = set() - - for script in scripts: - script_id = script.get('id') - script_name = sanitize_filename(script.get('name', 'Unnamed Script')) - category = script.get('category', '').strip() if script.get('category') else '' - category = sanitize_filename(category) - category_folder = script_folder / category if category else script_folder - category_raw_folder = script_raw_folder / category if category else script_raw_folder - - category_folder.mkdir(parents=True, exist_ok=True) - category_raw_folder.mkdir(parents=True, exist_ok=True) - - if not is_snippet: - download_url = f"{domain}/scripts/{script_id}/download/?with_snippets=false" - script_data = fetch_data(download_url, headers) - else: - script_data = script - - if script_data: - code = script_data.get('code') - shell = script.get('shell') - extension = { - 'powershell': '.ps1', - 'python': '.py', - 'cmd': '.bat', - 'shell': '.sh', - 'nushell': '.nu' - }.get(shell, '.txt') - - if not is_snippet: - shell_summary[shell] += 1 - - script_filename = f"{script_name}{extension}" - script_file_path = category_folder / script_filename - save_file(script_file_path, code) - - raw_filename = f"{script_id} - {script_name}.json" - raw_file_path = category_raw_folder / raw_filename - save_file(raw_file_path, {**script_data, **script}, is_json=True) - - current_scripts.add(script_file_path.relative_to(script_folder)) - current_scripts.add(raw_file_path.relative_to(script_raw_folder)) - - print(f"Processed {len(current_scripts)} {'snippets' if is_snippet else 'scripts'}.") - return current_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 fetch_data(f"{domain}/scripts/{sid}/download/?with_snippets=false", headers) + 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'}.") + return current def compute_hash(file_path): - """Compute SHA-256 hash of a file.""" - hash_sha256 = hashlib.sha256() try: with open(file_path, 'rb') as f: - for chunk in iter(lambda: f.read(4096), b""): - hash_sha256.update(chunk) + return hashlib.sha256(f.read()).hexdigest() except FileNotFoundError: return None - return hash_sha256.hexdigest() def save_file(path, content, is_json=False): - """Save the file unconditionally.""" - new_content = json.dumps(content, indent=4, ensure_ascii=False) if is_json else content - + data = json.dumps(content, indent=4, ensure_ascii=False) if is_json else content if ENABLE_WRITETOFILE: - with open(path, 'w', encoding="utf-8") as file: - file.write(new_content) + path.write_text(data, encoding="utf-8") print(f"File saved: {path}") else: print(f"File would be saved (simulation): {path}") - def fetch_data(url, headers): - print(f"Fetching data from {url}...") - response = requests.get(url, headers=headers) - if response.status_code == 200: - print(f"Data fetched successfully from {url}.") - return response.json() - else: - print(f"Error fetching data from {url}: {response.status_code}") - return [] + print(f"Fetching: {url}") + r = requests.get(url, headers=headers) + if r.ok: + print("Success.") + return r.json() + print(f"Error {r.status_code}") + return [] - -def compare_script_and_json(folders): - """Compare script files with their corresponding JSON files and return mismatches.""" +def write_modifications_to_api(base_dir, folders, api_token): + """Compare local script files and JSON definitions, then push mismatches to the API.""" print("Comparing script files with JSON files...") - mismatches = [] - existing_files = defaultdict(dict) - - for raw_file_path in folders['scriptsraw'].rglob('*.json'): - raw_filename = raw_file_path.stem # Get the filename without extension - raw_name_cleaned = re.sub(r'^\d+ - ', '', raw_filename).lower() # Clean filename - - matched_script_path = None - for script_file_path in folders['scripts'].rglob('*'): - if script_file_path.is_file(): - script_filename = script_file_path.stem.lower() - if script_filename == raw_name_cleaned: - matched_script_path = script_file_path - break - - if matched_script_path: - print(f"Matched script file: {matched_script_path} with raw file: {raw_file_path}") - - script_hash = compute_hash(matched_script_path) - - with open(raw_file_path, 'r', encoding='utf-8') as json_file: - raw_data = json.load(json_file) - json_script_content = raw_data.get('code', '') - - # Compare the hashes of the actual script content - json_script_hash = hashlib.sha256(json_script_content.encode('utf-8')).hexdigest() - - print(f"Script file hash: {script_hash}") - print(f"JSON 'code' field hash: {json_script_hash}") - - if script_hash != json_script_hash: - print("\n--- Script File Content (first 10 lines) ---") - with open(matched_script_path, 'r', encoding='utf-8') as script_file: - for i, line in enumerate(script_file): - if i < 10: - print(line.strip()) - else: - break - - print("\n--- JSON 'Code' Field Content (first 10 lines) ---") - json_lines = json_script_content.splitlines() - for i, line in enumerate(json_lines): - if i < 10: - print(line.strip()) - else: - break - - mismatches.append({ - 'script_path': matched_script_path, - 'raw_path': raw_file_path, - 'script_hash': script_hash, - 'json_script_hash': json_script_hash - }) - - existing_files[matched_script_path.relative_to(folders['scripts'])] = { - 'script_path': matched_script_path, - 'raw_path': raw_file_path, - 'script_hash': script_hash, - 'json_script_hash': json_script_hash - } - else: - print(f"No matching script file found for JSON: {raw_file_path}") - - return mismatches -def write_modifications_to_api(base_dir, folders, api_token): - """Main function to compare files and send data to the API.""" - mismatches = compare_script_and_json(folders) - send_mismatched_data_to_api(mismatches, api_token) + for raw_path in folders['scriptsraw'].rglob('*.json'): + raw_name = re.sub(r'^\d+ - ', '', raw_path.stem).lower() + match = next((p for p in folders['scripts'].rglob('*') + if p.is_file() and p.stem.lower() == raw_name), None) + if not match: + print(f"No match for: {raw_path}") + continue -def update_api(script_id, payload, api_token): - # Convert 'code' to 'script_body' - if 'code' in payload: - payload['script_body'] = payload.pop('code') + print(f"Matched: {match} <-> {raw_path}") + script_hash = compute_hash(match) - url = f"{domain}/scripts/{script_id}/" - headers = { - 'X-API-KEY': api_token, - 'Content-Type': 'application/json', - 'Accept': 'application/json, text/plain, */*' - } - - # Log the script body length and truncated content - script_body_length = len(payload.get('script_body', '')) - truncated_body = (payload['script_body'][:1000] + '...') if script_body_length > 1000 else payload['script_body'] - print(f"Updating script {script_id}, length: {script_body_length}, payload: {json.dumps({**payload, 'script_body': truncated_body}, indent=2)}") - - # Make the request with a longer timeout - try: - response = requests.put(url, headers=headers, json=payload, timeout=120) - except requests.exceptions.RequestException as e: - print(f"Request failed: {e}") - return - - # Print response for debugging - print(f"Response status code: {response.status_code}, Response content: {response.text}") - - # Check response status - if response.status_code == 200: - print(f"Script {script_id} updated successfully.") - elif response.status_code == 401: - print(f"Failed to update script {script_id}: 401 Unauthorized. Check API token.") - elif response.status_code == 404: - print(f"Failed to update script {script_id}: 404 Not Found. The resource may not exist.") - else: - print(f"Failed to update script {script_id}: {response.status_code} {response.text}") + with raw_path.open(encoding='utf-8') as f: + raw_data = json.load(f) + code = raw_data.get('code', '') + code_hash = hashlib.sha256(code.encode('utf-8')).hexdigest() + print(f"Script hash: {script_hash}\nJSON hash: {code_hash}") + if script_hash != code_hash: + print("\n--- Script (first 10 lines) ---") + with match.open(encoding='utf-8') as f: + for i, line in enumerate(f): + if i >= 10: break + print(line.strip()) + print("\n--- JSON Code (first 10 lines) ---") + for line in code.splitlines()[:10]: + print(line.strip()) -def send_mismatched_data_to_api(mismatches, api_token): - """Send mismatched script data to the API.""" - for mismatch in mismatches: - script_path = mismatch.get('script_path') - raw_path = mismatch.get('raw_path') + with match.open(encoding='utf-8') as f: + updated_payload = {**raw_data, 'code': f.read()} - with open(raw_path, 'r', encoding='utf-8') as f: - raw_data = json.load(f) + try: + if ENABLE_WRITEBACK: + print(f"Updating API for {match}...") + update_api(raw_data.get('id'), updated_payload, api_token) + else: + print(f"Simulated push for {match}:") + updated_payload['script_body'] = updated_payload.pop('code') + print(json.dumps(updated_payload, indent=4)) + sys.stdout.flush() + except BrokenPipeError: + sys.stderr.close() + sys.stdout.close() + + +def update_api(script_id, payload): + """Update the API with the provided script ID and payload.""" + payload['script_body'] = payload.pop('code', '') - updated_payload = {**raw_data, 'code': open(script_path, 'r').read()} + url = f"{domain}/scripts/{script_id}/" + body = payload['script_body'] - # Convert 'code' to 'script_body' before updating the API - try: - if ENABLE_WRITEBACK: - print(f"Updating API with payload for {script_path}:") - # Call the update function with api_token - update_api(raw_data.get('id'), updated_payload, api_token) - else: - print(f"Payload that would be pushed for {script_path}:") - # Preview the payload with 'script_body' instead of 'code' - updated_payload['script_body'] = updated_payload.pop('code') - print(json.dumps(updated_payload, indent=4)) - sys.stdout.flush() # Explicitly flush stdout - except BrokenPipeError: - sys.stderr.close() - sys.stdout.close() + print(f"Updating {script_id}, length: {len(body)}, preview: {body[:1000]}{'...' if len(body) > 1000 else ''}") + try: + res = requests.put(url, headers=headers, json=payload, timeout=120) + print(f"{script_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 {script_id}: {e}") def git_pull(base_dir): """Force pull the latest changes from the git repository, discarding local changes.""" @@ -366,157 +251,132 @@ def git_pull(base_dir): else: print("Git pull is disabled.") - - -def get_staged_changes(base_dir): - """Get a list of staged files along with their status (created, modified, deleted, renamed).""" - result = subprocess.run( - ['git', '-C', base_dir, 'diff', '--cached', '--name-status'], - capture_output=True, text=True, check=True - ) - - changes = {"created": [], "modified": [], "deleted": [], "renamed": []} - - for line in result.stdout.strip().split("\n"): - if not line: - continue - - parts = line.split("\t") - status, file = parts[0], parts[-1] - - # Exclude files in the scriptsraw/ folder - if file.startswith("scriptsraw/"): - 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) - elif status.startswith("R"): - changes["renamed"].append(f"{parts[1]} -> {parts[2]}") - - return changes - -def generate_commit_message(changes, max_files=5): - """Generate a meaningful commit message with filenames, truncating if needed.""" - if not any(changes.values()): - return "Minor update" - - parts = [] - - def format_files(change_type, files): - return f"{change_type} {len(files)}: {', '.join(files[:max_files])}" + ("..." if len(files) > max_files else "") - - if changes["created"]: - parts.append(format_files("Created", changes["created"])) - if changes["modified"]: - parts.append(format_files("Modified", changes["modified"])) - if changes["deleted"]: - parts.append(format_files("Deleted", changes["deleted"])) - if changes["renamed"]: - parts.append(format_files("Renamed", changes["renamed"])) - - return "; ".join(parts) - - def git_push(base_dir): """Push local changes to the git repository.""" if ENABLE_GIT_PUSH: - print("Starting git push...") try: # Check if a rebase is in progress 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("Rebase in progress. Please complete or abort the rebase manually.") - sys.exit(1) - - # Get the current branch - branch_result = subprocess.run(['git', '-C', base_dir, 'rev-parse', '--abbrev-ref', 'HEAD'], - capture_output=True, text=True) - branch_name = branch_result.stdout.strip() + sys.exit("Rebase in progress. Complete or abort it.") + # Get current branch + branch_name = subprocess.run( + ['git', '-C', base_dir, 'rev-parse', '--abbrev-ref', 'HEAD'], + capture_output=True, text=True + ).stdout.strip() or "update-scripts" if branch_name == 'HEAD': - branch_name = "update-scripts" subprocess.check_call(['git', '-C', base_dir, 'checkout', '-b', branch_name]) - print(f"Switched to new branch '{branch_name}'") - status_result = subprocess.run(['git', '-C', base_dir, 'status', '--porcelain'], - capture_output=True, text=True) + # Get staged changes + 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', '.']) - staged_changes = get_staged_changes(base_dir) - commit_message = generate_commit_message(staged_changes) - + # Get the list of staged changes + result = subprocess.run( + ['git', '-C', base_dir, 'diff', '--cached', '--name-status'], + capture_output=True, text=True, check=True + ) + changes = {"created": [], "modified": [], "deleted": [], "renamed": []} + for line in result.stdout.strip().split("\n"): + if not line: continue + status, file = line.split("\t") + if file.startswith("scriptsraw/"): 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) + elif status.startswith("R"): changes["renamed"].append(f"{line.split()[1]} -> {line.split()[2]}") + + # Generate commit message + def generate_commit_message(changes, max_files=5): + if not any(changes.values()): return "Minor update" + parts = [f"{change_type} {len(files)}: {', '.join(files[:max_files])}{'...' if len(files) > max_files else ''}" + for change_type, files in changes.items() if files] + return "; ".join(parts) + + commit_message = generate_commit_message(changes) + + # Commit changes subprocess.check_call(['git', '-C', base_dir, 'commit', '-m', commit_message]) print(f"Committed changes to branch '{branch_name}': {commit_message}") + + # Push changes + subprocess.check_call(['git', '-C', base_dir, 'push', 'origin', branch_name]) + print(f"Changes pushed to branch '{branch_name}'") else: print("No changes to commit.") - - subprocess.check_call(['git', '-C', base_dir, 'push', 'origin', branch_name]) - print(f"Changes pushed to branch '{branch_name}'") except subprocess.CalledProcessError as e: print(f"Git operation failed: {e}") else: print("Git push is disabled.") -def download_scripts(): +def main(): global domain, headers - domain = os.getenv('DOMAIN') - api_token = os.getenv('API_TOKEN') - scriptpath = os.getenv('SCRIPTPATH') - - if not domain or not api_token or not scriptpath: + # Fetch environment variables needed + domain, api_token, scriptpath = os.getenv('DOMAIN'), os.getenv('API_TOKEN'), os.getenv('SCRIPTPATH') + if not all([domain, api_token, scriptpath]): print("Error: DOMAIN, API_TOKEN, and SCRIPTPATH must be set in the environment.") sys.exit(1) + # Set headers for API requests headers = {"X-API-KEY": api_token} + + # Resolve the base directory where scripts will be saved and prepared base_dir = Path(scriptpath).resolve() - folders = { - "scripts": base_dir / "scripts", - "scriptsraw": base_dir / "scriptsraw", - "snippets": base_dir / "snippets", - "snippetsraw": base_dir / "snippetsraw" - } + + # Define folders for storing scripts, raw scripts, snippets, and raw snippets + folders = {name: base_dir / name for name in ["scripts", "scriptsraw", "snippets", "snippetsraw"]} for folder in folders.values(): folder.mkdir(parents=True, exist_ok=True) - shell_summary = defaultdict(int) - current_scripts = set() + # Initialize counters and sets + shell_summary, current_scripts = defaultdict(int), set() + # 1 Git pull if ENABLE_GIT_PULL: git_pull(base_dir) + else: + print("Git pull is disabled.") + # 2 Write any modifications made to scripts back to the API write_modifications_to_api(base_dir, folders, api_token) + # 3 Fetch and process user-defined scripts print("Fetching user-defined scripts...") user_defined_scripts = fetch_data(f"{domain}/scripts/?showHiddenScripts=true", headers) 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)) + + # Process the user-defined scripts and add them to the current set + current_scripts.update(process_scripts(user_defined_scripts, folders["scripts"], folders["scriptsraw"], shell_summary)) + # Fetch and process snippets print("Fetching snippets...") snippets = fetch_data(f"{domain}/scripts/snippets/", headers) - current_scripts.update(process_scripts(snippets, folders['snippets'], folders['snippetsraw'], shell_summary, is_snippet=True)) + + # Process the snippets and add them to the current set + current_scripts.update(process_scripts(snippets, folders["snippets"], folders["snippetsraw"], shell_summary, is_snippet=True)) + # Remove any obsolete files that are no longer needed for folder in folders.values(): delete_obsolete_files(folder, current_scripts) + # 4 If Git push is enabled, push the local changes to the repository if ENABLE_GIT_PUSH: git_push(base_dir) + else: + print("Git push is disabled.") + # 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}: {count}") - - + print("Shell summary:", "\n".join(f"{shell}: {count}" for shell, count in shell_summary.items())) if __name__ == "__main__": - download_scripts() \ No newline at end of file + main() \ No newline at end of file From 70f146b03d72b6e17074c00ddde3898ef8ddd781 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 7 Apr 2025 10:35:25 +0000 Subject: [PATCH 282/447] Update ./scripts/Backend/Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 188 +++++++++++------- 1 file changed, 111 insertions(+), 77 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index 1d468bf2..0a32b31e 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -68,18 +68,17 @@ 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 .TODO Add reporting support - add writeback support for snippets - simplify the functions that does the writeback Move raws from "scriptsraw" to scripts/subfolder/raws/ to group them with their scripts add logging - add counters and separators at the end of each function send workflow flags to ENV default to true - + Delete script support from git (dedicated function required as the current delete_obsolete_files only work based on api) + Review flow of step 3 for optimisations + """ import subprocess @@ -113,7 +112,7 @@ def delete_obsolete_files(folder, current_scripts): def process_scripts(scripts, script_folder, script_raw_folder, shell_summary, is_snippet=False): - print(f"Processing {'snippets' if is_snippet else 'user-defined scripts'}...") + print(f"Processing {'snippets' if is_snippet else 'scripts'}...") current = set() for s in scripts: @@ -172,70 +171,98 @@ def write_modifications_to_api(base_dir, folders, api_token): """Compare local script files and JSON definitions, then push mismatches to the API.""" print("Comparing script files with JSON files...") mismatches = [] - - for raw_path in folders['scriptsraw'].rglob('*.json'): - raw_name = re.sub(r'^\d+ - ', '', raw_path.stem).lower() - match = next((p for p in folders['scripts'].rglob('*') - if p.is_file() and p.stem.lower() == raw_name), None) - - if not match: - print(f"No match for: {raw_path}") - continue - - print(f"Matched: {match} <-> {raw_path}") - script_hash = compute_hash(match) - - with raw_path.open(encoding='utf-8') as f: - raw_data = json.load(f) - code = raw_data.get('code', '') - code_hash = hashlib.sha256(code.encode('utf-8')).hexdigest() - - print(f"Script hash: {script_hash}\nJSON hash: {code_hash}") - - if script_hash != code_hash: - print("\n--- Script (first 10 lines) ---") - with match.open(encoding='utf-8') as f: - for i, line in enumerate(f): - if i >= 10: break + + total_files_checked = 0 + total_matches = 0 + total_mismatches = 0 + total_updated = 0 + total_skipped = 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() + match = next((p for p in folders[folder_name].rglob('*') + if p.is_file() and p.stem.lower() == raw_name), None) + + if not match: + print(f"No match for {'snippet' if is_snippet else 'script'}: {raw_path}") + total_skipped += 1 + continue + + print(f"Matched {'snippet' if is_snippet else 'script'}: {match} <-> {raw_path}") + total_matches += 1 + file_hash = compute_hash(match) + + with raw_path.open(encoding='utf-8') as f: + raw_data = json.load(f) + code = raw_data.get('code', '') + code_hash = hashlib.sha256(code.encode('utf-8')).hexdigest() + + print(f"{'Snippet' if is_snippet else 'Script'} hash: {file_hash}\nJSON hash: {code_hash}") + + if file_hash != code_hash: + total_mismatches += 1 + print(f"\n--- {'Snippet' if is_snippet else 'Script'} (first 10 lines) ---") + with match.open(encoding='utf-8') as f: + for i, line in enumerate(f): + if i >= 10: break + print(line.strip()) + + print(f"\n--- JSON Code (first 10 lines) ---") + for line in code.splitlines()[:10]: print(line.strip()) - print("\n--- JSON Code (first 10 lines) ---") - for line in code.splitlines()[:10]: - print(line.strip()) - - with match.open(encoding='utf-8') as f: - updated_payload = {**raw_data, 'code': f.read()} - - try: - if ENABLE_WRITEBACK: - print(f"Updating API for {match}...") - update_api(raw_data.get('id'), updated_payload, api_token) - else: - print(f"Simulated push for {match}:") - updated_payload['script_body'] = updated_payload.pop('code') - print(json.dumps(updated_payload, indent=4)) - sys.stdout.flush() - except BrokenPipeError: - sys.stderr.close() - sys.stdout.close() - - -def update_api(script_id, payload): - """Update the API with the provided script ID and payload.""" - payload['script_body'] = payload.pop('code', '') + with match.open(encoding='utf-8') as f: + updated_payload = {**raw_data, 'code': f.read()} + + try: + if ENABLE_WRITEBACK: + print(f"Updating API for {'snippet' if is_snippet else 'script'} {match}...") + update_api(raw_data.get('id'), updated_payload, api_token, is_snippet) + total_updated += 1 + 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() + except BrokenPipeError: + sys.stderr.close() + sys.stdout.close() + + print("\nComparison Complete:") + print(f"Total files checked: {total_files_checked}") + print(f"Total matches: {total_matches}") + print(f"Total mismatches: {total_mismatches}") + print(f"Total updates: {total_updated}") + print(f"Total skipped: {total_skipped}") + +def update_api(item_id, payload, api_token, is_snippet=False): + """Update the API with the provided item ID and payload.""" + + # Correctly handle 'code' or 'script_body' based on whether it's a snippet or a script + 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}/" - url = f"{domain}/scripts/{script_id}/" - body = payload['script_body'] + body = payload['code'] if is_snippet else payload['script_body'] - print(f"Updating {script_id}, length: {len(body)}, preview: {body[:1000]}{'...' if len(body) > 1000 else ''}") + 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(url, headers=headers, json=payload, timeout=120) - print(f"{script_id} update: {res.status_code} {res.reason}") + res = requests.put(endpoint, headers={"X-API-KEY": api_token}, 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 {script_id}: {e}") + print(f"Request error for {'snippet' if is_snippet else 'script'} {item_id}: {e}") + def git_pull(base_dir): """Force pull the latest changes from the git repository, discarding local changes.""" @@ -288,7 +315,7 @@ def git_push(base_dir): for line in result.stdout.strip().split("\n"): if not line: continue status, file = line.split("\t") - if file.startswith("scriptsraw/"): continue + if 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) @@ -340,43 +367,50 @@ def main(): # Initialize counters and sets shell_summary, current_scripts = defaultdict(int), set() - # 1 Git pull + # 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 any modifications made to scripts back to the API + # 2. Write modifications to the API + print("\n===== Step 2: Write Modifications to API =====") write_modifications_to_api(base_dir, folders, api_token) + print("===== End of Step 2 =====\n") - # 3 Fetch and process user-defined scripts - print("Fetching user-defined scripts...") + # 3. Fetch and process scripts + print("\n===== Step 3: Fetch and Process Scripts and Snippets =====") + print("Fetching scripts...") user_defined_scripts = fetch_data(f"{domain}/scripts/?showHiddenScripts=true", headers) user_defined_scripts = [item for item in user_defined_scripts if item.get('script_type') == 'userdefined'] - - # Process the user-defined scripts and add them to the current set current_scripts.update(process_scripts(user_defined_scripts, folders["scripts"], folders["scriptsraw"], shell_summary)) # Fetch and process snippets print("Fetching snippets...") snippets = fetch_data(f"{domain}/scripts/snippets/", headers) - - # Process the snippets and add them to the current set current_scripts.update(process_scripts(snippets, folders["snippets"], folders["snippetsraw"], shell_summary, is_snippet=True)) - # Remove any obsolete files that are no longer needed + + # 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:", "\n".join(f"{shell}: {count}" for shell, count in shell_summary.items())) + + + # Remove any obsolete files that are no longer existing in the api for folder in folders.values(): delete_obsolete_files(folder, current_scripts) - # 4 If Git push is enabled, push the local changes to the repository + 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.") - - # 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:", "\n".join(f"{shell}: {count}" for shell, count in shell_summary.items())) + print("===== End of Step 4 =====\n") if __name__ == "__main__": - main() \ No newline at end of file + main() From f01f42420a0635cd7f46ec92c61de4b8375e5ea3 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 7 Apr 2025 11:38:23 +0000 Subject: [PATCH 283/447] Update ./scripts/Backend/Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 200 +++++++++++------- 1 file changed, 122 insertions(+), 78 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index 0a32b31e..5f6d3818 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -69,6 +69,8 @@ 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 .TODO @@ -78,6 +80,7 @@ send workflow flags to ENV default to true Delete script support from git (dedicated function required as the current delete_obsolete_files only work based on api) Review flow of step 3 for optimisations + move all big var to global and ensure they are used from global only. """ @@ -98,6 +101,9 @@ ENABLE_WRITEBACK = True ENABLE_WRITETOFILE = True +# Can be changed to "main" or other if needed. +git_pull_branch = 'master' + def delete_obsolete_files(folder, current_scripts): print(f"Cleaning {folder}...") obsolete = {f for f in folder.rglob('*') if f.is_file() and f.relative_to(folder) not in current_scripts} @@ -264,89 +270,119 @@ def update_api(item_id, payload, api_token, is_snippet=False): print(f"Request error for {'snippet' if is_snippet else 'script'} {item_id}: {e}") + def git_pull(base_dir): """Force pull the latest changes from the git repository, discarding local changes.""" - if ENABLE_GIT_PULL: - print("Starting force pull...") - try: - subprocess.check_call(['git', '-C', base_dir, 'fetch', 'origin']) - subprocess.check_call(['git', '-C', base_dir, 'reset', '--hard', 'origin/master']) - print("Successfully force-pulled the latest changes from the repository.") - except subprocess.CalledProcessError as e: - print(f"Failed to force-pull changes from Git: {e}") - sys.exit(1) - else: - print("Git pull is disabled.") + if not os.path.isdir(base_dir): + print(f"Invalid directory: {base_dir}") + sys.exit(1) + + print("Starting force pull...") + try: + subprocess.check_call(['git', '-C', base_dir, 'fetch', 'origin']) + subprocess.check_call(['git', '-C', base_dir, 'reset', '--hard', f'origin/{git_pull_branch}']) + print(f"Successfully force-pulled the latest changes from the '{git_pull_branch}' branch.") + except subprocess.CalledProcessError as e: + print(f"Failed to force-pull changes from Git: {e}") + sys.exit(1) + def git_push(base_dir): """Push local changes to the git repository.""" - if ENABLE_GIT_PUSH: - try: - # Check if a rebase is in progress - rebase_in_progress = subprocess.run( - ['git', '-C', base_dir, 'rebase', '--show-current-patch'], - capture_output=True, text=True - ).returncode == 0 - if rebase_in_progress: - sys.exit("Rebase in progress. Complete or abort it.") - - # Get current branch - branch_name = subprocess.run( - ['git', '-C', base_dir, 'rev-parse', '--abbrev-ref', 'HEAD'], - capture_output=True, text=True - ).stdout.strip() or "update-scripts" - if branch_name == 'HEAD': - subprocess.check_call(['git', '-C', base_dir, 'checkout', '-b', branch_name]) - - # Get staged changes - status_result = subprocess.run( - ['git', '-C', base_dir, 'status', '--porcelain'], - capture_output=True, text=True + try: + # Check if a rebase is in progress + rebase_in_progress = subprocess.run( + ['git', '-C', base_dir, 'rebase', '--show-current-patch'], + capture_output=True, text=True + ).returncode == 0 + if rebase_in_progress: + sys.exit("Rebase in progress. Complete or abort it.") + + # Get current branch + branch_name = subprocess.run( + ['git', '-C', base_dir, 'rev-parse', '--abbrev-ref', 'HEAD'], + capture_output=True, text=True + ).stdout.strip() or "update-scripts" + if branch_name == 'HEAD': + subprocess.check_call(['git', '-C', base_dir, 'checkout', '-b', branch_name]) + + # Get staged changes + 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', '.']) + + # Get the list of staged changes + result = subprocess.run( + ['git', '-C', base_dir, 'diff', '--cached', '--name-status'], + capture_output=True, text=True, check=True ) - if status_result.stdout: - subprocess.check_call(['git', '-C', base_dir, 'add', '.']) - - # Get the list of staged changes - result = subprocess.run( - ['git', '-C', base_dir, 'diff', '--cached', '--name-status'], - capture_output=True, text=True, check=True - ) - changes = {"created": [], "modified": [], "deleted": [], "renamed": []} - for line in result.stdout.strip().split("\n"): - if not line: continue - status, file = line.split("\t") - if 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) - elif status.startswith("R"): changes["renamed"].append(f"{line.split()[1]} -> {line.split()[2]}") - - # Generate commit message - def generate_commit_message(changes, max_files=5): - if not any(changes.values()): return "Minor update" - parts = [f"{change_type} {len(files)}: {', '.join(files[:max_files])}{'...' if len(files) > max_files else ''}" - for change_type, files in changes.items() if files] - return "; ".join(parts) - - commit_message = generate_commit_message(changes) - - # Commit changes - subprocess.check_call(['git', '-C', base_dir, 'commit', '-m', commit_message]) - print(f"Committed changes to branch '{branch_name}': {commit_message}") - - # Push changes - subprocess.check_call(['git', '-C', base_dir, 'push', 'origin', branch_name]) - print(f"Changes pushed to branch '{branch_name}'") - else: - print("No changes to commit.") - except subprocess.CalledProcessError as e: - print(f"Git operation failed: {e}") - else: - print("Git push is disabled.") + changes = {"created": [], "modified": [], "deleted": [], "renamed": []} + for line in result.stdout.strip().split("\n"): + if not line: continue + status, file = line.split("\t") + if 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) + elif status.startswith("R"): changes["renamed"].append(f"{line.split()[1]} -> {line.split()[2]}") + + # Generate commit message + def generate_commit_message(changes, max_files=5): + if not any(changes.values()): return "Minor update" + parts = [f"{change_type} {len(files)}: {', '.join(files[:max_files])}{'...' if len(files) > max_files else ''}" + for change_type, files in changes.items() if files] + return "; ".join(parts) + + commit_message = generate_commit_message(changes) + + # Commit changes + subprocess.check_call(['git', '-C', base_dir, 'commit', '-m', commit_message]) + print(f"Committed changes to branch '{branch_name}': {commit_message}") + + # Push changes + subprocess.check_call(['git', '-C', base_dir, 'push', 'origin', branch_name]) + print(f"Changes pushed to branch '{branch_name}'") + else: + print("No changes to commit.") + except subprocess.CalledProcessError as e: + print(f"Git operation failed: {e}") + + +def check_git_health(base_dir): + """Check if the Git folder is healthy by ensuring it's initialized, clean, and the git command is available.""" + try: + # Check if the 'git' command is available by calling 'git --version' + subprocess.check_call(['git', '--version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + # Check if the directory is a Git repo by running 'git rev-parse' + subprocess.check_call(['git', '-C', base_dir, 'rev-parse', '--is-inside-work-tree'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + # Check if the repo has any uncommitted changes by running 'git status --porcelain' + status = subprocess.check_output(['git', '-C', base_dir, 'status', '--porcelain']).decode().strip() + if status: + print("Error: There are uncommitted changes in the Git repository.") + return False + + # Check if the repo is on the expected branch by running 'git symbolic-ref --short HEAD' + 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 + + return True + except subprocess.CalledProcessError: + print("Error: The 'git' command is not available or the folder is not a valid Git repository.") + return False def main(): global domain, headers + # 0. General Prep: Setup Environment and Git Folder Health Check + print("\n===== Step 0: General Prep =====") + # Fetch environment variables needed domain, api_token, scriptpath = os.getenv('DOMAIN'), os.getenv('API_TOKEN'), os.getenv('SCRIPTPATH') if not all([domain, api_token, scriptpath]): @@ -364,11 +400,17 @@ def main(): for folder in folders.values(): folder.mkdir(parents=True, exist_ok=True) - # Initialize counters and sets - shell_summary, current_scripts = defaultdict(int), set() + # Check the health of the Git folder + if check_git_health(base_dir): + print("Git folder is healthy.") + else: + print("Error: Git folder is not healthy.") + sys.exit(1) + print("===== End of Step 0: General Prep =====\n") # 1. Git Pull print("\n===== Step 1: Git Pull =====") + print(f"Branch to pull: '{git_pull_branch}'") if ENABLE_GIT_PULL: git_pull(base_dir) else: @@ -382,6 +424,8 @@ def main(): # 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 scripts...") user_defined_scripts = fetch_data(f"{domain}/scripts/?showHiddenScripts=true", headers) user_defined_scripts = [item for item in user_defined_scripts if item.get('script_type') == 'userdefined'] @@ -392,11 +436,9 @@ def main(): snippets = fetch_data(f"{domain}/scripts/snippets/", headers) 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:", "\n".join(f"{shell}: {count}" for shell, count in shell_summary.items())) - + print("Shell summary:", "\n".join(f"{shell}: {count}" for shell, count in shell_summary.items())) # Remove any obsolete files that are no longer existing in the api for folder in folders.values(): @@ -412,5 +454,7 @@ def main(): print("Git push is disabled.") print("===== End of Step 4 =====\n") + + if __name__ == "__main__": main() From 91c1e20949fc1f98ed5583b20fa11c39e62f8dce Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:10:44 +0000 Subject: [PATCH 284/447] Update ./scripts/Build/Upgrade OS to Windows Server X Standard.ps1 --- ...pgrade OS to Windows Server X Standard.ps1 | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) 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 index 47ca0d6c..e3f59686 100644 --- a/scripts_staging/Build/Upgrade OS to Windows Server X Standard.ps1 +++ b/scripts_staging/Build/Upgrade OS to Windows Server X Standard.ps1 @@ -22,6 +22,7 @@ All keys are valid for initial installation and from https://learn.microsoft.com 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 @@ -101,33 +102,40 @@ function Verify-Checksum { return (Get-FileChecksum -filePath $filePath) -eq $expectedChecksum } -# 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-Host "Starting in-place upgrade..." - Start-Process -FilePath $setupPath -ArgumentList $upgradeArgs -Wait -NoNewWindow - Write-Host "Upgrade process initiated." -} - # Function to check requirements function Check-Requirements { param ([string]$targetedVersion, [string]$baseUrl) - if (-not $targetedVersion -or -not $baseUrl) { Write-Host "Missing parameters. Exiting."; exit 1 } - if (-not $serverVersions.ContainsKey($targetedVersion)) { Write-Host "Invalid version: $targetedVersion. Exiting."; exit 1 } + 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-Host "Unsupported language: $systemLocale. Exiting."; exit 1 } + 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-Host "7-Zip not found. Exiting."; exit 1 } + 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-Host "Not enough disk space. 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 @@ -144,20 +152,21 @@ $extractFolder = "C:\\Windows\\Temp\\windows_server_extract" # Validate or download ISO if (!(Verify-Checksum -filePath $isoFile -expectedChecksum $metadata.checksum)) { - Write-Host "Downloading ISO..." + Write-Log "Downloading ISO..." Invoke-WebRequest -Uri "$baseUrl$($metadata.file)" -OutFile $isoFile if (!(Verify-Checksum -filePath $isoFile -expectedChecksum $metadata.checksum)) { - Write-Host "Checksum verification failed. Exiting."; exit 1 + Write-Log "Checksum verification failed. Exiting." + exit 1 } } # Clean and extract ISO if (Test-Path $extractFolder) { Remove-Item -Recurse -Force $extractFolder } -Write-Host "Extracting ISO..." +Write-Log "Extracting ISO..." Start-Process -FilePath $sevenZipPath -ArgumentList "x `"$isoFile`" -o`"$extractFolder`" -y" -Wait # Delete ISO file to free up space -Write-Host "Deleting 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 @@ -165,6 +174,7 @@ $setupPath = Get-ChildItem -Path $extractFolder -Recurse -Filter "setup.exe" -Fi if ($setupPath) { Perform-InPlaceUpgrade -setupPath $setupPath.FullName -licenseKey $metadata.licenseKey } else { - Write-Host "setup.exe not found. Exiting."; exit 1 + Write-Log "setup.exe not found. Exiting." + exit 1 } From f3b71f20f999c9cd845e65c2693a3f56e1ebf1b0 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 9 Apr 2025 06:48:16 +0000 Subject: [PATCH 285/447] Update ./scripts/Checks/Activation status.ps1 --- scripts_staging/Checks/Activation status.ps1 | 22 ++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/scripts_staging/Checks/Activation status.ps1 b/scripts_staging/Checks/Activation status.ps1 index 4c8fc8e1..f7123824 100644 --- a/scripts_staging/Checks/Activation status.ps1 +++ b/scripts_staging/Checks/Activation status.ps1 @@ -13,15 +13,25 @@ #public .CHANGELOG + 09.04.25 SAN move to Get-CimInstance and other improvements #> -$activationStatus = Get-WmiObject -Query "SELECT * FROM SoftwareLicensingProduct WHERE PartialProductKey <> NULL" -if ($activationStatus.LicenseStatus -eq 1) { - Write-Host "Windows is activated." - exit 0 -} else { - Write-Host "Windows is not activated." +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 } + From 6d53ffebec9e434f92014586edc6e446292ffe9f Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 10 Apr 2025 15:04:58 +0000 Subject: [PATCH 286/447] Update ./scripts/Backend/Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index 5f6d3818..2c18100e 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -71,6 +71,7 @@ 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 07/04/25 SAN removed pathvalidate dependency .TODO @@ -92,7 +93,6 @@ from collections import defaultdict from pathlib import Path import requests -from pathvalidate import sanitize_filename import re # Toggle flags @@ -116,6 +116,10 @@ def delete_obsolete_files(folder, current_scripts): try: d.rmdir(); print(f"Removed empty dir: {d}") except Exception as e: print(f"Could not delete dir {d}: {e}") +def sanitize_filename(name: str) -> str: + name = name.replace('\0', '') + + return re.sub(r'[<>:"/\\|?*]', '', 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'}...") From dc41534c1159a595d889319898d3d4b4ed62564e Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 10 Apr 2025 15:06:11 +0000 Subject: [PATCH 287/447] Update ./scripts/Backend/Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index 2c18100e..4c9fb5fa 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -71,7 +71,7 @@ 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 07/04/25 SAN removed pathvalidate dependency + v9.0.2.3 10/04/25 SAN removed pathvalidate dependency .TODO From 6b9d2aef4c71541f3e11181c61da5d27879fac56 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 10 Apr 2025 15:24:26 +0000 Subject: [PATCH 288/447] Update ./scripts/Backend/Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 52 +++++++++++++------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index 4c9fb5fa..f6674cae 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -15,18 +15,23 @@ 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. The mapped folder should already be configured with git + ------------------------------------------ + 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 and added in the vars as per the exemples bellow. 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 scripts out to 4 folders: + 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 for later processing, currently used for hash comparison + 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 for later import/migration + 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. @@ -72,6 +77,7 @@ 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 .TODO @@ -358,29 +364,38 @@ def generate_commit_message(changes, max_files=5): def check_git_health(base_dir): """Check if the Git folder is healthy by ensuring it's initialized, clean, and the git command is available.""" try: - # Check if the 'git' command is available by calling 'git --version' subprocess.check_call(['git', '--version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - - # Check if the directory is a Git repo by running 'git rev-parse' + except subprocess.CalledProcessError: + print("Error: The 'git' command is not available.") + return False + + 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 repo has any uncommitted changes by running 'git status --porcelain' + try: status = subprocess.check_output(['git', '-C', base_dir, 'status', '--porcelain']).decode().strip() if status: print("Error: There are uncommitted changes in the Git repository.") return False + except subprocess.CalledProcessError: + print("Error: Failed to check Git status.") + return False - # Check if the repo is on the expected branch by running 'git symbolic-ref --short HEAD' + 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 - - return True except subprocess.CalledProcessError: - print("Error: The 'git' command is not available or the folder is not a valid Git repository.") + print("Error: Unable to determine the current Git branch.") return False + return True + + def main(): global domain, headers @@ -404,12 +419,15 @@ def main(): for folder in folders.values(): folder.mkdir(parents=True, exist_ok=True) - # Check the health of the Git folder - if check_git_health(base_dir): - print("Git folder is healthy.") + # 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("Error: Git folder is not healthy.") - sys.exit(1) + print("Skipping Git health check because both pull and push are disabled.") print("===== End of Step 0: General Prep =====\n") # 1. Git Pull From 8c64f8e2ed98a4d77f0ea5143fdd0d04c0a9db0f Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 10 Apr 2025 23:20:47 +0000 Subject: [PATCH 289/447] Update ./scripts/Backend/Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 119 +++++++++++++----- 1 file changed, 89 insertions(+), 30 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index f6674cae..c9175735 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -20,7 +20,7 @@ ------------------------------------------ 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 and added in the vars as per the exemples bellow. + 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. @@ -37,8 +37,8 @@ If there are no changes, no commit will be made. .EXEMPLE - DOMAIN=https://api-rmm - DOMAIN=https://{{global.RMM_API_URL}} + 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 @@ -78,16 +78,18 @@ 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 .TODO + Add reporting support Move raws from "scriptsraw" to scripts/subfolder/raws/ to group them with their scripts - add logging send workflow flags to ENV default to true - Delete script support from git (dedicated function required as the current delete_obsolete_files only work based on api) + Delete script support from git ? (dedicated function required as the current delete_obsolete_files only work based on api) Review flow of step 3 for optimisations move all big var to global and ensure they are used from global only. + add api check for write. """ @@ -100,6 +102,7 @@ from pathlib import Path import requests import re +import socket # Toggle flags ENABLE_GIT_PULL = True @@ -362,19 +365,22 @@ def generate_commit_message(changes, max_files=5): def check_git_health(base_dir): - """Check if the Git folder is healthy by ensuring it's initialized, clean, and the git command is available.""" + git_dir = Path(base_dir) / '.git' + if not git_dir.exists(): + print(f"Error: .git folder not found in {base_dir}") + return False 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 - + 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 - + try: status = subprocess.check_output(['git', '-C', base_dir, 'status', '--porcelain']).decode().strip() if status: @@ -383,7 +389,7 @@ def check_git_health(base_dir): except subprocess.CalledProcessError: print("Error: Failed to check Git status.") return False - + try: current_branch = subprocess.check_output(['git', '-C', base_dir, 'symbolic-ref', '--short', 'HEAD']).decode().strip() if current_branch != git_pull_branch: @@ -396,38 +402,93 @@ def check_git_health(base_dir): return True -def main(): - global domain, headers +def pre_flight(): + global domain, api_token, scriptpath, headers - # 0. General Prep: Setup Environment and Git Folder Health Check - print("\n===== Step 0: General Prep =====") - - # Fetch environment variables needed - domain, api_token, scriptpath = os.getenv('DOMAIN'), os.getenv('API_TOKEN'), os.getenv('SCRIPTPATH') - if not all([domain, api_token, scriptpath]): - print("Error: DOMAIN, API_TOKEN, and SCRIPTPATH must be set in the environment.") + domain = os.getenv('DOMAIN') + api_token = os.getenv('API_TOKEN') + scriptpath = os.getenv('SCRIPTPATH') + + missing = [var for var in ['DOMAIN', 'API_TOKEN', 'SCRIPTPATH'] if not globals().get(var.lower())] + if missing: + print(f"✗ Error: Missing environment variable(s): {', '.join(missing)}") + for var in missing: + if var == 'DOMAIN': print(" - DOMAIN: The URL of your RMM API.(eg. api-rmm.exemple.com)") + if var == 'API_TOKEN': print(" - API_TOKEN: An API token for a user with permission to access and write scripts.") + if var == 'SCRIPTPATH': print(" - SCRIPTPATH: The local folder path for Git commands.") sys.exit(1) - # Set headers for API requests headers = {"X-API-KEY": api_token} + domain_for_connection = domain.replace("https://", "").replace("http://", "") + + try: + socket.create_connection((domain_for_connection, 443), timeout=5) + print(f"✓ Connectivity to {domain} on port 443 OK.") + except Exception as e: + print(f"✗ Error: Unable to connect to {domain} on port 443 - {e}") + sys.exit(1) - # Resolve the base directory where scripts will be saved and prepared - base_dir = Path(scriptpath).resolve() + if not domain.startswith("http://") and not domain.startswith("https://"): + domain = "https://" + domain - # Define folders for storing scripts, raw scripts, snippets, and raw snippets - folders = {name: base_dir / name for name in ["scripts", "scriptsraw", "snippets", "snippetsraw"]} - for folder in folders.values(): - folder.mkdir(parents=True, exist_ok=True) + try: + response = requests.get(f"{domain}/scripts/", headers=headers, timeout=5) + if response.status_code == 200: + print("✓ API token validated successfully for read.") + else: + print(f"✗ Error: Invalid API token. Status code: {response.status_code}") + sys.exit(1) + except Exception as e: + print(f"✗ Error: API token validation failed - {e}") + sys.exit(1) + +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. General Prep: Setup Environment and Git Folder Health Check + print("\n===== Step 0: General Prep =====") + + # ENV vars & network checks + pre_flight() + + # Folder structure check + base_dir = Path(scriptpath).resolve() + 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.") + # 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.") + print("✓ Git repo is healthy.") else: - print("Error: Git folder is not healthy.") + print("✗ Error: Git folder is not healthy.") sys.exit(1) else: print("Skipping Git health check because both pull and push are disabled.") + print("===== End of Step 0: General Prep =====\n") # 1. Git Pull @@ -476,7 +537,5 @@ def main(): print("Git push is disabled.") print("===== End of Step 4 =====\n") - - if __name__ == "__main__": - main() + main() \ No newline at end of file From 680cdaa06004fbae227111ea5ffea1e5e08b16b9 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 11 Apr 2025 09:06:58 +0000 Subject: [PATCH 290/447] Update ./scripts/Backend/Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 74 ++++++++++++++----- 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index c9175735..4558a038 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -103,6 +103,7 @@ import requests import re import socket +from requests.exceptions import RequestException, HTTPError # Toggle flags ENABLE_GIT_PULL = True @@ -126,9 +127,21 @@ def delete_obsolete_files(folder, current_scripts): except Exception as e: print(f"Could not delete dir {d}: {e}") def sanitize_filename(name: str) -> str: - name = name.replace('\0', '') - - return re.sub(r'[<>:"/\\|?*]', '', name).strip() + 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: {', '.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'}...") @@ -143,7 +156,7 @@ def process_scripts(scripts, script_folder, script_raw_folder, shell_summary, is folder.mkdir(parents=True, exist_ok=True) raw_folder.mkdir(parents=True, exist_ok=True) - data = s if is_snippet else fetch_data(f"{domain}/scripts/{sid}/download/?with_snippets=false", headers) + data = s if is_snippet else fetch_data(f"{domain}/scripts/{sid}/download/?with_snippets=false") if not data: continue code = data.get('code') @@ -177,7 +190,7 @@ def save_file(path, content, is_json=False): else: print(f"File would be saved (simulation): {path}") -def fetch_data(url, headers): +def fetch_data(url): print(f"Fetching: {url}") r = requests.get(url, headers=headers) if r.ok: @@ -186,7 +199,7 @@ def fetch_data(url, headers): print(f"Error {r.status_code}") return [] -def write_modifications_to_api(base_dir, folders, api_token): +def write_modifications_to_api(base_dir, folders): """Compare local script files and JSON definitions, then push mismatches to the API.""" print("Comparing script files with JSON files...") mismatches = [] @@ -241,7 +254,7 @@ def write_modifications_to_api(base_dir, folders, api_token): try: if ENABLE_WRITEBACK: print(f"Updating API for {'snippet' if is_snippet else 'script'} {match}...") - update_api(raw_data.get('id'), updated_payload, api_token, is_snippet) + update_api(raw_data.get('id'), updated_payload, is_snippet) total_updated += 1 else: print(f"Simulated push for {'snippet' if is_snippet else 'script'} {match}:") @@ -259,7 +272,7 @@ def write_modifications_to_api(base_dir, folders, api_token): print(f"Total updates: {total_updated}") print(f"Total skipped: {total_skipped}") -def update_api(item_id, payload, api_token, is_snippet=False): +def update_api(item_id, payload, is_snippet=False): """Update the API with the provided item ID and payload.""" # Correctly handle 'code' or 'script_body' based on whether it's a snippet or a script @@ -275,7 +288,7 @@ def update_api(item_id, payload, api_token, is_snippet=False): 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={"X-API-KEY": api_token}, json=payload, timeout=120) + 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) @@ -403,24 +416,23 @@ def check_git_health(base_dir): def pre_flight(): - global domain, api_token, scriptpath, headers - + global domain, scriptpath, headers domain = os.getenv('DOMAIN') api_token = os.getenv('API_TOKEN') scriptpath = os.getenv('SCRIPTPATH') - missing = [var for var in ['DOMAIN', 'API_TOKEN', 'SCRIPTPATH'] if not globals().get(var.lower())] + 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(" - DOMAIN: The URL of your RMM API.(eg. api-rmm.exemple.com)") + if var == 'DOMAIN': print(" - DOMAIN: The URL of your RMM API. (e.g. api-rmm.example.com)") if var == 'API_TOKEN': print(" - API_TOKEN: An API token for a user with permission to access and write scripts.") if var == 'SCRIPTPATH': print(" - SCRIPTPATH: The local folder path for Git commands.") sys.exit(1) headers = {"X-API-KEY": api_token} domain_for_connection = domain.replace("https://", "").replace("http://", "") - + try: socket.create_connection((domain_for_connection, 443), timeout=5) print(f"✓ Connectivity to {domain} on port 443 OK.") @@ -431,17 +443,38 @@ def pre_flight(): if not domain.startswith("http://") and not domain.startswith("https://"): domain = "https://" + domain + 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("✓ API token validated successfully for read.") + print(f"✓ Token valid for read access: {obfuscated}") else: - print(f"✗ Error: Invalid API token. Status code: {response.status_code}") + print(f"✗ Token read access denied (status {response.status_code})") sys.exit(1) except Exception as e: - print(f"✗ Error: API token validation failed - {e}") + print(f"✗ Token read access check failed: {e}") sys.exit(1) + ''' + this does not work the api will create an empty file need to find another way. + try: + response = requests.post(f"{domain}/scripts/", headers=headers, json={}, timeout=5) + if response.status_code in (200, 201, 400): + print(f"✓ Token valid for write access: {obfuscated}") + elif response.status_code == 403: + print("✗ Token write access denied (status 403)") + sys.exit(1) + else: + print(f"✗ Token write access denied (status {response.status_code})") + sys.exit(1) + except Exception as e: + print(f"✗ Token write access check failed: {e}") + sys.exit(1) + ''' + + return + def check_and_create_folders(base_path, subfolders): try: if not base_path.exists(): @@ -462,6 +495,7 @@ def check_and_create_folders(base_path, subfolders): sys.exit(1) def main(): + # 0. General Prep: Setup Environment and Git Folder Health Check print("\n===== Step 0: General Prep =====") @@ -502,7 +536,7 @@ def main(): # 2. Write modifications to the API print("\n===== Step 2: Write Modifications to API =====") - write_modifications_to_api(base_dir, folders, api_token) + write_modifications_to_api(base_dir, folders) print("===== End of Step 2 =====\n") # 3. Fetch and process scripts @@ -510,13 +544,13 @@ def main(): # Initialize counters and sets shell_summary, current_scripts = defaultdict(int), set() print("Fetching scripts...") - user_defined_scripts = fetch_data(f"{domain}/scripts/?showHiddenScripts=true", headers) + user_defined_scripts = fetch_data(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...") - snippets = fetch_data(f"{domain}/scripts/snippets/", headers) + snippets = fetch_data(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 From 3ed646ceb2a0a9b6116dd746e9bb15fb9a87e523 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 11 Apr 2025 09:24:00 +0000 Subject: [PATCH 291/447] Update ./scripts/Backend/Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 74 +++++-------------- 1 file changed, 20 insertions(+), 54 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index 4558a038..c9175735 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -103,7 +103,6 @@ import requests import re import socket -from requests.exceptions import RequestException, HTTPError # Toggle flags ENABLE_GIT_PULL = True @@ -127,21 +126,9 @@ def delete_obsolete_files(folder, current_scripts): except Exception as e: print(f"Could not delete dir {d}: {e}") 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: {', '.join(removed_chars)}") - - return name.strip() + name = name.replace('\0', '') + + return re.sub(r'[<>:"/\\|?*]', '', 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'}...") @@ -156,7 +143,7 @@ def process_scripts(scripts, script_folder, script_raw_folder, shell_summary, is folder.mkdir(parents=True, exist_ok=True) raw_folder.mkdir(parents=True, exist_ok=True) - data = s if is_snippet else fetch_data(f"{domain}/scripts/{sid}/download/?with_snippets=false") + data = s if is_snippet else fetch_data(f"{domain}/scripts/{sid}/download/?with_snippets=false", headers) if not data: continue code = data.get('code') @@ -190,7 +177,7 @@ def save_file(path, content, is_json=False): else: print(f"File would be saved (simulation): {path}") -def fetch_data(url): +def fetch_data(url, headers): print(f"Fetching: {url}") r = requests.get(url, headers=headers) if r.ok: @@ -199,7 +186,7 @@ def fetch_data(url): print(f"Error {r.status_code}") return [] -def write_modifications_to_api(base_dir, folders): +def write_modifications_to_api(base_dir, folders, api_token): """Compare local script files and JSON definitions, then push mismatches to the API.""" print("Comparing script files with JSON files...") mismatches = [] @@ -254,7 +241,7 @@ def write_modifications_to_api(base_dir, folders): try: if ENABLE_WRITEBACK: print(f"Updating API for {'snippet' if is_snippet else 'script'} {match}...") - update_api(raw_data.get('id'), updated_payload, is_snippet) + update_api(raw_data.get('id'), updated_payload, api_token, is_snippet) total_updated += 1 else: print(f"Simulated push for {'snippet' if is_snippet else 'script'} {match}:") @@ -272,7 +259,7 @@ def write_modifications_to_api(base_dir, folders): print(f"Total updates: {total_updated}") print(f"Total skipped: {total_skipped}") -def update_api(item_id, payload, is_snippet=False): +def update_api(item_id, payload, api_token, is_snippet=False): """Update the API with the provided item ID and payload.""" # Correctly handle 'code' or 'script_body' based on whether it's a snippet or a script @@ -288,7 +275,7 @@ def update_api(item_id, payload, is_snippet=False): 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) + res = requests.put(endpoint, headers={"X-API-KEY": api_token}, json=payload, timeout=120) print(f"{item_id} update: {res.status_code} {res.reason}") if res.status_code != 200: print(res.text) @@ -416,23 +403,24 @@ def check_git_health(base_dir): def pre_flight(): - global domain, scriptpath, headers + global domain, api_token, scriptpath, headers + 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] + missing = [var for var in ['DOMAIN', 'API_TOKEN', 'SCRIPTPATH'] if not globals().get(var.lower())] if missing: print(f"✗ Error: Missing environment variable(s): {', '.join(missing)}") for var in missing: - if var == 'DOMAIN': print(" - DOMAIN: The URL of your RMM API. (e.g. api-rmm.example.com)") + if var == 'DOMAIN': print(" - DOMAIN: The URL of your RMM API.(eg. api-rmm.exemple.com)") if var == 'API_TOKEN': print(" - API_TOKEN: An API token for a user with permission to access and write scripts.") if var == 'SCRIPTPATH': print(" - SCRIPTPATH: The local folder path for Git commands.") sys.exit(1) headers = {"X-API-KEY": api_token} domain_for_connection = domain.replace("https://", "").replace("http://", "") - + try: socket.create_connection((domain_for_connection, 443), timeout=5) print(f"✓ Connectivity to {domain} on port 443 OK.") @@ -443,38 +431,17 @@ def pre_flight(): if not domain.startswith("http://") and not domain.startswith("https://"): domain = "https://" + domain - 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}") + print("✓ API token validated successfully for read.") else: - print(f"✗ Token read access denied (status {response.status_code})") + print(f"✗ Error: Invalid API token. Status code: {response.status_code}") sys.exit(1) except Exception as e: - print(f"✗ Token read access check failed: {e}") + print(f"✗ Error: API token validation failed - {e}") sys.exit(1) - ''' - this does not work the api will create an empty file need to find another way. - try: - response = requests.post(f"{domain}/scripts/", headers=headers, json={}, timeout=5) - if response.status_code in (200, 201, 400): - print(f"✓ Token valid for write access: {obfuscated}") - elif response.status_code == 403: - print("✗ Token write access denied (status 403)") - sys.exit(1) - else: - print(f"✗ Token write access denied (status {response.status_code})") - sys.exit(1) - except Exception as e: - print(f"✗ Token write access check failed: {e}") - sys.exit(1) - ''' - - return - def check_and_create_folders(base_path, subfolders): try: if not base_path.exists(): @@ -495,7 +462,6 @@ def check_and_create_folders(base_path, subfolders): sys.exit(1) def main(): - # 0. General Prep: Setup Environment and Git Folder Health Check print("\n===== Step 0: General Prep =====") @@ -536,7 +502,7 @@ def main(): # 2. Write modifications to the API print("\n===== Step 2: Write Modifications to API =====") - write_modifications_to_api(base_dir, folders) + write_modifications_to_api(base_dir, folders, api_token) print("===== End of Step 2 =====\n") # 3. Fetch and process scripts @@ -544,13 +510,13 @@ def main(): # Initialize counters and sets shell_summary, current_scripts = defaultdict(int), set() print("Fetching scripts...") - user_defined_scripts = fetch_data(f"{domain}/scripts/?showHiddenScripts=true") + user_defined_scripts = fetch_data(f"{domain}/scripts/?showHiddenScripts=true", headers) 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...") - snippets = fetch_data(f"{domain}/scripts/snippets/") + snippets = fetch_data(f"{domain}/scripts/snippets/", headers) 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 From dc840c1aa1f8a7e74eb957795c60f6c31edbbb25 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 11 Apr 2025 09:24:28 +0000 Subject: [PATCH 292/447] Update ./scripts/Backend/Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 74 ++++++++++++++----- 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index c9175735..4558a038 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -103,6 +103,7 @@ import requests import re import socket +from requests.exceptions import RequestException, HTTPError # Toggle flags ENABLE_GIT_PULL = True @@ -126,9 +127,21 @@ def delete_obsolete_files(folder, current_scripts): except Exception as e: print(f"Could not delete dir {d}: {e}") def sanitize_filename(name: str) -> str: - name = name.replace('\0', '') - - return re.sub(r'[<>:"/\\|?*]', '', name).strip() + 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: {', '.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'}...") @@ -143,7 +156,7 @@ def process_scripts(scripts, script_folder, script_raw_folder, shell_summary, is folder.mkdir(parents=True, exist_ok=True) raw_folder.mkdir(parents=True, exist_ok=True) - data = s if is_snippet else fetch_data(f"{domain}/scripts/{sid}/download/?with_snippets=false", headers) + data = s if is_snippet else fetch_data(f"{domain}/scripts/{sid}/download/?with_snippets=false") if not data: continue code = data.get('code') @@ -177,7 +190,7 @@ def save_file(path, content, is_json=False): else: print(f"File would be saved (simulation): {path}") -def fetch_data(url, headers): +def fetch_data(url): print(f"Fetching: {url}") r = requests.get(url, headers=headers) if r.ok: @@ -186,7 +199,7 @@ def fetch_data(url, headers): print(f"Error {r.status_code}") return [] -def write_modifications_to_api(base_dir, folders, api_token): +def write_modifications_to_api(base_dir, folders): """Compare local script files and JSON definitions, then push mismatches to the API.""" print("Comparing script files with JSON files...") mismatches = [] @@ -241,7 +254,7 @@ def write_modifications_to_api(base_dir, folders, api_token): try: if ENABLE_WRITEBACK: print(f"Updating API for {'snippet' if is_snippet else 'script'} {match}...") - update_api(raw_data.get('id'), updated_payload, api_token, is_snippet) + update_api(raw_data.get('id'), updated_payload, is_snippet) total_updated += 1 else: print(f"Simulated push for {'snippet' if is_snippet else 'script'} {match}:") @@ -259,7 +272,7 @@ def write_modifications_to_api(base_dir, folders, api_token): print(f"Total updates: {total_updated}") print(f"Total skipped: {total_skipped}") -def update_api(item_id, payload, api_token, is_snippet=False): +def update_api(item_id, payload, is_snippet=False): """Update the API with the provided item ID and payload.""" # Correctly handle 'code' or 'script_body' based on whether it's a snippet or a script @@ -275,7 +288,7 @@ def update_api(item_id, payload, api_token, is_snippet=False): 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={"X-API-KEY": api_token}, json=payload, timeout=120) + 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) @@ -403,24 +416,23 @@ def check_git_health(base_dir): def pre_flight(): - global domain, api_token, scriptpath, headers - + global domain, scriptpath, headers domain = os.getenv('DOMAIN') api_token = os.getenv('API_TOKEN') scriptpath = os.getenv('SCRIPTPATH') - missing = [var for var in ['DOMAIN', 'API_TOKEN', 'SCRIPTPATH'] if not globals().get(var.lower())] + 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(" - DOMAIN: The URL of your RMM API.(eg. api-rmm.exemple.com)") + if var == 'DOMAIN': print(" - DOMAIN: The URL of your RMM API. (e.g. api-rmm.example.com)") if var == 'API_TOKEN': print(" - API_TOKEN: An API token for a user with permission to access and write scripts.") if var == 'SCRIPTPATH': print(" - SCRIPTPATH: The local folder path for Git commands.") sys.exit(1) headers = {"X-API-KEY": api_token} domain_for_connection = domain.replace("https://", "").replace("http://", "") - + try: socket.create_connection((domain_for_connection, 443), timeout=5) print(f"✓ Connectivity to {domain} on port 443 OK.") @@ -431,17 +443,38 @@ def pre_flight(): if not domain.startswith("http://") and not domain.startswith("https://"): domain = "https://" + domain + 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("✓ API token validated successfully for read.") + print(f"✓ Token valid for read access: {obfuscated}") else: - print(f"✗ Error: Invalid API token. Status code: {response.status_code}") + print(f"✗ Token read access denied (status {response.status_code})") sys.exit(1) except Exception as e: - print(f"✗ Error: API token validation failed - {e}") + print(f"✗ Token read access check failed: {e}") sys.exit(1) + ''' + this does not work the api will create an empty file need to find another way. + try: + response = requests.post(f"{domain}/scripts/", headers=headers, json={}, timeout=5) + if response.status_code in (200, 201, 400): + print(f"✓ Token valid for write access: {obfuscated}") + elif response.status_code == 403: + print("✗ Token write access denied (status 403)") + sys.exit(1) + else: + print(f"✗ Token write access denied (status {response.status_code})") + sys.exit(1) + except Exception as e: + print(f"✗ Token write access check failed: {e}") + sys.exit(1) + ''' + + return + def check_and_create_folders(base_path, subfolders): try: if not base_path.exists(): @@ -462,6 +495,7 @@ def check_and_create_folders(base_path, subfolders): sys.exit(1) def main(): + # 0. General Prep: Setup Environment and Git Folder Health Check print("\n===== Step 0: General Prep =====") @@ -502,7 +536,7 @@ def main(): # 2. Write modifications to the API print("\n===== Step 2: Write Modifications to API =====") - write_modifications_to_api(base_dir, folders, api_token) + write_modifications_to_api(base_dir, folders) print("===== End of Step 2 =====\n") # 3. Fetch and process scripts @@ -510,13 +544,13 @@ def main(): # Initialize counters and sets shell_summary, current_scripts = defaultdict(int), set() print("Fetching scripts...") - user_defined_scripts = fetch_data(f"{domain}/scripts/?showHiddenScripts=true", headers) + user_defined_scripts = fetch_data(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...") - snippets = fetch_data(f"{domain}/scripts/snippets/", headers) + snippets = fetch_data(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 From a65ebb79f0a222b83634a41fe0202e49afb24ccc Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 11 Apr 2025 09:24:49 +0000 Subject: [PATCH 293/447] Update ./scripts/Backend/Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 74 +++++-------------- 1 file changed, 20 insertions(+), 54 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index 4558a038..c9175735 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -103,7 +103,6 @@ import requests import re import socket -from requests.exceptions import RequestException, HTTPError # Toggle flags ENABLE_GIT_PULL = True @@ -127,21 +126,9 @@ def delete_obsolete_files(folder, current_scripts): except Exception as e: print(f"Could not delete dir {d}: {e}") 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: {', '.join(removed_chars)}") - - return name.strip() + name = name.replace('\0', '') + + return re.sub(r'[<>:"/\\|?*]', '', 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'}...") @@ -156,7 +143,7 @@ def process_scripts(scripts, script_folder, script_raw_folder, shell_summary, is folder.mkdir(parents=True, exist_ok=True) raw_folder.mkdir(parents=True, exist_ok=True) - data = s if is_snippet else fetch_data(f"{domain}/scripts/{sid}/download/?with_snippets=false") + data = s if is_snippet else fetch_data(f"{domain}/scripts/{sid}/download/?with_snippets=false", headers) if not data: continue code = data.get('code') @@ -190,7 +177,7 @@ def save_file(path, content, is_json=False): else: print(f"File would be saved (simulation): {path}") -def fetch_data(url): +def fetch_data(url, headers): print(f"Fetching: {url}") r = requests.get(url, headers=headers) if r.ok: @@ -199,7 +186,7 @@ def fetch_data(url): print(f"Error {r.status_code}") return [] -def write_modifications_to_api(base_dir, folders): +def write_modifications_to_api(base_dir, folders, api_token): """Compare local script files and JSON definitions, then push mismatches to the API.""" print("Comparing script files with JSON files...") mismatches = [] @@ -254,7 +241,7 @@ def write_modifications_to_api(base_dir, folders): try: if ENABLE_WRITEBACK: print(f"Updating API for {'snippet' if is_snippet else 'script'} {match}...") - update_api(raw_data.get('id'), updated_payload, is_snippet) + update_api(raw_data.get('id'), updated_payload, api_token, is_snippet) total_updated += 1 else: print(f"Simulated push for {'snippet' if is_snippet else 'script'} {match}:") @@ -272,7 +259,7 @@ def write_modifications_to_api(base_dir, folders): print(f"Total updates: {total_updated}") print(f"Total skipped: {total_skipped}") -def update_api(item_id, payload, is_snippet=False): +def update_api(item_id, payload, api_token, is_snippet=False): """Update the API with the provided item ID and payload.""" # Correctly handle 'code' or 'script_body' based on whether it's a snippet or a script @@ -288,7 +275,7 @@ def update_api(item_id, payload, is_snippet=False): 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) + res = requests.put(endpoint, headers={"X-API-KEY": api_token}, json=payload, timeout=120) print(f"{item_id} update: {res.status_code} {res.reason}") if res.status_code != 200: print(res.text) @@ -416,23 +403,24 @@ def check_git_health(base_dir): def pre_flight(): - global domain, scriptpath, headers + global domain, api_token, scriptpath, headers + 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] + missing = [var for var in ['DOMAIN', 'API_TOKEN', 'SCRIPTPATH'] if not globals().get(var.lower())] if missing: print(f"✗ Error: Missing environment variable(s): {', '.join(missing)}") for var in missing: - if var == 'DOMAIN': print(" - DOMAIN: The URL of your RMM API. (e.g. api-rmm.example.com)") + if var == 'DOMAIN': print(" - DOMAIN: The URL of your RMM API.(eg. api-rmm.exemple.com)") if var == 'API_TOKEN': print(" - API_TOKEN: An API token for a user with permission to access and write scripts.") if var == 'SCRIPTPATH': print(" - SCRIPTPATH: The local folder path for Git commands.") sys.exit(1) headers = {"X-API-KEY": api_token} domain_for_connection = domain.replace("https://", "").replace("http://", "") - + try: socket.create_connection((domain_for_connection, 443), timeout=5) print(f"✓ Connectivity to {domain} on port 443 OK.") @@ -443,38 +431,17 @@ def pre_flight(): if not domain.startswith("http://") and not domain.startswith("https://"): domain = "https://" + domain - 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}") + print("✓ API token validated successfully for read.") else: - print(f"✗ Token read access denied (status {response.status_code})") + print(f"✗ Error: Invalid API token. Status code: {response.status_code}") sys.exit(1) except Exception as e: - print(f"✗ Token read access check failed: {e}") + print(f"✗ Error: API token validation failed - {e}") sys.exit(1) - ''' - this does not work the api will create an empty file need to find another way. - try: - response = requests.post(f"{domain}/scripts/", headers=headers, json={}, timeout=5) - if response.status_code in (200, 201, 400): - print(f"✓ Token valid for write access: {obfuscated}") - elif response.status_code == 403: - print("✗ Token write access denied (status 403)") - sys.exit(1) - else: - print(f"✗ Token write access denied (status {response.status_code})") - sys.exit(1) - except Exception as e: - print(f"✗ Token write access check failed: {e}") - sys.exit(1) - ''' - - return - def check_and_create_folders(base_path, subfolders): try: if not base_path.exists(): @@ -495,7 +462,6 @@ def check_and_create_folders(base_path, subfolders): sys.exit(1) def main(): - # 0. General Prep: Setup Environment and Git Folder Health Check print("\n===== Step 0: General Prep =====") @@ -536,7 +502,7 @@ def main(): # 2. Write modifications to the API print("\n===== Step 2: Write Modifications to API =====") - write_modifications_to_api(base_dir, folders) + write_modifications_to_api(base_dir, folders, api_token) print("===== End of Step 2 =====\n") # 3. Fetch and process scripts @@ -544,13 +510,13 @@ def main(): # Initialize counters and sets shell_summary, current_scripts = defaultdict(int), set() print("Fetching scripts...") - user_defined_scripts = fetch_data(f"{domain}/scripts/?showHiddenScripts=true") + user_defined_scripts = fetch_data(f"{domain}/scripts/?showHiddenScripts=true", headers) 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...") - snippets = fetch_data(f"{domain}/scripts/snippets/") + snippets = fetch_data(f"{domain}/scripts/snippets/", headers) 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 From 64543ef1887d7e7832988ed86ebdafdc2444239b Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 11 Apr 2025 09:28:23 +0000 Subject: [PATCH 294/447] Update ./scripts/Backend/Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 74 ++++++++++++++----- 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index c9175735..4558a038 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -103,6 +103,7 @@ import requests import re import socket +from requests.exceptions import RequestException, HTTPError # Toggle flags ENABLE_GIT_PULL = True @@ -126,9 +127,21 @@ def delete_obsolete_files(folder, current_scripts): except Exception as e: print(f"Could not delete dir {d}: {e}") def sanitize_filename(name: str) -> str: - name = name.replace('\0', '') - - return re.sub(r'[<>:"/\\|?*]', '', name).strip() + 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: {', '.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'}...") @@ -143,7 +156,7 @@ def process_scripts(scripts, script_folder, script_raw_folder, shell_summary, is folder.mkdir(parents=True, exist_ok=True) raw_folder.mkdir(parents=True, exist_ok=True) - data = s if is_snippet else fetch_data(f"{domain}/scripts/{sid}/download/?with_snippets=false", headers) + data = s if is_snippet else fetch_data(f"{domain}/scripts/{sid}/download/?with_snippets=false") if not data: continue code = data.get('code') @@ -177,7 +190,7 @@ def save_file(path, content, is_json=False): else: print(f"File would be saved (simulation): {path}") -def fetch_data(url, headers): +def fetch_data(url): print(f"Fetching: {url}") r = requests.get(url, headers=headers) if r.ok: @@ -186,7 +199,7 @@ def fetch_data(url, headers): print(f"Error {r.status_code}") return [] -def write_modifications_to_api(base_dir, folders, api_token): +def write_modifications_to_api(base_dir, folders): """Compare local script files and JSON definitions, then push mismatches to the API.""" print("Comparing script files with JSON files...") mismatches = [] @@ -241,7 +254,7 @@ def write_modifications_to_api(base_dir, folders, api_token): try: if ENABLE_WRITEBACK: print(f"Updating API for {'snippet' if is_snippet else 'script'} {match}...") - update_api(raw_data.get('id'), updated_payload, api_token, is_snippet) + update_api(raw_data.get('id'), updated_payload, is_snippet) total_updated += 1 else: print(f"Simulated push for {'snippet' if is_snippet else 'script'} {match}:") @@ -259,7 +272,7 @@ def write_modifications_to_api(base_dir, folders, api_token): print(f"Total updates: {total_updated}") print(f"Total skipped: {total_skipped}") -def update_api(item_id, payload, api_token, is_snippet=False): +def update_api(item_id, payload, is_snippet=False): """Update the API with the provided item ID and payload.""" # Correctly handle 'code' or 'script_body' based on whether it's a snippet or a script @@ -275,7 +288,7 @@ def update_api(item_id, payload, api_token, is_snippet=False): 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={"X-API-KEY": api_token}, json=payload, timeout=120) + 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) @@ -403,24 +416,23 @@ def check_git_health(base_dir): def pre_flight(): - global domain, api_token, scriptpath, headers - + global domain, scriptpath, headers domain = os.getenv('DOMAIN') api_token = os.getenv('API_TOKEN') scriptpath = os.getenv('SCRIPTPATH') - missing = [var for var in ['DOMAIN', 'API_TOKEN', 'SCRIPTPATH'] if not globals().get(var.lower())] + 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(" - DOMAIN: The URL of your RMM API.(eg. api-rmm.exemple.com)") + if var == 'DOMAIN': print(" - DOMAIN: The URL of your RMM API. (e.g. api-rmm.example.com)") if var == 'API_TOKEN': print(" - API_TOKEN: An API token for a user with permission to access and write scripts.") if var == 'SCRIPTPATH': print(" - SCRIPTPATH: The local folder path for Git commands.") sys.exit(1) headers = {"X-API-KEY": api_token} domain_for_connection = domain.replace("https://", "").replace("http://", "") - + try: socket.create_connection((domain_for_connection, 443), timeout=5) print(f"✓ Connectivity to {domain} on port 443 OK.") @@ -431,17 +443,38 @@ def pre_flight(): if not domain.startswith("http://") and not domain.startswith("https://"): domain = "https://" + domain + 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("✓ API token validated successfully for read.") + print(f"✓ Token valid for read access: {obfuscated}") else: - print(f"✗ Error: Invalid API token. Status code: {response.status_code}") + print(f"✗ Token read access denied (status {response.status_code})") sys.exit(1) except Exception as e: - print(f"✗ Error: API token validation failed - {e}") + print(f"✗ Token read access check failed: {e}") sys.exit(1) + ''' + this does not work the api will create an empty file need to find another way. + try: + response = requests.post(f"{domain}/scripts/", headers=headers, json={}, timeout=5) + if response.status_code in (200, 201, 400): + print(f"✓ Token valid for write access: {obfuscated}") + elif response.status_code == 403: + print("✗ Token write access denied (status 403)") + sys.exit(1) + else: + print(f"✗ Token write access denied (status {response.status_code})") + sys.exit(1) + except Exception as e: + print(f"✗ Token write access check failed: {e}") + sys.exit(1) + ''' + + return + def check_and_create_folders(base_path, subfolders): try: if not base_path.exists(): @@ -462,6 +495,7 @@ def check_and_create_folders(base_path, subfolders): sys.exit(1) def main(): + # 0. General Prep: Setup Environment and Git Folder Health Check print("\n===== Step 0: General Prep =====") @@ -502,7 +536,7 @@ def main(): # 2. Write modifications to the API print("\n===== Step 2: Write Modifications to API =====") - write_modifications_to_api(base_dir, folders, api_token) + write_modifications_to_api(base_dir, folders) print("===== End of Step 2 =====\n") # 3. Fetch and process scripts @@ -510,13 +544,13 @@ def main(): # Initialize counters and sets shell_summary, current_scripts = defaultdict(int), set() print("Fetching scripts...") - user_defined_scripts = fetch_data(f"{domain}/scripts/?showHiddenScripts=true", headers) + user_defined_scripts = fetch_data(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...") - snippets = fetch_data(f"{domain}/scripts/snippets/", headers) + snippets = fetch_data(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 From e44197bece38c16324e87443d11d8b67b7515512 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 11 Apr 2025 09:37:20 +0000 Subject: [PATCH 295/447] Update ./scripts/Backend/Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 39 +++++++------------ 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index 4558a038..e66222e5 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -79,6 +79,7 @@ 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 .TODO @@ -88,7 +89,6 @@ send workflow flags to ENV default to true Delete script support from git ? (dedicated function required as the current delete_obsolete_files only work based on api) Review flow of step 3 for optimisations - move all big var to global and ensure they are used from global only. add api check for write. """ @@ -190,14 +190,20 @@ def save_file(path, content, is_json=False): else: print(f"File would be saved (simulation): {path}") + def fetch_data(url): - print(f"Fetching: {url}") - r = requests.get(url, headers=headers) - if r.ok: - print("Success.") - return r.json() - print(f"Error {r.status_code}") - return [] + 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 write_modifications_to_api(base_dir, folders): """Compare local script files and JSON definitions, then push mismatches to the API.""" @@ -456,23 +462,6 @@ def pre_flight(): print(f"✗ Token read access check failed: {e}") sys.exit(1) - ''' - this does not work the api will create an empty file need to find another way. - try: - response = requests.post(f"{domain}/scripts/", headers=headers, json={}, timeout=5) - if response.status_code in (200, 201, 400): - print(f"✓ Token valid for write access: {obfuscated}") - elif response.status_code == 403: - print("✗ Token write access denied (status 403)") - sys.exit(1) - else: - print(f"✗ Token write access denied (status {response.status_code})") - sys.exit(1) - except Exception as e: - print(f"✗ Token write access check failed: {e}") - sys.exit(1) - ''' - return def check_and_create_folders(base_path, subfolders): From df700767a9c0feec07a6d9631f4334f5ecf545f0 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 11 Apr 2025 12:21:07 +0000 Subject: [PATCH 296/447] Update ./scripts/Backend/Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 273 ++++++++++++------ 1 file changed, 179 insertions(+), 94 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index e66222e5..b81d9269 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -37,30 +37,38 @@ 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 Y Exports functional, adds script ID to from as "id - " - v5.a Y "id - " for only raw folder. Fixed to use X-API-KEY - v5.1 Y Sanitizing script names when has / in it - v5.2 Y moving url and api token to .env file - v5.3 Y Making script folders be subfolders of where export.py file is - v5.4 Y making filenames utf-8 compliant - v5.5 7/11/2024 X Save PowerShell scripts with .ps1 and Python scripts with .py extensions - v5.6 7/11/2024 X Count the total number of scripts and print at the end - v5.7 7/11/2024 X Print a summary of all the different types of shells exported - v5.8 7/11/2024 X Add support for additional shell extension types - v5.9 7/11/2024 X Detect deleted scripts and delete them from both folders - v6 7/31/2024 SAN Add support for specifying the save folder via the SCRIPTPATH environment variable - v6.0.1 7/31/2024 SAN Add Git integration to push changes to the configured Git repository + 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 @@ -80,16 +88,14 @@ 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 .TODO - Add reporting support Move raws from "scriptsraw" to scripts/subfolder/raws/ to group them with their scripts - send workflow flags to ENV default to true - Delete script support from git ? (dedicated function required as the current delete_obsolete_files only work based on api) - Review flow of step 3 for optimisations - add api check for write. + Delete script support from git ? (dedicated function required in 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) + Review flow of step 3 for optimisations + Split step 2 into more functions for clarity """ @@ -104,27 +110,55 @@ import re import socket from requests.exceptions import RequestException, HTTPError +import os -# Toggle flags -ENABLE_GIT_PULL = True -ENABLE_GIT_PUSH = True -ENABLE_WRITEBACK = True -ENABLE_WRITETOFILE = True -# Can be changed to "main" or other if needed. -git_pull_branch = 'master' + +# Retrieve the git pull branch or default to 'master' +git_pull_branch = os.getenv('GIT_PULL_BRANCH', 'master') +if git_pull_branch != 'master': print(f"Git Pull Branch: {git_pull_branch}") + +# 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): print(f"Cleaning {folder}...") obsolete = {f for f in folder.rglob('*') 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.") + for f in obsolete: - try: f.unlink(); print(f"Deleted: {f}") - except Exception as e: print(f"Error deleting {f}: {e}") + if ENABLE_WRITETOFILE: + try: + f.unlink() + print(f"Deleted file no longer in the API: {f}") + except Exception as e: + print(f"Error deleting file {f}: {e}") + else: + print(f"Simulated deletion of file no longer in the API: {f}") + + empty_dirs = [d for d in sorted(folder.rglob('*'), 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.") + + for d in empty_dirs: + if ENABLE_WRITETOFILE: + try: + d.rmdir() + print(f"Removed empty directory: {d}") + except Exception as e: + print(f"Could not delete dir {d}: {e}") + else: + print(f"Simulated removal of empty directory: {d}") - for d in sorted(folder.rglob('*'), key=lambda p: -len(p.parts)): - if d.is_dir() and not any(d.iterdir()): - try: d.rmdir(); print(f"Removed empty dir: {d}") - except Exception as e: print(f"Could not delete dir {d}: {e}") def sanitize_filename(name: str) -> str: removed_chars = [] @@ -139,7 +173,7 @@ def sanitize_filename(name: str) -> str: name = re.sub(r'[<>:"/\\|?*]', '', name) if removed_chars: - print(f"Removed: {', '.join(removed_chars)}") + print(f"Removed from file name: {', '.join(removed_chars)}") return name.strip() @@ -156,7 +190,7 @@ def process_scripts(scripts, script_folder, script_raw_folder, shell_summary, is folder.mkdir(parents=True, exist_ok=True) raw_folder.mkdir(parents=True, exist_ok=True) - data = s if is_snippet else fetch_data(f"{domain}/scripts/{sid}/download/?with_snippets=false") + 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') @@ -191,7 +225,7 @@ def save_file(path, content, is_json=False): print(f"File would be saved (simulation): {path}") -def fetch_data(url): +def pull_from_api(url): try: print(f"Fetching: {url}") r = requests.get(url, headers=headers) @@ -260,7 +294,7 @@ def write_modifications_to_api(base_dir, folders): try: if ENABLE_WRITEBACK: print(f"Updating API for {'snippet' if is_snippet else 'script'} {match}...") - update_api(raw_data.get('id'), updated_payload, is_snippet) + update_to_api(raw_data.get('id'), updated_payload, is_snippet) total_updated += 1 else: print(f"Simulated push for {'snippet' if is_snippet else 'script'} {match}:") @@ -278,10 +312,9 @@ def write_modifications_to_api(base_dir, folders): print(f"Total updates: {total_updated}") print(f"Total skipped: {total_skipped}") -def update_api(item_id, payload, is_snippet=False): +def update_to_api(item_id, payload, is_snippet=False): """Update the API with the provided item ID and payload.""" - # Correctly handle 'code' or 'script_body' based on whether it's a snippet or a script if is_snippet: payload['code'] = payload.pop('code', '') endpoint = f"{domain}/scripts/snippets/{item_id}/" @@ -319,26 +352,36 @@ def git_pull(base_dir): sys.exit(1) + +def generate_commit_message(base_dir, max_files=5): + """Generate a commit message based on staged changes.""" + # Get the list of staged changes + result = subprocess.run( + ['git', '-C', base_dir, 'diff', '--cached', '--name-status'], + capture_output=True, text=True, check=True + ) + + changes = {"created": [], "modified": [], "deleted": [], "renamed": []} + for line in result.stdout.strip().split("\n"): + if not line: continue + status, file = line.split("\t") + if 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) + elif status.startswith("R"): changes["renamed"].append(f"{line.split()[1]} -> {line.split()[2]}") + + if not any(changes.values()): + return "Minor update" + + parts = [f"{change_type} {len(files)}: {', '.join(files[:max_files])}{'...' if len(files) > max_files else ''}" + for change_type, files in changes.items() if files] + return "; ".join(parts) + def git_push(base_dir): """Push local changes to the git repository.""" try: - # Check if a rebase is in progress - rebase_in_progress = subprocess.run( - ['git', '-C', base_dir, 'rebase', '--show-current-patch'], - capture_output=True, text=True - ).returncode == 0 - if rebase_in_progress: - sys.exit("Rebase in progress. Complete or abort it.") - - # Get current branch - branch_name = subprocess.run( - ['git', '-C', base_dir, 'rev-parse', '--abbrev-ref', 'HEAD'], - capture_output=True, text=True - ).stdout.strip() or "update-scripts" - if branch_name == 'HEAD': - subprocess.check_call(['git', '-C', base_dir, 'checkout', '-b', branch_name]) - - # Get staged changes + # Get staged changes if none do nothing status_result = subprocess.run( ['git', '-C', base_dir, 'status', '--porcelain'], capture_output=True, text=True @@ -346,37 +389,14 @@ def git_push(base_dir): if status_result.stdout: subprocess.check_call(['git', '-C', base_dir, 'add', '.']) - # Get the list of staged changes - result = subprocess.run( - ['git', '-C', base_dir, 'diff', '--cached', '--name-status'], - capture_output=True, text=True, check=True - ) - changes = {"created": [], "modified": [], "deleted": [], "renamed": []} - for line in result.stdout.strip().split("\n"): - if not line: continue - status, file = line.split("\t") - if 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) - elif status.startswith("R"): changes["renamed"].append(f"{line.split()[1]} -> {line.split()[2]}") - - # Generate commit message - def generate_commit_message(changes, max_files=5): - if not any(changes.values()): return "Minor update" - parts = [f"{change_type} {len(files)}: {', '.join(files[:max_files])}{'...' if len(files) > max_files else ''}" - for change_type, files in changes.items() if files] - return "; ".join(parts) - - commit_message = generate_commit_message(changes) - - # Commit changes + 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 to branch '{branch_name}': {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}'") - # Push changes - subprocess.check_call(['git', '-C', base_dir, 'push', 'origin', branch_name]) - print(f"Changes pushed to branch '{branch_name}'") else: print("No changes to commit.") except subprocess.CalledProcessError as e: @@ -384,22 +404,56 @@ def generate_commit_message(changes, max_files=5): def check_git_health(base_dir): - git_dir = Path(base_dir) / '.git' - if not git_dir.exists(): - print(f"Error: .git folder not found in {base_dir}") - return False + """Check the health of the Git repository.""" + + # 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: @@ -409,6 +463,17 @@ def check_git_health(base_dir): 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: @@ -417,10 +482,29 @@ def check_git_health(base_dir): 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', 'HEAD..origin/{}'.format(git_pull_branch)]).decode().strip() + if int(commits_behind) > 0: + print(f"Warning: 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, scriptpath, headers domain = os.getenv('DOMAIN') @@ -485,10 +569,10 @@ def check_and_create_folders(base_path, subfolders): def main(): - # 0. General Prep: Setup Environment and Git Folder Health Check + # 0. Prep: Verify Dependencies, Set Up Environment, and Git Health Check print("\n===== Step 0: General Prep =====") - # ENV vars & network checks + # ENV vars & network checks pre_flight() # Folder structure check @@ -533,13 +617,13 @@ def main(): # Initialize counters and sets shell_summary, current_scripts = defaultdict(int), set() print("Fetching scripts...") - user_defined_scripts = fetch_data(f"{domain}/scripts/?showHiddenScripts=true") + 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...") - snippets = fetch_data(f"{domain}/scripts/snippets/") + 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 @@ -547,6 +631,7 @@ def main(): print("Shell summary:", "\n".join(f"{shell}: {count}" for shell, count in shell_summary.items())) # 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) From 238a182d7a47cc3c215bfafcd9de95f9e6572f7a Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 14 Apr 2025 08:39:35 +0000 Subject: [PATCH 297/447] Update ./scripts/Backend/Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 133 ++++++++++++------ 1 file changed, 93 insertions(+), 40 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index b81d9269..33a2f1c5 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -89,13 +89,26 @@ 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 .TODO + Review flow of step 3 for optimisations + + Revamp folder structure: + Move raws from "scriptsraw" to Category/folder/raws/ + add "uncategorised" folder + remove "scripts" top level folder while keeping snippets + + 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) Add reporting support - Move raws from "scriptsraw" to scripts/subfolder/raws/ to group them with their scripts - Delete script support from git ? (dedicated function required in 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) - Review flow of step 3 for optimisations - Split step 2 into more functions for clarity """ @@ -110,7 +123,6 @@ import re import socket from requests.exceptions import RequestException, HTTPError -import os @@ -128,6 +140,8 @@ 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): print(f"Cleaning {folder}...") obsolete = {f for f in folder.rglob('*') if f.is_file() and f.relative_to(folder) not in current_scripts} @@ -159,7 +173,6 @@ def delete_obsolete_files(folder, current_scripts): else: print(f"Simulated removal of empty directory: {d}") - def sanitize_filename(name: str) -> str: removed_chars = [] @@ -238,9 +251,62 @@ def pull_from_api(url): 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): - """Compare local script files and JSON definitions, then push mismatches to the API.""" print("Comparing script files with JSON files...") mismatches = [] @@ -257,8 +323,13 @@ def write_modifications_to_api(base_dir, folders): for raw_path in folder.rglob('*.json'): total_files_checked += 1 raw_name = re.sub(r'^\d+ - ', '', raw_path.stem).lower() - match = next((p for p in folders[folder_name].rglob('*') - if p.is_file() and p.stem.lower() == raw_name), None) + 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_skipped += 1 + continue if not match: print(f"No match for {'snippet' if is_snippet else 'script'}: {raw_path}") @@ -267,43 +338,28 @@ def write_modifications_to_api(base_dir, folders): print(f"Matched {'snippet' if is_snippet else 'script'}: {match} <-> {raw_path}") total_matches += 1 - file_hash = compute_hash(match) - with raw_path.open(encoding='utf-8') as f: - raw_data = json.load(f) - code = raw_data.get('code', '') - code_hash = hashlib.sha256(code.encode('utf-8')).hexdigest() + file_hash, code_hash, raw_data = compare_files_and_hashes(match, raw_path) - print(f"{'Snippet' if is_snippet else 'Script'} hash: {file_hash}\nJSON hash: {code_hash}") - - if file_hash != code_hash: + 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) ---") - with match.open(encoding='utf-8') as f: - for i, line in enumerate(f): - if i >= 10: break - print(line.strip()) + 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 code.splitlines()[:10]: + for line in raw_data.get('code', '').splitlines()[:10]: print(line.strip()) - with match.open(encoding='utf-8') as f: - updated_payload = {**raw_data, 'code': f.read()} + updated = update_api_if_needed(match, raw_data, is_snippet) - 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) - total_updated += 1 - 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() - except BrokenPipeError: - sys.stderr.close() - sys.stdout.close() + if updated: + total_updated += 1 print("\nComparison Complete:") print(f"Total files checked: {total_files_checked}") @@ -351,8 +407,6 @@ def git_pull(base_dir): print(f"Failed to force-pull changes from Git: {e}") sys.exit(1) - - def generate_commit_message(base_dir, max_files=5): """Generate a commit message based on staged changes.""" # Get the list of staged changes @@ -402,7 +456,6 @@ def git_push(base_dir): except subprocess.CalledProcessError as e: print(f"Git operation failed: {e}") - def check_git_health(base_dir): """Check the health of the Git repository.""" From 94a585c37fdd2a78030c7fae93b8472e033d982e Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 15 Apr 2025 06:36:43 +0000 Subject: [PATCH 298/447] Update ./scripts/TasksUpdater/Updater P3 Run WU.ps1 --- .../TasksUpdater/Updater P3 Run WU.ps1 | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 b/scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 index df4a8227..0bc6ed41 100644 --- a/scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 +++ b/scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 @@ -3,8 +3,9 @@ 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 reboots the system if required. - It is designed to run daily to ensure all modules are up to date and log the update process for tracking purposes. + 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: @@ -12,13 +13,14 @@ * 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. - It then schedules or executes Windows updates at the parsed time, ensuring compliance with the predefined schedule. .EXAMPLE Schedules={{agent.Schedules}} Company_folder_path={{global.Company_folder_path}} + BLACKLISTED_KBS=KB1234567,KB1234567,KB1234567 .NOTES Author: SAN // MSA @@ -33,10 +35,11 @@ 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. #> + # Name will be used for both the name of the log file and what line of the Schedules to parse $PartName = "WindowsUpdate" @@ -74,6 +77,21 @@ if (Get-Module -ListAvailable -Name PSWindowsUpdate) { } } +# 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" From 5c59fac95b3398c5bb08751d1c18cdc87c23f0d8 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 15 Apr 2025 10:50:47 +0000 Subject: [PATCH 299/447] Update ./scripts/Checks/Backup Veeam SPC.py --- scripts_staging/Checks/Backup Veeam SPC.py | 245 +++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 scripts_staging/Checks/Backup Veeam SPC.py 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 From 16f217f8acb8b2e7b6330debd01f7c75d47f5dac Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 15 Apr 2025 10:53:47 +0000 Subject: [PATCH 300/447] Update ./scripts/Backend/Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index 33a2f1c5..131d1027 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -92,6 +92,7 @@ v9.0.3.1 14/04/25 SAN split step 2 into functions for easier upgrade .TODO + Handle rights issues when executing git commands Review flow of step 3 for optimisations Revamp folder structure: From 1c81d90de291effda115e7d751778b2a376b8f8d Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 15 Apr 2025 13:48:57 +0000 Subject: [PATCH 301/447] Update ./scripts/Tools/Measures TCP Latency.ps1 --- .../Tools/Measures TCP Latency.ps1 | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 scripts_staging/Tools/Measures TCP Latency.ps1 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 From bc172d688f4a9fc1822f9ddb0241db5d450793ac Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 16 Apr 2025 09:08:05 +0000 Subject: [PATCH 302/447] Update ./scripts/Archives/Backup Veeam api v1_7.py --- .../Archives/Backup Veeam api v1_7.py | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 scripts_staging/Archives/Backup Veeam api v1_7.py 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() From 8129d4a8901f2355d2bb7fce8926650e648013c0 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 16 Apr 2025 09:08:48 +0000 Subject: [PATCH 303/447] Update ./scripts/Backend/Repo package updater.py --- .../Backend/Repo package updater.py | 178 ++++++++++-------- 1 file changed, 100 insertions(+), 78 deletions(-) diff --git a/scripts_staging/Backend/Repo package updater.py b/scripts_staging/Backend/Repo package updater.py index 5c25c91b..569404d5 100644 --- a/scripts_staging/Backend/Repo package updater.py +++ b/scripts_staging/Backend/Repo package updater.py @@ -1,10 +1,12 @@ #!/usr/bin/python3 -#public import os -import requests -import subprocess import re import time +import requests +import subprocess +from fnmatch import fnmatch +from pathlib import Path + """ .SYNOPSIS @@ -19,18 +21,30 @@ - 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 packages to env + 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 """ @@ -87,92 +101,100 @@ "icinga2" ] -# Retrieve variables from environment -outdir = os.getenv("CHOCOLATEY_OUTDIR") -local_choco_server = os.getenv("CHOCOLATEY_LOCAL_SERVER") -api_key = os.getenv("CHOCOLATEY_API_KEY") -base_url = os.getenv("CHOCOLATEY_BASE_URL", "https://community.chocolatey.org/api/v2/package/") -# Check if all required environment variables are set -if not outdir: - print("Error: CHOCOLATEY_OUTDIR environment variable is not set.") - exit(1) -if not local_choco_server: - print("Error: CHOCOLATEY_LOCAL_SERVER environment variable is not set.") - exit(1) -if not api_key: - print("Error: CHOCOLATEY_API_KEY environment variable is not set.") - exit(1) -# Ensure the output directory exists, if not, create it -if not os.path.exists(outdir): +# 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) -# Variable to track if any failure occurred error_occurred = False -# Iterate through each package name -for package_name in package_names: - # Wait before downloading the next package +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) - - # Construct the full URL for the package package_url = base_url + package_name - # Retry logic for downloading the package - for attempt in range(3): - try: - # Send a GET request to download the package - response = requests.get(package_url) - - # Check if the request was successful - if response.status_code == 200: - # Get the default filename from the response headers - default_filename = os.path.basename(response.url) - filepath = os.path.join(outdir, default_filename) - - # Extract version from filename - version_match = re.search(r"(\d+\.\d+(\.\d+)?)", default_filename) - if version_match: - version = version_match.group(1) - - # Debug: Print filename and version - print(f"Package '{package_name}': Filename = {default_filename}, Version = {version}") - - # TODO: Query the local Chocolatey server to check if this package version already exists - # If version already exists on the server, skip downloading - if any(version in filename for filename in os.listdir(outdir)): - print(f"Package '{package_name}' with version {version} already exists in the directory. Skipping.") - break - - # Save the package to a file in the specified directory using the default filename - with open(filepath, "wb") as file: - file.write(response.content) - print(f"Package '{package_name}' downloaded successfully.") - - # Push the package to the local Chocolatey server - push_command = f"choco push \"{filepath}\" --source={local_choco_server} --api-key='{api_key}' --force" - subprocess.run(push_command, shell=True, check=True) - print(f"Package '{package_name}' pushed to the local Chocolatey server.") - break - - else: - print(f"Failed to download package '{package_name}'. Status code: {response.status_code}") - - except (requests.exceptions.RequestException, subprocess.CalledProcessError) as e: - print(f"An error occurred while processing package '{package_name}': {str(e)}") - - # Retry after 1 minute if an error occurred and this is not the final attempt - if attempt < 2: - print(f"Retrying download for '{package_name}' in 1 minute... (Attempt {attempt + 2}/3)") + 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) - # If all 3 attempts failed, mark error_occurred as True - else: - print(f"Failed to process package '{package_name}' after 3 attempts.") + 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 -# Exit with status code 1 if any error occurred + if debug: + print("[DEBUG] Dry run mode: exiting after first package.") + break + if error_occurred: exit(1) \ No newline at end of file From 518b693adea74b3d152d5a199a6acb21e35a25d5 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 16 Apr 2025 09:09:34 +0000 Subject: [PATCH 304/447] Update ./scripts/Checks/Backup Veeam agent.ps1 --- scripts_staging/Checks/Backup Veeam agent.ps1 | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 scripts_staging/Checks/Backup Veeam agent.ps1 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 +} From 0f7eca548b25b60ac54d97b7092678a7def2bbe4 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 22 Apr 2025 07:42:23 +0000 Subject: [PATCH 305/447] Update file: Upgrade OS to Windows Server X Standard.ps1 --- .../Upgrade OS to Windows Server X Standard.ps1 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 index e3f59686..bddd129b 100644 --- a/scripts_staging/Build/Upgrade OS to Windows Server X Standard.ps1 +++ b/scripts_staging/Build/Upgrade OS to Windows Server X Standard.ps1 @@ -37,48 +37,48 @@ $serverVersions = @{ "2016" = @{ "en" = @{ "file" = "en_windows_server_2016_vl_x64_dvd_11636701.iso" - "checksum" = "47919CE8B4993F531CA1FA3F85941F4A72B47EBAA4D3A321FECF83CA9D17E6B8" # pragma: allowlist-secret + "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 + "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 + "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 + "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 + "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 + "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 + "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 + "checksum" = "45384960A3F430D26454955D1198A6E38E7AA98C9E3906AC1AE9367229C103D0" # pragma: allowlist-secret #trufflehog:ignore "licenseKey" = "TVRH6-WHNXV-R9WG3-9XRFY-MY832" } } From 098a8c16421f0c68ec46515389289d227963b7cb Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 22 Apr 2025 08:18:00 +0000 Subject: [PATCH 306/447] Update file: Disk RW.ps1 --- scripts_staging/Checks/Disk RW.ps1 | 76 +++++++++++++----------------- 1 file changed, 32 insertions(+), 44 deletions(-) diff --git a/scripts_staging/Checks/Disk RW.ps1 b/scripts_staging/Checks/Disk RW.ps1 index a43e4eec..94dc5258 100644 --- a/scripts_staging/Checks/Disk RW.ps1 +++ b/scripts_staging/Checks/Disk RW.ps1 @@ -28,38 +28,26 @@ .CHANGELOG SAN 11.12.24 Moved vars to env + SAN 17.04.25 Default to temp dir if no value provided and code cleanup #> -# Variables for thresholds with default values from environment or script -$ReadWarnThresholdMBps = [int]$env:READ_WARN_THRESHOLD_MBPS -$ReadErrorThresholdMBps = [int]$env:READ_ERROR_THRESHOLD_MBPS -$WriteWarnThresholdMBps = [int]$env:WRITE_WARN_THRESHOLD_MBPS -$WriteErrorThresholdMBps = [int]$env:WRITE_ERROR_THRESHOLD_MBPS +# 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) -# Set default values if environment variables are not set -if (-not $ReadWarnThresholdMBps) { $ReadWarnThresholdMBps = 2000 } -if (-not $ReadErrorThresholdMBps) { $ReadErrorThresholdMBps = 1500 } -if (-not $WriteWarnThresholdMBps) { $WriteWarnThresholdMBps = 80 } -if (-not $WriteErrorThresholdMBps) { $WriteErrorThresholdMBps = 50 } - -# Function to test disk speed using a test file +# Function to test disk speed function Test-DiskSpeed { param ( - [string]$TestFile = $env:target_file, # Get target file from environment variable + [string]$TestFile = $(if ($env:target_file) { $env:target_file } else { "C:\Windows\Temp\disk_test.tmp" }), [int]$FileSizeInMB = 1024 ) - # Check if the environment variable is set - if (-not $TestFile) { - Write-Output "Error: Environment variable 'target_file' is not set or is empty." - exit 1 # Exit with warning code if the variable is not set or empty - } - - # Create a buffer for writing $buffer = New-Object byte[] (1MB) $rnd = New-Object Random - # Write speed test + # Write test $writeStart = Get-Date $stream = [System.IO.File]::Create($TestFile) for ($i = 0; $i -lt $FileSizeInMB; $i++) { @@ -67,43 +55,43 @@ function Test-DiskSpeed { $stream.Write($buffer, 0, $buffer.Length) } $stream.Close() - $writeEnd = Get-Date - $writeDuration = ($writeEnd - $writeStart).TotalSeconds - $writeSpeedMBps = $FileSizeInMB / $writeDuration + $writeDuration = (Get-Date) - $writeStart + $writeSpeedMBps = $FileSizeInMB / $writeDuration.TotalSeconds - # Read speed test + # Read test $readStart = Get-Date $stream = [System.IO.File]::OpenRead($TestFile) - while ($stream.Read($buffer, 0, $buffer.Length)) { - # Reading the file - } + while ($stream.Read($buffer, 0, $buffer.Length)) { } $stream.Close() - $readEnd = Get-Date - $readDuration = ($readEnd - $readStart).TotalSeconds - $readSpeedMBps = $FileSizeInMB / $readDuration + $readDuration = (Get-Date) - $readStart + $readSpeedMBps = $FileSizeInMB / $readDuration.TotalSeconds - # Cleanup - Remove-Item $TestFile + Remove-Item -Force $TestFile return [pscustomobject]@{ WriteSpeedMBps = [math]::Round($writeSpeedMBps, 2) - ReadSpeedMBps = [math]::Round($readSpeedMBps, 2) + ReadSpeedMBps = [math]::Round($readSpeedMBps, 2) + TestFile = $TestFile } } -# Run the test +# Run and evaluate $speedResults = Test-DiskSpeed -# Output the results Write-Output "W: $($speedResults.WriteSpeedMBps) MB/s" Write-Output "R: $($speedResults.ReadSpeedMBps) MB/s" -Write-Output "T: $env:target_file " - -# Check conditions for exit codes based on thresholds -if ($speedResults.WriteSpeedMBps -lt $WriteErrorThresholdMBps -or $speedResults.ReadSpeedMBps -lt $ReadErrorThresholdMBps) { - exit 2 # Error condition if below error thresholds -} elseif ($speedResults.WriteSpeedMBps -lt $WriteWarnThresholdMBps -or $speedResults.ReadSpeedMBps -lt $ReadWarnThresholdMBps) { - exit 1 # Warning condition if below warning thresholds +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 # All good + exit 0 } From eb0076de3adb550c7c6e7db2bad4899a884bb1d4 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 24 Apr 2025 08:29:07 +0000 Subject: [PATCH 307/447] Update file: Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index 131d1027..36e8a6d4 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -90,6 +90,7 @@ 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 .TODO Handle rights issues when executing git commands @@ -569,9 +570,9 @@ def pre_flight(): if missing: print(f"✗ Error: Missing environment variable(s): {', '.join(missing)}") for var in missing: - if var == 'DOMAIN': print(" - DOMAIN: The URL of your RMM API. (e.g. api-rmm.example.com)") - if var == 'API_TOKEN': print(" - API_TOKEN: An API token for a user with permission to access and write scripts.") - if var == 'SCRIPTPATH': print(" - SCRIPTPATH: The local folder path for Git commands.") + 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) headers = {"X-API-KEY": api_token} @@ -581,7 +582,8 @@ def pre_flight(): socket.create_connection((domain_for_connection, 443), timeout=5) print(f"✓ Connectivity to {domain} on port 443 OK.") except Exception as e: - print(f"✗ Error: Unable to connect to {domain} on port 443 - {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) if not domain.startswith("http://") and not domain.startswith("https://"): @@ -594,10 +596,10 @@ def pre_flight(): if response.status_code == 200: print(f"✓ Token valid for read access: {obfuscated}") else: - print(f"✗ Token read access denied (status {response.status_code})") + 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}") + print(f"✗ Token read access check failed: {e} - Obfuscated Token: {obfuscated}") sys.exit(1) return From 94cf346cba5188977b4bea0975b5fa275e461202 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 24 Apr 2025 09:00:25 +0000 Subject: [PATCH 308/447] Update file: Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index 36e8a6d4..3080c2a9 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -91,6 +91,8 @@ 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 + .TODO Handle rights issues when executing git commands @@ -409,29 +411,47 @@ def git_pull(base_dir): print(f"Failed to force-pull changes from Git: {e}") sys.exit(1) + def generate_commit_message(base_dir, max_files=5): """Generate a commit message based on staged changes.""" - # Get the list of staged changes result = subprocess.run( ['git', '-C', base_dir, 'diff', '--cached', '--name-status'], capture_output=True, text=True, check=True ) - + changes = {"created": [], "modified": [], "deleted": [], "renamed": []} + for line in result.stdout.strip().split("\n"): - if not line: continue - status, file = line.split("\t") - if 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) - elif status.startswith("R"): changes["renamed"].append(f"{line.split()[1]} -> {line.split()[2]}") + 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 old.startswith("scriptsraw/") or old.startswith("snippetsraw/"): + continue + changes["renamed"].append(f"{old} -> {new}") + elif len(parts) >= 2: + file = parts[1] + if 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 = [f"{change_type} {len(files)}: {', '.join(files[:max_files])}{'...' if len(files) > max_files else ''}" - for change_type, files in changes.items() if files] + parts = [ + f"{change_type} {len(files)}: {', '.join(files[:max_files])}{'...' if len(files) > max_files else ''}" + for change_type, files in changes.items() if files + ] + return "; ".join(parts) def git_push(base_dir): From 0986434e1d7b39f17797dc19b13af1aa05e52229 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 24 Apr 2025 13:02:03 +0000 Subject: [PATCH 309/447] Update file: Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 155 +++++++++++------- 1 file changed, 92 insertions(+), 63 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index 3080c2a9..8518bd04 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -92,6 +92,7 @@ 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 .TODO @@ -112,6 +113,7 @@ 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 """ @@ -394,32 +396,45 @@ def update_to_api(item_id, payload, is_snippet=False): 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 the latest changes from the git repository, discarding local changes.""" - if not os.path.isdir(base_dir): - print(f"Invalid directory: {base_dir}") - sys.exit(1) - print("Starting force pull...") + print("Starting pull process...", flush=True) try: - subprocess.check_call(['git', '-C', base_dir, 'fetch', 'origin']) - subprocess.check_call(['git', '-C', base_dir, 'reset', '--hard', f'origin/{git_pull_branch}']) - print(f"Successfully force-pulled the latest changes from the '{git_pull_branch}' 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(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(f"Failed to force-pull changes from Git: {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): - """Generate a commit message based on staged changes.""" +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": []} + changes = { + "created": [], + "modified": [], + "deleted": [], + "renamed": [] + } + + emoji_map = { + "created": "➕", + "modified": "📝", + "deleted": "🗑️", + "renamed": "🔁" + } for line in result.stdout.strip().split("\n"): if not line: @@ -430,12 +445,12 @@ def generate_commit_message(base_dir, max_files=5): if status.startswith("R") and len(parts) == 3: old, new = parts[1], parts[2] - if old.startswith("scriptsraw/") or old.startswith("snippetsraw/"): + 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 file.startswith("scriptsraw/") or file.startswith("snippetsraw/"): + if skip_raw_dirs and (file.startswith("scriptsraw/") or file.startswith("snippetsraw/")): continue if status.startswith("A"): changes["created"].append(file) @@ -447,10 +462,21 @@ def generate_commit_message(base_dir, max_files=5): if not any(changes.values()): return "Minor update" - parts = [ - f"{change_type} {len(files)}: {', '.join(files[:max_files])}{'...' if len(files) > max_files else ''}" - for change_type, files in changes.items() if files - ] + 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) @@ -478,31 +504,34 @@ def git_push(base_dir): except subprocess.CalledProcessError as e: print(f"Git operation failed: {e}") +import subprocess +from pathlib import Path + def check_git_health(base_dir): """Check the health of the Git repository.""" - + # 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.") + 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.") + 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.") + 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}") + print(f"❌ Error: Failed to check index lock - {e}") return False # Check if a rebase is in progress @@ -512,72 +541,72 @@ def check_git_health(base_dir): capture_output=True, text=True ).returncode == 0 if rebase_in_progress: - print("Error: Rebase in progress. Complete or abort it.") + print("❌ Error: Rebase in progress. Complete or abort it.") return False except subprocess.CalledProcessError: - print("Error: Failed to check rebase status.") + 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.") + print("❌ Error: There are unresolved merge conflicts.") return False except subprocess.CalledProcessError: - print("Error: Failed to check for merge conflicts.") + 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("Error: There are uncommitted changes in the Git repository.") - return False + print("⚠️ Warning: There are uncommitted changes in the Git repository.") except subprocess.CalledProcessError: - print("Error: Failed to check Git status.") + 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.") + print("❌ Error: There are untracked files in the Git repository.") return False except subprocess.CalledProcessError: - print("Error: Failed to check for untracked files.") + 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}'.") + 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.") + 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.") + print("❌ Error: No remote repository is configured.") return False except subprocess.CalledProcessError: - print("Error: Failed to retrieve remote repository information.") + 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', 'HEAD..origin/{}'.format(git_pull_branch)]).decode().strip() + 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"Warning: You are {commits_behind} commits behind the remote branch.") + print(f"❌ Error: You are {commits_behind} commits behind the remote branch.") return False except subprocess.CalledProcessError: - print("Error: Failed to check commit history.") + print("❌ Error: Failed to check commit history.") return False - + + print("✅ Git repository health check passed.") return True def pre_flight(): @@ -588,7 +617,7 @@ def pre_flight(): 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)}") + 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.") @@ -600,10 +629,10 @@ def pre_flight(): try: socket.create_connection((domain_for_connection, 443), timeout=5) - print(f"✓ Connectivity to {domain} on port 443 OK.") + 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})") + print(f"❌ Error: Unable to connect to {domain} on port 443 - {e} (Obfuscated API Token: {obfuscated})") sys.exit(1) if not domain.startswith("http://") and not domain.startswith("https://"): @@ -614,12 +643,12 @@ def pre_flight(): try: response = requests.get(f"{domain}/scripts/", headers=headers, timeout=5) if response.status_code == 200: - print(f"✓ Token valid for read access: {obfuscated}") + print(f"✅ Token valid for read access: {obfuscated}") else: - print(f"✗ Token read access denied (status {response.status_code}) - Obfuscated Token: {obfuscated}") + 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}") + print(f"❌ Token read access check failed: {e} - Obfuscated Token: {obfuscated}") sys.exit(1) return @@ -628,18 +657,18 @@ 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()}.") + print(f"✅ Root folder created at {base_path.resolve()}.") else: - print(f"✓ Root folder exists at {base_path.resolve()}.") + 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.") + 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()}.") + 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: Failed to create folder(s).") print(f"Error: {e}") sys.exit(1) @@ -660,14 +689,14 @@ def main(): "snippetsraw": base_dir / "snippetsraw" } check_and_create_folders(base_dir, folders) - print("✓ All folders created and verified.") + print("✅ All folders created and verified.") # 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.") + print("✅ Git repo is healthy.") else: - print("✗ Error: Git folder is not healthy.") + print("❌ Error: Git folder is not healthy.") sys.exit(1) else: print("Skipping Git health check because both pull and push are disabled.") From 58666006a43e585eb2ebca9e7c4f3e55d0abdfb9 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 24 Apr 2025 13:15:10 +0000 Subject: [PATCH 310/447] Update file: Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index 8518bd04..df897f3c 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -131,7 +131,6 @@ from requests.exceptions import RequestException, HTTPError - # Retrieve the git pull branch or default to 'master' git_pull_branch = os.getenv('GIT_PULL_BRANCH', 'master') if git_pull_branch != 'master': print(f"Git Pull Branch: {git_pull_branch}") @@ -147,7 +146,6 @@ if not ENABLE_WRITETOFILE: print("Write to file is disabled.") - def delete_obsolete_files(folder, current_scripts): print(f"Cleaning {folder}...") obsolete = {f for f in folder.rglob('*') if f.is_file() and f.relative_to(folder) not in current_scripts} @@ -504,9 +502,6 @@ def git_push(base_dir): except subprocess.CalledProcessError as e: print(f"Git operation failed: {e}") -import subprocess -from pathlib import Path - def check_git_health(base_dir): """Check the health of the Git repository.""" From ed4d35f359aa7a7d8954cd8f7275f222dc445d25 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 28 Apr 2025 18:58:40 +0000 Subject: [PATCH 311/447] Update file: Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 108 +++++++++++------- 1 file changed, 66 insertions(+), 42 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index df897f3c..656e7b19 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -93,6 +93,7 @@ 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.0 24/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 .TODO @@ -147,35 +148,49 @@ def delete_obsolete_files(folder, current_scripts): - print(f"Cleaning {folder}...") - obsolete = {f for f in folder.rglob('*') if f.is_file() and f.relative_to(folder) not in 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.") + 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") - for f in obsolete: - if ENABLE_WRITETOFILE: - try: - f.unlink() - print(f"Deleted file no longer in the API: {f}") - except Exception as e: - print(f"Error deleting file {f}: {e}") - else: - print(f"Simulated deletion of file no longer in the API: {f}") + empty_dirs = [d for d in sorted(all_paths, key=lambda p: -len(p.parts)) if d.is_dir() and not any(d.iterdir())] - empty_dirs = [d for d in sorted(folder.rglob('*'), 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.") - - for d in empty_dirs: - if ENABLE_WRITETOFILE: - try: - d.rmdir() - print(f"Removed empty directory: {d}") - except Exception as e: - print(f"Could not delete dir {d}: {e}") - else: - print(f"Simulated removal of empty directory: {d}") + 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 = [] @@ -605,7 +620,8 @@ def check_git_health(base_dir): return True def pre_flight(): - global domain, scriptpath, headers + global domain, headers, base_dir + domain = os.getenv('DOMAIN') api_token = os.getenv('API_TOKEN') scriptpath = os.getenv('SCRIPTPATH') @@ -619,8 +635,13 @@ def pre_flight(): if var == 'SCRIPTPATH': print(f" - SCRIPTPATH: The local folder path for Git commands.") sys.exit(1) + #Build headers headers = {"X-API-KEY": api_token} - domain_for_connection = domain.replace("https://", "").replace("http://", "") + #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) @@ -630,11 +651,13 @@ def pre_flight(): 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: @@ -648,6 +671,7 @@ def pre_flight(): return + def check_and_create_folders(base_path, subfolders): try: if not base_path.exists(): @@ -671,21 +695,11 @@ def main(): # 0. Prep: Verify Dependencies, Set Up Environment, and Git Health Check print("\n===== Step 0: General Prep =====") - - # ENV vars & network checks + + + # ENV vars & network checks & prep vars pre_flight() - # Folder structure check - base_dir = Path(scriptpath).resolve() - 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.") - # Check the health of the Git repo if ENABLE_GIT_PULL or ENABLE_GIT_PUSH: if check_git_health(base_dir): @@ -695,7 +709,17 @@ def main(): 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 From c227d014620f0e8f8c99c132598e540ae21db255 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 29 Apr 2025 10:49:44 +0000 Subject: [PATCH 312/447] Update file: Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 69 ++++++++++++------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index 656e7b19..26b40fec 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -93,17 +93,16 @@ 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.0 24/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.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 .TODO - Handle rights issues when executing git commands Review flow of step 3 for optimisations - + Review the counters for step 3 Revamp folder structure: - Move raws from "scriptsraw" to Category/folder/raws/ + Move raws from "scriptsraw" to Category_name/raws/ add "uncategorised" folder - remove "scripts" top level folder while keeping snippets + 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": [ @@ -116,6 +115,7 @@ 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 + """ @@ -238,7 +238,7 @@ def process_scripts(scripts, script_folder, script_raw_folder, shell_summary, is 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'}.") + print(f"Processed {len(current)} {'snippets' if is_snippet else 'scripts'}.\n") return current def compute_hash(file_path): @@ -252,9 +252,9 @@ 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}") + print(f"File saved: {path.relative_to(base_dir) if base_dir else path}") else: - print(f"File would be saved (simulation): {path}") + print(f"File would be saved (simulation): {path.relative_to(base_dir) if base_dir else path}") def pull_from_api(url): @@ -325,6 +325,7 @@ def update_api_if_needed(match, raw_data, is_snippet): return False + def write_modifications_to_api(base_dir, folders): print("Comparing script files with JSON files...") mismatches = [] @@ -333,7 +334,7 @@ def write_modifications_to_api(base_dir, folders): total_matches = 0 total_mismatches = 0 total_updated = 0 - total_skipped = 0 + total_not_updated = 0 for folder_key, folder in folders.items(): is_snippet = folder_key == 'snippetsraw' @@ -347,15 +348,15 @@ def write_modifications_to_api(base_dir, folders): 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_skipped += 1 + total_not_updated += 1 continue if not match: - print(f"No match for {'snippet' if is_snippet else 'script'}: {raw_path}") - total_skipped += 1 + 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} <-> {raw_path}") + 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) @@ -379,13 +380,24 @@ def write_modifications_to_api(base_dir, folders): 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") - print("\nComparison Complete:") - print(f"Total files checked: {total_files_checked}") - print(f"Total matches: {total_matches}") - print(f"Total mismatches: {total_mismatches}") - print(f"Total updates: {total_updated}") - print(f"Total skipped: {total_skipped}") def update_to_api(item_id, payload, is_snippet=False): """Update the API with the provided item ID and payload.""" @@ -513,13 +525,22 @@ def git_push(base_dir): print(f"Changes pushed to branch '{git_pull_branch}'") else: - print("No changes to commit.") + 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) @@ -616,9 +637,9 @@ def check_git_health(base_dir): print("❌ Error: Failed to check commit history.") return False - print("✅ Git repository health check passed.") return True + def pre_flight(): global domain, headers, base_dir @@ -740,13 +761,13 @@ def main(): print("\n===== Step 3: Fetch and Process Scripts and Snippets =====") # Initialize counters and sets shell_summary, current_scripts = defaultdict(int), set() - print("Fetching scripts...") + 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...") + 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)) From f6cc48b094ebb408c080f38e85d098168c736348 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 29 Apr 2025 11:58:13 +0000 Subject: [PATCH 313/447] Add new file: CallPowerShell7Lite.ps1 --- .../snippets/CallPowerShell7Lite.ps1 | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 scripts_staging/snippets/CallPowerShell7Lite.ps1 diff --git a/scripts_staging/snippets/CallPowerShell7Lite.ps1 b/scripts_staging/snippets/CallPowerShell7Lite.ps1 new file mode 100644 index 00000000..8a7b3fa1 --- /dev/null +++ b/scripts_staging/snippets/CallPowerShell7Lite.ps1 @@ -0,0 +1,27 @@ +<# +.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 +#> + + +if (!($PSVersionTable.PSVersion.Major -ge 7)) { + if (Get-Command pwsh -ErrorAction SilentlyContinue) { + pwsh -File "`"$PSCommandPath`"" @PSBoundParameters + exit $LASTEXITCODE + } else { + Write-Output "PowerShell 7 is available" + exit 1 + } +} +$PSStyle.OutputRendering = "plaintext" From ee912516a8887dae88c62c5e787a0023e1c6fa3d Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 29 Apr 2025 19:19:49 +0000 Subject: [PATCH 314/447] Update file: Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index 26b40fec..7377b602 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -95,6 +95,7 @@ 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 .TODO Review flow of step 3 for optimisations @@ -421,14 +422,27 @@ def update_to_api(item_id, payload, is_snippet=False): 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 the latest changes from the git repository, discarding local changes.""" - +def git_pull(base_dir, git_pull_branch='main'): + """Force pull latest changes from the git repository if there are changes, discarding local changes.""" + print("Starting pull process...", flush=True) try: 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) @@ -773,7 +787,9 @@ def main(): # 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:", "\n".join(f"{shell}: {count}" for shell, count in shell_summary.items())) + 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") From c7d9100fcdf6800944c55daca90dde0e84d38a31 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:24:30 +0000 Subject: [PATCH 315/447] Update file: CallPowerShell7Lite.ps1 --- scripts_staging/snippets/CallPowerShell7Lite.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts_staging/snippets/CallPowerShell7Lite.ps1 b/scripts_staging/snippets/CallPowerShell7Lite.ps1 index 8a7b3fa1..1e638181 100644 --- a/scripts_staging/snippets/CallPowerShell7Lite.ps1 +++ b/scripts_staging/snippets/CallPowerShell7Lite.ps1 @@ -20,7 +20,7 @@ if (!($PSVersionTable.PSVersion.Major -ge 7)) { pwsh -File "`"$PSCommandPath`"" @PSBoundParameters exit $LASTEXITCODE } else { - Write-Output "PowerShell 7 is available" + Write-Output "PowerShell 7 is not available" exit 1 } } From 668755c0fa69c0814b79c4f83477ecece101c981 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:56:41 +0000 Subject: [PATCH 316/447] Update file: CallPowerShell7Lite.ps1 --- scripts_staging/snippets/CallPowerShell7Lite.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts_staging/snippets/CallPowerShell7Lite.ps1 b/scripts_staging/snippets/CallPowerShell7Lite.ps1 index 1e638181..5a6b86f2 100644 --- a/scripts_staging/snippets/CallPowerShell7Lite.ps1 +++ b/scripts_staging/snippets/CallPowerShell7Lite.ps1 @@ -20,7 +20,7 @@ if (!($PSVersionTable.PSVersion.Major -ge 7)) { pwsh -File "`"$PSCommandPath`"" @PSBoundParameters exit $LASTEXITCODE } else { - Write-Output "PowerShell 7 is not available" + Write-Output "ERROR: PowerShell 7 is not available. Exiting." exit 1 } } From f957ff2d1456dd9905448cb83b9fdd7b3022622b Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 30 Apr 2025 12:36:35 +0000 Subject: [PATCH 317/447] Update file: Sync TRMM with GIT.py --- scripts_staging/Backend/Sync TRMM with GIT.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index 7377b602..73709eda 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -96,8 +96,10 @@ 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: @@ -135,7 +137,6 @@ # Retrieve the git pull branch or default to 'master' git_pull_branch = os.getenv('GIT_PULL_BRANCH', 'master') -if git_pull_branch != 'master': print(f"Git Pull Branch: {git_pull_branch}") # Retrieve flags from environment variables (default to True unless set to 'false') ENABLE_GIT_PULL = os.getenv('ENABLE_GIT_PULL', 'True').lower() != 'false' @@ -422,11 +423,12 @@ def update_to_api(item_id, payload, is_snippet=False): 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, git_pull_branch='main'): +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) @@ -759,7 +761,6 @@ def main(): # 1. Git Pull print("\n===== Step 1: Git Pull =====") - print(f"Branch to pull: '{git_pull_branch}'") if ENABLE_GIT_PULL: git_pull(base_dir) else: From 23dc45b39367fcd1dbff054886df0d1c55d6d6d7 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 1 May 2025 19:18:25 +0000 Subject: [PATCH 318/447] Update file: GeneratedPassphrase.ps1 --- scripts_staging/snippets/GeneratedPassphrase.ps1 | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/scripts_staging/snippets/GeneratedPassphrase.ps1 b/scripts_staging/snippets/GeneratedPassphrase.ps1 index 240c16a6..09a4754f 100644 --- a/scripts_staging/snippets/GeneratedPassphrase.ps1 +++ b/scripts_staging/snippets/GeneratedPassphrase.ps1 @@ -1,4 +1,4 @@ -#public + <# .SYNOPSIS This script changes the password for the user to a randomly generated passphrase. @@ -10,6 +10,14 @@ .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 + +.TODO + Random symbol #> @@ -18,7 +26,7 @@ function GeneratedPassphrase { param ( [int]$NumWords = 3, # Number of words in the passphrase - [int]$MinWordLength = 4, # Minimum length of each word + [int]$MinWordLength = 5, # Minimum length of each word [int]$MaxWordLength = 10 # Maximum length of each word ) From a367c63f91a01214b90ed18db74c9b07d843b125 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 1 May 2025 19:26:09 +0000 Subject: [PATCH 319/447] Add new file: RustDesk install.ps1 --- scripts_staging/Lab/RustDesk install.ps1 | 72 ++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 scripts_staging/Lab/RustDesk install.ps1 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 From bb1fffd557c9cf8b89c1d14cbf3bc49fa3f389c9 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 1 May 2025 19:33:43 +0000 Subject: [PATCH 320/447] Add new file: RustDesk Get ID.ps1 --- scripts_staging/Lab/RustDesk Get ID.ps1 | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 scripts_staging/Lab/RustDesk Get ID.ps1 diff --git a/scripts_staging/Lab/RustDesk Get ID.ps1 b/scripts_staging/Lab/RustDesk Get ID.ps1 new file mode 100644 index 00000000..94235bb9 --- /dev/null +++ b/scripts_staging/Lab/RustDesk Get ID.ps1 @@ -0,0 +1,6 @@ +#public +#grab public id of restdesk to set a custom field +$ErrorActionPreference= 'silentlycontinue' + +cd $env:ProgramFiles\RustDesk\ +.\RustDesk.exe --get-id | out-host \ No newline at end of file From c6b85d6cbb5e0eee2933e7af09c154618b7c0022 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 1 May 2025 19:33:45 +0000 Subject: [PATCH 321/447] Add new file: RustDesk password set.ps1 --- scripts_staging/Lab/RustDesk password set.ps1 | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 scripts_staging/Lab/RustDesk password set.ps1 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 From 90f41b1e326bf6416acf0d07fffb9b8d74d3b355 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 1 May 2025 20:32:15 +0000 Subject: [PATCH 322/447] Update file: GeneratedPassphrase.ps1 --- .../snippets/GeneratedPassphrase.ps1 | 91 ++++++++++++++----- 1 file changed, 68 insertions(+), 23 deletions(-) diff --git a/scripts_staging/snippets/GeneratedPassphrase.ps1 b/scripts_staging/snippets/GeneratedPassphrase.ps1 index 09a4754f..64b8b53c 100644 --- a/scripts_staging/snippets/GeneratedPassphrase.ps1 +++ b/scripts_staging/snippets/GeneratedPassphrase.ps1 @@ -1,11 +1,11 @@ <# .SYNOPSIS - This script changes the password for the user to a randomly generated passphrase. + 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 generated "$GeneratedPassphrase" variable. + Snipet output called from function "GeneratedPassphrase" or preferably from the already pre-generated "$GeneratedPassphrase" variable. .NOTES Author : SAN @@ -13,43 +13,88 @@ 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 - Random symbol #> - -function GeneratedPassphrase { +function Generate-Passphrase { param ( - [int]$NumWords = 3, # Number of words in the passphrase - [int]$MinWordLength = 5, # Minimum length of each word - [int]$MaxWordLength = 10 # Maximum length of each word + [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 + } - # Expanded built-in 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") + $FilteredWords = $WordList | Where-Object { $_.Length -ge $MinWordLength -and $_.Length -le $MaxWordLength } - # Filter out words not within the specified length range and without spaces - $FilteredWords = $WordList | Where-Object { $_.Length -ge $MinWordLength -and $_.Length -le $MaxWordLength -and $_ -notmatch '\s' } + if ($FilteredWords.Count -lt $NumWords) { + Write-Error "Not enough words match the length constraints." + return $null + } - # Select random words from the filtered list - $PassphraseWords = Get-Random -InputObject $FilteredWords -Count $NumWords + $PassphraseWords = Get-Random -InputObject ($FilteredWords | Get-Unique) -Count $NumWords - # Capitalize the first letter of each word - $PassphraseWordsCapitalized = $PassphraseWords | ForEach-Object { $_.Substring(0,1).ToUpper() + $_.Substring(1) } + if ($UseCapitalization) { + $PassphraseWords = $PassphraseWords | ForEach-Object { $_.Substring(0,1).ToUpper() + $_.Substring(1) } + } - # Add a random number after one of the words - $RandomIndex = Get-Random -Minimum 0 -Maximum $NumWords - $PassphraseWordsCapitalized[$RandomIndex] += (Get-Random -Minimum 0 -Maximum 10) + if ($IncludeNumber) { + $RandomIndex = Get-Random -Minimum 0 -Maximum $NumWords + $PassphraseWords[$RandomIndex] += (Get-Random -Minimum $MinNumber -Maximum $MaxNumber) + } - # Join the words into a passphrase - $password = $PassphraseWordsCapitalized -join '-' + $Passphrase = $PassphraseWords -join $Separator - return $password + return $Passphrase } -$GeneratedPassphrase = GeneratedPassphrase \ No newline at end of file +$GeneratedPassphrase = Generate-Passphrase From b221d1ed1dda95b80502abb37e0164567ef5bc2a Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 1 May 2025 20:38:09 +0000 Subject: [PATCH 323/447] Update file: GeneratedPassphrase.ps1 --- scripts_staging/snippets/GeneratedPassphrase.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts_staging/snippets/GeneratedPassphrase.ps1 b/scripts_staging/snippets/GeneratedPassphrase.ps1 index 64b8b53c..5838b85f 100644 --- a/scripts_staging/snippets/GeneratedPassphrase.ps1 +++ b/scripts_staging/snippets/GeneratedPassphrase.ps1 @@ -24,7 +24,7 @@ #> -function Generate-Passphrase { +function GeneratedPassphrase { param ( [int] [ValidateRange(1, 20)] $NumWords = 3, # Number of words to generate in the passphrase. @@ -97,4 +97,4 @@ function Generate-Passphrase { return $Passphrase } -$GeneratedPassphrase = Generate-Passphrase +$GeneratedPassphrase = GeneratedPassphrase From 1ce392ed17b3a09ca6aa716ef0635f155eadcf92 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 2 May 2025 13:48:25 +0000 Subject: [PATCH 324/447] Add new file: TRMM agent deployment.ps1 --- .../Build/TRMM agent deployment.ps1 | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 scripts_staging/Build/TRMM agent deployment.ps1 diff --git a/scripts_staging/Build/TRMM agent deployment.ps1 b/scripts_staging/Build/TRMM agent deployment.ps1 new file mode 100644 index 00000000..02b20d45 --- /dev/null +++ b/scripts_staging/Build/TRMM agent deployment.ps1 @@ -0,0 +1,132 @@ +<# +.SYNOPSIS + Checks for connectivity to the rmm, when found installs Tactical RMM if not already installed. + +.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, and logs each step of the process. + 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/" + $RMM_URL = "rmm-xxxxxxxxxxxxx.xxxxxx.xxxxx" + $logDirectory = "C:\xxxxxxxx\logs" + +.CHANGELOG + SAN 02.05.25 Cleaned the code for publication and removed sensitive data + +.TODO + Querry the api with to get a "status"ok"" in json rather than a tcp check + Run with silent argument ? + +#> + + +$RMM_URL = "" # Provide the rmm URL for network check +$DeploymentURL = "" # Provide the Deployment URL +$logDirectory = "" # Provide optional log directory + + +$CHECK_PORT = 443 +$tacticalPath = "C:\ProgramData\TacticalRMM\temp" +$logFile = Join-Path -Path $logDirectory -ChildPath "deploy_log_$(Get-Date -Format 'ddMMyyyy').log" + +# Ensure the log directory exists +if (-not (Test-Path -Path $logDirectory)) { + New-Item -ItemType Directory -Path $logDirectory | Out-Null +} + +# Function to log messages +function Write-Log { + param ([string]$message) + + $timestamp = Get-Date -Format "dd-MM-yyyy HH:mm:ss" + $logMessage = "$timestamp - $message" + + # If the log file is empty, use Write-Host + if ((Test-Path -Path $logFile) -and ((Get-Item -Path $logFile).length -eq 0)) { + Write-Host $logMessage + } else { + Write-Host $logMessage + Add-Content -Path $logFile -Value $logMessage + } +} + +# Synchronize time +Write-Log "Synchronizing the system time..." +w32tm /resync +Start-Sleep -Seconds 5 +Restart-Service w32time +Start-Sleep -Seconds 5 + +# Function to check internet connectivity +function Check-InternetConnection { + Write-Log "Checking connectivity to $RMM_URL on port $CHECK_PORT..." + $connectionTest = Test-NetConnection -ComputerName $RMM_URL -Port $CHECK_PORT + + if ($connectionTest.TcpTestSucceeded) { + Write-Log "Connection to $RMM_URL successful." + return $true + } else { + Write-Log "Connection failed. Retrying in 20 seconds..." + Start-Sleep -Seconds 20 + return $false + } +} + +# Retry until connection is established +do { + $networkAvailable = Check-InternetConnection +} until ($networkAvailable) + +Start-Sleep -Seconds 5 + +# Check if Tactical RMM 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 not found. Proceeding with installation..." + + if (-not (Test-Path -Path $tacticalPath)) { + New-Item -ItemType Directory -Path $tacticalPath | Out-Null + } + + 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." + } + + # Download and run the installer + if ($DeploymentURL) { + Write-Log "Downloading Tactical RMM installer..." + Invoke-WebRequest -Uri $DeploymentURL -OutFile "$tacticalPath\tactical.exe" + Write-Log "Launching installer..." + Start-Process -FilePath "$tacticalPath\tactical.exe" -NoNewWindow -Wait + Write-Log "Installation completed." + } else { + Write-Log "Error: Deployment URL is not set." + } + + Start-Sleep -Seconds 15 + exit 0 +} else { + Write-Log "Tactical RMM already installed. Exiting..." + Start-Sleep -Seconds 15 + exit 0 +} From f925800985931b4b00efcc400a49a44a5e56fa24 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 2 May 2025 14:36:51 +0000 Subject: [PATCH 325/447] Update file: Kill Switch Manager.ps1 --- scripts_staging/Tasks/Kill Switch Manager.ps1 | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/scripts_staging/Tasks/Kill Switch Manager.ps1 b/scripts_staging/Tasks/Kill Switch Manager.ps1 index 03d0c99d..47942ce0 100644 --- a/scripts_staging/Tasks/Kill Switch Manager.ps1 +++ b/scripts_staging/Tasks/Kill Switch Manager.ps1 @@ -5,7 +5,7 @@ .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 securely. + 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. @@ -16,14 +16,13 @@ This can be specified through the environment variable `companyfolder`. .EXAMPLE - $env:killswitchdomain="example.com" - $env:companyfolder="C:\CompanyFolder" - - Run the script to set up the kill switch for Tactical RMM. + killswitchdomain=kill.alltacticalagents.example.com + companyfolder=C:\CompanyFolder + companyfolder={{global.Company_folder_path}} .NOTES Author: SAN - Date: ??? + Date: 01.01.2024 #public .CHANGELOG @@ -31,7 +30,13 @@ .TODO Integrate this script into the deployment process. - Add global var to var + Cleanup the code + split script content to snippet + add company name folder for task + hide script + scripts subfolder setup ? + hide error when first setup + #> From ff4aa6469b97a84c0422d7d34ad9354164f528aa Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 2 May 2025 20:00:25 +0000 Subject: [PATCH 326/447] Add new file: Demo powershell visibility window.ps1 --- .../Lab/Demo powershell visibility window.ps1 | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 scripts_staging/Lab/Demo powershell visibility window.ps1 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 From 7ecf685a766ac3b3b6a5e01336a6c627268bd137 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 2 May 2025 21:28:23 +0000 Subject: [PATCH 327/447] Update file: TRMM agent deployment.ps1 --- .../Build/TRMM agent deployment.ps1 | 169 +++++++++++------- 1 file changed, 106 insertions(+), 63 deletions(-) diff --git a/scripts_staging/Build/TRMM agent deployment.ps1 b/scripts_staging/Build/TRMM agent deployment.ps1 index 02b20d45..ac65bb6d 100644 --- a/scripts_staging/Build/TRMM agent deployment.ps1 +++ b/scripts_staging/Build/TRMM agent deployment.ps1 @@ -1,11 +1,11 @@ <# .SYNOPSIS - Checks for connectivity to the rmm, when found installs Tactical RMM if not already installed. + 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, and logs each step of the process. + 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 @@ -17,32 +17,22 @@ .EXEMPLE $DeploymentURL = "https://api-rmm-xxxxxxx.xxxxxxxx.xxx/clients/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/deploy/" - $RMM_URL = "rmm-xxxxxxxxxxxxx.xxxxxx.xxxxx" - $logDirectory = "C:\xxxxxxxx\logs" .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 - Querry the api with to get a "status"ok"" in json rather than a tcp check - Run with silent argument ? #> +$DeploymentURL = "" # Provide Deployment URL -$RMM_URL = "" # Provide the rmm URL for network check -$DeploymentURL = "" # Provide the Deployment URL -$logDirectory = "" # Provide optional log directory - - -$CHECK_PORT = 443 -$tacticalPath = "C:\ProgramData\TacticalRMM\temp" -$logFile = Join-Path -Path $logDirectory -ChildPath "deploy_log_$(Get-Date -Format 'ddMMyyyy').log" - -# Ensure the log directory exists -if (-not (Test-Path -Path $logDirectory)) { - New-Item -ItemType Directory -Path $logDirectory | Out-Null -} +$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 { @@ -51,54 +41,106 @@ function Write-Log { $timestamp = Get-Date -Format "dd-MM-yyyy HH:mm:ss" $logMessage = "$timestamp - $message" - # If the log file is empty, use Write-Host - if ((Test-Path -Path $logFile) -and ((Get-Item -Path $logFile).length -eq 0)) { - Write-Host $logMessage - } else { - Write-Host $logMessage + Write-Host $logMessage + + if ($logFile) { Add-Content -Path $logFile -Value $logMessage } } -# Synchronize time -Write-Log "Synchronizing the system time..." -w32tm /resync -Start-Sleep -Seconds 5 -Restart-Service w32time -Start-Sleep -Seconds 5 +# 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 + } -# Function to check internet connectivity -function Check-InternetConnection { - Write-Log "Checking connectivity to $RMM_URL on port $CHECK_PORT..." - $connectionTest = Test-NetConnection -ComputerName $RMM_URL -Port $CHECK_PORT + 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 + } +} - if ($connectionTest.TcpTestSucceeded) { - Write-Log "Connection to $RMM_URL successful." - return $true - } else { - Write-Log "Connection failed. Retrying in 20 seconds..." - Start-Sleep -Seconds 20 +# 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 } } -# Retry until connection is established +# 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 { - $networkAvailable = Check-InternetConnection -} until ($networkAvailable) + $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 + } -Start-Sleep -Seconds 5 + Write-Log "Retrying in $TryEvery seconds... (Attempt #$attempt)" + Start-Sleep -Seconds $TryEvery +} until ($rmmReady) -# Check if Tactical RMM is already installed +# 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 not found. Proceeding with installation..." - - if (-not (Test-Path -Path $tacticalPath)) { - New-Item -ItemType Directory -Path $tacticalPath | Out-Null - } - + 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 @@ -112,21 +154,22 @@ if (-not $tacticalInstalled) { Write-Log "Third-party antivirus detected. Skipping exclusion rules." } - # Download and run the installer - if ($DeploymentURL) { - Write-Log "Downloading Tactical RMM installer..." - Invoke-WebRequest -Uri $DeploymentURL -OutFile "$tacticalPath\tactical.exe" - Write-Log "Launching installer..." - Start-Process -FilePath "$tacticalPath\tactical.exe" -NoNewWindow -Wait - Write-Log "Installation completed." - } else { - Write-Log "Error: Deployment URL is not set." + #Create download destination + if (-not (Test-Path -Path $DownloadPath)) { + New-Item -ItemType Directory -Path $DownloadPath | Out-Null } - Start-Sleep -Seconds 15 + # 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 already installed. Exiting..." - Start-Sleep -Seconds 15 + Write-Log "Tactical RMM agent is already installed. Exiting..." + Start-Sleep -Seconds $SleepBeforeExit exit 0 } From 4b3f5d58eb9551ca02cb7ae8b5e5ac394bec776d Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Sat, 3 May 2025 17:51:03 +0000 Subject: [PATCH 328/447] Update file: Change default chocolatey repo to internal.ps1 --- ...ge default chocolatey repo to internal.ps1 | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/scripts_staging/Build/Change default chocolatey repo to internal.ps1 b/scripts_staging/Build/Change default chocolatey repo to internal.ps1 index 025602c7..e2784fce 100644 --- a/scripts_staging/Build/Change default chocolatey repo to internal.ps1 +++ b/scripts_staging/Build/Change default chocolatey repo to internal.ps1 @@ -3,11 +3,13 @@ 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. + 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 @@ -16,7 +18,7 @@ .CHANGELOG SAN 11.12.24 Moved new info to env - + SAN 03.05.25 Added a flag to keep or not the default repo #> @@ -28,11 +30,17 @@ $defaultUrl = "https://chocolatey.org/api/v2/" $defaultPriority = 10 $defaultName = "chocolatey" -# Remove settings -choco source remove -n $defaultName -y +# 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 choco source remove -n $newName -y +choco source remove -n $defaultName -y -# Add the new Chocolatey repository with the specified priority +# Add the new internal Chocolatey repository choco source add -n $newName -s $newUrl --priority $newPriority -# Add the default Chocolatey repository with a low priority -choco source add -n $defaultName -s $defaultUrl --priority $defaultPriority + +# Conditionally re-add the default repository +if ($keepDefaultRepo) { + choco source add -n $defaultName -s $defaultUrl --priority $defaultPriority +} From f86eed0b83b9810cede33b0023b4f400caf1d5d5 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 6 May 2025 08:28:54 +0000 Subject: [PATCH 329/447] Update file: Repo package updater.py --- scripts_staging/Backend/Repo package updater.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts_staging/Backend/Repo package updater.py b/scripts_staging/Backend/Repo package updater.py index 569404d5..c54dea5a 100644 --- a/scripts_staging/Backend/Repo package updater.py +++ b/scripts_staging/Backend/Repo package updater.py @@ -54,6 +54,7 @@ "chocolatey-compatibility.extension", "chocolatey-core.extension", "chocolatey-windowsupdate.extension", + "chocolatey.server", "KB2919355", "KB2919442", "KB3118401", From e7580721325da01e5eb83e0f0aa9e587321e5956 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 7 May 2025 13:44:50 +0000 Subject: [PATCH 330/447] Update file: RustDesk Get ID.ps1 --- scripts_staging/Lab/RustDesk Get ID.ps1 | 36 ++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/scripts_staging/Lab/RustDesk Get ID.ps1 b/scripts_staging/Lab/RustDesk Get ID.ps1 index 94235bb9..0faafc76 100644 --- a/scripts_staging/Lab/RustDesk Get ID.ps1 +++ b/scripts_staging/Lab/RustDesk Get ID.ps1 @@ -1,6 +1,40 @@ #public #grab public id of restdesk to set a custom field + +#V1 $ErrorActionPreference= 'silentlycontinue' cd $env:ProgramFiles\RustDesk\ -.\RustDesk.exe --get-id | out-host \ No newline at end of file +.\RustDesk.exe --get-id | out-host + +exit + +#V2 +$ErrorActionPreference = 'SilentlyContinue' +$maxAttempts = 40 +$attempt = 0 + +# Change directory to RustDesk install folder +Set-Location "$env:ProgramFiles\RustDesk" + +while ($attempt -lt $maxAttempts) { + $output = .\RustDesk.exe --get-id + + if ($output -and $output.Trim() -ne "") { + Write-Host "Public ID obtained: $output" + break + } else { + Write-Host "Attempt $($attempt + 1): No ID received, retrying in 30 seconds..." + Start-Sleep -Seconds 30 + $attempt++ + } +} + +if ($attempt -eq $maxAttempts) { + Write-Host "Failed to get RustDesk ID after 20 minutes." + exit 1 +} + +Write-Host "$output" + +exit \ No newline at end of file From 398adb0f940e0986e78100c446d16d821d62f8f9 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 7 May 2025 13:50:15 +0000 Subject: [PATCH 331/447] Update file: RustDesk Get ID.ps1 --- scripts_staging/Lab/RustDesk Get ID.ps1 | 30 ------------------------- 1 file changed, 30 deletions(-) diff --git a/scripts_staging/Lab/RustDesk Get ID.ps1 b/scripts_staging/Lab/RustDesk Get ID.ps1 index 0faafc76..a844160d 100644 --- a/scripts_staging/Lab/RustDesk Get ID.ps1 +++ b/scripts_staging/Lab/RustDesk Get ID.ps1 @@ -8,33 +8,3 @@ cd $env:ProgramFiles\RustDesk\ .\RustDesk.exe --get-id | out-host exit - -#V2 -$ErrorActionPreference = 'SilentlyContinue' -$maxAttempts = 40 -$attempt = 0 - -# Change directory to RustDesk install folder -Set-Location "$env:ProgramFiles\RustDesk" - -while ($attempt -lt $maxAttempts) { - $output = .\RustDesk.exe --get-id - - if ($output -and $output.Trim() -ne "") { - Write-Host "Public ID obtained: $output" - break - } else { - Write-Host "Attempt $($attempt + 1): No ID received, retrying in 30 seconds..." - Start-Sleep -Seconds 30 - $attempt++ - } -} - -if ($attempt -eq $maxAttempts) { - Write-Host "Failed to get RustDesk ID after 20 minutes." - exit 1 -} - -Write-Host "$output" - -exit \ No newline at end of file From e621b498a8c55c8019a9145b365a9733fef5f93e Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 8 May 2025 10:09:54 +0000 Subject: [PATCH 332/447] Update file: Get last shutdown info.ps1 --- .../Tools/Get last shutdown info.ps1 | 158 +++++++++++++++--- 1 file changed, 138 insertions(+), 20 deletions(-) diff --git a/scripts_staging/Tools/Get last shutdown info.ps1 b/scripts_staging/Tools/Get last shutdown info.ps1 index 8749570c..cce5f392 100644 --- a/scripts_staging/Tools/Get last shutdown info.ps1 +++ b/scripts_staging/Tools/Get last shutdown info.ps1 @@ -1,41 +1,159 @@ <# .SYNOPSIS - Retrieves and displays system uptime and shutdown event information. + Retrieves and logs system uptime and shutdown event information. .DESCRIPTION - This script retrieves the last boot time of the system, calculates the uptime (in days, hours, and minutes), - and retrieves the most recent shutdown event from the system's event log (EventID 1074). + 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'. + +.PARAMETER sendtolog + Environment variable used to trigger logging to a CSV file when set to "1". + +.EXEMPLE + sendtolog=1 .NOTES Author: SAN - Date: 03.10.24 + 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 -$shutdownEvent = Get-WinEvent -LogName System -FilterXPath "*[System/EventID=1074]" | Select-Object -First 1 +$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 "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" -Write-Output "Last Boot Time : $($lastBootTime)" -Write-Output "Uptime (since last boot) : $($uptime.Days) days, $($uptime.Hours) hours, $($uptime.Minutes) minutes" -Write-Output "Event Log Time : $($shutdownEvent.TimeCreated)" +if ($shutdownEvent) { + $eventTime = $shutdownEvent.TimeCreated + $eventId = $shutdownEvent.Id + $provider = $shutdownEvent.ProviderName + $msg = $shutdownEvent.Message -replace '\r\n',' ' -Write-Output "===========================" -Write-Output " All Event Properties" -Write-Output "===========================" + 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." + } + } + } -Write-Output "Initiating Process/Executable : $($shutdownEvent.Properties[0].Value)" -Write-Output "Initiating Machine : $($shutdownEvent.Properties[1].Value)" -Write-Output "Shutdown Reason : $($shutdownEvent.Properties[2].Value)" -Write-Output "Shutdown Code : $($shutdownEvent.Properties[3].Value)" -Write-Output "Shutdown Type : $($shutdownEvent.Properties[4].Value)" -Write-Output "Additional Info : $($shutdownEvent.Properties[5].Value)" -Write-Output "User Account : $($shutdownEvent.Properties[6].Value)" + 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." +} From c3d76b6dc0380070fad4f011670713dabd03178b Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 8 May 2025 12:27:35 +0000 Subject: [PATCH 333/447] Add new file: Trigger tasks on boot.ps1 --- .../Checks/Trigger tasks on boot.ps1 | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 scripts_staging/Checks/Trigger tasks on boot.ps1 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..7d443ef8 --- /dev/null +++ b/scripts_staging/Checks/Trigger tasks on boot.ps1 @@ -0,0 +1,103 @@ +<# +.SYNOPSIS + Exits with code 1 if automation should trigger (key exists); 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 1. + If the key does not exist (i.e., first run since boot), it creates the key and exits with code 0 to indicate no further action is needed. + + This approach was implemented as a workaround for TacticalRMM's lack of native "on-boot" task support. + It enables TRMM policies to detect the key’s existence and act accordingly by triggering automation. + +.NOTES + Author: SAN + Date: 08.05.25 + #public + +.TODO + Better outputs + +#> + + + +Add-Type -TypeDefinition @" +using System; +using System.Runtime.InteropServices; + +public class VolatileRegistry +{ + [DllImport("advapi32.dll", CharSet = CharSet.Unicode)] + public static extern int RegCreateKeyEx( + UIntPtr hKey, + string lpSubKey, + int Reserved, + string lpClass, + uint dwOptions, + int samDesired, + IntPtr lpSecurityAttributes, + out IntPtr phkResult, + out int lpdwDisposition + ); + + public static UIntPtr HKEY_LOCAL_MACHINE = (UIntPtr)0x80000002; + public const uint REG_OPTION_VOLATILE = 0x00000001; + public const int KEY_ALL_ACCESS = 0xF003F; + public const int KEY_WOW64_64KEY = 0x0100; + + public static bool CreateVolatileKey(string subKey, out string message) + { + IntPtr hKey; + int disposition; + try + { + int result = RegCreateKeyEx( + HKEY_LOCAL_MACHINE, + subKey, + 0, + null, + REG_OPTION_VOLATILE, + KEY_ALL_ACCESS | KEY_WOW64_64KEY, + IntPtr.Zero, + out hKey, + out disposition + ); + + message = string.Format("RegCreateKeyEx returned: {0}, disposition: {1}", result, disposition); + + if (result != 0) + { + throw new System.Exception("Failed to create registry key."); + } + + return disposition == 1; // Key was created (disposition == 1) + } + catch (System.Exception ex) + { + message = "Error: " + ex.Message; + return false; + } + } +} +"@ + +$subKey = "SOFTWARE\\TacticalRMM\\BootTrigger" +[string]$msg = $null + +try { + $created = [VolatileRegistry]::CreateVolatileKey($subKey, [ref]$msg) + + Write-Output $msg + + if ($created) { + # First run since boot — trigger automation + exit 1 + } else { + # Already triggered this boot — do nothing + exit 0 + } +} catch { + Write-Error "An unexpected error occurred: $_" + exit 1 +} \ No newline at end of file From f7e5fdc80fecee24c9e315ba51ce6e48ebae273a Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 8 May 2025 12:35:11 +0000 Subject: [PATCH 334/447] Update file: Trigger tasks on boot.ps1 --- scripts_staging/Checks/Trigger tasks on boot.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts_staging/Checks/Trigger tasks on boot.ps1 b/scripts_staging/Checks/Trigger tasks on boot.ps1 index 7d443ef8..31192c5b 100644 --- a/scripts_staging/Checks/Trigger tasks on boot.ps1 +++ b/scripts_staging/Checks/Trigger tasks on boot.ps1 @@ -1,14 +1,14 @@ <# .SYNOPSIS - Exits with code 1 if automation should trigger (key exists); exits with 0 otherwise. + 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 1. - If the key does not exist (i.e., first run since boot), it creates the key and exits with code 0 to indicate no further action is needed. + 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 1 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 policies to detect the key’s existence and act accordingly by triggering automation. + It enables TRMM tasks to detect the key’s lack of existence and act accordingly by triggering automations on failure of the check. .NOTES Author: SAN From f4bdb1cc732fe1795a72f4a6b59eb16c2788950e Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 8 May 2025 12:46:18 +0000 Subject: [PATCH 335/447] Update file: Trigger tasks on boot.ps1 --- scripts_staging/Checks/Trigger tasks on boot.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts_staging/Checks/Trigger tasks on boot.ps1 b/scripts_staging/Checks/Trigger tasks on boot.ps1 index 31192c5b..75c8d3d0 100644 --- a/scripts_staging/Checks/Trigger tasks on boot.ps1 +++ b/scripts_staging/Checks/Trigger tasks on boot.ps1 @@ -99,5 +99,5 @@ try { } } catch { Write-Error "An unexpected error occurred: $_" - exit 1 + exit 15 } \ No newline at end of file From cac83874023f8d311c1814f8df7123967ae1420a Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 8 May 2025 13:05:41 +0000 Subject: [PATCH 336/447] Update file: Trigger tasks on boot.ps1 --- .../Checks/Trigger tasks on boot.ps1 | 71 ++++++++++++------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/scripts_staging/Checks/Trigger tasks on boot.ps1 b/scripts_staging/Checks/Trigger tasks on boot.ps1 index 75c8d3d0..158fa313 100644 --- a/scripts_staging/Checks/Trigger tasks on boot.ps1 +++ b/scripts_staging/Checks/Trigger tasks on boot.ps1 @@ -5,24 +5,36 @@ .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 1 to trigger on failure tasks. + 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. + .NOTES Author: SAN Date: 08.05.25 #public -.TODO - Better outputs +.CHANGELOG + 08.05.25 SAN added check to avoid runing C when not needed to help with runtime and better outputs #> - - -Add-Type -TypeDefinition @" +$subKey = "SOFTWARE\\TacticalRMM\\BootTrigger" +[string]$msg = $null +$ExitCreated = 66 + +# 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" + exit 0 +} 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; @@ -63,15 +75,21 @@ public class VolatileRegistry out hKey, out disposition ); - - message = string.Format("RegCreateKeyEx returned: {0}, disposition: {1}", result, disposition); + if (result == 0) + { + message = string.Format("OK: Registry key created with disposition {0}.", disposition); + } + else + { + message = string.Format("KO: Failed to create registry key. Error code: {0}", result); + } if (result != 0) { - throw new System.Exception("Failed to create registry key."); + throw new System.Exception("KO: Failed to create registry key."); } - return disposition == 1; // Key was created (disposition == 1) + return disposition == 1; } catch (System.Exception ex) { @@ -81,23 +99,22 @@ public class VolatileRegistry } } "@ - -$subKey = "SOFTWARE\\TacticalRMM\\BootTrigger" -[string]$msg = $null - -try { - $created = [VolatileRegistry]::CreateVolatileKey($subKey, [ref]$msg) - - Write-Output $msg - - if ($created) { - # First run since boot — trigger automation - exit 1 - } else { - # Already triggered this boot — do nothing + 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')) { + # First run since boot, and the message says OK — trigger automation + exit $ExitCreated + } else { + # Key creation failed + Write-Error "Failed to create the key" + exit 0 + } + } catch { + Write-Error "An unexpected error occurred: $_" exit 0 } -} catch { - Write-Error "An unexpected error occurred: $_" - exit 15 } \ No newline at end of file From eaeac5b4474bbb4165013c488c1e1f59ad295548 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 8 May 2025 13:10:57 +0000 Subject: [PATCH 337/447] Update file: Trigger tasks on boot.ps1 --- scripts_staging/Checks/Trigger tasks on boot.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts_staging/Checks/Trigger tasks on boot.ps1 b/scripts_staging/Checks/Trigger tasks on boot.ps1 index 158fa313..8dd4bedf 100644 --- a/scripts_staging/Checks/Trigger tasks on boot.ps1 +++ b/scripts_staging/Checks/Trigger tasks on boot.ps1 @@ -10,11 +10,13 @@ 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 From 1505323aa7dc30b4e182ce66fdd862e51424fdd2 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 8 May 2025 13:24:55 +0000 Subject: [PATCH 338/447] Update file: Get last shutdown info.ps1 --- scripts_staging/Tools/Get last shutdown info.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts_staging/Tools/Get last shutdown info.ps1 b/scripts_staging/Tools/Get last shutdown info.ps1 index cce5f392..f511564d 100644 --- a/scripts_staging/Tools/Get last shutdown info.ps1 +++ b/scripts_staging/Tools/Get last shutdown info.ps1 @@ -8,11 +8,11 @@ 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'. -.PARAMETER sendtolog - Environment variable used to trigger logging to a CSV file when set to "1". .EXEMPLE sendtolog=1 + Company_folder_path={{global.Company_folder_path}} + Company_folder_path=c:\folder .NOTES Author: SAN @@ -23,7 +23,7 @@ .CHANGELOG SAN 12.12.24 Code cleanup SAN 08.05.25 added detailed event property logging, added 6008 and cleanup output - + #> From 364dcdc89026a8716d1a17de0ead42219716176f Mon Sep 17 00:00:00 2001 From: redanthrax Date: Thu, 8 May 2025 09:45:27 -0700 Subject: [PATCH 339/447] Windows 11 Upgrade Script --- scripts_wip/Win_Windows_11_Upgrade.ps1 | 895 +++++++++++++++++++++++++ 1 file changed, 895 insertions(+) create mode 100644 scripts_wip/Win_Windows_11_Upgrade.ps1 diff --git a/scripts_wip/Win_Windows_11_Upgrade.ps1 b/scripts_wip/Win_Windows_11_Upgrade.ps1 new file mode 100644 index 00000000..dfef59c6 --- /dev/null +++ b/scripts_wip/Win_Windows_11_Upgrade.ps1 @@ -0,0 +1,895 @@ +<# +.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) +#> + +# Define parameters +param( + [switch]$Force, + [switch]$WaitForCompletion +) + +#--------------------------------- +# Configuration +#--------------------------------- +$Config = @{ + Win11SetupUrl = "https://go.microsoft.com/fwlink/?linkid=2171764" + SetupPath = "$env:TEMP\Win11InstallationAssistant.exe" + 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-SystemCompatibility { + <# + .SYNOPSIS + Validates system requirements for Windows 11. + #> + $results = @{ TPM = $false; SecureBoot = $false; System = $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 $_ + } + + 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' } + + foreach ($driver in $blockedDriverNodes) { + $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 Start-Upgrade { + <# + .SYNOPSIS + Initiates the Windows 11 upgrade process. + #> + param ( + [switch]$WaitForCompletion + ) + + 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 /auto upgrade /copylogs $dir /migratedrivers all" -PassThru -ErrorAction Stop + + if ($WaitForCompletion) { + Write-Log "Monitoring upgrade process to completion..." "INFO" + $success = Monitor-UpgradeProcess -Process $process + return $success + } + else { + Write-Log "Upgrade process initiated. Use -WaitForCompletion to monitor progress." "INFO" + return $true + } + } + catch { + Handle-Error -Message "Failed to initiate upgrade." -Exception $_ + return $false + } +} + +function Monitor-UpgradeProcess { + <# + .SYNOPSIS + Monitors the upgrade process and checks for compatibility issues. + #> + param ( + [Parameter(Mandatory)] + [System.Diagnostics.Process]$Process + ) + + $timeout = (Get-Date).AddMinutes($Config.TimeoutMinutes) + $lastUpdate = Get-Date + + Write-Progress -Activity "Monitoring Windows 11 Upgrade" -Status "Checking compatibility..." + + while (-not $Process.HasExited -and (Get-Date) -lt $timeout) { + if ((Get-Date) -ge $lastUpdate.AddSeconds($Config.StatusIntervalSec)) { + Write-Log "Monitoring upgrade... (Elapsed: $((Get-Date) - $Process.StartTime).ToString('hh\:mm\:ss'))" "INFO" + $lastUpdate = Get-Date + } + + Start-Sleep -Seconds 5 + } + + Write-Progress -Activity "Monitoring Windows 11 Upgrade" -Completed + + if ($Process.HasExited) { + if ($Process.ExitCode -eq 0) { + # Verify OS version + $osInfo = Get-CimInstance Win32_OperatingSystem + $version = [Version]$osInfo.Version + $buildNumber = [int]$osInfo.BuildNumber + if ($version.Major -gt 10 -or ($version.Major -eq 10 -and $buildNumber -ge 22000)) { + Write-Log "Windows 11 detected. Upgrade completed successfully." "INFO" + return $true + } + else { + Write-Log "Upgrade process completed, but Windows 11 not detected. Checking logs for errors..." "ERROR" + $errorEntries = Get-UpgradeLogErrors + if ($errorEntries.Count -gt 0) { + Show-UpgradeFailureInfo -ErrorEntries $errorEntries + } + + return $false + } + } + else { + Write-Log "Upgrade process failed with exit code $($Process.ExitCode)." "ERROR" + $errorEntries = Get-UpgradeLogErrors + if ($errorEntries.Count -gt 0) { + Show-UpgradeFailureInfo -ErrorEntries $errorEntries + } + + return $false + } + } + + Write-Log "Upgrade monitoring timed out after $($Config.TimeoutMinutes) minutes." "ERROR" + return $false +} + +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 + } +} + +#--------------------------------- +# Main Execution +#--------------------------------- + +Write-Log "Starting Windows 11 Upgrade Assistant..." "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" + exit 1 + } +} + +# Validate system compatibility +$compat = Test-SystemCompatibility +if (-not ($compat.TPM -and $compat.SecureBoot -and $compat.System)) { + Write-Log "System does not meet Windows 11 requirements." "ERROR" + exit 1 +} + +# Start upgrade +$upgradeSuccess = Start-Upgrade -WaitForCompletion:$WaitForCompletion +if (-not $upgradeSuccess) { + Write-Log "Upgrade failed. Check logs for details." "ERROR" + exit 1 +} + +Write-Log "Upgrade process completed successfully." "INFO" \ No newline at end of file From d41e4c3dfc37f6984986c5cb3789eeaaeddf35f0 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 12 May 2025 06:55:43 +0000 Subject: [PATCH 340/447] Update file: Trigger tasks on boot.ps1 --- .../Checks/Trigger tasks on boot.ps1 | 63 +++++++++---------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/scripts_staging/Checks/Trigger tasks on boot.ps1 b/scripts_staging/Checks/Trigger tasks on boot.ps1 index 8dd4bedf..733bc94d 100644 --- a/scripts_staging/Checks/Trigger tasks on boot.ps1 +++ b/scripts_staging/Checks/Trigger tasks on boot.ps1 @@ -20,12 +20,15 @@ .CHANGELOG 08.05.25 SAN added check to avoid runing C when not needed to help with runtime and better outputs + 08.05.25 SAN optimised C code and cleaned exit codes #> $subKey = "SOFTWARE\\TacticalRMM\\BootTrigger" [string]$msg = $null $ExitCreated = 66 +$ExitError = 0 +$ExitOK = 0 # Check if the registry key exists $regKeyExists = Test-Path "HKLM:\$subKey" @@ -33,7 +36,7 @@ $regKeyExists = Test-Path "HKLM:\$subKey" if ($regKeyExists) { # Key already exists, proceed with no action Write-Output "OK: Already triggered this boot" - exit 0 + exit $ExitOK } else { # Key doesn't exist, load and execute C# code to create the volatile registry key Add-Type -TypeDefinition @" @@ -55,7 +58,10 @@ public class VolatileRegistry out int lpdwDisposition ); - public static UIntPtr HKEY_LOCAL_MACHINE = (UIntPtr)0x80000002; + [DllImport("advapi32.dll")] + public static extern int RegCloseKey(IntPtr hKey); + + public static readonly UIntPtr HKEY_LOCAL_MACHINE = (UIntPtr)0x80000002; public const uint REG_OPTION_VOLATILE = 0x00000001; public const int KEY_ALL_ACCESS = 0xF003F; public const int KEY_WOW64_64KEY = 0x0100; @@ -64,42 +70,33 @@ public class VolatileRegistry { IntPtr hKey; int disposition; - try + + int result = RegCreateKeyEx( + HKEY_LOCAL_MACHINE, + subKey, + 0, + null, + REG_OPTION_VOLATILE, + KEY_ALL_ACCESS | KEY_WOW64_64KEY, + IntPtr.Zero, + out hKey, + out disposition + ); + + if (result == 0) { - int result = RegCreateKeyEx( - HKEY_LOCAL_MACHINE, - subKey, - 0, - null, - REG_OPTION_VOLATILE, - KEY_ALL_ACCESS | KEY_WOW64_64KEY, - IntPtr.Zero, - out hKey, - out disposition - ); - if (result == 0) - { - message = string.Format("OK: Registry key created with disposition {0}.", disposition); - } - else - { - message = string.Format("KO: Failed to create registry key. Error code: {0}", result); - } - - if (result != 0) - { - throw new System.Exception("KO: Failed to create registry key."); - } - - return disposition == 1; + message = string.Format("OK: Registry key created (disposition: {0}).", disposition); + RegCloseKey(hKey); + return true; } - catch (System.Exception ex) + else { - message = "Error: " + ex.Message; + message = string.Format("KO: Failed to create registry key. Error code: {0}", result); return false; } } } + "@ try { # Run the C# code to create the volatile registry key @@ -113,10 +110,10 @@ public class VolatileRegistry } else { # Key creation failed Write-Error "Failed to create the key" - exit 0 + exit $ExitError } } catch { Write-Error "An unexpected error occurred: $_" - exit 0 + exit $ExitError } } \ No newline at end of file From e45551fcac03a8417dc25ad888cfd18e81ded527 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 12 May 2025 07:36:17 +0000 Subject: [PATCH 341/447] Update file: Trigger tasks on boot.ps1 --- .../Checks/Trigger tasks on boot.ps1 | 71 +++++++++++++++---- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/scripts_staging/Checks/Trigger tasks on boot.ps1 b/scripts_staging/Checks/Trigger tasks on boot.ps1 index 733bc94d..df950de5 100644 --- a/scripts_staging/Checks/Trigger tasks on boot.ps1 +++ b/scripts_staging/Checks/Trigger tasks on boot.ps1 @@ -20,11 +20,12 @@ .CHANGELOG 08.05.25 SAN added check to avoid runing C when not needed to help with runtime and better outputs - 08.05.25 SAN optimised C code and cleaned exit codes + 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" +$subKey = "SOFTWARE\\TacticalRMM\\BootTrigger5" [string]$msg = $null $ExitCreated = 66 $ExitError = 0 @@ -36,17 +37,19 @@ $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)] - public static extern int RegCreateKeyEx( + private static extern int RegCreateKeyEx( UIntPtr hKey, string lpSubKey, int Reserved, @@ -54,21 +57,41 @@ public class VolatileRegistry uint dwOptions, int samDesired, IntPtr lpSecurityAttributes, - out IntPtr phkResult, + out SafeRegistryHandle phkResult, out int lpdwDisposition ); [DllImport("advapi32.dll")] - public static extern int RegCloseKey(IntPtr hKey); + 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 const int KEY_ALL_ACCESS = 0xF003F; - public const int KEY_WOW64_64KEY = 0x0100; public static bool CreateVolatileKey(string subKey, out string message) { - IntPtr hKey; + message = string.Empty; + + if (string.IsNullOrWhiteSpace(subKey)) + { + message = "KO: Invalid subKey value."; + return false; + } + + SafeRegistryHandle hKey; int disposition; int result = RegCreateKeyEx( @@ -77,7 +100,7 @@ public class VolatileRegistry 0, null, REG_OPTION_VOLATILE, - KEY_ALL_ACCESS | KEY_WOW64_64KEY, + (int)(RegistryAccess.KEY_ALL_ACCESS | RegistryAccess.KEY_WOW64_64KEY), IntPtr.Zero, out hKey, out disposition @@ -85,9 +108,11 @@ public class VolatileRegistry if (result == 0) { - message = string.Format("OK: Registry key created (disposition: {0}).", disposition); - RegCloseKey(hKey); - return true; + using (hKey) + { + message = string.Format("OK: Registry key created (disposition: {0}).", disposition); + return true; + } } else { @@ -96,8 +121,14 @@ public class VolatileRegistry } } } - "@ + $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) @@ -105,15 +136,25 @@ public class VolatileRegistry 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 { - # Key creation failed + 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 { - Write-Error "An unexpected error occurred: $_" + $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 From 3dca8c8f30feb4c3ab25ba3f780fda2c07e711d6 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 12 May 2025 07:51:07 +0000 Subject: [PATCH 342/447] Update file: Trigger tasks on boot.ps1 --- scripts_staging/Checks/Trigger tasks on boot.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts_staging/Checks/Trigger tasks on boot.ps1 b/scripts_staging/Checks/Trigger tasks on boot.ps1 index df950de5..a2b9f46f 100644 --- a/scripts_staging/Checks/Trigger tasks on boot.ps1 +++ b/scripts_staging/Checks/Trigger tasks on boot.ps1 @@ -25,7 +25,7 @@ #> -$subKey = "SOFTWARE\\TacticalRMM\\BootTrigger5" +$subKey = "SOFTWARE\\TacticalRMM\\BootTrigger" [string]$msg = $null $ExitCreated = 66 $ExitError = 0 From 7d6f169238ac0240866d211a7f5e2682f6081284 Mon Sep 17 00:00:00 2001 From: redanthrax Date: Fri, 16 May 2025 11:41:17 -0700 Subject: [PATCH 343/447] Add Windows 11 Upgrade Script with all features upgrades to script --- scripts_wip/Win_Windows_11_Upgrade.ps1 | 397 +++++++++++++++++-------- 1 file changed, 265 insertions(+), 132 deletions(-) diff --git a/scripts_wip/Win_Windows_11_Upgrade.ps1 b/scripts_wip/Win_Windows_11_Upgrade.ps1 index dfef59c6..2a303cc9 100644 --- a/scripts_wip/Win_Windows_11_Upgrade.ps1 +++ b/scripts_wip/Win_Windows_11_Upgrade.ps1 @@ -10,12 +10,21 @@ 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, - [switch]$WaitForCompletion + [string]$IsoLocation ) #--------------------------------- @@ -24,6 +33,10 @@ param( $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" @@ -200,12 +213,46 @@ function Test-Admin { # 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. + Validates system requirements for Windows 11, including WinRE status. #> - $results = @{ TPM = $false; SecureBoot = $false; System = $false } + $results = @{ TPM = $false; SecureBoot = $false; System = $false; WinRE = $false } # Check TPM try { @@ -246,6 +293,14 @@ function Test-SystemCompatibility { 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 } @@ -266,13 +321,19 @@ function Get-DriverBlocks { [xml]$scanResult = Get-Content $scanResultPath -ErrorAction Stop $blockedDrivers = $scanResult.CompatReport.DriverPackages.DriverPackage | Where-Object { $_.BlockMigration -eq 'True' } - foreach ($driver in $blockedDriverNodes) { + # 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 { @@ -282,130 +343,6 @@ function Get-DriverBlocks { return $blockedDrivers } -function Start-Upgrade { - <# - .SYNOPSIS - Initiates the Windows 11 upgrade process. - #> - param ( - [switch]$WaitForCompletion - ) - - 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 /auto upgrade /copylogs $dir /migratedrivers all" -PassThru -ErrorAction Stop - - if ($WaitForCompletion) { - Write-Log "Monitoring upgrade process to completion..." "INFO" - $success = Monitor-UpgradeProcess -Process $process - return $success - } - else { - Write-Log "Upgrade process initiated. Use -WaitForCompletion to monitor progress." "INFO" - return $true - } - } - catch { - Handle-Error -Message "Failed to initiate upgrade." -Exception $_ - return $false - } -} - -function Monitor-UpgradeProcess { - <# - .SYNOPSIS - Monitors the upgrade process and checks for compatibility issues. - #> - param ( - [Parameter(Mandatory)] - [System.Diagnostics.Process]$Process - ) - - $timeout = (Get-Date).AddMinutes($Config.TimeoutMinutes) - $lastUpdate = Get-Date - - Write-Progress -Activity "Monitoring Windows 11 Upgrade" -Status "Checking compatibility..." - - while (-not $Process.HasExited -and (Get-Date) -lt $timeout) { - if ((Get-Date) -ge $lastUpdate.AddSeconds($Config.StatusIntervalSec)) { - Write-Log "Monitoring upgrade... (Elapsed: $((Get-Date) - $Process.StartTime).ToString('hh\:mm\:ss'))" "INFO" - $lastUpdate = Get-Date - } - - Start-Sleep -Seconds 5 - } - - Write-Progress -Activity "Monitoring Windows 11 Upgrade" -Completed - - if ($Process.HasExited) { - if ($Process.ExitCode -eq 0) { - # Verify OS version - $osInfo = Get-CimInstance Win32_OperatingSystem - $version = [Version]$osInfo.Version - $buildNumber = [int]$osInfo.BuildNumber - if ($version.Major -gt 10 -or ($version.Major -eq 10 -and $buildNumber -ge 22000)) { - Write-Log "Windows 11 detected. Upgrade completed successfully." "INFO" - return $true - } - else { - Write-Log "Upgrade process completed, but Windows 11 not detected. Checking logs for errors..." "ERROR" - $errorEntries = Get-UpgradeLogErrors - if ($errorEntries.Count -gt 0) { - Show-UpgradeFailureInfo -ErrorEntries $errorEntries - } - - return $false - } - } - else { - Write-Log "Upgrade process failed with exit code $($Process.ExitCode)." "ERROR" - $errorEntries = Get-UpgradeLogErrors - if ($errorEntries.Count -gt 0) { - Show-UpgradeFailureInfo -ErrorEntries $errorEntries - } - - return $false - } - } - - Write-Log "Upgrade monitoring timed out after $($Config.TimeoutMinutes) minutes." "ERROR" - return $false -} function Check-PreviousUpgradeAttempt { <# @@ -514,9 +451,11 @@ function Show-PreviousUpgradeAttemptInfo { 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 { @@ -860,11 +799,178 @@ function Parse-PnpUtilDrivers { } } +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 Upgrade Assistant..." "INFO" +Write-Log "Starting Windows 11 Script..." "INFO" Test-Admin # Check for previous upgrade attempts @@ -874,22 +980,49 @@ if ($previousAttempt.PreviousAttemptFound -and -not $Force) { 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)) { +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 = Start-Upgrade -WaitForCompletion:$WaitForCompletion +$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 process completed successfully." "INFO" \ No newline at end of file +Write-Log "Upgrade started successfully." "INFO" \ No newline at end of file From 0deff80ac1af852c1295cca20e75bf233e353bbd Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 22 May 2025 10:21:29 +0000 Subject: [PATCH 344/447] Update file: CallPowerShell7.ps1 --- scripts_staging/snippets/CallPowerShell7.ps1 | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts_staging/snippets/CallPowerShell7.ps1 b/scripts_staging/snippets/CallPowerShell7.ps1 index 4c1793ed..e578bff6 100644 --- a/scripts_staging/snippets/CallPowerShell7.ps1 +++ b/scripts_staging/snippets/CallPowerShell7.ps1 @@ -1,5 +1,3 @@ -#public - <# .SYNOPSIS Script to ensure PowerShell 7+ is installed and set up properly. @@ -10,6 +8,11 @@ .NOTES Author: SAN + #public + Date: 01.01.24 + +.CHANGELOG + 22.05.25 SAN Added UTF8 to fix encoding issue with russian & french chars #> @@ -52,4 +55,5 @@ if (!($PSVersionTable.PSVersion.Major -ge 7)) { } #Set the correct rendering for pwsh -$PSStyle.OutputRendering = "plaintext" \ No newline at end of file +$PSStyle.OutputRendering = "plaintext" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 \ No newline at end of file From d354db0ca3cc29d1ca475e07a22aa134636afe77 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Thu, 22 May 2025 10:21:30 +0000 Subject: [PATCH 345/447] Update file: CallPowerShell7Lite.ps1 --- scripts_staging/snippets/CallPowerShell7Lite.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts_staging/snippets/CallPowerShell7Lite.ps1 b/scripts_staging/snippets/CallPowerShell7Lite.ps1 index 5a6b86f2..83e62750 100644 --- a/scripts_staging/snippets/CallPowerShell7Lite.ps1 +++ b/scripts_staging/snippets/CallPowerShell7Lite.ps1 @@ -12,6 +12,9 @@ Author: SAN Date: 29/04/2025 #public + +.CHANGELOG + 22.05.25 SAN Added UTF8 to fix encoding issue with russian & french chars #> @@ -24,4 +27,5 @@ if (!($PSVersionTable.PSVersion.Major -ge 7)) { exit 1 } } +[Console]::OutputEncoding = [Text.Encoding]::UTF8 $PSStyle.OutputRendering = "plaintext" From 37c91e16da36f7a43718041978126f4920e4e06e Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 27 May 2025 10:04:27 +0200 Subject: [PATCH 346/447] Add new file: Mail notification password expiry.ps1 --- .../Mail notification password expiry.ps1 | 458 ++++++++++++++++++ 1 file changed, 458 insertions(+) create mode 100644 scripts_staging/Backend/Mail notification password expiry.ps1 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..b1afd9ad --- /dev/null +++ b/scripts_staging/Backend/Mail notification password expiry.ps1 @@ -0,0 +1,458 @@ +<# +.SYNOPSIS + Script permettant de récupérer et analyser les utilisateurs dont le mot de passe est sur le point d'expirer dans Active Directory. +.DESCRIPTION + Ce script se connecte à Active Directory pour rechercher les utilisateurs dans une unité organisationnelle spécifiée. + Il récupère la date de dernière mise à jour du mot de passe pour chaque utilisateur et la compare à la politique de mot de passe du domaine. + En fonction du nombre de jours restant avant l'expiration, les comptes sont classés en trois catégories : + - Expiré : Le mot de passe a déjà expiré. + - Critique : Le mot de passe est très proche de l'expiration, selon le seuil critique configuré. + - Avertissement : Le mot de passe approche de l'expiration, selon le seuil d'avertissement configuré. + Le script génère un rapport HTML contenant : + • Les détails de la politique de mot de passe du domaine (durée maximale, durée minimale, longueur minimale, complexité, historique et seuils de verrouillage). + • Un résumé statistique indiquant le nombre d'utilisateurs par catégorie. + • Une liste détaillée des comptes répartis par catégorie. + Les options de test ont été supprimées. +.PARAMETER TargetOU + Spécifie l'OU dans laquelle rechercher les utilisateurs. Exemple : "OU=Utilisateurs,DC=domaine,DC=local". +.PARAMETER WarningThreshold + Nombre de jours avant expiration déclenchant un avertissement (par défaut : 15). +.PARAMETER CriticalThreshold + Nombre de jours avant expiration déclenchant une alerte critique (par défaut : 7). +.PARAMETER IncludeDisabled + Indique si les comptes désactivés doivent être inclus dans le rapport (false par défaut). +.PARAMETER IncludeNeverExpires + Indique si les comptes dont le mot de passe n'expire jamais doivent être inclus dans le rapport (false par défaut). +.EXAMPLE + .\Check-PasswordExpiration.ps1 -TargetOU "OU=Utilisateurs,DC=domaine,DC=local" + Exécute le script avec l'OU spécifiée et les seuils par défaut. +.EXAMPLE + .\Check-PasswordExpiration.ps1 -TargetOU "OU=Utilisateurs,DC=domaine,DC=local" -WarningThreshold 20 -CriticalThreshold 10 + Exécute le script avec des seuils personnalisés pour les alertes d’avertissement et critiques. +.NOTES + Author: Peter Quellennec + Date: 27/05/25 + #public +#> +{{CallPowerShell7Lite}} + +$TargetOU = $env:TARGET_OU +$SmtpServer = $env:SMTP_SERVER +$SmtpPort = [int]$env:SMTP_PORT +$AdminEmail = $env:ADMIN_EMAIL +$FromEmail = $env:FROM_EMAIL +$WarningThreshold = [int]$env:WARNING_THRESHOLD +$CriticalThreshold = [int]$env:CRITICAL_THRESHOLD + +function Convert-ToBoolean($value) { + return $value -match '^(1|true|yes)$' +} + +$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 + ) + + $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 du mot de passe: $($passwordPolicy.MaxPasswordAge.Days) jours

+

Durée minimale du mot de passe: $($passwordPolicy.MinPasswordAge.Days) jours

+

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

+

Complexité requise: $($passwordPolicy.ComplexityEnabled)

+

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

+

Verrouillage de compte: $($passwordPolicy.LockoutThreshold) tentatives (durée: $($passwordPolicy.LockoutDuration.Minutes) minutes, observation: $($passwordPolicy.LockoutObservationWindow.Minutes) minutes)

+
+ +
+

Seuil d'avertissement : $warningThreshold jours

+

Seuil critique : $criticalThreshold jours

+

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

+
+"@ + + if ($expiredUsers) { + $html += "

Comptes expirés $($expiredUsers.Count)

" + $html += $expiredUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment + } + + if ($criticalUsers) { + $html += "

Comptes critiques $($criticalUsers.Count)

" + $html += $criticalUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment + } + + if ($warningUsers) { + $html += "

Comptes en avertissement $($warningUsers.Count)

" + $html += $warningUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment + } + + if ($IncludeNeverExpires -and $neverExpiresUsers) { + $html += "

Comptes avec mot de passe n expirant jamais $($neverExpiresUsers.Count)

" + $html += $neverExpiresUsers | Select-Object Name, SamAccountName, Email, Enabled | ConvertTo-Html -Fragment + } + + if ($neverLoggedInUsers) { + $html += "

Comptes jamais connectés $($neverLoggedInUsers.Count)

" + $html += $neverLoggedInUsers | Select-Object Name, SamAccountName, Email, Enabled | ConvertTo-Html -Fragment + } + + if ($IncludeDisabled -and $disabledUsers) { + $html += "

Comptes désactivés $($disabledUsers.Count)

" + $html += $disabledUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={if($_.ExpirationDate){$_.ExpirationDate.ToString("dd/MM/yyyy")}else{"N/A"}}}, DaysLeft | ConvertTo-Html -Fragment + } + + $html += @" +

Généré le : $(Get-Date -Format "dd/MM/yyyy HH:mm")

+ + +"@ + + return $html +} + +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 + } + $mailMessage = New-Object System.Net.Mail.MailMessage + $mailMessage.From = $FromAddress + foreach ($recipient in $Recipients) { $mailMessage.To.Add($recipient) } + $mailMessage.Subject = $Subject + $mailMessage.Body = $Body + $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) + 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 + ) + $mailMessage = New-Object System.Net.Mail.MailMessage + $mailMessage.From = $FromAddress + $mailMessage.To.Add($Recipient) + $mailMessage.Subject = $Subject + $mailMessage.Body = $Body + $mailMessage.IsBodyHtml = $true + $smtpClient = New-Object System.Net.Mail.SmtpClient($SmtpServer, $Port) + 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 = @" + + + + + + + +

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.

+

Cordialement,

+

Équipe IT

+ + +"@ + 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 ($reportData.Count -gt 0) { + $adminEmails = $AdminEmail + $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 @() +} + From 30176483927f52bbd4ce3c59cdc047adf710f4dc Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 27 May 2025 10:59:25 +0200 Subject: [PATCH 347/447] Update file: Mail notification password expiry.ps1 --- scripts_staging/Backend/Mail notification password expiry.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts_staging/Backend/Mail notification password expiry.ps1 b/scripts_staging/Backend/Mail notification password expiry.ps1 index b1afd9ad..de6035b2 100644 --- a/scripts_staging/Backend/Mail notification password expiry.ps1 +++ b/scripts_staging/Backend/Mail notification password expiry.ps1 @@ -41,6 +41,7 @@ $SmtpServer = $env:SMTP_SERVER $SmtpPort = [int]$env:SMTP_PORT $AdminEmail = $env:ADMIN_EMAIL $FromEmail = $env:FROM_EMAIL +$signmail = $env:SIGNMAIL $WarningThreshold = [int]$env:WARNING_THRESHOLD $CriticalThreshold = [int]$env:CRITICAL_THRESHOLD @@ -434,8 +435,7 @@ foreach ($user in $reportData | Where-Object { $_.Status -in @("Warning", "Criti

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.

-

Cordialement,

-

Équipe IT

+

$signmail

"@ From 18525f4acf6715f702e241dc110d4aa0f12fbb9e Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 27 May 2025 11:04:45 +0200 Subject: [PATCH 348/447] Update file: Mail notification password expiry.ps1 --- scripts_staging/Backend/Mail notification password expiry.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts_staging/Backend/Mail notification password expiry.ps1 b/scripts_staging/Backend/Mail notification password expiry.ps1 index de6035b2..3172842a 100644 --- a/scripts_staging/Backend/Mail notification password expiry.ps1 +++ b/scripts_staging/Backend/Mail notification password expiry.ps1 @@ -299,6 +299,7 @@ function Send-UserNotification { [string]$Recipient, [string]$Subject, [string]$Body, + [string]$signmail, [string]$SmtpServer, [int]$Port = 25, [string]$FromAddress @@ -308,6 +309,7 @@ function Send-UserNotification { $mailMessage.To.Add($Recipient) $mailMessage.Subject = $Subject $mailMessage.Body = $Body + $mailMessage.signmail= $signmail $mailMessage.IsBodyHtml = $true $smtpClient = New-Object System.Net.Mail.SmtpClient($SmtpServer, $Port) try { From 9909ca8434555c92a0b3e74e7d3fa9667e902f47 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 27 May 2025 11:15:53 +0200 Subject: [PATCH 349/447] Update file: Mail notification password expiry.ps1 --- .../Mail notification password expiry.ps1 | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/scripts_staging/Backend/Mail notification password expiry.ps1 b/scripts_staging/Backend/Mail notification password expiry.ps1 index 3172842a..9ae1dd95 100644 --- a/scripts_staging/Backend/Mail notification password expiry.ps1 +++ b/scripts_staging/Backend/Mail notification password expiry.ps1 @@ -44,6 +44,7 @@ $FromEmail = $env:FROM_EMAIL $signmail = $env:SIGNMAIL $WarningThreshold = [int]$env:WARNING_THRESHOLD $CriticalThreshold = [int]$env:CRITICAL_THRESHOLD +$EmailSignature = $env:EMAIL_SIGNATURE function Convert-ToBoolean($value) { return $value -match '^(1|true|yes)$' @@ -257,6 +258,22 @@ function ConvertTo-HtmlReport { return $html } +function Get-EmailSignature { + if ($EmailSignature) { + return $EmailSignature + } + + return @" +
+

+ Service Informatique
+ Téléphone : +33 (0)1 XX XX XX XX
+ Email : support@domain.com
+ Ce message est généré automatiquement, merci de ne pas y répondre directement. +

+
+"@ +} function Send-EmailReport { param( @@ -299,7 +316,6 @@ function Send-UserNotification { [string]$Recipient, [string]$Subject, [string]$Body, - [string]$signmail, [string]$SmtpServer, [int]$Port = 25, [string]$FromAddress @@ -437,7 +453,7 @@ foreach ($user in $reportData | Where-Object { $_.Status -in @("Warning", "Criti

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.

-

$signmail

+ "@ From ceac53443f3c81870dd786d75ee4ad9c92008d2e Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 27 May 2025 11:21:17 +0200 Subject: [PATCH 350/447] Update file: Mail notification password expiry.ps1 --- .../Backend/Mail notification password expiry.ps1 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts_staging/Backend/Mail notification password expiry.ps1 b/scripts_staging/Backend/Mail notification password expiry.ps1 index 9ae1dd95..ba4227fd 100644 --- a/scripts_staging/Backend/Mail notification password expiry.ps1 +++ b/scripts_staging/Backend/Mail notification password expiry.ps1 @@ -290,6 +290,7 @@ function Send-EmailReport { Write-Host "Les emails ne sont envoyés que le lundi. Arrêt de l'envoi." return } + $signature = Get-EmailSignature $mailMessage = New-Object System.Net.Mail.MailMessage $mailMessage.From = $FromAddress foreach ($recipient in $Recipients) { $mailMessage.To.Add($recipient) } @@ -320,6 +321,11 @@ function Send-UserNotification { [int]$Port = 25, [string]$FromAddress ) + $signature = Get-EmailSignature + if ($Body -match '') { + } else { + $bodyWithSignature = "$Body$signature" + } $mailMessage = New-Object System.Net.Mail.MailMessage $mailMessage.From = $FromAddress $mailMessage.To.Add($Recipient) From d9d6f36b805807d2d410267a075bb96a84f192bd Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 3 Jun 2025 11:19:11 +0200 Subject: [PATCH 351/447] Update file: Updater P3 Run PS.ps1 --- scripts_staging/TasksUpdater/Updater P3 Run PS.ps1 | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/scripts_staging/TasksUpdater/Updater P3 Run PS.ps1 b/scripts_staging/TasksUpdater/Updater P3 Run PS.ps1 index 71c67fcc..03936c11 100644 --- a/scripts_staging/TasksUpdater/Updater P3 Run PS.ps1 +++ b/scripts_staging/TasksUpdater/Updater P3 Run PS.ps1 @@ -26,9 +26,11 @@ .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" @@ -39,9 +41,6 @@ $PartName = "ModuleUpdate" # Call the logging snippet env Company_folder_path will be passed {{Logging}} -# Call the pwsh snippet -{{CallPowerShell7}} - # Set TLS version to 1.2 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 From b44477bf660e3ca2a65e57b64638f93354de0546 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 3 Jun 2025 11:19:13 +0200 Subject: [PATCH 352/447] Update file: Updater P3 Run WU.ps1 --- scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 b/scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 index 0bc6ed41..8885d014 100644 --- a/scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 +++ b/scripts_staging/TasksUpdater/Updater P3 Run WU.ps1 @@ -36,9 +36,11 @@ 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" @@ -49,9 +51,6 @@ $PartName = "WindowsUpdate" # Call the logging snippet env Company_folder_path will be passed {{Logging}} -# Call the pwsh snippet -{{CallPowerShell7}} - # Set TLS version to 1.2 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 From 8cac05c13df417c9b30b14768d43894780c9384b Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 5 Jun 2025 02:26:58 -0400 Subject: [PATCH 353/447] Add script to trigger remote wipe via MDM --- .../Win_ResetviaMDM.ps1 | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) rename {scripts_wip => scripts_staging}/Win_ResetviaMDM.ps1 (74%) diff --git a/scripts_wip/Win_ResetviaMDM.ps1 b/scripts_staging/Win_ResetviaMDM.ps1 similarity index 74% rename from scripts_wip/Win_ResetviaMDM.ps1 rename to scripts_staging/Win_ResetviaMDM.ps1 index 33c09909..21314fc4 100644 --- a/scripts_wip/Win_ResetviaMDM.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" @@ -16,4 +25,4 @@ try { } catch [Exception] { write-host $_ | out-string -} \ No newline at end of file +} From 217db5909f4e80beff3d0a1e8da7ac5df9dbb58f Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 5 Jun 2025 02:33:34 -0400 Subject: [PATCH 354/447] Enhance Win_NetworkScanner to include MAC address lookup and update parameter descriptions --- scripts_staging/Win_NetworkScanner.py | 73 ++++++++++++++++----------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/scripts_staging/Win_NetworkScanner.py b/scripts_staging/Win_NetworkScanner.py index bc3d9634..8413e867 100644 --- a/scripts_staging/Win_NetworkScanner.py +++ b/scripts_staging/Win_NetworkScanner.py @@ -4,14 +4,16 @@ 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 +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 integrated reverse DNS lookup into the ping function with 1-second timeout -v1.6 5/31/2024 align output to columns and ports low to high -v1.7 2/18/2025 fix columns with long host names and added response time +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 @@ -26,7 +28,6 @@ import argparse -# Function to get the IP address of the primary network interface def get_host_ip(): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: @@ -39,7 +40,6 @@ def get_host_ip(): return IP -# Function to ping an IP address, check if it is alive, measure response time, and optionally perform a reverse DNS lookup def ping_ip(ip, alive_hosts, do_reverse_dns): try: output = subprocess.check_output( @@ -50,9 +50,7 @@ def ping_ip(ip, alive_hosts, do_reverse_dns): 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 - ) # If no time found, use -1 + response_time = int(response_time.group(1)) if response_time else -1 hostname = "NA" if do_reverse_dns: @@ -65,12 +63,22 @@ def ping_ip(ip, alive_hosts, do_reverse_dns): finally: s.close() - alive_hosts.append((alive_ip, hostname, response_time)) + alive_hosts.append( + (alive_ip, hostname, response_time, "") + ) # Placeholder for MAC except Exception: pass -# Function to check for open ports +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: @@ -81,18 +89,19 @@ def check_ports(ip, port, open_ports): pass -# Parse command-line arguments def parse_arguments(): parser = argparse.ArgumentParser( - description="Scan network subnet for alive hosts, open ports, and optionally perform reverse DNS lookup." + 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() -# Main function to detect the subnet and scan it def main(): args = parse_arguments() host_ip = get_host_ip() @@ -111,12 +120,15 @@ def main(): for t in threads: t.join() - # Sort the alive hosts numerically + 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]) - # Launch port checks port_check_threads = [] - for host, _, _ in alive_hosts: + 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() @@ -125,28 +137,31 @@ def main(): for t in port_check_threads: t.join() - # Determine column widths dynamically max_hostname_length = max( - (len(hostname) for _, hostname, _ in alive_hosts), default=8 + (len(hostname) for _, hostname, _, _ in alive_hosts), default=8 ) ip_column_width = 16 - hostname_column_width = max(max_hostname_length, 12) + 2 # Minimum width of 12 + hostname_column_width = max(max_hostname_length, 12) + 2 response_time_column_width = 8 - ports_column_width = 50 # Static width for ports + mac_column_width = 20 if args.mac else 0 + ports_column_width = 50 - # Print header - header = f"{'IP':<{ip_column_width}}{'(ms)':<{response_time_column_width}}{'Hostname':<{hostname_column_width}}{'Open Ports':<{ports_column_width}}" + 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)) - # Print results - for host, hostname, response_time in alive_hosts: + 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" - print( - f"{str(host):<{ip_column_width}}{response_time_str:<{response_time_column_width}}{hostname:<{hostname_column_width}}{ports_str:<{ports_column_width}}" - ) + 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)}") From 469e6f7aa435460be5bf00ea40b087c2be4eeb48 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 6 Jun 2025 08:44:38 +0200 Subject: [PATCH 355/447] Update file: Mail notification password expiry.ps1 --- .../Mail notification password expiry.ps1 | 967 +++++++++--------- 1 file changed, 497 insertions(+), 470 deletions(-) diff --git a/scripts_staging/Backend/Mail notification password expiry.ps1 b/scripts_staging/Backend/Mail notification password expiry.ps1 index ba4227fd..94d8c374 100644 --- a/scripts_staging/Backend/Mail notification password expiry.ps1 +++ b/scripts_staging/Backend/Mail notification password expiry.ps1 @@ -1,482 +1,509 @@ -<# +<# .SYNOPSIS - Script permettant de récupérer et analyser les utilisateurs dont le mot de passe est sur le point d'expirer dans Active Directory. + Ensures the script is executed using PowerShell 7 or higher. + .DESCRIPTION - Ce script se connecte à Active Directory pour rechercher les utilisateurs dans une unité organisationnelle spécifiée. - Il récupère la date de dernière mise à jour du mot de passe pour chaque utilisateur et la compare à la politique de mot de passe du domaine. - En fonction du nombre de jours restant avant l'expiration, les comptes sont classés en trois catégories : - - Expiré : Le mot de passe a déjà expiré. - - Critique : Le mot de passe est très proche de l'expiration, selon le seuil critique configuré. - - Avertissement : Le mot de passe approche de l'expiration, selon le seuil d'avertissement configuré. - Le script génère un rapport HTML contenant : - • Les détails de la politique de mot de passe du domaine (durée maximale, durée minimale, longueur minimale, complexité, historique et seuils de verrouillage). - • Un résumé statistique indiquant le nombre d'utilisateurs par catégorie. - • Une liste détaillée des comptes répartis par catégorie. - Les options de test ont été supprimées. -.PARAMETER TargetOU - Spécifie l'OU dans laquelle rechercher les utilisateurs. Exemple : "OU=Utilisateurs,DC=domaine,DC=local". -.PARAMETER WarningThreshold - Nombre de jours avant expiration déclenchant un avertissement (par défaut : 15). -.PARAMETER CriticalThreshold - Nombre de jours avant expiration déclenchant une alerte critique (par défaut : 7). -.PARAMETER IncludeDisabled - Indique si les comptes désactivés doivent être inclus dans le rapport (false par défaut). -.PARAMETER IncludeNeverExpires - Indique si les comptes dont le mot de passe n'expire jamais doivent être inclus dans le rapport (false par défaut). -.EXAMPLE - .\Check-PasswordExpiration.ps1 -TargetOU "OU=Utilisateurs,DC=domaine,DC=local" - Exécute le script avec l'OU spécifiée et les seuils par défaut. -.EXAMPLE - .\Check-PasswordExpiration.ps1 -TargetOU "OU=Utilisateurs,DC=domaine,DC=local" -WarningThreshold 20 -CriticalThreshold 10 - Exécute le script avec des seuils personnalisés pour les alertes d’avertissement et critiques. + 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: Peter Quellennec - Date: 27/05/25 + Author: PQU + Date: 29/04/2025 #public -#> -{{CallPowerShell7Lite}} - -$TargetOU = $env:TARGET_OU -$SmtpServer = $env:SMTP_SERVER -$SmtpPort = [int]$env:SMTP_PORT -$AdminEmail = $env:ADMIN_EMAIL -$FromEmail = $env:FROM_EMAIL -$signmail = $env:SIGNMAIL -$WarningThreshold = [int]$env:WARNING_THRESHOLD -$CriticalThreshold = [int]$env:CRITICAL_THRESHOLD -$EmailSignature = $env:EMAIL_SIGNATURE - -function Convert-ToBoolean($value) { - return $value -match '^(1|true|yes)$' -} - -$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 -} +.CHANGELOG + 22.05.25 SAN Added UTF8 to fix encoding issue with russian & french chars + 06.06.25 PQU Added support for multiple admin emails +#> -function ConvertTo-HtmlReport { - param ( - $expiredUsers, - $criticalUsers, - $warningUsers, - $neverExpiresUsers, - $neverLoggedInUsers, - $disabledUsers, - $targetOU, - $passwordPolicy, - $warningThreshold, - $criticalThreshold - ) - $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 du mot de passe: $($passwordPolicy.MaxPasswordAge.Days) jours

-

Durée minimale du mot de passe: $($passwordPolicy.MinPasswordAge.Days) jours

-

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

-

Complexité requise: $($passwordPolicy.ComplexityEnabled)

-

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

-

Verrouillage de compte: $($passwordPolicy.LockoutThreshold) tentatives (durée: $($passwordPolicy.LockoutDuration.Minutes) minutes, observation: $($passwordPolicy.LockoutObservationWindow.Minutes) minutes)

-
- -
-

Seuil d'avertissement : $warningThreshold jours

-

Seuil critique : $criticalThreshold jours

-

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

-
+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" + + + $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 + + function Convert-ToBoolean($value) { + return $value -match '^(1|true|yes)$' + } + + $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 + ) + + $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 du mot de passe: $($passwordPolicy.MaxPasswordAge.Days) jours

+

Durée minimale du mot de passe: $($passwordPolicy.MinPasswordAge.Days) jours

+

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

+

Complexité requise: $($passwordPolicy.ComplexityEnabled)

+

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

+

Verrouillage de compte: $($passwordPolicy.LockoutThreshold) tentatives (durée: $($passwordPolicy.LockoutDuration.Minutes) minutes, observation: $($passwordPolicy.LockoutObservationWindow.Minutes) minutes)

+
+ +
+

Seuil d'avertissement : $warningThreshold jours

+

Seuil critique : $criticalThreshold jours

+

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

+
+ "@ + + if ($expiredUsers) { + $html += "

Comptes expirés $($expiredUsers.Count)

" + $html += $expiredUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment + } + + if ($criticalUsers) { + $html += "

Comptes critiques $($criticalUsers.Count)

" + $html += $criticalUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment + } + + if ($warningUsers) { + $html += "

Comptes en avertissement $($warningUsers.Count)

" + $html += $warningUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment + } + + if ($IncludeNeverExpires -and $neverExpiresUsers) { + $html += "

Comptes avec mot de passe n expirant jamais $($neverExpiresUsers.Count)

" + $html += $neverExpiresUsers | Select-Object Name, SamAccountName, Email, Enabled | ConvertTo-Html -Fragment + } + + if ($neverLoggedInUsers) { + $html += "

Comptes jamais connectés $($neverLoggedInUsers.Count)

" + $html += $neverLoggedInUsers | Select-Object Name, SamAccountName, Email, Enabled | ConvertTo-Html -Fragment + } + + if ($IncludeDisabled -and $disabledUsers) { + $html += "

Comptes désactivés $($disabledUsers.Count)

" + $html += $disabledUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={if($_.ExpirationDate){$_.ExpirationDate.ToString("dd/MM/yyyy")}else{"N/A"}}}, DaysLeft | ConvertTo-Html -Fragment + } + + $html += @" +

Généré le : $(Get-Date -Format "dd/MM/yyyy HH:mm")

+ + "@ - - if ($expiredUsers) { - $html += "

Comptes expirés $($expiredUsers.Count)

" - $html += $expiredUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment - } - - if ($criticalUsers) { - $html += "

Comptes critiques $($criticalUsers.Count)

" - $html += $criticalUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment - } - - if ($warningUsers) { - $html += "

Comptes en avertissement $($warningUsers.Count)

" - $html += $warningUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment - } - - if ($IncludeNeverExpires -and $neverExpiresUsers) { - $html += "

Comptes avec mot de passe n expirant jamais $($neverExpiresUsers.Count)

" - $html += $neverExpiresUsers | Select-Object Name, SamAccountName, Email, Enabled | ConvertTo-Html -Fragment - } - - if ($neverLoggedInUsers) { - $html += "

Comptes jamais connectés $($neverLoggedInUsers.Count)

" - $html += $neverLoggedInUsers | Select-Object Name, SamAccountName, Email, Enabled | ConvertTo-Html -Fragment - } - - if ($IncludeDisabled -and $disabledUsers) { - $html += "

Comptes désactivés $($disabledUsers.Count)

" - $html += $disabledUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={if($_.ExpirationDate){$_.ExpirationDate.ToString("dd/MM/yyyy")}else{"N/A"}}}, DaysLeft | ConvertTo-Html -Fragment - } - - $html += @" -

Généré le : $(Get-Date -Format "dd/MM/yyyy HH:mm")

- - + + return $html + } + + function Get-EmailSignature { + if ($EmailSignature) { + return "" + } + + return @" + "@ - - return $html -} -function Get-EmailSignature { - if ($EmailSignature) { - return $EmailSignature - } - - return @" -
-

- Service Informatique
- Téléphone : +33 (0)1 XX XX XX XX
- Email : support@domain.com
- Ce message est généré automatiquement, merci de ne pas y répondre directement. -

-
+ } + + 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 + $signature + + "@ -} - -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 - $mailMessage = New-Object System.Net.Mail.MailMessage - $mailMessage.From = $FromAddress - foreach ($recipient in $Recipients) { $mailMessage.To.Add($recipient) } - $mailMessage.Subject = $Subject - $mailMessage.Body = $Body - $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) - 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 - if ($Body -match '') { - } else { - $bodyWithSignature = "$Body$signature" - } - $mailMessage = New-Object System.Net.Mail.MailMessage - $mailMessage.From = $FromAddress - $mailMessage.To.Add($Recipient) - $mailMessage.Subject = $Subject - $mailMessage.Body = $Body - $mailMessage.signmail= $signmail - $mailMessage.IsBodyHtml = $true - $smtpClient = New-Object System.Net.Mail.SmtpClient($SmtpServer, $Port) - 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 = @" - - - - - - - -

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.

- - - + + $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 = @" + + + + + + + +

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.

+

Cordialement,

+ + "@ - 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 ($reportData.Count -gt 0) { - $adminEmails = $AdminEmail - $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 @() -} - + 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é." + } From ac803924ebb71fc7840339f46900802125104b49 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Fri, 6 Jun 2025 11:50:51 +0200 Subject: [PATCH 356/447] Update file: Change user password.ps1 --- .../Tasks/Change user password.ps1 | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/scripts_staging/Tasks/Change user password.ps1 b/scripts_staging/Tasks/Change user password.ps1 index 2638cb7a..c927c076 100644 --- a/scripts_staging/Tasks/Change user password.ps1 +++ b/scripts_staging/Tasks/Change user password.ps1 @@ -12,8 +12,10 @@ 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 - Do not allow to change the password on non primary DC it causes conflicts move param to env #> @@ -22,17 +24,38 @@ param( [string]$username ) -#Call snippet +# 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}} -$newPassword = $GeneratedPassphrase # Set the new password for the user -net user $username $newPassword +net user $username $GeneratedPassphrase # Check if the password change was successful if ($LASTEXITCODE -eq 0) { - Write-Host "$newPassword" + Write-Host "$GeneratedPassphrase" } else { - Write-Host "Password change for $username failed. Please check for errors." + Write-Host "Password change for $username failed." exit 1 -} \ No newline at end of file +} From 911f42457667d4fc1597cb860e92871e24a0d071 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 9 Jun 2025 09:45:08 +0200 Subject: [PATCH 357/447] Update file: Mail notification password expiry.ps1 --- .../Mail notification password expiry.ps1 | 937 +++++++++--------- 1 file changed, 452 insertions(+), 485 deletions(-) diff --git a/scripts_staging/Backend/Mail notification password expiry.ps1 b/scripts_staging/Backend/Mail notification password expiry.ps1 index 94d8c374..1e05d928 100644 --- a/scripts_staging/Backend/Mail notification password expiry.ps1 +++ b/scripts_staging/Backend/Mail notification password expiry.ps1 @@ -1,509 +1,476 @@ <# .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: PQU Date: 29/04/2025 #public - .CHANGELOG 22.05.25 SAN Added UTF8 to fix encoding issue with russian & french chars 06.06.25 PQU Added support for multiple admin emails #> - - if (!($PSVersionTable.PSVersion.Major -ge 7)) { if (Get-Command pwsh -ErrorAction SilentlyContinue) { - pwsh -File "`"$PSCommandPath`"" @PSBoundParameters - exit $LASTEXITCODE + 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" - - - $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 - - function Convert-ToBoolean($value) { - return $value -match '^(1|true|yes)$' - } - - $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 - ) - - $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 du mot de passe: $($passwordPolicy.MaxPasswordAge.Days) jours

-

Durée minimale du mot de passe: $($passwordPolicy.MinPasswordAge.Days) jours

-

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

-

Complexité requise: $($passwordPolicy.ComplexityEnabled)

-

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

-

Verrouillage de compte: $($passwordPolicy.LockoutThreshold) tentatives (durée: $($passwordPolicy.LockoutDuration.Minutes) minutes, observation: $($passwordPolicy.LockoutObservationWindow.Minutes) minutes)

-
- -
-

Seuil d'avertissement : $warningThreshold jours

-

Seuil critique : $criticalThreshold jours

-

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

-
- "@ - - if ($expiredUsers) { - $html += "

Comptes expirés $($expiredUsers.Count)

" - $html += $expiredUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment - } - - if ($criticalUsers) { - $html += "

Comptes critiques $($criticalUsers.Count)

" - $html += $criticalUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment - } - - if ($warningUsers) { - $html += "

Comptes en avertissement $($warningUsers.Count)

" - $html += $warningUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment - } - - if ($IncludeNeverExpires -and $neverExpiresUsers) { - $html += "

Comptes avec mot de passe n expirant jamais $($neverExpiresUsers.Count)

" - $html += $neverExpiresUsers | Select-Object Name, SamAccountName, Email, Enabled | ConvertTo-Html -Fragment - } - - if ($neverLoggedInUsers) { - $html += "

Comptes jamais connectés $($neverLoggedInUsers.Count)

" - $html += $neverLoggedInUsers | Select-Object Name, SamAccountName, Email, Enabled | ConvertTo-Html -Fragment - } - - if ($IncludeDisabled -and $disabledUsers) { - $html += "

Comptes désactivés $($disabledUsers.Count)

" - $html += $disabledUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={if($_.ExpirationDate){$_.ExpirationDate.ToString("dd/MM/yyyy")}else{"N/A"}}}, DaysLeft | ConvertTo-Html -Fragment - } - - $html += @" -

Généré le : $(Get-Date -Format "dd/MM/yyyy HH:mm")

- - + Write-Output "ERROR: PowerShell 7 is not available. Exiting." + exit 1 + } +} +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$PSStyle.OutputRendering = "plaintext" + +$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 + +function Convert-ToBoolean($value) { + return $value -match '^(1|true|yes)$' +} + +$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 + ) + $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 du mot de passe: $($passwordPolicy.MaxPasswordAge.Days) jours

+

Durée minimale du mot de passe: $($passwordPolicy.MinPasswordAge.Days) jours

+

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

+

Complexité requise: $($passwordPolicy.ComplexityEnabled)

+

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

+

Verrouillage de compte: $($passwordPolicy.LockoutThreshold) tentatives (durée: $($passwordPolicy.LockoutDuration.Minutes) minutes, observation: $($passwordPolicy.LockoutObservationWindow.Minutes) minutes)

+
+
+

Seuil d'avertissement : $warningThreshold jours

+

Seuil critique : $criticalThreshold jours

+

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

+
"@ - - return $html - } - - function Get-EmailSignature { - if ($EmailSignature) { - return "" - } - - return @" - + + if ($expiredUsers) { + $html += "

Comptes expirés $($expiredUsers.Count)

" + $html += $expiredUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment + } + + if ($criticalUsers) { + $html += "

Comptes critiques $($criticalUsers.Count)

" + $html += $criticalUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment + } + + if ($warningUsers) { + $html += "

Comptes en avertissement $($warningUsers.Count)

" + $html += $warningUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment + } + + if ($IncludeNeverExpires -and $neverExpiresUsers) { + $html += "

Comptes avec mot de passe n'expirant jamais $($neverExpiresUsers.Count)

" + $html += $neverExpiresUsers | Select-Object Name, SamAccountName, Email, Enabled | ConvertTo-Html -Fragment + } + + if ($neverLoggedInUsers) { + $html += "

Comptes jamais connectés $($neverLoggedInUsers.Count)

" + $html += $neverLoggedInUsers | Select-Object Name, SamAccountName, Email, Enabled | ConvertTo-Html -Fragment + } + + if ($IncludeDisabled -and $disabledUsers) { + $html += "

Comptes désactivés $($disabledUsers.Count)

" + $html += $disabledUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={if($_.ExpirationDate){$_.ExpirationDate.ToString("dd/MM/yyyy")}else{"N/A"}}}, DaysLeft | ConvertTo-Html -Fragment + } + + $html += @" +

Généré le : $(Get-Date -Format "dd/MM/yyyy HH:mm")

+ + "@ - } - - 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 - $signature - - + 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 +$signature + + "@ } - - $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 = @" - - - - - - - -

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.

-

Cordialement,

- - + $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 = @" + + + + + + + +

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.

+

Cordialement,

+ + "@ - 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é." - } + 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 From 3ccb4671f4b0e8ac3d7c6448f443ad0ec647d210 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 9 Jun 2025 09:50:31 +0200 Subject: [PATCH 358/447] Update file: Mail notification password expiry.ps1 --- .../Mail notification password expiry.ps1 | 236 ++++++++++++++---- 1 file changed, 182 insertions(+), 54 deletions(-) diff --git a/scripts_staging/Backend/Mail notification password expiry.ps1 b/scripts_staging/Backend/Mail notification password expiry.ps1 index 1e05d928..83d0563d 100644 --- a/scripts_staging/Backend/Mail notification password expiry.ps1 +++ b/scripts_staging/Backend/Mail notification password expiry.ps1 @@ -139,92 +139,146 @@ function ConvertTo-HtmlReport { $warningThreshold, $criticalThreshold ) + $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 du mot de passe: $($passwordPolicy.MaxPasswordAge.Days) jours

-

Durée minimale du mot de passe: $($passwordPolicy.MinPasswordAge.Days) jours

+

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 du mot de passe: $($passwordPolicy.PasswordHistoryCount) mots de passe

-

Verrouillage de compte: $($passwordPolicy.LockoutThreshold) tentatives (durée: $($passwordPolicy.LockoutDuration.Minutes) minutes, observation: $($passwordPolicy.LockoutObservationWindow.Minutes) minutes)

+

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

+

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

-
-

Seuil d'avertissement : $warningThreshold jours

-

Seuil critique : $criticalThreshold jours

-

Statistiques : + +

+

Statistiques globales

+

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

-"@ - if ($expiredUsers) { - $html += "

Comptes expirés $($expiredUsers.Count)

" - $html += $expiredUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment + @if ($expiredUsers) { +
+

Comptes expirés

+ $expiredUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment +
} - if ($criticalUsers) { - $html += "

Comptes critiques $($criticalUsers.Count)

" - $html += $criticalUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment + @if ($criticalUsers) { +
+

Comptes critiques

+ $criticalUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment +
} - if ($warningUsers) { - $html += "

Comptes en avertissement $($warningUsers.Count)

" - $html += $warningUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment + @if ($warningUsers) { +
+

Comptes en avertissement

+ $warningUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment +
} - if ($IncludeNeverExpires -and $neverExpiresUsers) { - $html += "

Comptes avec mot de passe n'expirant jamais $($neverExpiresUsers.Count)

" - $html += $neverExpiresUsers | Select-Object Name, SamAccountName, Email, Enabled | ConvertTo-Html -Fragment + @if ($IncludeNeverExpires -and $neverExpiresUsers) { +
+

Comptes avec mot de passe n'expirant jamais

+ $neverExpiresUsers | Select-Object Name, SamAccountName, Email, Enabled | ConvertTo-Html -Fragment +
} - if ($neverLoggedInUsers) { - $html += "

Comptes jamais connectés $($neverLoggedInUsers.Count)

" - $html += $neverLoggedInUsers | Select-Object Name, SamAccountName, Email, Enabled | ConvertTo-Html -Fragment + @if ($neverLoggedInUsers) { +
+

Comptes jamais connectés

+ $neverLoggedInUsers | Select-Object Name, SamAccountName, Email, Enabled | ConvertTo-Html -Fragment +
} - if ($IncludeDisabled -and $disabledUsers) { - $html += "

Comptes désactivés $($disabledUsers.Count)

" - $html += $disabledUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={if($_.ExpirationDate){$_.ExpirationDate.ToString("dd/MM/yyyy")}else{"N/A"}}}, DaysLeft | ConvertTo-Html -Fragment + @if ($IncludeDisabled -and $disabledUsers) { +
+

Comptes désactivés

+ $disabledUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={if($_.ExpirationDate){$_.ExpirationDate.ToString("dd/MM/yyyy")}else{"N/A"}}}, DaysLeft | ConvertTo-Html -Fragment +
} - $html += @" -

Généré le : $(Get-Date -Format "dd/MM/yyyy HH:mm")

+ +
"@ @@ -440,18 +494,92 @@ foreach ($user in $reportData | Where-Object { $_.Status -in @("Warning", "Criti +
+

⚠️ Avertissement : Expiration de votre mot de passe

Bonjour $($user.Name),

-

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

+

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.

-

Cordialement,

+ + + + + + + + + + + + + + + + + + + + + + +
Nom$($user.Name)
SAM Account Name$($user.SamAccountName)
Email$($user.Email)
Date d'expiration$expirationDate
Jours restants$($user.DaysLeft)
+ +

Cordialement,
+ Service Informatique

+
"@ From 111dde795744ff49883c3e4ff6e879b2056bb3b9 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 9 Jun 2025 09:58:25 +0200 Subject: [PATCH 359/447] Update file: Mail notification password expiry.ps1 --- scripts_staging/Backend/Mail notification password expiry.ps1 | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts_staging/Backend/Mail notification password expiry.ps1 b/scripts_staging/Backend/Mail notification password expiry.ps1 index 83d0563d..77fd05dc 100644 --- a/scripts_staging/Backend/Mail notification password expiry.ps1 +++ b/scripts_staging/Backend/Mail notification password expiry.ps1 @@ -144,8 +144,6 @@ function ConvertTo-HtmlReport { - - Rapport d'expiration des mots de passe +
$body -$signature +
"@ @@ -536,12 +583,6 @@ foreach ($user in $reportData | Where-Object { $_.Status -in @("Warning", "Criti background-color: #3498db; color: white; } - .footer { - margin-top: 40px; - font-size: 0.9em; - color: #777; - text-align: center; - } @@ -574,9 +615,6 @@ foreach ($user in $reportData | Where-Object { $_.Status -in @("Warning", "Criti $($user.DaysLeft) - -

Cordialement,
- Service Informatique

From 7f9bcd6ab6d1214a2004ffcffbd2335613c2b863 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 9 Jun 2025 14:45:31 +0200 Subject: [PATCH 361/447] Update file: Mail notification password expiry.ps1 --- .../Backend/Mail notification password expiry.ps1 | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts_staging/Backend/Mail notification password expiry.ps1 b/scripts_staging/Backend/Mail notification password expiry.ps1 index 13741e89..4410b0ee 100644 --- a/scripts_staging/Backend/Mail notification password expiry.ps1 +++ b/scripts_staging/Backend/Mail notification password expiry.ps1 @@ -400,13 +400,15 @@ function Send-UserNotification { margin-top: 20px; } th, td { - padding: 12px; + padding: 8px; /* Réduit l'espace */ border-bottom: 1px solid #ddd; text-align: left; + font-size: 14px; /* Police compacte */ } th { background-color: #3498db; color: white; + font-size: 14px; /* Police compacte */ } @@ -421,7 +423,7 @@ $body $mailMessage = New-Object System.Net.Mail.MailMessage $mailMessage.From = $FromAddress $mailMessage.To.Add($Recipient) - $mailMessage.Subject = $Subject + $mailMessage.Subject = $subject $mailMessage.Body = $bodyWithSignature $mailMessage.IsBodyHtml = $true $smtpClient = New-Object System.Net.Mail.SmtpClient($SmtpServer, $Port) @@ -575,13 +577,15 @@ foreach ($user in $reportData | Where-Object { $_.Status -in @("Warning", "Criti margin-top: 20px; } th, td { - padding: 12px; + padding: 8px; /* Réduit l'espace */ border-bottom: 1px solid #ddd; text-align: left; + font-size: 14px; /* Police compacte */ } th { background-color: #3498db; color: white; + font-size: 14px; /* Police compacte */ } From eae28d216f0f55e5aacc844183e41dc0f8a1717c Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 9 Jun 2025 14:50:43 +0200 Subject: [PATCH 362/447] Update file: Mail notification password expiry.ps1 --- .../Mail notification password expiry.ps1 | 152 ++++++++++++++++-- 1 file changed, 140 insertions(+), 12 deletions(-) diff --git a/scripts_staging/Backend/Mail notification password expiry.ps1 b/scripts_staging/Backend/Mail notification password expiry.ps1 index 4410b0ee..5f91e853 100644 --- a/scripts_staging/Backend/Mail notification password expiry.ps1 +++ b/scripts_staging/Backend/Mail notification password expiry.ps1 @@ -231,45 +231,173 @@ function ConvertTo-HtmlReport {

- @if ($expiredUsers) { + @if ($expiredUsers.Count -gt 0) {

Comptes expirés

- $expiredUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment + + + + + + + + + + + + + $($expiredUsers | ForEach-Object { + " + + + + + + + " + }) + +
NomSAM Account NameEmailDate d'expirationJours restantsActivé
$($_.Name)$($_.SamAccountName)$($_.Email)$($_.ExpirationDate.ToString('dd/MM/yyyy'))$($_.DaysLeft)$($_.Enabled)
} - @if ($criticalUsers) { + @if ($criticalUsers.Count -gt 0) {

Comptes critiques

- $criticalUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment + + + + + + + + + + + + + $($criticalUsers | ForEach-Object { + " + + + + + + + " + }) + +
NomSAM Account NameEmailDate d'expirationJours restantsActivé
$($_.Name)$($_.SamAccountName)$($_.Email)$($_.ExpirationDate.ToString('dd/MM/yyyy'))$($_.DaysLeft)$($_.Enabled)
} - @if ($warningUsers) { + @if ($warningUsers.Count -gt 0) {

Comptes en avertissement

- $warningUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={$_.ExpirationDate.ToString("dd/MM/yyyy")}}, DaysLeft, Enabled | ConvertTo-Html -Fragment + + + + + + + + + + + + + $($warningUsers | ForEach-Object { + " + + + + + + + " + }) + +
NomSAM Account NameEmailDate d'expirationJours restantsActivé
$($_.Name)$($_.SamAccountName)$($_.Email)$($_.ExpirationDate.ToString('dd/MM/yyyy'))$($_.DaysLeft)$($_.Enabled)
} - @if ($IncludeNeverExpires -and $neverExpiresUsers) { + @if ($IncludeNeverExpires -and $neverExpiresUsers.Count -gt 0) {

Comptes avec mot de passe n'expirant jamais

- $neverExpiresUsers | Select-Object Name, SamAccountName, Email, Enabled | ConvertTo-Html -Fragment + + + + + + + + + + + $($neverExpiresUsers | ForEach-Object { + " + + + + + " + }) + +
NomSAM Account NameEmailActivé
$($_.Name)$($_.SamAccountName)$($_.Email)$($_.Enabled)
} - @if ($neverLoggedInUsers) { + @if ($neverLoggedInUsers.Count -gt 0) {

Comptes jamais connectés

- $neverLoggedInUsers | Select-Object Name, SamAccountName, Email, Enabled | ConvertTo-Html -Fragment + + + + + + + + + + + $($neverLoggedInUsers | ForEach-Object { + " + + + + + " + }) + +
NomSAM Account NameEmailActivé
$($_.Name)$($_.SamAccountName)$($_.Email)$($_.Enabled)
} - @if ($IncludeDisabled -and $disabledUsers) { + @if ($IncludeDisabled -and $disabledUsers.Count -gt 0) {

Comptes désactivés

- $disabledUsers | Select-Object Name, SamAccountName, Email, @{Name="ExpirationDate";Expression={if($_.ExpirationDate){$_.ExpirationDate.ToString("dd/MM/yyyy")}else{"N/A"}}}, DaysLeft | ConvertTo-Html -Fragment + + + + + + + + + + + + $($disabledUsers | ForEach-Object { + " + + + + + + " + }) + +
NomSAM Account NameEmailDate d'expirationJours restants
$($_.Name)$($_.SamAccountName)$($_.Email)$($_.ExpirationDate.ToString('dd/MM/yyyy'))$($_.DaysLeft)
} From f477e086b3f30594fc4a7b4feaae450ef310ed9b Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 9 Jun 2025 14:56:31 +0200 Subject: [PATCH 363/447] Update file: Mail notification password expiry.ps1 --- .../Mail notification password expiry.ps1 | 348 +++++++++--------- 1 file changed, 183 insertions(+), 165 deletions(-) diff --git a/scripts_staging/Backend/Mail notification password expiry.ps1 b/scripts_staging/Backend/Mail notification password expiry.ps1 index 5f91e853..43578d00 100644 --- a/scripts_staging/Backend/Mail notification password expiry.ps1 +++ b/scripts_staging/Backend/Mail notification password expiry.ps1 @@ -139,100 +139,21 @@ function ConvertTo-HtmlReport { $warningThreshold, $criticalThreshold ) - $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) -

-
- - @if ($expiredUsers.Count -gt 0) { -
+ # Génération des sections conditionnelles + $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

@@ -246,23 +167,26 @@ function ConvertTo-HtmlReport { - $($expiredUsers | ForEach-Object { - " - - - - - - - " - }) + $rows
$($_.Name)$($_.SamAccountName)$($_.Email)$($_.ExpirationDate.ToString('dd/MM/yyyy'))$($_.DaysLeft)$($_.Enabled)
+"@ } - - @if ($criticalUsers.Count -gt 0) { -
+ $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

@@ -276,23 +200,26 @@ function ConvertTo-HtmlReport { - $($criticalUsers | ForEach-Object { - " - - - - - - - " - }) + $rows
$($_.Name)$($_.SamAccountName)$($_.Email)$($_.ExpirationDate.ToString('dd/MM/yyyy'))$($_.DaysLeft)$($_.Enabled)
+"@ } - - @if ($warningUsers.Count -gt 0) { -
+ $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

@@ -306,23 +233,24 @@ function ConvertTo-HtmlReport { - $($warningUsers | ForEach-Object { - " - - - - - - - " - }) + $rows
$($_.Name)$($_.SamAccountName)$($_.Email)$($_.ExpirationDate.ToString('dd/MM/yyyy'))$($_.DaysLeft)$($_.Enabled)
+"@ } - - @if ($IncludeNeverExpires -and $neverExpiresUsers.Count -gt 0) { -
+ $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

@@ -334,21 +262,24 @@ function ConvertTo-HtmlReport { - $($neverExpiresUsers | ForEach-Object { - " - - - - - " - }) + $rows
$($_.Name)$($_.SamAccountName)$($_.Email)$($_.Enabled)
+"@ } - - @if ($neverLoggedInUsers.Count -gt 0) { -
+ $neverLoggedInSection = "" + if ($neverLoggedInUsers.Count -gt 0) { + $rows = $neverLoggedInUsers | ForEach-Object { + " + $($_.Name) + $($_.SamAccountName) + $($_.Email) + $($_.Enabled) + " + } | Out-String + $neverLoggedInSection = @" +

Comptes jamais connectés

@@ -360,21 +291,25 @@ function ConvertTo-HtmlReport { - $($neverLoggedInUsers | ForEach-Object { - " - - - - - " - }) + $rows
$($_.Name)$($_.SamAccountName)$($_.Email)$($_.Enabled)
+"@ } - - @if ($IncludeDisabled -and $disabledUsers.Count -gt 0) { -
+ $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

@@ -387,20 +322,103 @@ function ConvertTo-HtmlReport { - $($disabledUsers | ForEach-Object { - " - - - - - - " - }) + $rows
$($_.Name)$($_.SamAccountName)$($_.Email)$($_.ExpirationDate.ToString('dd/MM/yyyy'))$($_.DaysLeft)
+"@ } - + # Assemblage du rapport HTML final + $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 From d842f70db0e57507b151753e0a3686232881081a Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 9 Jun 2025 15:04:31 +0200 Subject: [PATCH 364/447] Update file: Mail notification password expiry.ps1 --- .../Backend/Mail notification password expiry.ps1 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts_staging/Backend/Mail notification password expiry.ps1 b/scripts_staging/Backend/Mail notification password expiry.ps1 index 43578d00..2ef00e08 100644 --- a/scripts_staging/Backend/Mail notification password expiry.ps1 +++ b/scripts_staging/Backend/Mail notification password expiry.ps1 @@ -520,7 +520,7 @@ function Send-UserNotification { .container { max-width: 700px; margin: 0 auto; - padding: 30px; + padding: 15px; // reduced from 30px to 15px background-color: #fff; border-radius: 10px; box-shadow: 0 0 10px rgba(0,0,0,0.05); @@ -528,11 +528,11 @@ function Send-UserNotification { h1 { color: #2c3e50; border-bottom: 2px solid #3498db; - padding-bottom: 10px; + padding-bottom: 5px; // reduced from 10px to 5px } .status { font-weight: bold; - margin-top: 15px; + margin-top: 5px; // reduced from 15px to 5px display: inline-block; padding: 6px 12px; border-radius: 5px; @@ -543,18 +543,18 @@ function Send-UserNotification { table { width: 100%; border-collapse: collapse; - margin-top: 20px; + margin-top: 5px; // reduced from 10px to 5px } th, td { - padding: 8px; /* Réduit l'espace */ + padding: 2px; /* reduced from 4px to 2px */ border-bottom: 1px solid #ddd; text-align: left; - font-size: 14px; /* Police compacte */ + font-size: 14px; } th { background-color: #3498db; color: white; - font-size: 14px; /* Police compacte */ + font-size: 14px; } From 5c58624995124d64f009a457195feadb285f720e Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 9 Jun 2025 15:11:58 +0200 Subject: [PATCH 365/447] Update file: Mail notification password expiry.ps1 --- .../Backend/Mail notification password expiry.ps1 | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts_staging/Backend/Mail notification password expiry.ps1 b/scripts_staging/Backend/Mail notification password expiry.ps1 index 2ef00e08..29b0ed73 100644 --- a/scripts_staging/Backend/Mail notification password expiry.ps1 +++ b/scripts_staging/Backend/Mail notification password expiry.ps1 @@ -520,7 +520,7 @@ function Send-UserNotification { .container { max-width: 700px; margin: 0 auto; - padding: 15px; // reduced from 30px to 15px + padding: 15px; // réduit par rapport à 30px background-color: #fff; border-radius: 10px; box-shadow: 0 0 10px rgba(0,0,0,0.05); @@ -528,11 +528,11 @@ function Send-UserNotification { h1 { color: #2c3e50; border-bottom: 2px solid #3498db; - padding-bottom: 5px; // reduced from 10px to 5px + padding-bottom: 5px; // réduit par rapport à 10px } .status { font-weight: bold; - margin-top: 5px; // reduced from 15px to 5px + margin-top: 5px; // réduit par rapport à 15px display: inline-block; padding: 6px 12px; border-radius: 5px; @@ -543,13 +543,14 @@ function Send-UserNotification { table { width: 100%; border-collapse: collapse; - margin-top: 5px; // reduced from 10px to 5px + margin-top: 2px; // espace vertical réduit } th, td { - padding: 2px; /* reduced from 4px to 2px */ + padding: 0px 2px; /* Padding réduit pour correspondre à la police */ border-bottom: 1px solid #ddd; text-align: left; font-size: 14px; + line-height: 1.0; /* Hauteur de ligne réduite */ } th { background-color: #3498db; From 8d80ca339a5e4e650ef9b6f8f71e76474d100ec0 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 9 Jun 2025 15:17:24 +0200 Subject: [PATCH 366/447] Update file: Mail notification password expiry.ps1 --- scripts_staging/Backend/Mail notification password expiry.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts_staging/Backend/Mail notification password expiry.ps1 b/scripts_staging/Backend/Mail notification password expiry.ps1 index 29b0ed73..ba8b4987 100644 --- a/scripts_staging/Backend/Mail notification password expiry.ps1 +++ b/scripts_staging/Backend/Mail notification password expiry.ps1 @@ -546,11 +546,11 @@ function Send-UserNotification { margin-top: 2px; // espace vertical réduit } th, td { - padding: 0px 2px; /* Padding réduit pour correspondre à la police */ + padding: 0px 2px; /* Padding réduit pour minimiser la hauteur des lignes */ border-bottom: 1px solid #ddd; text-align: left; font-size: 14px; - line-height: 1.0; /* Hauteur de ligne réduite */ + line-height: 0.8; /* Hauteur de ligne réduite pour correspondre à la taille de la police */ } th { background-color: #3498db; From 527c98b1e79f8fc41cd0ebb3f11b37fcad4781cc Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Mon, 9 Jun 2025 15:22:49 +0200 Subject: [PATCH 367/447] Update file: Mail notification password expiry.ps1 --- scripts_staging/Backend/Mail notification password expiry.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts_staging/Backend/Mail notification password expiry.ps1 b/scripts_staging/Backend/Mail notification password expiry.ps1 index ba8b4987..80f2a04b 100644 --- a/scripts_staging/Backend/Mail notification password expiry.ps1 +++ b/scripts_staging/Backend/Mail notification password expiry.ps1 @@ -551,6 +551,7 @@ function Send-UserNotification { text-align: left; font-size: 14px; line-height: 0.8; /* Hauteur de ligne réduite pour correspondre à la taille de la police */ + height: 18px; // fixed row height regardless of font size } th { background-color: #3498db; From 224999dc960df857406398fab718c25b333e1afa Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Tue, 10 Jun 2025 08:46:01 +0200 Subject: [PATCH 368/447] Update file: Mail notification password expiry.ps1 --- .../Backend/Mail notification password expiry.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts_staging/Backend/Mail notification password expiry.ps1 b/scripts_staging/Backend/Mail notification password expiry.ps1 index 80f2a04b..a0f31222 100644 --- a/scripts_staging/Backend/Mail notification password expiry.ps1 +++ b/scripts_staging/Backend/Mail notification password expiry.ps1 @@ -13,6 +13,7 @@ .CHANGELOG 22.05.25 SAN Added UTF8 to fix encoding issue with russian & french chars 06.06.25 PQU Added support for multiple admin emails + 10.06.25 PQU Modification of the visual of the email #> if (!($PSVersionTable.PSVersion.Major -ge 7)) { if (Get-Command pwsh -ErrorAction SilentlyContinue) { @@ -139,7 +140,7 @@ function ConvertTo-HtmlReport { $warningThreshold, $criticalThreshold ) - # Génération des sections conditionnelles + $expiredSection = "" if ($expiredUsers.Count -gt 0) { $rows = $expiredUsers | ForEach-Object { @@ -328,7 +329,7 @@ function ConvertTo-HtmlReport {
"@ } - # Assemblage du rapport HTML final + $html = @" From 750a0fb9b3a4ceb22c8f6c45612e7ca8a00eda38 Mon Sep 17 00:00:00 2001 From: P6g9YHK6 <17877371+P6g9YHK6@users.noreply.github.com> Date: Wed, 11 Jun 2025 09:31:23 +0200 Subject: [PATCH 369/447] Update file: Mail notification password expiry.ps1 --- .../Mail notification password expiry.ps1 | 64 ++++++++++++------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/scripts_staging/Backend/Mail notification password expiry.ps1 b/scripts_staging/Backend/Mail notification password expiry.ps1 index a0f31222..10ce765f 100644 --- a/scripts_staging/Backend/Mail notification password expiry.ps1 +++ b/scripts_staging/Backend/Mail notification password expiry.ps1 @@ -1,31 +1,49 @@ <# .SYNOPSIS - Ensures the script is executed using PowerShell 7 or higher. + Analyse et notification avancée des utilisateurs dont le mot de passe Active Directory approche de l’expiration. .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. + Ce script interroge Active Directory pour lister les utilisateurs d’une OU cible et calcule la date d’expiration de leur mot de passe selon la politique du domaine. + Les comptes sont classés selon l’urgence : + - Expiré : mot de passe déjà expiré + - Critique : expiration imminente (seuil critique) + - Avertissement : expiration proche (seuil d’avertissement) + Notifications automatiques : + • Email pour tous les utilisateurs concernés + Un rapport HTML détaillé est généré : + • Politique de mot de passe du domaine + • Statistiques par catégorie + • Liste détaillée des comptes par statut +.PARAMETER TargetOU + OU cible pour la recherche des utilisateurs (ex : "OU=Utilisateurs,DC=domaine,DC=local") +.PARAMETER WarningThreshold + Jours avant expiration pour déclencher un avertissement (défaut : 15) +.PARAMETER CriticalThreshold + Jours avant expiration pour déclencher une alerte critique (défaut : 7) +.PARAMETER IncludeDisabled + Inclure les comptes désactivés dans le rapport (défaut : false) +.PARAMETER IncludeNeverExpires + Inclure les comptes dont le mot de passe n’expire jamais (défaut : false) +.PARAMETER EmailSignature + Signature personnalisée pour les emails (optionnel) .NOTES + Prérequis : + - Module ActiveDirectory + - Accès SMTP pour l’envoi d’emails + - Droits d’administration pour les tâches planifiées Author: PQU Date: 29/04/2025 #public .CHANGELOG - 22.05.25 SAN Added UTF8 to fix encoding issue with russian & french chars - 06.06.25 PQU Added support for multiple admin emails - 10.06.25 PQU Modification of the visual of the email + 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. #> -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 - } + + +{{CallPowerShell7}} + +function Convert-ToBoolean($value) { + return $value -match '^(1|true|yes)$' } -[Console]::OutputEncoding = [Text.Encoding]::UTF8 -$PSStyle.OutputRendering = "plaintext" $TargetOU = $env:TARGET_OU $SmtpServer = $env:SMTP_SERVER @@ -35,15 +53,13 @@ $FromEmail = $env:FROM_EMAIL $WarningThreshold = [int]$env:WARNING_THRESHOLD $CriticalThreshold = [int]$env:CRITICAL_THRESHOLD $EmailSignature = $env:EMAIL_SIGNATURE - -function Convert-ToBoolean($value) { - return $value -match '^(1|true|yes)$' -} - $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 @@ -438,7 +454,7 @@ function Get-EmailSignature {