diff --git a/scripts_staging/Backend/Sync TRMM with GIT.py b/scripts_staging/Backend/Sync TRMM with GIT.py index 73709eda..baca173f 100644 --- a/scripts_staging/Backend/Sync TRMM with GIT.py +++ b/scripts_staging/Backend/Sync TRMM with GIT.py @@ -118,6 +118,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 + add variables for name and email for the commit """ diff --git a/scripts_staging/Checks/Backup Veeam agent.ps1 b/scripts_staging/Checks/Backup Veeam agent.ps1 index 62da85d0..e7a12e25 100644 --- a/scripts_staging/Checks/Backup Veeam agent.ps1 +++ b/scripts_staging/Checks/Backup Veeam agent.ps1 @@ -17,89 +17,137 @@ .CHANGELOG 15/04/25 SAN Code Cleaup & Publication + 12.02.26 SAN Code improvement and fix for v13 .TODO Var to env - get latest logs in case of error and output the logs #> +# CONFIG $RootDirectory = "C:\ProgramData\Veeam\Endpoint" $ThresholdHours = 48 -$DateFormat = "dd.MM.yyyy HH:mm:ss" +$DateFormat = "dd.MM.yyyy HH:mm:ss.fff" $LogPattern = "Job session '.*' has been completed, status: '(.*?)'," +$FailureLogLines = 50 + 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 + if (-not (Test-Path $RootDirectory)) { + throw "Directory not found: $RootDirectory" + } + + $logFile = Get-ChildItem -Path $RootDirectory -Filter "*.Backup.log" -Recurse -ErrorAction Stop | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 + + if (-not $logFile) { + throw "No .Backup.log files found." } + + return $logFile } 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 + param ( + [Parameter(Mandatory)] + [System.IO.FileInfo]$LogFile + ) + + $recentLine = Select-String -Path $LogFile.FullName -Pattern $LogPattern -ErrorAction Stop | + Select-Object -Last 1 + + if (-not $recentLine) { + throw "No matching job completion entry found in log." + } + + if ($recentLine.Line -match "\[(.*?)\].*status: '(.*?)',") { + return @{ + DateTime = $matches[1] + Status = $matches[2] } - } catch { - Write-Output "KO: Error processing the log file: $_" - exit 1 } + + throw "Log entry found but parsing failed." } function Check-JobStatus { param ( - [string]$dateTime, - [string]$status + [Parameter(Mandatory)][string]$DateTime, + [Parameter(Mandatory)][string]$Status ) + $culture = [System.Globalization.CultureInfo]::InvariantCulture + 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 + $logDate = [datetime]::ParseExact($DateTime, $DateFormat, $culture) + } + catch { + throw "Timestamp format invalid: '$DateTime'" + } + + $timeSpan = New-TimeSpan -Start $logDate -End (Get-Date) + + if ($Status -ne "Success") { + return @{ + Code = 1 + Message = "KO: Job status is '$Status'" } - } catch { - Write-Output "KO: Error checking job status: $_" - exit 1 + } + + if ($timeSpan.TotalHours -gt $ThresholdHours) { + return @{ + Code = 1 + Message = "KO: Last backup older than $ThresholdHours hours (Last run: $DateTime)" + } + } + + return @{ + Code = 0 + Message = "OK: Job succeeded at $DateTime" } } +function Write-FailureDetails { + param ( + [string]$Message, + [System.IO.FileInfo]$LogFile + ) + + Write-Output $Message + + if ($LogFile -and (Test-Path $LogFile.FullName)) { + Write-Output "---- Last $FailureLogLines log lines ----" + try { + Get-Content -Path $LogFile.FullName -Tail $FailureLogLines -ErrorAction Stop | + ForEach-Object { Write-Output $_ } + } + catch { + Write-Output "Unable to read log tail: $($_.Exception.Message)" + } + } +} + +#MAIN + +$logFile = $null + try { $logFile = Get-RecentLogFile + $jobInfo = Get-JobStatusFromLog -LogFile $logFile + $result = Check-JobStatus -DateTime $jobInfo.DateTime -Status $jobInfo.Status - 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." + if ($result.Code -eq 0) { + Write-Output $result.Message + exit 0 + } + else { + Write-FailureDetails -Message $result.Message -LogFile $logFile exit 1 } -} catch { - Write-Output "KO: Unexpected error: $_" - exit 1 } +catch { + $errorMessage = "KO: $($_.Exception.Message)" + Write-FailureDetails -Message $errorMessage -LogFile $logFile + exit 1 +} \ No newline at end of file diff --git a/scripts_staging/Checks/Internet uplink.ps1 b/scripts_staging/Checks/Internet uplink.ps1 index 891d59bf..6aa986a2 100644 --- a/scripts_staging/Checks/Internet uplink.ps1 +++ b/scripts_staging/Checks/Internet uplink.ps1 @@ -21,6 +21,8 @@ .CHANGELOG 25.03.25 SAN Format output + 08.01.26 SAN updated AWS http://ec2-reachability.amazonaws.com/ + .TODO Include customizable input for the list of IP addresses. Enhance error handling for unreachable hosts. @@ -46,8 +48,8 @@ $ipAddresses = @( @{ 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" } + @{ IP="3.80.0.0"; Owner="Amazon AWS" }, + @{ IP="3.64.0.0"; Owner="Amazon AWS" } ) $pingFailed = $false diff --git a/scripts_staging/Checks/Last errors logs.ps1 b/scripts_staging/Checks/Last errors logs.ps1 index 55e1f306..ce79ea72 100644 --- a/scripts_staging/Checks/Last errors logs.ps1 +++ b/scripts_staging/Checks/Last errors logs.ps1 @@ -1,79 +1,114 @@ <# .SYNOPSIS - This script retrieves and processes error events from the Windows Event Log within the last 48 and 12 hours. + Monitors Windows 'System' event logs and reports error events with configurable thresholds. .DESCRIPTION - This script is useful for monitoring and alerting on error events in the Windows Event Log. - The script processes error logs from the 'System' log only, only critical errors are counted and displayed. - - 1. Retrieves the last 20 error events from the 'System' log in the last 48 hours, excluding specified event IDs. - 2. Counts and displays the number of error events found in the last 48 hours (after filtering out ignored events). - 3. Retrieves error events from the last 12 hours and checks if there are 4 or more errors. - 4. If 4 or more errors are found in the last 12 hours, the script exits with an error code (1). - 5. If fewer than 4 errors are found, the script exits with a success code (0). - -.EXEMPLE - debug=true + This script retrieves error events from the Windows 'System' log over a configurable lookback period + (default 48 hours) and evaluates them within a configurable evaluation window + (default 12 hours). It filters out events by specified Event IDs and keywords. + + The script supports three severity thresholds (set unrealistic threshold to disable): + - INFO: default 1 event + - WARN: default 2 events + - ERROR: default 4 events + + Behavior: + 1. Retrieves error events (Level=2) from the 'System' log within the lookback period. + 2. Filters out ignored Event IDs and keywords (defaults or via environment variables). + 3. Evaluates events within the evaluation window: + - If count >= ERROR threshold, exits with ERROR exit code (3). + - If count >= WARN threshold, exits with WARN exit code (2). + - Otherwise, exits with INFO exit code (1). + 4. Debug mode outputs filtered events and thresholds. + +.EXAMPLE + DEBUG=true FILTER_ID=1111,22222,3333 FILTER_KEYWORD=keyword1,keyword2 + INFO_THRESHOLD=1 + WARN_THRESHOLD=2 + ERROR_THRESHOLD=4 + LOOKBACK_HOURS=72 + EVALUATION_WINDOW_HOURS=24 .NOTES Author: SAN Date: 24.10.2024 - #public - - 10016 safe to ignore - https://learn.microsoft.com/en-us/troubleshoot/windows-client/application-management/event-10016-logged-when-accessing-dcom - 36874 to ignore - Fixing the issue would be more dangerous than leaving it be it would require blocking tls 1.2 and forcing 1.1 with unsafe cyphers and loosing connection to devices that do not support 1.1 + #PUBLIC + Default ignored Event IDs: + 10016 - safe to ignore, see: + https://learn.microsoft.com/en-us/troubleshoot/windows-client/application-management/event-10016-logged-when-accessing-dcom + 36874 - ignored due to TLS/connection constraints .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 + 04.12.24 SAN: Added environment variable support for ignored Event IDs. + 12.12.24 SAN: Added keyword filters and support for dynamic filter addition via environment variables. + 02.04.26 SAN: Added configurable info/warn/error thresholds with exit codes. #> -$defaultEventIds = @(10016,36874) -$defaultKeywords = @("gupdate","anotherkeyword") +$defaultEventIds = @(10016, 36874) +$defaultKeywords = @("gupdate", "anotherkeyword") -$debug = [System.Environment]::GetEnvironmentVariable("DEBUG") -$filterIdEnv = [System.Environment]::GetEnvironmentVariable("FILTER_ID") -$filterKeywordEnv = [System.Environment]::GetEnvironmentVariable("FILTER_KEYWORD") +$defaultInfoThreshold = 1 +$defaultWarnThreshold = 2 +$defaultErrorThreshold = 4 -$ignoredEventIds = if ($filterIdEnv) { - $filterIdEnv.Split(",") + $defaultEventIds -} else { - $defaultEventIds -} +$defaultLookbackHours = 48 +$defaultEvaluationWindowHours = 12 -$ignoredKeywords = if ($filterKeywordEnv) { - $filterKeywordEnv.Split(",") + $defaultKeywords -} else { - $defaultKeywords -} +$infoExitCode = 1 +$warnExitCode = 2 +$errorExitCode = 3 -$start48h = (Get-Date).AddHours(-48) -$start12h = (Get-Date).AddHours(-12) +$debug = $env:DEBUG +$filterIdEnv = $env:FILTER_ID +$filterKeywordEnv = $env:FILTER_KEYWORD -$allErrors48h = Get-WinEvent -FilterHashtable @{LogName='System'; Level=2; StartTime=$start48h} -ErrorAction SilentlyContinue +$infoThresholdEnv = $env:INFO_THRESHOLD +$warnThresholdEnv = $env:WARN_THRESHOLD +$errorThresholdEnv = $env:ERROR_THRESHOLD -$eventsWithIdFilter = $allErrors48h | Where-Object { $ignoredEventIds -contains $_.Id.ToString() } +$lookbackEnv = $env:LOOKBACK_HOURS +$evaluationEnv = $env:EVALUATION_WINDOW_HOURS -$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 +$ignoredEventIds = if ($filterIdEnv) { ($filterIdEnv.Split(",") | ForEach-Object { $_.Trim() }) + $defaultEventIds } else { $defaultEventIds } +$ignoredKeywords = if ($filterKeywordEnv) { ($filterKeywordEnv.Split(",") | ForEach-Object { $_.Trim() }) + $defaultKeywords } else { $defaultKeywords } + +$infoThreshold = if ($infoThresholdEnv) { [int]$infoThresholdEnv } else { $defaultInfoThreshold } +$warnThreshold = if ($warnThresholdEnv) { [int]$warnThresholdEnv } else { $defaultWarnThreshold } +$errorThreshold = if ($errorThresholdEnv) { [int]$errorThresholdEnv } else { $defaultErrorThreshold } + +$lookbackHours = if ($lookbackEnv) { [int]$lookbackEnv } else { $defaultLookbackHours } +$evaluationWindowHours = if ($evaluationEnv) { [int]$evaluationEnv } else { $defaultEvaluationWindowHours } + +$lookbackStartTime = (Get-Date).AddHours(-$lookbackHours) +$evaluationStartTime = (Get-Date).AddHours(-$evaluationWindowHours) + +$allErrors = Get-WinEvent -FilterHashtable @{ LogName='System'; Level=2; StartTime=$lookbackStartTime } -ErrorAction SilentlyContinue + +function Test-KeywordMatch { + param ($event, $keywords) + $eventData = $event.Properties -join " " + " " + $event.Message + foreach ($keyword in $keywords) { if ($eventData -match "(?i)\b$($keyword)\b") { return $true } } + return $false +} + +$filteredErrors = $allErrors | Where-Object { + $eventIdMatches = $ignoredEventIds -contains $_.Id.ToString() + $keywordMatches = Test-KeywordMatch $_ $ignoredKeywords + -not ($eventIdMatches -or $keywordMatches) +} + +if ($debug -eq "true") { + Write-Output "DEBUG MODE ENABLED" + Write-Output "Ignored Event IDs: $ignoredEventIds" + Write-Output "Ignored Keywords: $ignoredKeywords" + Write-Output "INFO Threshold: $infoThreshold" + Write-Output "WARN Threshold: $warnThreshold" + Write-Output "ERROR Threshold: $errorThreshold" + Write-Output "Lookback Hours: $lookbackHours" + Write-Output "Evaluation Window Hours: $evaluationWindowHours" } if ($debug -eq "true") { @@ -82,75 +117,50 @@ if ($debug -eq "true") { 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." - } + Write-Output "Filtered Events by Event ID in the last $lookbackHours 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 $lookbackHours hours." } if ($eventsWithKeywordFilter.Count -gt 0) { - Write-Output "Filtered Events by Keyword in the last 48 hours:" - $eventsWithKeywordFilter | ForEach-Object { - Write-Output "TimeCreated: $($_.TimeCreated)" - Write-Output "Event ID: $($_.Id)" - Write-Output "Message: $($_.Message)" - Write-Output "----------------------------------------" - } - } else { - Write-Output "No events found matching the specified Keywords in the last 48 hours." - } + Write-Output "Filtered Events by Keyword in the last $lookbackHours 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 $lookbackHours hours." } } -$remainingErrors48h = $allErrors48h | Where-Object { - $eventIdMatches = $ignoredEventIds -contains $_.Id.ToString() - $eventData = $_.Properties -join " " - $eventData += " " + $_.Message - $keywordMatches = $false - $ignoredKeywords | ForEach-Object { - $keyword = $_.Trim() - if ($eventData -match "(?i)\b$($keyword)\b") { - $keywordMatches = $true - } - } - if ($eventIdMatches -or $keywordMatches) { - $false - } else { - $true - } -} +$errorsInEvaluationWindow = $filteredErrors | Where-Object { $_.TimeCreated -gt $evaluationStartTime } +$count = $errorsInEvaluationWindow.Count -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 { +if ($count -ge $errorThreshold) { + Write-Output "CRITICAL: $count error events in last $evaluationWindowHours hours (threshold: $errorThreshold)." + $errorsInEvaluationWindow | ForEach-Object { Write-Output "TimeCreated: $($_.TimeCreated)" Write-Output "Event ID: $($_.Id)" Write-Output "Message: $($_.Message)" Write-Output "----------------------------------------" } + exit $errorExitCode } - -$errors12h = $remainingErrors48h | Where-Object { $_.TimeCreated -gt $start12h } - -if ($errors12h.Count -ge 4) { - Write-Output "Error: 4 or more error events found in the last 12 hours." - Write-Output "Error Events in the last 12 hours (excluding ignored event IDs and keywords):" - $errors12h | ForEach-Object { +elseif ($count -ge $warnThreshold) { + Write-Output "WARNING: $count error events in last $evaluationWindowHours hours (threshold: $warnThreshold)." + $errorsInEvaluationWindow | ForEach-Object { Write-Output "TimeCreated: $($_.TimeCreated)" Write-Output "Event ID: $($_.Id)" Write-Output "Message: $($_.Message)" Write-Output "----------------------------------------" } - exit 1 -} else { - if ($errors12h.Count -eq 0) { - Write-Output "OK: No error events found in the last 12 hours." - } else { - Write-Output "OK: Less than 4 error events found in the last 12 hours." + exit $warnExitCode +} +elseif ($count -ge $infoThreshold) { + Write-Output "INFO: $count error event(s) in last $evaluationWindowHours hours (below warning threshold: $warnThreshold)." + $errorsInEvaluationWindow | ForEach-Object { + Write-Output "TimeCreated: $($_.TimeCreated)" + Write-Output "Event ID: $($_.Id)" + Write-Output "Message: $($_.Message)" + Write-Output "----------------------------------------" } - exit 0 + exit $infoExitCode } +else { + Write-Output "OK: $count error events in last $evaluationWindowHours hours (below all thresholds)." + exit 0 +} \ No newline at end of file diff --git a/scripts_staging/Checks/Task Scheduler scanner.ps1 b/scripts_staging/Checks/Task Scheduler scanner.ps1 index 82382c0f..b6f4138e 100644 --- a/scripts_staging/Checks/Task Scheduler scanner.ps1 +++ b/scripts_staging/Checks/Task Scheduler scanner.ps1 @@ -11,6 +11,7 @@ .EXAMPLE company_name={{global.Company_Name}} company_name=Consonto + debug=1 .NOTES @@ -21,9 +22,9 @@ .CHANGELOG 02.07.25 SAN Added company name to the folders 17.07.25 SAN added powertoys + 17.02.26 SAN Format output + debug flag .TODO - Use a flag for debug set ignore value from env #> @@ -31,7 +32,8 @@ # Set the debug flag -$debug = 0 +$debug = if ($env:debug -eq "1") { 1 } else { 0 } + # Retrieve all scheduled tasks $tasks = Get-ScheduledTask @@ -135,13 +137,10 @@ foreach ($task in $tasks) { } } -# Check if the taskDetails array is empty if ($taskDetails.Count -eq 0) { - Write-Output "No rogue tasks found" + Write-Output "OK: No rogue tasks found" } else { - Write-Output "Rogue tasks found, please execute with a service user" - # Output the task details + Write-Output "KO: Rogue tasks found, please execute with a service user" $taskDetails | Format-Table -AutoSize - # Exit with status code 1 exit 1 } \ No newline at end of file diff --git a/scripts_staging/Checks/Windows Services.ps1 b/scripts_staging/Checks/Windows Services.ps1 index 83db3abf..1a0f11ff 100644 --- a/scripts_staging/Checks/Windows Services.ps1 +++ b/scripts_staging/Checks/Windows Services.ps1 @@ -29,6 +29,7 @@ 27.03.25 SAN added kerberos local key to default 31.03.25 SAN Added a new patern for ignroring user services (servicename_XXX) while keeping their system counterpart inculded 17.07.25 SAN Added InventorySvc it is expected to randomly turn on and off + 08.01.26 SAN Added AppX Deployment Service (AppXSVC) to ignored due to an update #> @@ -64,7 +65,9 @@ $ignoredByDefault = @( "ShellHWDetection", # Frequently failing; unclear if actionable "DropboxUpdater", "LocalKDC", # https://learn.microsoft.com/en-us/answers/questions/2136070/windows-server-2025-kerberos-local-key-distributio - "InventorySvc" #https://learn.microsoft.com/en-us/answers/questions/2258983/inventory-and-compatibility-appraisal-service-in-m + "InventorySvc", # https://learn.microsoft.com/en-us/answers/questions/2258983/inventory-and-compatibility-appraisal-service-in-m + "AppXSVC" # https://learn.microsoft.com/en-us/answers/questions/5660157/appxsvc-forced-to-automatic-on-server-2025 + ) # Define a list of services to ignore that match the pattern "nameoftheservice_xxxx" diff --git a/scripts_staging/Tools/Change boot mode.ps1 b/scripts_staging/Tools/Change boot mode.ps1 new file mode 100644 index 00000000..a75b05f2 --- /dev/null +++ b/scripts_staging/Tools/Change boot mode.ps1 @@ -0,0 +1,69 @@ +<# +.SYNOPSIS + Toggles Windows Safe Mode on the next boot based on environment variable state. + +.DESCRIPTION + This script cycles between Safe Mode Minimal, Safe Mode with Networking, + Safe Mode with Command Prompt, and Normal boot using a machine-level + environment variable 'SAFEBOOT_MODE'. After changing the boot configuration, + it automatically restarts the system. + +.NOTES + Author: SAN + Date: 25.02.26 + #public + +.EXEMPLE + SAFEBOOT_MODE=MINIMAL + SAFEBOOT_MODE=NETWORK + SAFEBOOT_MODE=CMD + SAFEBOOT_MODE=NORMAL + +.CHANGELOG + +#> + + +$toggleVar = "SAFEBOOT_MODE" +$currentMode = $env:SAFEBOOT_MODE + +switch ($currentMode) { + + "MINIMAL" { + Write-Host "Switching to Safe Mode with Networking..." + bcdedit /set {current} safeboot network | Out-Null + bcdedit /deletevalue {current} safebootalternateshell 2>$null | Out-Null + [Environment]::SetEnvironmentVariable($toggleVar, "NETWORK", "Machine") + } + + "NETWORK" { + Write-Host "Switching to Safe Mode with Command Prompt..." + bcdedit /set {current} safeboot minimal | Out-Null + bcdedit /set {current} safebootalternateshell yes | Out-Null + [Environment]::SetEnvironmentVariable($toggleVar, "CMD", "Machine") + } + + "CMD" { + Write-Host "Returning to Normal Boot..." + bcdedit /deletevalue {current} safeboot 2>$null | Out-Null + bcdedit /deletevalue {current} safebootalternateshell 2>$null | Out-Null + [Environment]::SetEnvironmentVariable($toggleVar, "NORMAL", "Machine") + } + + "NORMAL" { + Write-Host "Switching to Safe Mode (Minimal)..." + bcdedit /set {current} safeboot minimal | Out-Null + [Environment]::SetEnvironmentVariable($toggleVar, "MINIMAL", "Machine") + } + + Default { + Write-Host "Unknown or unset mode. Returning to Normal Boot..." + bcdedit /deletevalue {current} safeboot 2>$null | Out-Null + bcdedit /deletevalue {current} safebootalternateshell 2>$null | Out-Null + [Environment]::SetEnvironmentVariable($toggleVar, "NORMAL", "Machine") + } +} + +Start-Sleep -Seconds 2 +Write-Host "Rebooting..." +Restart-Computer -Force \ No newline at end of file diff --git a/scripts_staging/snippets/Update TRMM agent.ps1 b/scripts_staging/snippets/Update TRMM agent.ps1 index 6b32487e..68f858e4 100644 --- a/scripts_staging/snippets/Update TRMM agent.ps1 +++ b/scripts_staging/snippets/Update TRMM agent.ps1 @@ -1,32 +1,34 @@ <# .SYNOPSIS - Downloads and installs the latest or specified version of the Tactical RMM agent, with support for signed and unsigned downloads. + Downloads and installs the latest or specified version of the Tactical RMM agent. .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. + This script installs the Tactical RMM agent using either a signed or unsigned installer. + + - If the environment variable `trmm_sign_download_token` is present, a signed download is assumed automatically. + - If no code signing token is provided, the unsigned installer is downloaded from GitHub. + - The installer is downloaded to: C:\ProgramData\TacticalRMM\temp + - The installer is launched in a detached execution context so it survives TRMM agent restarts. + + The code signing token is the one provided by amidaware and can be found in the code signing section of TRMM. .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`. + Version to install. Use "latest" or leave unset to auto-detect latest GitHub release. + Provided via 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_sign_download_token + Signed download token (optional). + Provided via environment variable: trmm_sign_download_token .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. + API target for signed downloads (required if token is provided otherwise optional). + Provided via environment variable: trmm_api_target -.EXEMPLE - trmm_sign_download_token={{global.trmm_sign_download_token}} +.EXAMPLE version=latest - version=2.7.0 - trmm_api_target=api.exemple.com + trmm_sign_download_token={{global.trmm_sign_download_token}} trmm_api_target={{global.RMM_API_URL}} - + .NOTES Author: SAN Date: 29.10.24 @@ -34,126 +36,96 @@ .CHANGELOG 29.10.24 SAN Initial script with signed and unsigned download support. - 21.12.24 SAN updated the script to not require "issigned" - 22.12.24 SAN default to latest when no version is set - -.TODO - Add a small (15 seconds) delay to the execution of the exe to ensure trmm is capable of properly capturing the output of the script before the agent kills the service - + 21.12.24 SAN Removed explicit issigned requirement. + 22.12.24 SAN Default to latest when no version is set. + 15.01.26 Updated download path, token handling, and output sanitization. #> -# 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 + +$version = $env:version +$signedDownloadToken = $env:trmm_sign_download_token +$apiTarget = $env:trmm_api_target $repoUrl = "https://api.github.com/repos/amidaware/rmmagent/releases/latest" +$downloadDir = "C:\ProgramData\TacticalRMM\temp" +$outputFile = $null + + +if (-not (Test-Path $downloadDir)) { + New-Item -Path $downloadDir -ItemType Directory -Force | Out-Null +} -# 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*" } + $appName = "Tactical RMM Agent" - 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 - } - } + $apps = Get-ItemProperty ` + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" , + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" ` + -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName -like "*$appName*" } - return $null - } + return $apps.DisplayVersion } try { - # Set up headers for GitHub API request - $headers = @{ - "User-Agent" = "PowerShell Script" - } + $headers = @{ "User-Agent" = "PowerShell Script" } - # If version is not set, default to "latest" if (-not $version) { $version = "latest" } + if ($version -eq "latest") { - Write-Output "Fetching the latest version information of the TRMM agent from GitHub..." - $response = Invoke-RestMethod -Uri $repoUrl -Headers $headers -Method Get -ErrorAction Stop - $version = $response.tag_name.TrimStart('v') # Remove 'v' prefix if exists - Write-Output "Latest version found: $version" + Write-Output "Fetching latest Tactical RMM agent version..." + $response = Invoke-RestMethod -Uri $repoUrl -Headers $headers -ErrorAction Stop + $version = $response.tag_name.TrimStart('v') + Write-Output "Latest version resolved: $version" } else { - Write-Output "Using specified version: $version" + Write-Output "Requested 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" + Write-Output "Installed version detected: $installedVersion" if ($installedVersion -eq $version) { - Write-Output "The installed version matches the desired version. No upgrade required." + Write-Output "Installed version matches requested version. Exiting." 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..." + Write-Output "Tactical RMM Agent not currently installed." } - # Define the temp directory for downloading - $tempDir = [System.IO.Path]::GetTempPath() - $outputFile = Join-Path -Path $tempDir -ChildPath "tacticalagent-v$version.exe" + $outputFile = Join-Path $downloadDir "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..." + Write-Output "ERROR: trmm_api_target is required when using signed downloads." exit 1 } - # Download the signed agent using the token + + Write-Output "Signed download detected." $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 + Write-Output "Unsigned download detected." $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 - } + Write-Output "Downloading agent installer..." + Invoke-WebRequest -Uri $downloadUrl -OutFile $outputFile -ErrorAction Stop + Write-Output "Download completed successfully." + + Write-Output "Launching installer in detached context..." - # 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 - } + $processStartInfo.UseShellExecute = $true + $processStartInfo.CreateNoWindow = $true + $processStartInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden + + [System.Diagnostics.Process]::Start($processStartInfo) | Out-Null + + Write-Output "Installer launched successfully. Exiting script." + } catch { - # Handle unexpected errors with output - Write-Output "An unexpected error occurred: $($_.Exception.Message)" + Write-Output "Fatal error: $($_.Exception.Message)" exit 1 }