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