diff --git a/Powershell Scripts/Active Directory - Active Users Report.ps1 b/Powershell Scripts/Active Directory - Active Users Report.ps1 index ed18e9e..21c9fc4 100644 --- a/Powershell Scripts/Active Directory - Active Users Report.ps1 +++ b/Powershell Scripts/Active Directory - Active Users Report.ps1 @@ -1,233 +1,151 @@ # Generates a report for the number of active users in active directory that have logged in the specified time frame. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Generates a report for the number of active users in active directory that have logged in the specified time frame. -.DESCRIPTION - Generates a report for the number of active users in active directory that have logged in the specified time frame. - -.EXAMPLE - (No Parameters) - - Number of active users: 2 - Total users (including active and inactive): 5 - Percent Active: 40% - - SamAccountName UserPrincipalName mail LastLogonDate - -------------- ----------------- ---- ------------- - kbohlander kbohlander@test.lan 6/5/2023 8:58:20 AM - tuser tuser@test.lan tuser@test.com 6/6/2023 8:30:23 AM - -PARAMETER: -NumberOfDays "ReplaceWithAnumber" - How long ago in days to report on. -.EXAMPLE - -NumberOfDays "1" (If today was 6/7/2023) - - Number of active users: 2 - Total users (including active and inactive): 5 - Percent Active: 40% - - SamAccountName UserPrincipalName mail LastLogonDate - -------------- ----------------- ---- ------------- - tuser tuser@test.lan tuser@test.com 6/6/2023 8:30:23 AM - -PARAMETER: -ExcludeDisabledUsers - Excludes the user from the report if they're currently disabled. - -PARAMETER: -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" - Name of a multiline custom field to save the results to. -.EXAMPLE - -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" - - Number of active users: 2 - Total users (including active and inactive): 5 - Percent Active: 40% - - SamAccountName UserPrincipalName mail LastLogonDate - -------------- ----------------- ---- ------------- - kbohlander kbohlander@test.lan 6/5/2023 8:58:20 AM - tuser tuser@test.lan tuser@test.com 6/6/2023 8:30:23 AM -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Renamed script and added Script Variable support -#> - -[CmdletBinding()] -param ( - [Parameter()] - [int]$NumberOfDays = 30, - [Parameter()] - [String]$CustomFieldName, - [Parameter()] - [Switch]$ExcludeDisabledUsers = [System.Convert]::ToBoolean($env:excludeDisabledUsersFromReport) -) - -begin { - # Tests for administrative rights which is required to get the last logon date. - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - # Tests if the device the script is running on is a dmona controller. - function Test-IsDomainController { - return $(Get-CimInstance -ClassName Win32_OperatingSystem).ProductType -eq 2 - } - - # This function is to make it easier to set Ninja Custom Fields. - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields we can set. If no type is given we'll assume the input doesn't have to be changed in any way. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The below field requires additional information in order to set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If we received some sort of error it should have an exception property and we'll exit the function with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli is expecting the time to be representing as a Unix Epoch string. So we'll convert what we were given into that format. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw "Value is not present in dropdown" - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - - # Todays date - $Today = Get-Date - - if ($env:numberOfDaysToReportOn -and $env:numberOfDaysToReportOn -notlike "null") { $NumberOfDays = $env:numberOfDaysToReportOn } - if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomFieldName = $env:customFieldName } -} -process { - # Erroring out when ran without administrator rights - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Erroring out when ran on a non-domain controller - if (-not (Test-IsDomainController)) { - Write-Error -Message "The script needs to be run on a domain controller!" - exit 1 - } - - # If disabled users are to be excluded we're going to fetch different properties and Filter out disabled users - if ($ExcludeDisabledUsers) { - $Users = Get-ADUser -Filter * -Properties SamAccountName, UserPrincipalName, mail, LastLogonDate, Enabled | - Where-Object { $_.Enabled -eq $True } - $ActiveUsers = Get-ADUser -Filter { LastLogonDate -ge 0 } -Properties SamAccountName, UserPrincipalName, mail, LastLogonDate, Enabled | - Where-Object { (New-TimeSpan $_.LastLogonDate $Today).Days -le $NumberOfDays -and $_.Enabled -eq $True } | - Select-Object SamAccountName, UserPrincipalName, mail, LastLogonDate - } - else { - $Users = Get-ADUser -Filter * -Properties SamAccountName, UserPrincipalName, mail, LastLogonDate - $ActiveUsers = Get-ADUser -Filter { LastLogonDate -ge 0 } -Properties SamAccountName, UserPrincipalName, mail, LastLogonDate | - Where-Object { (New-TimeSpan $_.LastLogonDate $Today).Days -le $NumberOfDays } | - Select-Object SamAccountName, UserPrincipalName, mail, LastLogonDate - } - - # Creating a generic list to start assembling the report - $Report = New-Object System.Collections.Generic.List[string] - - # Actual report assembly each section will be print on its own line - $Report.Add("Active users: $(($ActiveUsers | Measure-Object).Count)") - $Report.Add("Total users: $(($Users | Measure-Object).Count)") - $Report.Add("Percent Active: $(if((($Users | Measure-Object).Count) -gt 0){[Math]::Round(($ActiveUsers | Measure-Object).Count / (($Users | Measure-Object).Count) * 100, 2)}else{0})%") - - # Set's up table to use in the report - $Report.Add($($ActiveUsers | Format-Table | Out-String)) - - if ($ActiveUsers) { - # Exports report to activity log - $Report | Write-Host - - if ($CustomFieldName) { - # Saves report to custom field. - try { - Set-NinjaProperty -Name $CustomFieldName -Value ($Report | Out-String) - } - catch { - # If we ran into some sort of error we'll output it here. - Write-Error -Message $_.ToString() -Category InvalidOperation -Exception (New-Object System.Exception) - exit 1 - } - } - } - else { - Write-Error "[Error] No active users found!" - exit 1 - } -} -end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + Generates a report for the number of active users in active directory that have logged in the specified time frame. +.DESCRIPTION + Generates a report for the number of active users in active directory that have logged in the specified time frame. + +.EXAMPLE + (No Parameters) + + Number of active users: 2 + Total users (including active and inactive): 5 + Percent Active: 40% + + SamAccountName UserPrincipalName mail LastLogonDate + -------------- ----------------- ---- ------------- + kbohlander kbohlander@test.lan 6/5/2023 8:58:20 AM + tuser tuser@test.lan tuser@test.com 6/6/2023 8:30:23 AM + +PARAMETER: -NumberOfDays "ReplaceWithAnumber" + How long ago in days to report on. +.EXAMPLE + -NumberOfDays "1" (If today was 6/7/2023) + + Number of active users: 2 + Total users (including active and inactive): 5 + Percent Active: 40% + + SamAccountName UserPrincipalName mail LastLogonDate + -------------- ----------------- ---- ------------- + tuser tuser@test.lan tuser@test.com 6/6/2023 8:30:23 AM + +PARAMETER: -ExcludeDisabledUsers + Excludes the user from the report if they're currently disabled. + +PARAMETER: -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" + Name of a multiline custom field to save the results to. +.EXAMPLE + -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" + + Number of active users: 2 + Total users (including active and inactive): 5 + Percent Active: 40% + + SamAccountName UserPrincipalName mail LastLogonDate + -------------- ----------------- ---- ------------- + kbohlander kbohlander@test.lan 6/5/2023 8:58:20 AM + tuser tuser@test.lan tuser@test.com 6/6/2023 8:30:23 AM +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Renamed script and added Script Variable support +#> + +[CmdletBinding()] +param ( + [Parameter()] + [int]$NumberOfDays = 30, + [Parameter()] + [String]$CustomFieldName, + [Parameter()] + [Switch]$ExcludeDisabledUsers = [System.Convert]::ToBoolean($env:excludeDisabledUsersFromReport) +) + +begin { + # Tests for administrative rights which is required to get the last logon date. + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + # Tests if the device the script is running on is a dmona controller. + function Test-IsDomainController { + return $(Get-CimInstance -ClassName Win32_OperatingSystem).ProductType -eq 2 + } + + # This function is to make it easier to set Ninja Custom Fields. + + # Todays date + $Today = Get-Date + + if ($env:numberOfDaysToReportOn -and $env:numberOfDaysToReportOn -notlike "null") { $NumberOfDays = $env:numberOfDaysToReportOn } + if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomFieldName = $env:customFieldName } +} +process { + # Erroring out when ran without administrator rights + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Erroring out when ran on a non-domain controller + if (-not (Test-IsDomainController)) { + Write-Error -Message "The script needs to be run on a domain controller!" + exit 1 + } + + # If disabled users are to be excluded we're going to fetch different properties and Filter out disabled users + if ($ExcludeDisabledUsers) { + $Users = Get-ADUser -Filter * -Properties SamAccountName, UserPrincipalName, mail, LastLogonDate, Enabled | + Where-Object { $_.Enabled -eq $True } + $ActiveUsers = Get-ADUser -Filter { LastLogonDate -ge 0 } -Properties SamAccountName, UserPrincipalName, mail, LastLogonDate, Enabled | + Where-Object { (New-TimeSpan $_.LastLogonDate $Today).Days -le $NumberOfDays -and $_.Enabled -eq $True } | + Select-Object SamAccountName, UserPrincipalName, mail, LastLogonDate + } + else { + $Users = Get-ADUser -Filter * -Properties SamAccountName, UserPrincipalName, mail, LastLogonDate + $ActiveUsers = Get-ADUser -Filter { LastLogonDate -ge 0 } -Properties SamAccountName, UserPrincipalName, mail, LastLogonDate | + Where-Object { (New-TimeSpan $_.LastLogonDate $Today).Days -le $NumberOfDays } | + Select-Object SamAccountName, UserPrincipalName, mail, LastLogonDate + } + + # Creating a generic list to start assembling the report + $Report = New-Object System.Collections.Generic.List[string] + + # Actual report assembly each section will be print on its own line + $Report.Add("Active users: $(($ActiveUsers | Measure-Object).Count)") + $Report.Add("Total users: $(($Users | Measure-Object).Count)") + $Report.Add("Percent Active: $(if((($Users | Measure-Object).Count) -gt 0){[Math]::Round(($ActiveUsers | Measure-Object).Count / (($Users | Measure-Object).Count) * 100, 2)}else{0})%") + + # Set's up table to use in the report + $Report.Add($($ActiveUsers | Format-Table | Out-String)) + + if ($ActiveUsers) { + # Exports report to activity log + $Report | Write-Host + + if ($CustomFieldName) { + # Saves report to custom field. + try { + } + catch { + # If we ran into some sort of error we'll output it here. + Write-Error -Message $_.ToString() -Category InvalidOperation -Exception (New-Object System.Exception) + exit 1 + } + } + } + else { + Write-Error "[Error] No active users found!" + exit 1 + } +} +end { + +} + diff --git a/Powershell Scripts/Active Directory - Domain Controller Health Report.ps1 b/Powershell Scripts/Active Directory - Domain Controller Health Report.ps1 index 967b779..74764a7 100644 --- a/Powershell Scripts/Active Directory - Domain Controller Health Report.ps1 +++ b/Powershell Scripts/Active Directory - Domain Controller Health Report.ps1 @@ -1,373 +1,224 @@ # Analyzes the state of a domain controller and reports any problems to help with troubleshooting. Optionally, set a WYSIWYG custom field. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Analyzes the state of a domain controller and reports any problems to help with troubleshooting. Optionally, set a WYSIWYG custom field. -.DESCRIPTION - Analyzes the state of a domain controller and reports any problems to help with troubleshooting. Optionally, set a WYSIWYG custom field. -.EXAMPLE - (No Parameters) - -Retrieving Directory Server Diagnosis Test Results. - -Passing Tests: CheckSDRefDom, Connectivity, CrossRefValidation, DFSREvent, FrsEvent, Intersite, KccEvent, KnowsOfRoleHolders, MachineAccount, NCSecDesc, NetLogons, ObjectsReplicated, Replications, RidManager, Services, SystemLog, SysVolCheck, VerifyReferences - -[Alert] Failed Tests Detected! -Failed Tests: Advertising, LocatorCheck - -### Detailed Output ### - -Directory Server Diagnosis -Performing initial setup: - Trying to find home server... - Home Server = SRV16-DC2-TEST - * Identified AD Forest. - Done gathering initial info. -Doing initial required tests - Testing server: Default-First-Site-Name\SRV16-DC2-TEST - Starting test: Connectivity - ......................... SRV16-DC2-TEST passed test Connectivity -Doing primary tests - Testing server: Default-First-Site-Name\SRV16-DC2-TEST - Starting test: Advertising - Warning: SRV16-DC2-TEST is not advertising as a time server. - ......................... SRV16-DC2-TEST failed test Advertising - Running partition tests on : ForestDnsZones - Running partition tests on : DomainDnsZones - Running partition tests on : Schema - Running partition tests on : Configuration - Running partition tests on : test - Running enterprise tests on : test.lan - -Directory Server Diagnosis -Performing initial setup: - Trying to find home server... - Home Server = SRV16-DC2-TEST - * Identified AD Forest. - Done gathering initial info. -Doing initial required tests - Testing server: Default-First-Site-Name\SRV16-DC2-TEST - Starting test: Connectivity - ......................... SRV16-DC2-TEST passed test Connectivity -Doing primary tests - Testing server: Default-First-Site-Name\SRV16-DC2-TEST - Running partition tests on : ForestDnsZones - Running partition tests on : DomainDnsZones - Running partition tests on : Schema - Running partition tests on : Configuration - Running partition tests on : test - Running enterprise tests on : test.lan - Starting test: LocatorCheck - Warning: DcGetDcName(TIME_SERVER) call failed, error 1355 - A Time Server could not be located. - The server holding the PDC role is down. - Warning: DcGetDcName(GOOD_TIME_SERVER_PREFERRED) call failed, error - 1355 - A Good Time Server could not be located. - ......................... test.lan failed test LocatorCheck - -PARAMETER: -wysiwygCustomField "ReplaceMeWithaWYSIWYGcustomField" - Name of a WYSIWYG custom field to optionally save the results to. -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$wysiwygCustomField -) - -begin { - # If script form variables are used, replace command line parameters with their value. - if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $wysiwygCustomField = $env:wysiwygCustomFieldName } - - # Function to test if the current machine is a domain controller - function Test-IsDomainController { - $OS = if ($PSVersionTable.PSVersion.Major -lt 5) { - Get-WmiObject -Class Win32_OperatingSystem - } - else { - Get-CimInstance -ClassName Win32_OperatingSystem - } - - # Check if the OS is a domain controller (ProductType 2) - if ($OS.ProductType -eq "2") { - return $true - } - } - - function Get-DCDiagResults { - # Define the list of DCDiag tests to run - $DCDiagTestsToRun = "Connectivity", "Advertising", "FrsEvent", "DFSREvent", "SysVolCheck", "KccEvent", "KnowsOfRoleHolders", "MachineAccount", "NCSecDesc", "NetLogons", "ObjectsReplicated", "Replications", "RidManager", "Services", "SystemLog", "VerifyReferences", "CheckSDRefDom", "CrossRefValidation", "LocatorCheck", "Intersite" - - foreach ($DCTest in $DCDiagTestsToRun) { - # Run DCDiag for the current test and save the output to a file - $DCDiag = Start-Process -FilePath "DCDiag.exe" -ArgumentList "/test:$DCTest", "/f:$env:TEMP\dc-diag-$DCTest.txt" -PassThru -Wait -NoNewWindow - - # Check if the DCDiag test failed - if ($DCDiag.ExitCode -ne 0) { - Write-Host "[Error] Running $DCTest!" - exit 1 - } - - # Read the raw results from the output file and filter out empty lines - $RawResult = Get-Content -Path "$env:TEMP\dc-diag-$DCTest.txt" | Where-Object { $_.Trim() } - - # Find the status line indicating whether the test passed or failed - $StatusLine = $RawResult | Where-Object { $_ -match "\. .* test $DCTest" } - - # Extract the status (passed or failed) from the status line - $Status = $StatusLine -split ' ' | Where-Object { $_ -like "passed" -or $_ -like "failed" } - - # Create a custom object to store the test results - [PSCustomObject]@{ - Test = $DCTest - Status = $Status - Result = $RawResult - } - - # Remove the temporary output file - Remove-Item -Path "$env:TEMP\dc-diag-$DCTest.txt" - } - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - # If requested to set the field value for a Ninja document, specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is specified, assume that the input does not need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to set. - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # Redirect error output to the success stream to handle errors more easily if nothing is found or something else goes wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received with an exception property, exit the function with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The types below require values not typically given to be set. The code below will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # Although it's highly likely we were given a value like "True" or a boolean data type, it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, match the given value with a GUID. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli expects the GUID of the option we're trying to select, so match the value we were given with a GUID. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # Set the field differently depending on whether it's a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # Check if the script is run with Administrator privileges - if (!(Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Check if the script is run on a Domain Controller - if (!(Test-IsDomainController)) { - Write-Host -Object "[Error] This script needs to be run on a Domain Controller." - exit 1 - } - - # Initialize lists to store passing and failing tests - $PassingTests = New-Object System.Collections.Generic.List[object] - $FailedTests = New-Object System.Collections.Generic.List[object] - - # Notify the user that the tests are being retrieved - Write-Host -Object "`nRetrieving Directory Server Diagnosis Test Results." - $TestResults = Get-DCDiagResults - - # Process each test result - foreach ($Result in $TestResults) { - $TestFailed = $False - - # Check if any status in the result indicates a failure - $Result.Status | ForEach-Object { - if ($_ -notmatch "pass") { - $TestFailed = $True - } - } - - # Add the result to the appropriate list - if ($TestFailed) { - $FailedTests.Add($Result) - } - else { - $PassingTests.Add($Result) - } - } - - # Optionally set a WYSIWYG custom field if specified - if ($wysiwygCustomField) { - try { - Write-Host -Object "`nBuilding HTML for Custom Field." - - # Create an HTML report for the custom field - $HTML = New-Object System.Collections.Generic.List[object] - - $HTML.Add("

Directory Server Diagnosis Test Results (DCDiag.exe)

") - $FailedPercentage = $([math]::Round((($FailedTests.Count / ($FailedTests.Count + $PassingTests.Count)) * 100), 2)) - $SuccessPercentage = 100 - $FailedPercentage - $HTML.Add( - @" -
-
-
-
- -"@ - ) - - # Add failed tests to the HTML report - $FailedTests | Sort-Object Test | ForEach-Object { - $HTML.Add( - @" -
- -
-
$($_.Test)
-
- $($_.Result | Out-String) -
-
-
-"@ - ) - } - - # Add passing tests to the HTML report - $PassingTests | Sort-Object Test | ForEach-Object { - $HTML.Add( - @" -
- -
-
$($_.Test)
-
- Test passed. -
-
-
-"@ - ) - } - - # Set the custom field with the HTML report - Write-Host -Object "Attempting to set Custom Field '$wysiwygCustomField'." - Set-NinjaProperty -Name $wysiwygCustomField -Value $HTML - Write-Host -Object "Successfully set Custom Field '$wysiwygCustomField'!" - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - - # Display the list of passing tests - if ($PassingTests.Count -gt 0) { - Write-Host -Object "" - Write-Host -Object "Passing Tests: " -NoNewline - Write-Host -Object ($PassingTests.Test | Sort-Object) -Separator ", " - Write-Host -Object "" - } - - # Display the list of failed tests with detailed output - if ($FailedTests.Count -gt 0) { - Write-Host -Object "[Alert] Failed Tests Detected!" - Write-Host -Object "Failed Tests: " -NoNewline - Write-Host -Object ($FailedTests.Test | Sort-Object) -Separator ", " - - Write-Host -Object "`n### Detailed Output ###" - $FailedTests | Sort-Object Test | ForEach-Object { - Write-Host -Object "" - Write-Host -Object ($_.Result | Out-String) - Write-Host -Object "" - } - } - else { - Write-Host -Object "All Directory Server Diagnosis Tests Pass!" - } - - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Analyzes the state of a domain controller and reports any problems to help with troubleshooting. Optionally, set a WYSIWYG custom field. +.DESCRIPTION + Analyzes the state of a domain controller and reports any problems to help with troubleshooting. Optionally, set a WYSIWYG custom field. +.EXAMPLE + (No Parameters) + +Retrieving Directory Server Diagnosis Test Results. + +Passing Tests: CheckSDRefDom, Connectivity, CrossRefValidation, DFSREvent, FrsEvent, Intersite, KccEvent, KnowsOfRoleHolders, MachineAccount, NCSecDesc, NetLogons, ObjectsReplicated, Replications, RidManager, Services, SystemLog, SysVolCheck, VerifyReferences + +[Alert] Failed Tests Detected! +Failed Tests: Advertising, LocatorCheck + +### Detailed Output ### + +Directory Server Diagnosis +Performing initial setup: + Trying to find home server... + Home Server = SRV16-DC2-TEST + * Identified AD Forest. + Done gathering initial info. +Doing initial required tests + Testing server: Default-First-Site-Name\SRV16-DC2-TEST + Starting test: Connectivity + ......................... SRV16-DC2-TEST passed test Connectivity +Doing primary tests + Testing server: Default-First-Site-Name\SRV16-DC2-TEST + Starting test: Advertising + Warning: SRV16-DC2-TEST is not advertising as a time server. + ......................... SRV16-DC2-TEST failed test Advertising + Running partition tests on : ForestDnsZones + Running partition tests on : DomainDnsZones + Running partition tests on : Schema + Running partition tests on : Configuration + Running partition tests on : test + Running enterprise tests on : test.lan + +Directory Server Diagnosis +Performing initial setup: + Trying to find home server... + Home Server = SRV16-DC2-TEST + * Identified AD Forest. + Done gathering initial info. +Doing initial required tests + Testing server: Default-First-Site-Name\SRV16-DC2-TEST + Starting test: Connectivity + ......................... SRV16-DC2-TEST passed test Connectivity +Doing primary tests + Testing server: Default-First-Site-Name\SRV16-DC2-TEST + Running partition tests on : ForestDnsZones + Running partition tests on : DomainDnsZones + Running partition tests on : Schema + Running partition tests on : Configuration + Running partition tests on : test + Running enterprise tests on : test.lan + Starting test: LocatorCheck + Warning: DcGetDcName(TIME_SERVER) call failed, error 1355 + A Time Server could not be located. + The server holding the PDC role is down. + Warning: DcGetDcName(GOOD_TIME_SERVER_PREFERRED) call failed, error + 1355 + A Good Time Server could not be located. + ......................... test.lan failed test LocatorCheck + +PARAMETER: -wysiwygCustomField "ReplaceMeWithaWYSIWYGcustomField" + Name of a WYSIWYG custom field to optionally save the results to. +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$wysiwygCustomField +) + +begin { + # If script form variables are used, replace command line parameters with their value. + if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $wysiwygCustomField = $env:wysiwygCustomFieldName } + + # Function to test if the current machine is a domain controller + function Test-IsDomainController { + $OS = if ($PSVersionTable.PSVersion.Major -lt 5) { + Get-WmiObject -Class Win32_OperatingSystem + } + else { + Get-CimInstance -ClassName Win32_OperatingSystem + } + + # Check if the OS is a domain controller (ProductType 2) + if ($OS.ProductType -eq "2") { + return $true + } + } + + function Get-DCDiagResults { + # Define the list of DCDiag tests to run + $DCDiagTestsToRun = "Connectivity", "Advertising", "FrsEvent", "DFSREvent", "SysVolCheck", "KccEvent", "KnowsOfRoleHolders", "MachineAccount", "NCSecDesc", "NetLogons", "ObjectsReplicated", "Replications", "RidManager", "Services", "SystemLog", "VerifyReferences", "CheckSDRefDom", "CrossRefValidation", "LocatorCheck", "Intersite" + + foreach ($DCTest in $DCDiagTestsToRun) { + # Run DCDiag for the current test and save the output to a file + $DCDiag = Start-Process -FilePath "DCDiag.exe" -ArgumentList "/test:$DCTest", "/f:$env:TEMP\dc-diag-$DCTest.txt" -PassThru -Wait -NoNewWindow + + # Check if the DCDiag test failed + if ($DCDiag.ExitCode -ne 0) { + Write-Host "[Error] Running $DCTest!" + exit 1 + } + + # Read the raw results from the output file and filter out empty lines + $RawResult = Get-Content -Path "$env:TEMP\dc-diag-$DCTest.txt" | Where-Object { $_.Trim() } + + # Find the status line indicating whether the test passed or failed + $StatusLine = $RawResult | Where-Object { $_ -match "\. .* test $DCTest" } + + # Extract the status (passed or failed) from the status line + $Status = $StatusLine -split ' ' | Where-Object { $_ -like "passed" -or $_ -like "failed" } + + # Create a custom object to store the test results + [PSCustomObject]@{ + Test = $DCTest + Status = $Status + Result = $RawResult + } + + # Remove the temporary output file + Remove-Item -Path "$env:TEMP\dc-diag-$DCTest.txt" + } + } + + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # Check if the script is run with Administrator privileges + if (!(Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Check if the script is run on a Domain Controller + if (!(Test-IsDomainController)) { + Write-Host -Object "[Error] This script needs to be run on a Domain Controller." + exit 1 + } + + # Initialize lists to store passing and failing tests + $PassingTests = New-Object System.Collections.Generic.List[object] + $FailedTests = New-Object System.Collections.Generic.List[object] + + # Notify the user that the tests are being retrieved + Write-Host -Object "`nRetrieving Directory Server Diagnosis Test Results." + $TestResults = Get-DCDiagResults + + # Process each test result + foreach ($Result in $TestResults) { + $TestFailed = $False + + # Check if any status in the result indicates a failure + $Result.Status | ForEach-Object { + if ($_ -notmatch "pass") { + $TestFailed = $True + } + } + + # Add the result to the appropriate list + if ($TestFailed) { + $FailedTests.Add($Result) + } + else { + $PassingTests.Add($Result) + } + } + + # Optionally set a WYSIWYG custom field if specified + if ($wysiwygCustomField) { + Write-Host "" + Write-Host "Note: Custom field '$wysiwygCustomField' was specified but NinjaOne integration has been removed." + Write-Host "All output has been displayed above." + } + + # Display the list of passing tests + if ($PassingTests.Count -gt 0) { + Write-Host -Object "" + Write-Host -Object "Passing Tests: " -NoNewline + Write-Host -Object ($PassingTests.Test | Sort-Object) -Separator ", " + Write-Host -Object "" + } + + # Display the list of failed tests with detailed output + if ($FailedTests.Count -gt 0) { + Write-Host -Object "[Alert] Failed Tests Detected!" + Write-Host -Object "Failed Tests: " -NoNewline + Write-Host -Object ($FailedTests.Test | Sort-Object) -Separator ", " + + Write-Host -Object "`n### Detailed Output ###" + $FailedTests | Sort-Object Test | ForEach-Object { + Write-Host -Object "" + Write-Host -Object ($_.Result | Out-String) + Write-Host -Object "" + } + } + else { + Write-Host -Object "All Directory Server Diagnosis Tests Pass!" + } + + exit $ExitCode +} +end { + +} diff --git a/Powershell Scripts/Active Directory - Get Inactive Computers.ps1 b/Powershell Scripts/Active Directory - Get Inactive Computers.ps1 index 0193274..84578da 100644 --- a/Powershell Scripts/Active Directory - Get Inactive Computers.ps1 +++ b/Powershell Scripts/Active Directory - Get Inactive Computers.ps1 @@ -1,231 +1,140 @@ # Gets computers that have been inactive for a specified number of days. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Gets computers that have been inactive for a specified number of days. -.DESCRIPTION - Gets computers that have been inactive for a specified number of days. - The number of days to consider a computer inactive can be specified as a parameter or saved to a custom field. - -PARAMETER: -InactiveDays 30 - The number of days to consider a computer inactive. Computers that have been inactive for this number of days will be included in the report. -.EXAMPLE - -InactiveDays 30 - ## EXAMPLE OUTPUT WITH InactiveDays ## - [Info] Searching for computers that are inactive for 30 days or more. - [Info] Found 11 inactive computers. - -PARAMETER: -InactiveDays 30 -WysiwygCustomField "ReplaceMeWithAnyWysiwygCustomField" - The number of days to consider a computer inactive. Computers that have been inactive for this number of days will be included in the report. -.EXAMPLE - -InactiveDays 30 -WysiwygCustomField "ReplaceMeWithAnyWysiwygCustomField" - ## EXAMPLE OUTPUT WITH WysiwygCustomField ## - [Info] Searching for computers that are inactive for 30 days or more. - [Info] Found 11 inactive computers. - [Info] Attempting to set Custom Field 'Inactive Computers'. - [Info] Successfully set Custom Field 'Inactive Computers'! - -.NOTES - Minimum OS Architecture Supported: Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - $InactiveDays, - [Parameter()] - [String]$WysiwygCustomField -) - -begin { - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 10000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") - } - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to be set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received it will have an exception property, the function will exit with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below types require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } -} -process { - if (-not (Test-IsElevated)) { - Write-Host "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Get Script Variables and override parameters with them - if ($env:inactiveDays -and $env:inactiveDays -notlike "null") { - $InactiveDays = $env:inactiveDays - } - if ($env:wysiwygCustomField -and $env:wysiwygCustomField -notlike "null") { - $WysiwygCustomField = $env:wysiwygCustomField - } - - # Parameter Requirements - if ([string]::IsNullOrWhiteSpace($InactiveDays)) { - Write-Host "[Error] Inactive Days is required." - exit 1 - } - elseif ([int]::TryParse($InactiveDays, [ref]$null) -eq $false) { - Write-Host "[Error] Inactive Days must be a number." - exit 1 - } - - # Check that Active Directory module is available - if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) { - Write-Host "[Error] Active Directory module is not available. Please install it and try again." - exit 1 - } - - try { - # Get the date in the past $InactiveDays days - $InactiveDate = (Get-Date).AddDays(-$InactiveDays) - # Get the SearchBase for the domain - $Domain = "DC=$( - $(Get-CimInstance Win32_ComputerSystem).Domain -split "\." -join ",DC=" - )" - Write-Host "[Info] Searching for computers that are inactive for $InactiveDays days or more." - - # For Splatting parameters into Get-ADComputer - $GetComputerSplat = @{ - Property = "Name", "LastLogonTimeStamp", "OperatingSystem" - # LastLogonTimeStamp is converted to a DateTime object from the Get-ADComputer cmdlet - Filter = { (Enabled -eq "true") -and (LastLogonTimeStamp -le $InactiveDate) } - SearchBase = $Domain - } - - # Get inactive computers that are not active in the past $InactiveDays days - $InactiveComputers = Get-ADComputer @GetComputerSplat | Select-Object "Name", @{ - # Format the LastLogonTimeStamp property to a human-readable date - Name = "LastLogon" - Expression = { - if ($_.LastLogonTimeStamp -gt 0) { - # Convert LastLogonTimeStamp to a datetime - $lastLogon = [DateTime]::FromFileTime($_.LastLogonTimeStamp) - # Format the datetime - $lastLogonFormatted = $lastLogon.ToString("MM/dd/yyyy hh:mm:ss tt") - return $lastLogonFormatted - } - else { - return "01/01/1601 00:00:00 AM" - } - } - }, "OperatingSystem" - - if ($InactiveComputers -and $InactiveComputers.Count -gt 0) { - Write-Host "[Info] Found $($InactiveComputers.Count) inactive computers." - } - else { - Write-Host "[Info] No inactive computers were found." - } - } - catch { - Write-Host "[Error] Failed to get inactive computers. Please try again." - exit 1 - } - - # Save the results to a custom field - if ($WysiwygCustomField) { - try { - Write-Host "[Info] Attempting to set Custom Field '$WysiwygCustomField'." - Set-NinjaProperty -Name $WysiwygCustomField -Value $($InactiveComputers | ConvertTo-Html -Fragment | Out-String) - Write-Host "[Info] Successfully set Custom Field '$WysiwygCustomField'!" - } - catch { - Write-Host "[Error] Failed to set Custom Field '$WysiwygCustomField'." - $ExitCode = 1 - } - } - - $InactiveComputers | Format-Table -AutoSize | Out-String -Width 4000 | Write-Host - - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Gets computers that have been inactive for a specified number of days. +.DESCRIPTION + Gets computers that have been inactive for a specified number of days. + The number of days to consider a computer inactive can be specified as a parameter or saved to a custom field. + +PARAMETER: -InactiveDays 30 + The number of days to consider a computer inactive. Computers that have been inactive for this number of days will be included in the report. +.EXAMPLE + -InactiveDays 30 + ## EXAMPLE OUTPUT WITH InactiveDays ## + [Info] Searching for computers that are inactive for 30 days or more. + [Info] Found 11 inactive computers. + +PARAMETER: -InactiveDays 30 -WysiwygCustomField "ReplaceMeWithAnyWysiwygCustomField" + The number of days to consider a computer inactive. Computers that have been inactive for this number of days will be included in the report. +.EXAMPLE + -InactiveDays 30 -WysiwygCustomField "ReplaceMeWithAnyWysiwygCustomField" + ## EXAMPLE OUTPUT WITH WysiwygCustomField ## + [Info] Searching for computers that are inactive for 30 days or more. + [Info] Found 11 inactive computers. + [Info] Attempting to set Custom Field 'Inactive Computers'. + [Info] Successfully set Custom Field 'Inactive Computers'! + +.NOTES + Minimum OS Architecture Supported: Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + $InactiveDays, + [Parameter()] + [String]$WysiwygCustomField +) + +begin { + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + +} +process { + if (-not (Test-IsElevated)) { + Write-Host "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Get Script Variables and override parameters with them + if ($env:inactiveDays -and $env:inactiveDays -notlike "null") { + $InactiveDays = $env:inactiveDays + } + if ($env:wysiwygCustomField -and $env:wysiwygCustomField -notlike "null") { + $WysiwygCustomField = $env:wysiwygCustomField + } + + # Parameter Requirements + if ([string]::IsNullOrWhiteSpace($InactiveDays)) { + Write-Host "[Error] Inactive Days is required." + exit 1 + } + elseif ([int]::TryParse($InactiveDays, [ref]$null) -eq $false) { + Write-Host "[Error] Inactive Days must be a number." + exit 1 + } + + # Check that Active Directory module is available + if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) { + Write-Host "[Error] Active Directory module is not available. Please install it and try again." + exit 1 + } + + try { + # Get the date in the past $InactiveDays days + $InactiveDate = (Get-Date).AddDays(-$InactiveDays) + # Get the SearchBase for the domain + $Domain = "DC=$( + $(Get-CimInstance Win32_ComputerSystem).Domain -split "\." -join ",DC=" + )" + Write-Host "[Info] Searching for computers that are inactive for $InactiveDays days or more." + + # For Splatting parameters into Get-ADComputer + $GetComputerSplat = @{ + Property = "Name", "LastLogonTimeStamp", "OperatingSystem" + # LastLogonTimeStamp is converted to a DateTime object from the Get-ADComputer cmdlet + Filter = { (Enabled -eq "true") -and (LastLogonTimeStamp -le $InactiveDate) } + SearchBase = $Domain + } + + # Get inactive computers that are not active in the past $InactiveDays days + $InactiveComputers = Get-ADComputer @GetComputerSplat | Select-Object "Name", @{ + # Format the LastLogonTimeStamp property to a human-readable date + Name = "LastLogon" + Expression = { + if ($_.LastLogonTimeStamp -gt 0) { + # Convert LastLogonTimeStamp to a datetime + $lastLogon = [DateTime]::FromFileTime($_.LastLogonTimeStamp) + # Format the datetime + $lastLogonFormatted = $lastLogon.ToString("MM/dd/yyyy hh:mm:ss tt") + return $lastLogonFormatted + } + else { + return "01/01/1601 00:00:00 AM" + } + } + }, "OperatingSystem" + + if ($InactiveComputers -and $InactiveComputers.Count -gt 0) { + Write-Host "[Info] Found $($InactiveComputers.Count) inactive computers." + } + else { + Write-Host "[Info] No inactive computers were found." + } + } + catch { + Write-Host "[Error] Failed to get inactive computers. Please try again." + exit 1 + } + + # Save the results to a custom field + if ($WysiwygCustomField) { + Write-Host "" + Write-Host "Note: Custom field '$WysiwygCustomField' was specified but NinjaOne integration has been removed." + Write-Host "All output has been displayed above." + } + + $InactiveComputers | Format-Table -AutoSize | Out-String -Width 4000 | Write-Host + + exit $ExitCode +} +end { + +} diff --git a/Powershell Scripts/Active Directory - Get Last Login Time for all Member PCs.ps1 b/Powershell Scripts/Active Directory - Get Last Login Time for all Member PCs.ps1 index 074c4a7..95c8db6 100644 --- a/Powershell Scripts/Active Directory - Get Last Login Time for all Member PCs.ps1 +++ b/Powershell Scripts/Active Directory - Get Last Login Time for all Member PCs.ps1 @@ -1,350 +1,230 @@ # Gets the last login time for all computers in Active Directory. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Gets the last login time for all computers in Active Directory. -.DESCRIPTION - Gets the last login time for all computers in Active Directory. - The last login time is retrieved from the LastLogonTimeStamp property of the computer object. - If the user name cannot be retrieved from an offline computer, the script will return Unknown. - If the computer name cannot be retrieved, the script will return Unknown. - -.EXAMPLE - (No Parameters) - ## EXAMPLE OUTPUT WITHOUT PARAMS ## - -PARAMETER: -WysiwygCustomField "myWysiwygCustomField" - Saves results to a WYSIWYG Custom Field. -.EXAMPLE - -WysiwygCustomField "myWysiwygCustomField" - ## EXAMPLE OUTPUT WITH WysiwygCustomField ## - [Info] Found 10 computers. - [Info] Attempting to set Custom Field 'myWysiwygCustomField'. - [Info] Successfully set Custom Field 'myWysiwygCustomField'! - -PARAMETER: -QueryForLastUserLogon "true" - When checked, the script will query for the last user logon time for each computer. - Note that this will take longer to run and will try to connect to each computer in the domain. -.EXAMPLE - -QueryForLastUserLogon "true" - ## EXAMPLE OUTPUT WITH QueryForLastUserLogon ## - [Warn] Remote computer WIN-1234567891 is not available. - [Info] Found 2 computers. - - Computer Last Logon Date Last Login in Days User - -------- --------------- ------------------ ---- - WIN-1234567891 2024-04-01 12:00 0 Unknown - WIN-1234567890 2024-04-01 12:00 0 Fred - WIN-9876543210 2023-04-01 12:00 32 Bob - -.NOTES - Minimum OS Architecture Supported: Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$WysiwygCustomField, - [Parameter()] - [Switch]$QueryForLastUserLogon -) - -begin { - # CIM timeout - $CIMTimeout = 10 - - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 10000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") - } - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to be set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received it will have an exception property, the function will exit with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below types require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } -} -process { - if (-not (Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Get Script Variables and override parameters with them - if ($env:wysiwygCustomField -and $env:wysiwygCustomField -notlike "null") { - $WysiwygCustomField = $env:wysiwygCustomField - } - if ($env:queryForLastUserLogon -and $env:queryForLastUserLogon -notlike "null") { - if ($env:queryForLastUserLogon -eq "true") { - $QueryForLastUserLogon = $true - } - else { - $QueryForLastUserLogon = $false - } - } - - # Check that Active Directory module is available - if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) { - Write-Host "[Error] Active Directory module is not available. Please install it and try again." - exit 1 - } - - # Get the computer system from the CIM - $ComputerSystem = $(Get-CimInstance -ClassName Win32_ComputerSystem) - - # Check if this script is running on a domain joined computer - if ($ComputerSystem.PartOfDomain -eq $false) { - Write-Host "[Error] This script must be run on a domain joined computer." - exit 1 - } - - # Check if this script is running on a domain controller - switch ($ComputerSystem.DomainRole) { - 0 { Write-Host "[Info] Running script on a Standalone Workstation." } - 1 { Write-Host "[Info] Running script on a Member Workstation." } - 2 { Write-Host "[Info] Running script on a Standalone Server." } - 3 { Write-Host "[Info] Running script on a Member Server." } - 4 { Write-Host "[Info] Running script on a Backup Domain Controller." } - 5 { Write-Host "[Info] Running script on a Primary Domain Controller." } - } - - # Get the SearchBase for the domain - $Domain = "DC=$($ComputerSystem.Domain -split "\." -join ",DC=")" - - # Get Computers from Active Directory - try { - $Computers = Get-ADComputer -Filter { (Enabled -eq $true) } -Properties Name, LastLogonTimeStamp -SearchBase "$Domain" -ErrorAction Stop - } - catch { - Write-Host "[Error] Failed to get computers. Make sure this is running on a domain controller." - exit 1 - } - - $IsFirstError = $true - - $LastLogonInfo = foreach ($Computer in $Computers) { - try { - # Get the LastLogonTimeStamp for the computer from Active Directory - $PCInfo = Get-ADComputer -Identity $Computer.Name -Properties LastLogonTimeStamp -ErrorAction Stop | Select-Object -Property @( - @{Name = "Computer"; Expression = { $_.Name } }, - @{Name = "LastLogon"; Expression = { [DateTime]::FromFileTime($_.LastLogonTimeStamp) } } - ) - } - catch { - # This should only happen if the script is not running as the system user on a domain controller or not as a domain admin - Write-Debug "[Debug] $($_.Exception.Message)" - Write-Host "[Warn] Failed to get details for $($Computer.Name) from Active Directory. Skipping." - continue - } - try { - if ($QueryForLastUserLogon) { - # Get the User Principal Name from the computer - $LastUserLogonInfo = Get-CimInstance -ClassName Win32_UserProfile -ComputerName $Computer.name -OperationTimeoutSec $CIMTimeout -ErrorAction Stop | Where-Object { $_.LocalPath -like "*Users*" } | Sort-Object -Property LastUseTime | Select-Object -Last 1 - $SecIdentifier = New-Object System.Security.Principal.SecurityIdentifier($LastUserLogonInfo.SID) -ErrorAction Stop - $UserName = $SecIdentifier.Translate([System.Security.Principal.NTAccount]) - } - } - catch { - if ($null -eq $UserName) { - if ($IsFirstError) { - # Only show on the first error - Write-Debug "[Debug] $($_.Exception.Message)" - Write-Host "[Error] Failed to connect to 1 or more computers via Get-CimInstance." - $IsFirstError = $false - } - Write-Host "[Warn] Remote computer $($Computer.Name) is not available or could not be queried." - } - } - - if ($null -eq $UserName) { - $UserName = [PSCustomObject]@{ - value = "Unknown" - } - } - if ($null -eq $PCInfo.LastLogon) { - $PCInfo = [PSCustomObject]@{ - Computer = $Computer.Name - LastLogon = "Unknown" - } - Write-Host "[Warn] Failed to get LastLogonTimeStamp for $($Computer.Name)." - } - - # Get the number of days since the last login - $LastLoginDays = try { - 0 - $(Get-Date -Date $PCInfo.LastLogon).Subtract($(Get-Date)).Days - } - catch { - # Return unknown if the date is invalid or does not exist - "Unknown" - } - - # Output the results - if ($QueryForLastUserLogon) { - [PSCustomObject]@{ - 'Computer' = $PCInfo.Computer - 'Last Logon Date' = $PCInfo.LastLogon - 'Last Login in Days' = $LastLoginDays - 'User' = $UserName.value - } - } - else { - [PSCustomObject]@{ - 'Computer' = $PCInfo.Computer - 'Last Logon Date' = $PCInfo.LastLogon - 'Last Login in Days' = $LastLoginDays - } - } - - $PCInfo = $null - $LastUserLogonInfo = $null - $SecIdentifier = $null - $UserName = $null - } - - # Output the number of computers found - if ($LastLogonInfo -and $LastLogonInfo.Count -gt 0) { - Write-Host "[Info] Found $($LastLogonInfo.Count) computers." - } - else { - Write-Host "[Error] No computers were found." - $ExitCode = 1 - } - - function Write-LastLoginInfo { - param () - $LastLogonInfo | Format-Table -AutoSize | Out-String -Width 4000 | Write-Host - } - - # Save the results to a custom field - if ($WysiwygCustomField) { - $LastLoginOkayDays = 30 - $LastLoginTooOldDays = 90 - # Convert the array to an HTML table - $HtmlTable = $LastLogonInfo | ConvertTo-Html -Fragment - # Set the color of the rows based on the last logon time - $HtmlTable = $HtmlTable -split [Environment]::NewLine | ForEach-Object { - if ($_ -match "(?'LastLoginDays'\d+)<\/td>") { - # Get the last login days from the HTML table - [int]$LastLoginDays = $Matches.LastLoginDays - if ($LastLoginDays -lt $LastLoginTooOldDays -and $LastLoginDays -ge $LastLoginOkayDays) { - # warning = 31 days to 89 days - $_ -replace "", '' - } - elseif ($LastLoginDays -ge $LastLoginTooOldDays) { - # danger = 90 days or more - $_ -replace "", '' - } - else { - # success = 30 days or less - $_ -replace "", '' - } - } - else { - $_ - } - } - # Set the width of the table to 10% to reduce the width of the table to its minimum possible width - $HtmlTable = $HtmlTable -replace "", "
" - try { - Write-Host "[Info] Attempting to set Custom Field '$WysiwygCustomField'." - Set-NinjaProperty -Name $WysiwygCustomField -Value $($HtmlTable | Out-String) - Write-Host "[Info] Successfully set Custom Field '$WysiwygCustomField'!" - } - catch { - Write-Host "[Error] Failed to set Custom Field '$WysiwygCustomField'." - Write-LastLoginInfo - $ExitCode = 1 - } - } - else { - Write-LastLoginInfo - } - - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Gets the last login time for all computers in Active Directory. +.DESCRIPTION + Gets the last login time for all computers in Active Directory. + The last login time is retrieved from the LastLogonTimeStamp property of the computer object. + If the user name cannot be retrieved from an offline computer, the script will return Unknown. + If the computer name cannot be retrieved, the script will return Unknown. + +.EXAMPLE + (No Parameters) + ## EXAMPLE OUTPUT WITHOUT PARAMS ## + +PARAMETER: -WysiwygCustomField "myWysiwygCustomField" + Saves results to a WYSIWYG Custom Field. +.EXAMPLE + -WysiwygCustomField "myWysiwygCustomField" + ## EXAMPLE OUTPUT WITH WysiwygCustomField ## + [Info] Found 10 computers. + [Info] Attempting to set Custom Field 'myWysiwygCustomField'. + [Info] Successfully set Custom Field 'myWysiwygCustomField'! + +PARAMETER: -QueryForLastUserLogon "true" + When checked, the script will query for the last user logon time for each computer. + Note that this will take longer to run and will try to connect to each computer in the domain. +.EXAMPLE + -QueryForLastUserLogon "true" + ## EXAMPLE OUTPUT WITH QueryForLastUserLogon ## + [Warn] Remote computer WIN-1234567891 is not available. + [Info] Found 2 computers. + + Computer Last Logon Date Last Login in Days User + -------- --------------- ------------------ ---- + WIN-1234567891 2024-04-01 12:00 0 Unknown + WIN-1234567890 2024-04-01 12:00 0 Fred + WIN-9876543210 2023-04-01 12:00 32 Bob + +.NOTES + Minimum OS Architecture Supported: Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$WysiwygCustomField, + [Parameter()] + [Switch]$QueryForLastUserLogon +) + +begin { + # CIM timeout + $CIMTimeout = 10 + + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + +} +process { + if (-not (Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Get Script Variables and override parameters with them + if ($env:wysiwygCustomField -and $env:wysiwygCustomField -notlike "null") { + $WysiwygCustomField = $env:wysiwygCustomField + } + if ($env:queryForLastUserLogon -and $env:queryForLastUserLogon -notlike "null") { + if ($env:queryForLastUserLogon -eq "true") { + $QueryForLastUserLogon = $true + } + else { + $QueryForLastUserLogon = $false + } + } + + # Check that Active Directory module is available + if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) { + Write-Host "[Error] Active Directory module is not available. Please install it and try again." + exit 1 + } + + # Get the computer system from the CIM + $ComputerSystem = $(Get-CimInstance -ClassName Win32_ComputerSystem) + + # Check if this script is running on a domain joined computer + if ($ComputerSystem.PartOfDomain -eq $false) { + Write-Host "[Error] This script must be run on a domain joined computer." + exit 1 + } + + # Check if this script is running on a domain controller + switch ($ComputerSystem.DomainRole) { + 0 { Write-Host "[Info] Running script on a Standalone Workstation." } + 1 { Write-Host "[Info] Running script on a Member Workstation." } + 2 { Write-Host "[Info] Running script on a Standalone Server." } + 3 { Write-Host "[Info] Running script on a Member Server." } + 4 { Write-Host "[Info] Running script on a Backup Domain Controller." } + 5 { Write-Host "[Info] Running script on a Primary Domain Controller." } + } + + # Get the SearchBase for the domain + $Domain = "DC=$($ComputerSystem.Domain -split "\." -join ",DC=")" + + # Get Computers from Active Directory + try { + $Computers = Get-ADComputer -Filter { (Enabled -eq $true) } -Properties Name, LastLogonTimeStamp -SearchBase "$Domain" -ErrorAction Stop + } + catch { + Write-Host "[Error] Failed to get computers. Make sure this is running on a domain controller." + exit 1 + } + + $IsFirstError = $true + + $LastLogonInfo = foreach ($Computer in $Computers) { + try { + # Get the LastLogonTimeStamp for the computer from Active Directory + $PCInfo = Get-ADComputer -Identity $Computer.Name -Properties LastLogonTimeStamp -ErrorAction Stop | Select-Object -Property @( + @{Name = "Computer"; Expression = { $_.Name } }, + @{Name = "LastLogon"; Expression = { [DateTime]::FromFileTime($_.LastLogonTimeStamp) } } + ) + } + catch { + # This should only happen if the script is not running as the system user on a domain controller or not as a domain admin + Write-Debug "[Debug] $($_.Exception.Message)" + Write-Host "[Warn] Failed to get details for $($Computer.Name) from Active Directory. Skipping." + continue + } + try { + if ($QueryForLastUserLogon) { + # Get the User Principal Name from the computer + $LastUserLogonInfo = Get-CimInstance -ClassName Win32_UserProfile -ComputerName $Computer.name -OperationTimeoutSec $CIMTimeout -ErrorAction Stop | Where-Object { $_.LocalPath -like "*Users*" } | Sort-Object -Property LastUseTime | Select-Object -Last 1 + $SecIdentifier = New-Object System.Security.Principal.SecurityIdentifier($LastUserLogonInfo.SID) -ErrorAction Stop + $UserName = $SecIdentifier.Translate([System.Security.Principal.NTAccount]) + } + } + catch { + if ($null -eq $UserName) { + if ($IsFirstError) { + # Only show on the first error + Write-Debug "[Debug] $($_.Exception.Message)" + Write-Host "[Error] Failed to connect to 1 or more computers via Get-CimInstance." + $IsFirstError = $false + } + Write-Host "[Warn] Remote computer $($Computer.Name) is not available or could not be queried." + } + } + + if ($null -eq $UserName) { + $UserName = [PSCustomObject]@{ + value = "Unknown" + } + } + if ($null -eq $PCInfo.LastLogon) { + $PCInfo = [PSCustomObject]@{ + Computer = $Computer.Name + LastLogon = "Unknown" + } + Write-Host "[Warn] Failed to get LastLogonTimeStamp for $($Computer.Name)." + } + + # Get the number of days since the last login + $LastLoginDays = try { + 0 - $(Get-Date -Date $PCInfo.LastLogon).Subtract($(Get-Date)).Days + } + catch { + # Return unknown if the date is invalid or does not exist + "Unknown" + } + + # Output the results + if ($QueryForLastUserLogon) { + [PSCustomObject]@{ + 'Computer' = $PCInfo.Computer + 'Last Logon Date' = $PCInfo.LastLogon + 'Last Login in Days' = $LastLoginDays + 'User' = $UserName.value + } + } + else { + [PSCustomObject]@{ + 'Computer' = $PCInfo.Computer + 'Last Logon Date' = $PCInfo.LastLogon + 'Last Login in Days' = $LastLoginDays + } + } + + $PCInfo = $null + $LastUserLogonInfo = $null + $SecIdentifier = $null + $UserName = $null + } + + # Output the number of computers found + if ($LastLogonInfo -and $LastLogonInfo.Count -gt 0) { + Write-Host "[Info] Found $($LastLogonInfo.Count) computers." + } + else { + Write-Host "[Error] No computers were found." + $ExitCode = 1 + } + + function Write-LastLoginInfo { + param () + $LastLogonInfo | Format-Table -AutoSize | Out-String -Width 4000 | Write-Host + } + + # Save the results to a custom field + if ($WysiwygCustomField) { + Write-Host "" + Write-Host "Note: Custom field '$WysiwygCustomField' was specified but NinjaOne integration has been removed." + Write-Host "All output has been displayed above." + } + else { + Write-LastLoginInfo + } + + exit $ExitCode +} +end { + +} diff --git a/Powershell Scripts/Active Directory - Get OU Members.ps1 b/Powershell Scripts/Active Directory - Get OU Members.ps1 index f71bc65..9541b8e 100644 --- a/Powershell Scripts/Active Directory - Get OU Members.ps1 +++ b/Powershell Scripts/Active Directory - Get OU Members.ps1 @@ -1,100 +1,98 @@ -# Gets the members of an OU from AD. -#Requires -Version 4.0 - -<# -.SYNOPSIS - Gets the members of an OU from AD. -.DESCRIPTION - Gets the members of an OU(Organizational Unit) from AD(Active Directory) and can save the results to a Custom Field. - -PARAMETER: -OU "Test" - A brief explanation of the parameter. -.EXAMPLE - -OU "Test" - - OU=Test,DC=something,DC=local - ----------------------------- - Test@something.local - -PARAMETER: -OU "Test" -CustomField "TestOU" - A brief explanation of the parameter. -.EXAMPLE - -OU "Test" -CustomField "TestOU" - - OU=Test,DC=something,DC=local - ----------------------------- - Test@something.local - -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows Server 2012 R2 (Domain Controller's Only) - Release Notes: Renamed script -#> - -[CmdletBinding()] -param ( - [Parameter()] - [string]$OU, - [string]$CustomField -) - -begin { - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - if (-not (Get-Module -Name ActiveDirectory -ListAvailable)) { - Write-Error "RSAT is required to get the membership. Please run this on a domain controller or on a machine with RSAT installed." - exit 1 - } -} -process { - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - if ($env:OuName -and $env:OuName -notlike "null") { - $OU = $env:OuName - } - - if ($env:CustomField -and $env:CustomField -notlike "null") { - $CustomField = $env:CustomField - } - - $Report = New-Object System.Collections.Generic.List[string] - - try { - $OUPaths = Get-ADOrganizationalUnit -Filter * | Where-Object { $_.DistinguishedName -like "OU=$OU*" } | Select-Object -ExpandProperty DistinguishedName - $OUPaths | ForEach-Object { - $Report.Add("`n$_") - - $TitleLength = $_.Length - $i = 1 - $Title = $Null - while ($i -le $TitleLength) { - $Title = "$Title-" - $i++ - } - $Report.Add($Title) - - Get-ADUser -Filter * -SearchBase $_ -ErrorAction SilentlyContinue | Select-Object -ExpandProperty UserPrincipalName -ErrorAction SilentlyContinue | ForEach-Object { $Report.Add("$_") } - } - } - catch { - Write-Error $_ - exit 1 - } - - $Report | Write-Host if ($CustomField) { - Write-Output "${CustomField}:$($Report | Out-String)" - } -} -end { - - - -} +# Gets the members of an OU from AD. +#Requires -Version 4.0 + +<# +.SYNOPSIS + Gets the members of an OU from AD. +.DESCRIPTION + Gets the members of an OU(Organizational Unit) from AD(Active Directory) and can save the results to a Custom Field. + +PARAMETER: -OU "Test" + A brief explanation of the parameter. +.EXAMPLE + -OU "Test" + + OU=Test,DC=something,DC=local + ----------------------------- + Test@something.local + +PARAMETER: -OU "Test" -CustomField "TestOU" + A brief explanation of the parameter. +.EXAMPLE + -OU "Test" -CustomField "TestOU" + + OU=Test,DC=something,DC=local + ----------------------------- + Test@something.local + +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows Server 2012 R2 (Domain Controller's Only) + Release Notes: Renamed script +#> + +[CmdletBinding()] +param ( + [Parameter()] + [string]$OU, + [string]$CustomField +) + +begin { + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if (-not (Get-Module -Name ActiveDirectory -ListAvailable)) { + Write-Error "RSAT is required to get the membership. Please run this on a domain controller or on a machine with RSAT installed." + exit 1 + } +} +process { + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + if ($env:OuName -and $env:OuName -notlike "null") { + $OU = $env:OuName + } + + if ($env:CustomField -and $env:CustomField -notlike "null") { + $CustomField = $env:CustomField + } + + $Report = New-Object System.Collections.Generic.List[string] + + try { + $OUPaths = Get-ADOrganizationalUnit -Filter * | Where-Object { $_.DistinguishedName -like "OU=$OU*" } | Select-Object -ExpandProperty DistinguishedName + $OUPaths | ForEach-Object { + $Report.Add("`n$_") + + $TitleLength = $_.Length + $i = 1 + $Title = $Null + while ($i -le $TitleLength) { + $Title = "$Title-" + $i++ + } + $Report.Add($Title) + + Get-ADUser -Filter * -SearchBase $_ -ErrorAction SilentlyContinue | Select-Object -ExpandProperty UserPrincipalName -ErrorAction SilentlyContinue | ForEach-Object { $Report.Add("$_") } + } + } + catch { + Write-Error $_ + exit 1 + } + + $Report | Write-Host if ($CustomField) { + Write-Output "${CustomField}:$($Report | Out-String)" + } +} +end { + +} diff --git a/Powershell Scripts/Active Directory - Get Organizational Unit (OU).ps1 b/Powershell Scripts/Active Directory - Get Organizational Unit (OU).ps1 index 0f62e03..f23c7ad 100644 --- a/Powershell Scripts/Active Directory - Get Organizational Unit (OU).ps1 +++ b/Powershell Scripts/Active Directory - Get Organizational Unit (OU).ps1 @@ -1,216 +1,107 @@ # Gets the Organizational Units (OUs) that this device is a member of in Active Directory or Azure AD. -<# -.SYNOPSIS - Gets the Organizational Units (OUs) that this device is a member of in Active Directory or Azure AD. -.DESCRIPTION - Gets the Organizational Units (OUs) that this device is a member of in Active Directory or Azure AD. -.EXAMPLE - -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" - - Attempting to set Custom Field 'ReplaceMeWithAnyMultilineCustomField'. - Successfully set Custom Field 'ReplaceMeWithAnyMultilineCustomField'! - - Organizational Units Found: - OU=Domain Controllers,OU=Computers,DC=test,DC=lan - OU=Servers,OU=Computers,DC=test,DC=lan - -PARAMETER: -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" - Name of a multiline custom field to save the results to. -.EXAMPLE - -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" - - Attempting to set Custom Field 'ReplaceMeWithAnyMultilineCustomField'. - Successfully set Custom Field 'ReplaceMeWithAnyMultilineCustomField'! - - Organizational Units Found: - OU=Domain Controllers,OU=Computers,DC=test,DC=lan - OU=Servers,OU=Computers,DC=test,DC=lan -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$CustomFieldName -) - -begin { - # If using script form variables, replace command line parameters with the form variables. - if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomFieldName = $env:customFieldName } - - # Function to check if the script is running with elevated (administrator) privileges - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - function Test-IsDomainJoined { - # Check the PowerShell version to determine the appropriate cmdlet to use - if ($PSVersionTable.PSVersion.Major -lt 5) { - return $(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain - } - else { - return $(Get-CimInstance -Class Win32_ComputerSystem).PartOfDomain - } - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - # Measure the number of characters in the provided value - $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # Throw an error if the value exceeds the character limit of 200,000 characters - if ($Characters -ge 200000) { - throw "Character limit exceeded: the value is greater than or equal to 200,000 characters." - } - - # Initialize a hashtable for additional documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define a list of valid field types - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - - # Warn the user if the provided type is not valid - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # Define types that require options to be retrieved - $NeedsOptions = "Dropdown" - - # If the property is being set in a document or field and the type needs options, retrieve them - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an error if there was an issue retrieving the property options - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Process the property value based on its type - switch ($Type) { - "Checkbox" { - # Convert the value to a boolean for Checkbox type - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Convert the value to a Unix timestamp for Date or Date Time type - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Convert the dropdown value to its corresponding GUID - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - # Throw an error if the value is not present in the dropdown options - if (!($Selection)) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # For other types, use the value as is - $NinjaValue = $Value - } - } - - # Set the property value in the document if a document name is provided - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - # Otherwise, set the standard property value - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - - # Throw an error if setting the property failed - if ($CustomField.Exception) { - throw $CustomField - } - } -} -process { - # Check if the script is running with elevated (administrator) privileges - if (!(Test-IsElevated)) { - Write-Host "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - $regPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine' - $DistinguishedName = Get-ItemProperty -Path $regPath -Name 'Distinguished-Name' -ErrorAction SilentlyContinue - - $OrganizationalUnit = if ($DistinguishedName -and $DistinguishedName.'Distinguished-Name') { - $OU = $DistinguishedName.'Distinguished-Name' -replace '^CN=.*?,', '' - Write-Output $OU - } - else { - Write-Host "[Warn] Failed to retrieve Organizational Unit from Group Policy State." - } - - $OrganizationalUnit = if (Test-ComputerSecureChannel -ErrorAction SilentlyContinue) { - "$OrganizationalUnit" - } - else { - "(Cached) $OrganizationalUnit" - } - - if ($OrganizationalUnit) { - Write-Host "[Info] The OU for $env:COMPUTERNAME is: $OrganizationalUnit" - } - else { - Write-Host "[Error] Failed to retrieve Organizational Units." - exit 1 - } - - - # If custom field name is provided, set the custom field with the list of OUs - if ($CustomFieldName) { - try { - Write-Host "[Info] Attempting to set Custom Field '$CustomFieldName'." - if ((Test-IsDomainJoined)) { - Set-NinjaProperty -Name $CustomFieldName -Value $($OrganizationalUnit | Out-String) - } - else { - Set-NinjaProperty -Name $CustomFieldName -Value "Workgroup" - } - Write-Host "[Info] Successfully set Custom Field '$CustomFieldName'!" - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - exit 1 - } - } - - exit 0 -} -end { - - - -} +<# +.SYNOPSIS + Gets the Organizational Units (OUs) that this device is a member of in Active Directory or Azure AD. +.DESCRIPTION + Gets the Organizational Units (OUs) that this device is a member of in Active Directory or Azure AD. +.EXAMPLE + -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" + + Attempting to set Custom Field 'ReplaceMeWithAnyMultilineCustomField'. + Successfully set Custom Field 'ReplaceMeWithAnyMultilineCustomField'! + + Organizational Units Found: + OU=Domain Controllers,OU=Computers,DC=test,DC=lan + OU=Servers,OU=Computers,DC=test,DC=lan + +PARAMETER: -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" + Name of a multiline custom field to save the results to. +.EXAMPLE + -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" + + Attempting to set Custom Field 'ReplaceMeWithAnyMultilineCustomField'. + Successfully set Custom Field 'ReplaceMeWithAnyMultilineCustomField'! + + Organizational Units Found: + OU=Domain Controllers,OU=Computers,DC=test,DC=lan + OU=Servers,OU=Computers,DC=test,DC=lan +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$CustomFieldName +) + +begin { + # If using script form variables, replace command line parameters with the form variables. + if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomFieldName = $env:customFieldName } + + # Function to check if the script is running with elevated (administrator) privileges + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + function Test-IsDomainJoined { + # Check the PowerShell version to determine the appropriate cmdlet to use + if ($PSVersionTable.PSVersion.Major -lt 5) { + return $(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain + } + else { + return $(Get-CimInstance -Class Win32_ComputerSystem).PartOfDomain + } + } + +} +process { + # Check if the script is running with elevated (administrator) privileges + if (!(Test-IsElevated)) { + Write-Host "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + $regPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine' + $DistinguishedName = Get-ItemProperty -Path $regPath -Name 'Distinguished-Name' -ErrorAction SilentlyContinue + + $OrganizationalUnit = if ($DistinguishedName -and $DistinguishedName.'Distinguished-Name') { + $OU = $DistinguishedName.'Distinguished-Name' -replace '^CN=.*?,', '' + Write-Output $OU + } + else { + Write-Host "[Warn] Failed to retrieve Organizational Unit from Group Policy State." + } + + $OrganizationalUnit = if (Test-ComputerSecureChannel -ErrorAction SilentlyContinue) { + "$OrganizationalUnit" + } + else { + "(Cached) $OrganizationalUnit" + } + + if ($OrganizationalUnit) { + Write-Host "[Info] The OU for $env:COMPUTERNAME is: $OrganizationalUnit" + } + else { + Write-Host "[Error] Failed to retrieve Organizational Units." + exit 1 + } + + # If custom field name is provided, set the custom field with the list of OUs + if ($CustomFieldName) { + Write-Host "" + Write-Host "Note: Custom field '$CustomFieldName' was specified but NinjaOne integration has been removed." + Write-Host "All output has been displayed above." + } + + exit 0 +} +end { + +} diff --git a/Powershell Scripts/Active Directory - Join Computer to a Domain.ps1 b/Powershell Scripts/Active Directory - Join Computer to a Domain.ps1 index 52c303f..39775c8 100644 --- a/Powershell Scripts/Active Directory - Join Computer to a Domain.ps1 +++ b/Powershell Scripts/Active Directory - Join Computer to a Domain.ps1 @@ -1,300 +1,294 @@ # Joins a computer to a domain. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Joins a computer to a domain. -.DESCRIPTION - Joins a computer to a domain. -.EXAMPLE - -DomainName "Domain.com" -UserName "Domain\MyDomainUser" -Password "Somepass1" - Joins a computer to a "Domain.com" domain and restarts the computer. Don't expect a success result in Ninja as the computer will reboot before the script can return a result. -.EXAMPLE - -DomainName "Domain.com" -UserName "Domain\MyDomainUser" -Password "Somepass1" -NoRestart - Joins a computer to a "Domain.com" domain and does not restart the computer. -.EXAMPLE - PS C:\> Join-Domain.ps1 -DomainName "domain.com" -UserName "Domain\MyDomainUser" -Password "Somepass1" -NoRestart - Joins a computer to a "Domain.com" domain and does not restart the computer. -.EXAMPLE - -DomainName "Domain.com" -UserName "Domain\MyDomainUser" -Password "Somepass1" -Server "192.168.0.1" - Not recommended if the computer this script is running on does not have one of the Domain Controllers set as its DNS server. - Joins a computer to a "Domain.com" domain, talks to the domain with the IP address of "192.168.0.1", and restarts the computer. -.OUTPUTS - String[] -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Updates outputs with metadata and gets password from secure custom field. -.COMPONENT - ManageUsers -#> - -[CmdletBinding()] -param ( - # Domain Name to join computer to - [Parameter()] - [String]$DomainName, - # Use a Domain UserName to join this computer to a domain, this requires the Password parameter to be used as well - [Parameter()] - [String]$UserName, - # Use a Domain Password to join this computer from a domain - [Parameter()] - [String]$Password, - # Used only when computer can't locate a domain controller via DNS or you wish to connect to a specific DC - [Parameter()] - [String]$Server, - # Do not restart computer after joining to a domain - [Parameter()] - [Switch]$NoRestart = [System.Convert]::ToBoolean($env:noRestart) -) - -begin { - function Get-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter()] - [String]$DocumentName - ) - - # If we're requested to get the field value from a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # These two types require more information to parse. - $NeedsOptions = "DropDown", "MultiSelect" - - # Grabbing document values requires a slightly different command. - if ($DocumentName) { - # Secure fields are only readable when they're a device custom field - if ($Type -Like "Secure") { throw [System.ArgumentOutOfRangeException]::New("$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality") } - - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - Write-Host "Retrieving value from Ninja Document..." - $NinjaPropertyValue = Ninja-Property-Docs-Get -AttributeName $Name @DocumentationParams 2>&1 - - # Certain fields require more information to parse. - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - # We'll redirect error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1 - - # Certain fields require more information to parse. - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If we received some sort of error it should have an exception property and we'll exit the function with that error information. - if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - if (-not $NinjaPropertyValue) { - throw [System.NullReferenceException]::New("The Custom Field '$Name' is empty!") - } - - # This switch will compare the type given with the quoted string. If it matches, it'll parse it further; otherwise, the default option will be selected. - switch ($Type) { - "Attachment" { - # Attachments come in a JSON format this will convert it into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Checkbox" { - # Checkbox's come in as a string representing an integer. We'll need to cast that string into an integer and then convert it to a more traditional boolean. - [System.Convert]::ToBoolean([int]$NinjaPropertyValue) - } - "Date or Date Time" { - # In Ninja Date and Date/Time fields are in Unix Epoch time in the UTC timezone the below should convert it into local time as a datetime object. - $UnixTimeStamp = $NinjaPropertyValue - $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp) - $TimeZone = [TimeZoneInfo]::Local - [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) - } - "Decimal" { - # In ninja decimals are strings that represent a decimal this will cast it into a double data type. - [double]$NinjaPropertyValue - } - "Device Dropdown" { - # Device Drop-Downs Fields come in a JSON format this will convert it into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Device MultiSelect" { - # Device Multi-Select Fields come in a JSON format this will convert it into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Dropdown" { - # Drop-Down custom fields come in as a comma-separated list of GUIDs; we'll compare these with all the options and return just the option values selected instead of a GUID. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name - } - "Integer" { - # Cast's the Ninja provided string into an integer. - [int]$NinjaPropertyValue - } - "MultiSelect" { - # Multi-Select custom fields come in as a comma-separated list of GUID's we'll compare these with all the options and return just the option values selected instead of a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = ($NinjaPropertyValue -split ',').trim() - - foreach ($Item in $Selection) { - $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name - } - } - "Organization Dropdown" { - # Turns the Ninja provided JSON into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization Location Dropdown" { - # Turns the Ninja provided JSON into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization Location MultiSelect" { - # Turns the Ninja provided JSON into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization MultiSelect" { - # Turns the Ninja provided JSON into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Time" { - # Time fields are given as a number of seconds starting from midnight. This will convert it into a datetime object. - $Seconds = $NinjaPropertyValue - $UTC = ([timespan]::fromseconds($Seconds)).ToString("hh\:mm\:ss") - $TimeZone = [TimeZoneInfo]::Local - $ConvertedTime = [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) - - Get-Date $ConvertedTime -DisplayHint Time - } - default { - # If no type was given or not one that matches the above types just output what we retrieved. - $NinjaPropertyValue - } - } - } - if ($env:domainToJoin -and $env:domainToJoin -notlike "null") { $DomainName = $env:domainToJoin } - if ($env:usernameToJoinDomainWith -and $env:usernameToJoinDomainWith -notlike "null") { - $UserName = $env:usernameToJoinDomainWith - $env:usernameToJoinDomainWith = $env:usernameToJoinDomainWith | ConvertTo-SecureString -AsPlainText -Force - } - # Get password from secure custom field - if ($env:passwordToJoinDomainWithCustomField -and $env:passwordToJoinDomainWithCustomField -notlike "null") { - try { - $Password = Get-NinjaProperty -Name $env:passwordToJoinDomainWithCustomField - } - catch { - Write-Host "[Error] Failed to get password from secure custom field." - exit 1 - } - } - if ($env:serverName -and $env:serverName -notlike "null") { $Server = $env:serverName } - - if (-not $DomainName) { Write-Host "[Error] Domain Name is required!"; Exit 1 } - if (-not $UserName) { Write-Host "[Error] A Username and Password is required to join a domain."; Exit 1 } - if (-not $Password) { Write-Host "[Error] A Username and Password is required to join a domain."; Exit 1 } - function Join-ComputerToDomainPS2 { - param ( - [String] - $DomainName, - [PSCredential] - $Credential, - $Restart, - $Server - ) - if ($Credential) { - # Use supplied Credentials - if ($Server) { - Add-Computer -DomainName $DomainName -Credential $Credential -Server $Server -Force -Confirm:$false -PassThru - } - else { - Add-Computer -DomainName $DomainName -Credential $Credential -Force -Confirm:$false -PassThru - } - } - else { - # No Credentials supplied, use current user - Add-Computer -DomainName $DomainName -Force -Confirm:$false -PassThru - } - } - Write-Output "[Info] Starting Join Domain" - - # Convert username and password into a credential object - $JoinCred = [PSCredential]::new($UserName, $(ConvertTo-SecureString -String $Password -AsPlainText -Force)) -} - -process { - Write-Output "[Info] Joining computer($env:COMPUTERNAME) to domain $DomainName" - $script:JoinResult = $false - try { - $JoinResult = if ($NoRestart) { - # Do not restart after joining - if ($PSVersionTable.PSVersion.Major -eq 2) { - if ($Server) { - (Join-ComputerToDomainPS2 -DomainName $DomainName -Credential $Credential -Server $Server).HasSucceeded - } - else { - (Join-ComputerToDomainPS2 -DomainName $DomainName -Credential $Credential).HasSucceeded - } - } - else { - if ($Server) { - (Add-Computer -DomainName $DomainName -Credential $JoinCred -Server $Server -Force -Confirm:$false -PassThru).HasSucceeded - } - else { - (Add-Computer -DomainName $DomainName -Credential $JoinCred -Force -Confirm:$false -PassThru).HasSucceeded - } - } - } - else { - # Restart after joining - if ($PSVersionTable.PSVersion.Major -eq 2) { - if ($Server) { - (Join-ComputerToDomainPS2 -DomainName $DomainName -Credential $Credential -Server $Server).HasSucceeded - } - else { - (Join-ComputerToDomainPS2 -DomainName $DomainName -Credential $Credential).HasSucceeded - } - } - else { - if ($Server) { - (Add-Computer -DomainName $DomainName -Credential $JoinCred -Restart -Server $Server -Force -Confirm:$false -PassThru).HasSucceeded - } - else { - (Add-Computer -DomainName $DomainName -Credential $JoinCred -Restart -Force -Confirm:$false -PassThru).HasSucceeded - } - } - } - } - catch { - Write-Host "[Error] Failed to Join Domain: $DomainName" - } - - if ($NoRestart -and $JoinResult) { - Write-Output "[Info] Joined computer($env:COMPUTERNAME) to Domain: $DomainName and not restarting computer" - } - elseif ($JoinResult) { - Write-Output "[Info] Joined computer($env:COMPUTERNAME) to Domain: $DomainName and restarting computer" - if ($PSVersionTable.PSVersion.Major -eq 2) { - shutdown.exe -r -t 60 - } - } - else { - Write-Output "[Error] Failed to Join computer($env:COMPUTERNAME) to Domain: $DomainName" - # Clean up credentials so that they don't leak outside this script - $JoinCred = $null - exit 1 - } - # Clean up credentials so that they don't leak outside this script - $JoinCred = $null - Write-Output "[Info] Completed Join Domain" -} - -end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + Joins a computer to a domain. +.DESCRIPTION + Joins a computer to a domain. +.EXAMPLE + -DomainName "Domain.com" -UserName "Domain\MyDomainUser" -Password "Somepass1" + Joins a computer to a "Domain.com" domain and restarts the computer. Don't expect a success result in Ninja as the computer will reboot before the script can return a result. +.EXAMPLE + -DomainName "Domain.com" -UserName "Domain\MyDomainUser" -Password "Somepass1" -NoRestart + Joins a computer to a "Domain.com" domain and does not restart the computer. +.EXAMPLE + PS C:\> Join-Domain.ps1 -DomainName "domain.com" -UserName "Domain\MyDomainUser" -Password "Somepass1" -NoRestart + Joins a computer to a "Domain.com" domain and does not restart the computer. +.EXAMPLE + -DomainName "Domain.com" -UserName "Domain\MyDomainUser" -Password "Somepass1" -Server "192.168.0.1" + Not recommended if the computer this script is running on does not have one of the Domain Controllers set as its DNS server. + Joins a computer to a "Domain.com" domain, talks to the domain with the IP address of "192.168.0.1", and restarts the computer. +.OUTPUTS + String[] +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Updates outputs with metadata and gets password from secure custom field. +.COMPONENT + ManageUsers +#> + +[CmdletBinding()] +param ( + # Domain Name to join computer to + [Parameter()] + [String]$DomainName, + # Use a Domain UserName to join this computer to a domain, this requires the Password parameter to be used as well + [Parameter()] + [String]$UserName, + # Use a Domain Password to join this computer from a domain + [Parameter()] + [String]$Password, + # Used only when computer can't locate a domain controller via DNS or you wish to connect to a specific DC + [Parameter()] + [String]$Server, + # Do not restart computer after joining to a domain + [Parameter()] + [Switch]$NoRestart = [System.Convert]::ToBoolean($env:noRestart) +) + +begin { + function Get-NinjaProperty { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [String]$Name, + [Parameter()] + [String]$Type, + [Parameter()] + [String]$DocumentName + ) + + # If we're requested to get the field value from a Ninja document we'll specify it here. + $DocumentationParams = @{} + if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } + + # These two types require more information to parse. + $NeedsOptions = "DropDown", "MultiSelect" + + # Grabbing document values requires a slightly different command. + if ($DocumentName) { + # Secure fields are only readable when they're a device custom field + if ($Type -Like "Secure") { throw [System.ArgumentOutOfRangeException]::New("$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality") } + + # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. + Write-Host "Retrieving value from Ninja Document..." + + # Certain fields require more information to parse. + if ($NeedsOptions -contains $Type) { + } + } + else { + # We'll redirect error output to the success stream to make it easier to error out if nothing was found or something else went wrong. + + # Certain fields require more information to parse. + if ($NeedsOptions -contains $Type) { + } + } + + # If we received some sort of error it should have an exception property and we'll exit the function with that error information. + if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } + if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } + + if (-not $NinjaPropertyValue) { + throw [System.NullReferenceException]::New("The Custom Field '$Name' is empty!") + } + + # This switch will compare the type given with the quoted string. If it matches, it'll parse it further; otherwise, the default option will be selected. + switch ($Type) { + "Attachment" { + # Attachments come in a JSON format this will convert it into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Checkbox" { + # Checkbox's come in as a string representing an integer. We'll need to cast that string into an integer and then convert it to a more traditional boolean. + [System.Convert]::ToBoolean([int]$NinjaPropertyValue) + } + "Date or Date Time" { + # In Ninja Date and Date/Time fields are in Unix Epoch time in the UTC timezone the below should convert it into local time as a datetime object. + $UnixTimeStamp = $NinjaPropertyValue + $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp) + $TimeZone = [TimeZoneInfo]::Local + [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) + } + "Decimal" { + # In ninja decimals are strings that represent a decimal this will cast it into a double data type. + [double]$NinjaPropertyValue + } + "Device Dropdown" { + # Device Drop-Downs Fields come in a JSON format this will convert it into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Device MultiSelect" { + # Device Multi-Select Fields come in a JSON format this will convert it into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Dropdown" { + # Drop-Down custom fields come in as a comma-separated list of GUIDs; we'll compare these with all the options and return just the option values selected instead of a GUID. + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name + } + "Integer" { + # Cast's the Ninja provided string into an integer. + [int]$NinjaPropertyValue + } + "MultiSelect" { + # Multi-Select custom fields come in as a comma-separated list of GUID's we'll compare these with all the options and return just the option values selected instead of a guid. + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Selection = ($NinjaPropertyValue -split ',').trim() + + foreach ($Item in $Selection) { + $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name + } + } + "Organization Dropdown" { + # Turns the Ninja provided JSON into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization Location Dropdown" { + # Turns the Ninja provided JSON into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization Location MultiSelect" { + # Turns the Ninja provided JSON into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization MultiSelect" { + # Turns the Ninja provided JSON into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Time" { + # Time fields are given as a number of seconds starting from midnight. This will convert it into a datetime object. + $Seconds = $NinjaPropertyValue + $UTC = ([timespan]::fromseconds($Seconds)).ToString("hh\:mm\:ss") + $TimeZone = [TimeZoneInfo]::Local + $ConvertedTime = [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) + + Get-Date $ConvertedTime -DisplayHint Time + } + default { + # If no type was given or not one that matches the above types just output what we retrieved. + $NinjaPropertyValue + } + } + } + if ($env:domainToJoin -and $env:domainToJoin -notlike "null") { $DomainName = $env:domainToJoin } + if ($env:usernameToJoinDomainWith -and $env:usernameToJoinDomainWith -notlike "null") { + $UserName = $env:usernameToJoinDomainWith + $env:usernameToJoinDomainWith = $env:usernameToJoinDomainWith | ConvertTo-SecureString -AsPlainText -Force + } + # Get password from secure custom field + if ($env:passwordToJoinDomainWithCustomField -and $env:passwordToJoinDomainWithCustomField -notlike "null") { + try { + $Password = Get-NinjaProperty -Name $env:passwordToJoinDomainWithCustomField + } + catch { + Write-Host "[Error] Failed to get password from secure custom field." + exit 1 + } + } + if ($env:serverName -and $env:serverName -notlike "null") { $Server = $env:serverName } + + if (-not $DomainName) { Write-Host "[Error] Domain Name is required!"; Exit 1 } + if (-not $UserName) { Write-Host "[Error] A Username and Password is required to join a domain."; Exit 1 } + if (-not $Password) { Write-Host "[Error] A Username and Password is required to join a domain."; Exit 1 } + function Join-ComputerToDomainPS2 { + param ( + [String] + $DomainName, + [PSCredential] + $Credential, + $Restart, + $Server + ) + if ($Credential) { + # Use supplied Credentials + if ($Server) { + Add-Computer -DomainName $DomainName -Credential $Credential -Server $Server -Force -Confirm:$false -PassThru + } + else { + Add-Computer -DomainName $DomainName -Credential $Credential -Force -Confirm:$false -PassThru + } + } + else { + # No Credentials supplied, use current user + Add-Computer -DomainName $DomainName -Force -Confirm:$false -PassThru + } + } + Write-Output "[Info] Starting Join Domain" + + # Convert username and password into a credential object + $JoinCred = [PSCredential]::new($UserName, $(ConvertTo-SecureString -String $Password -AsPlainText -Force)) +} + +process { + Write-Output "[Info] Joining computer($env:COMPUTERNAME) to domain $DomainName" + $script:JoinResult = $false + try { + $JoinResult = if ($NoRestart) { + # Do not restart after joining + if ($PSVersionTable.PSVersion.Major -eq 2) { + if ($Server) { + (Join-ComputerToDomainPS2 -DomainName $DomainName -Credential $Credential -Server $Server).HasSucceeded + } + else { + (Join-ComputerToDomainPS2 -DomainName $DomainName -Credential $Credential).HasSucceeded + } + } + else { + if ($Server) { + (Add-Computer -DomainName $DomainName -Credential $JoinCred -Server $Server -Force -Confirm:$false -PassThru).HasSucceeded + } + else { + (Add-Computer -DomainName $DomainName -Credential $JoinCred -Force -Confirm:$false -PassThru).HasSucceeded + } + } + } + else { + # Restart after joining + if ($PSVersionTable.PSVersion.Major -eq 2) { + if ($Server) { + (Join-ComputerToDomainPS2 -DomainName $DomainName -Credential $Credential -Server $Server).HasSucceeded + } + else { + (Join-ComputerToDomainPS2 -DomainName $DomainName -Credential $Credential).HasSucceeded + } + } + else { + if ($Server) { + (Add-Computer -DomainName $DomainName -Credential $JoinCred -Restart -Server $Server -Force -Confirm:$false -PassThru).HasSucceeded + } + else { + (Add-Computer -DomainName $DomainName -Credential $JoinCred -Restart -Force -Confirm:$false -PassThru).HasSucceeded + } + } + } + } + catch { + Write-Host "[Error] Failed to Join Domain: $DomainName" + } + + if ($NoRestart -and $JoinResult) { + Write-Output "[Info] Joined computer($env:COMPUTERNAME) to Domain: $DomainName and not restarting computer" + } + elseif ($JoinResult) { + Write-Output "[Info] Joined computer($env:COMPUTERNAME) to Domain: $DomainName and restarting computer" + if ($PSVersionTable.PSVersion.Major -eq 2) { + shutdown.exe -r -t 60 + } + } + else { + Write-Output "[Error] Failed to Join computer($env:COMPUTERNAME) to Domain: $DomainName" + # Clean up credentials so that they don't leak outside this script + $JoinCred = $null + exit 1 + } + # Clean up credentials so that they don't leak outside this script + $JoinCred = $null + Write-Output "[Info] Completed Join Domain" +} + +end { + +} + diff --git a/Powershell Scripts/Active Directory - Remove Computer from the Domain.ps1 b/Powershell Scripts/Active Directory - Remove Computer from the Domain.ps1 index d782838..0d3ec0f 100644 --- a/Powershell Scripts/Active Directory - Remove Computer from the Domain.ps1 +++ b/Powershell Scripts/Active Directory - Remove Computer from the Domain.ps1 @@ -1,245 +1,235 @@ # Removes the computer from the domain. -#Requires -Version 2.0 - -<# -.SYNOPSIS - Removes the computer from the domain. -.DESCRIPTION - Removes the computer from the domain. -.EXAMPLE - -UserName "MyDomainUser" -Password "Somepass1" - Removes the computer from the domain and restarts the computer. -.EXAMPLE - -UserName "MyDomainUser" -Password "Somepass1" -NoRestart - Removes the computer from the domain and does not restart the computer. -.EXAMPLE - PS C:\> Leave-Domain.ps1 -UserName "MyDomainUser" -Password "Somepass1" -NoRestart - Removes the computer from the domain and does not restart the computer. -.OUTPUTS - String[] -.NOTES - Minimum OS Architecture Supported: Windows 7, Windows Server 2012 - Release Notes: Gets passwords from secure custom fields -.COMPONENT - ManageUsers -#> - -[CmdletBinding()] -param ( - # Use a Domain UserName to remove this computer to a domain, this requires the Password parameter to be used as well - [Parameter()] - [String] - $UserName, - # Use a Domain Password to remove a computer from a domain - [Parameter()] - $Password, - # Do not restart computer after leaving to a domain - [Parameter()] - [Switch]$NoRestart = [System.Convert]::ToBoolean($env:noRestart) -) - -begin { - function Get-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter()] - [String]$DocumentName - ) - - # If we're requested to get the field value from a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # These two types require more information to parse. - $NeedsOptions = "DropDown", "MultiSelect" - - # Grabbing document values requires a slightly different command. - if ($DocumentName) { - # Secure fields are only readable when they're a device custom field - if ($Type -Like "Secure") { throw [System.ArgumentOutOfRangeException]::New("$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality") } - - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - Write-Host "Retrieving value from Ninja Document..." - $NinjaPropertyValue = Ninja-Property-Docs-Get -AttributeName $Name @DocumentationParams 2>&1 - - # Certain fields require more information to parse. - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - # We'll redirect error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1 - - # Certain fields require more information to parse. - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If we received some sort of error it should have an exception property and we'll exit the function with that error information. - if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - if (-not $NinjaPropertyValue) { - throw [System.NullReferenceException]::New("The Custom Field '$Name' is empty!") - } - - # This switch will compare the type given with the quoted string. If it matches, it'll parse it further; otherwise, the default option will be selected. - switch ($Type) { - "Attachment" { - # Attachments come in a JSON format this will convert it into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Checkbox" { - # Checkbox's come in as a string representing an integer. We'll need to cast that string into an integer and then convert it to a more traditional boolean. - [System.Convert]::ToBoolean([int]$NinjaPropertyValue) - } - "Date or Date Time" { - # In Ninja Date and Date/Time fields are in Unix Epoch time in the UTC timezone the below should convert it into local time as a DateTime object. - $UnixTimeStamp = $NinjaPropertyValue - $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp) - $TimeZone = [TimeZoneInfo]::Local - [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) - } - "Decimal" { - # In ninja decimals are strings that represent a decimal this will cast it into a double data type. - [double]$NinjaPropertyValue - } - "Device Dropdown" { - # Device Drop-Downs Fields come in a JSON format this will convert it into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Device MultiSelect" { - # Device Multi-Select Fields come in a JSON format this will convert it into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Dropdown" { - # Drop-Down custom fields come in as a comma-separated list of GUIDs; we'll compare these with all the options and return just the option values selected instead of a GUID. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name - } - "Integer" { - # Casts the Ninja provided string into an integer. - [int]$NinjaPropertyValue - } - "MultiSelect" { - # Multi-Select custom fields come in as a comma-separated list of GUIDs we'll compare these with all the options and return just the option values selected instead of a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = ($NinjaPropertyValue -split ',').trim() - - foreach ($Item in $Selection) { - $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name - } - } - "Organization Dropdown" { - # Turns the Ninja provided JSON into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization Location Dropdown" { - # Turns the Ninja provided JSON into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization Location MultiSelect" { - # Turns the Ninja provided JSON into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization MultiSelect" { - # Turns the Ninja provided JSON into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Time" { - # Time fields are given as a number of seconds starting from midnight. This will convert it into a DateTime object. - $Seconds = $NinjaPropertyValue - $UTC = ([TimeSpan]::FromSeconds($Seconds)).ToString("hh\:mm\:ss") - $TimeZone = [TimeZoneInfo]::Local - $ConvertedTime = [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) - - Get-Date $ConvertedTime -DisplayHint Time - } - default { - # If no type was given or not one that matches the above types just output what we retrieved. - $NinjaPropertyValue - } - } - } - - # Check if custom fields are used for the username and password - if ($env:domainUsername -and $env:domainUsername -notlike "null") { - $UserName = $env:domainUsername - } - - if ($env:domainPasswordWithCustomField -and $env:domainPasswordWithCustomField -notlike "null") { - try { - $Password = Get-NinjaProperty -Name $env:domainPasswordWithCustomField - if ([string]::IsNullOrWhiteSpace($Password)) { - Write-Host "[Warn] The Domain Password With Custom Field '$env:domainPasswordWithCustomField' is empty!" - throw - } - } - catch { - Write-Host "[Error] Failed to get password from secure custom field." - exit 1 - } - } - - - # Check if usernames and passwords where provided - if (-not ($UserName) -or -not ($Password) ) { Write-Error "A domain username and password is required."; Exit 1 } - - Write-Output "Starting Leave Domain" - - # Converts username and password into a credential object - $LeaveCred = [PSCredential]::new($UserName, $(ConvertTo-SecureString -String $Password -AsPlainText -Force)) - -} - -process { - Write-Output "Removing computer($env:COMPUTERNAME) from domain" - $script:LeaveResult = $false - try { - $LeaveResult = if ($NoRestart) { - (Remove-Computer -UnjoinDomainCredential $LeaveCred -PassThru -Force -Confirm:$false).HasSucceeded - # Do not restart after leaving - } - else { - # Restart after leaving - (Remove-Computer -UnjoinDomainCredential $LeaveCred -PassThru -Force -Restart -Confirm:$false).HasSucceeded - } - } - catch { - Write-Error "Failed to Leave Domain" - } - if ($LeaveResult) { - if ($NoRestart) { - Write-Output "Removed computer($env:COMPUTERNAME) from domain and not restarting computer" - } - else { - Write-Output "Removed computer($env:COMPUTERNAME) from domain and restarting computer" - } - } - else { - Write-Output "Failed to remove computer($env:COMPUTERNAME) from domain" - # Clean up credentials so that they don't leak outside this script - $LeaveCred = $null - exit 1 - } - - # Clean up credentials so that they don't leak outside this script - $LeaveCred = $null - Write-Output "Completed Leave Domain" -} - -end { - - - -} - - - - +#Requires -Version 2.0 + +<# +.SYNOPSIS + Removes the computer from the domain. +.DESCRIPTION + Removes the computer from the domain. +.EXAMPLE + -UserName "MyDomainUser" -Password "Somepass1" + Removes the computer from the domain and restarts the computer. +.EXAMPLE + -UserName "MyDomainUser" -Password "Somepass1" -NoRestart + Removes the computer from the domain and does not restart the computer. +.EXAMPLE + PS C:\> Leave-Domain.ps1 -UserName "MyDomainUser" -Password "Somepass1" -NoRestart + Removes the computer from the domain and does not restart the computer. +.OUTPUTS + String[] +.NOTES + Minimum OS Architecture Supported: Windows 7, Windows Server 2012 + Release Notes: Gets passwords from secure custom fields +.COMPONENT + ManageUsers +#> + +[CmdletBinding()] +param ( + # Use a Domain UserName to remove this computer to a domain, this requires the Password parameter to be used as well + [Parameter()] + [String] + $UserName, + # Use a Domain Password to remove a computer from a domain + [Parameter()] + $Password, + # Do not restart computer after leaving to a domain + [Parameter()] + [Switch]$NoRestart = [System.Convert]::ToBoolean($env:noRestart) +) + +begin { + function Get-NinjaProperty { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [String]$Name, + [Parameter()] + [String]$Type, + [Parameter()] + [String]$DocumentName + ) + + # If we're requested to get the field value from a Ninja document we'll specify it here. + $DocumentationParams = @{} + if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } + + # These two types require more information to parse. + $NeedsOptions = "DropDown", "MultiSelect" + + # Grabbing document values requires a slightly different command. + if ($DocumentName) { + # Secure fields are only readable when they're a device custom field + if ($Type -Like "Secure") { throw [System.ArgumentOutOfRangeException]::New("$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality") } + + # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. + Write-Host "Retrieving value from Ninja Document..." + + # Certain fields require more information to parse. + if ($NeedsOptions -contains $Type) { + } + } + else { + # We'll redirect error output to the success stream to make it easier to error out if nothing was found or something else went wrong. + + # Certain fields require more information to parse. + if ($NeedsOptions -contains $Type) { + } + } + + # If we received some sort of error it should have an exception property and we'll exit the function with that error information. + if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } + if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } + + if (-not $NinjaPropertyValue) { + throw [System.NullReferenceException]::New("The Custom Field '$Name' is empty!") + } + + # This switch will compare the type given with the quoted string. If it matches, it'll parse it further; otherwise, the default option will be selected. + switch ($Type) { + "Attachment" { + # Attachments come in a JSON format this will convert it into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Checkbox" { + # Checkbox's come in as a string representing an integer. We'll need to cast that string into an integer and then convert it to a more traditional boolean. + [System.Convert]::ToBoolean([int]$NinjaPropertyValue) + } + "Date or Date Time" { + # In Ninja Date and Date/Time fields are in Unix Epoch time in the UTC timezone the below should convert it into local time as a DateTime object. + $UnixTimeStamp = $NinjaPropertyValue + $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp) + $TimeZone = [TimeZoneInfo]::Local + [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) + } + "Decimal" { + # In ninja decimals are strings that represent a decimal this will cast it into a double data type. + [double]$NinjaPropertyValue + } + "Device Dropdown" { + # Device Drop-Downs Fields come in a JSON format this will convert it into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Device MultiSelect" { + # Device Multi-Select Fields come in a JSON format this will convert it into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Dropdown" { + # Drop-Down custom fields come in as a comma-separated list of GUIDs; we'll compare these with all the options and return just the option values selected instead of a GUID. + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name + } + "Integer" { + # Casts the Ninja provided string into an integer. + [int]$NinjaPropertyValue + } + "MultiSelect" { + # Multi-Select custom fields come in as a comma-separated list of GUIDs we'll compare these with all the options and return just the option values selected instead of a guid. + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Selection = ($NinjaPropertyValue -split ',').trim() + + foreach ($Item in $Selection) { + $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name + } + } + "Organization Dropdown" { + # Turns the Ninja provided JSON into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization Location Dropdown" { + # Turns the Ninja provided JSON into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization Location MultiSelect" { + # Turns the Ninja provided JSON into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization MultiSelect" { + # Turns the Ninja provided JSON into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Time" { + # Time fields are given as a number of seconds starting from midnight. This will convert it into a DateTime object. + $Seconds = $NinjaPropertyValue + $UTC = ([TimeSpan]::FromSeconds($Seconds)).ToString("hh\:mm\:ss") + $TimeZone = [TimeZoneInfo]::Local + $ConvertedTime = [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) + + Get-Date $ConvertedTime -DisplayHint Time + } + default { + # If no type was given or not one that matches the above types just output what we retrieved. + $NinjaPropertyValue + } + } + } + + # Check if custom fields are used for the username and password + if ($env:domainUsername -and $env:domainUsername -notlike "null") { + $UserName = $env:domainUsername + } + + if ($env:domainPasswordWithCustomField -and $env:domainPasswordWithCustomField -notlike "null") { + try { + $Password = Get-NinjaProperty -Name $env:domainPasswordWithCustomField + if ([string]::IsNullOrWhiteSpace($Password)) { + Write-Host "[Warn] The Domain Password With Custom Field '$env:domainPasswordWithCustomField' is empty!" + throw + } + } + catch { + Write-Host "[Error] Failed to get password from secure custom field." + exit 1 + } + } + + # Check if usernames and passwords where provided + if (-not ($UserName) -or -not ($Password) ) { Write-Error "A domain username and password is required."; Exit 1 } + + Write-Output "Starting Leave Domain" + + # Converts username and password into a credential object + $LeaveCred = [PSCredential]::new($UserName, $(ConvertTo-SecureString -String $Password -AsPlainText -Force)) + +} + +process { + Write-Output "Removing computer($env:COMPUTERNAME) from domain" + $script:LeaveResult = $false + try { + $LeaveResult = if ($NoRestart) { + (Remove-Computer -UnjoinDomainCredential $LeaveCred -PassThru -Force -Confirm:$false).HasSucceeded + # Do not restart after leaving + } + else { + # Restart after leaving + (Remove-Computer -UnjoinDomainCredential $LeaveCred -PassThru -Force -Restart -Confirm:$false).HasSucceeded + } + } + catch { + Write-Error "Failed to Leave Domain" + } + if ($LeaveResult) { + if ($NoRestart) { + Write-Output "Removed computer($env:COMPUTERNAME) from domain and not restarting computer" + } + else { + Write-Output "Removed computer($env:COMPUTERNAME) from domain and restarting computer" + } + } + else { + Write-Output "Failed to remove computer($env:COMPUTERNAME) from domain" + # Clean up credentials so that they don't leak outside this script + $LeaveCred = $null + exit 1 + } + + # Clean up credentials so that they don't leak outside this script + $LeaveCred = $null + Write-Output "Completed Leave Domain" +} + +end { + +} + diff --git a/Powershell Scripts/Active Directory - Replication Health Report.ps1 b/Powershell Scripts/Active Directory - Replication Health Report.ps1 index 458bf62..35a2821 100644 --- a/Powershell Scripts/Active Directory - Replication Health Report.ps1 +++ b/Powershell Scripts/Active Directory - Replication Health Report.ps1 @@ -1,403 +1,314 @@ # This will get the current status of AD Replication and alert if its abnormal, as well as provide some diagnostic info. -#Requires -Version 5.1 - -<# -.SYNOPSIS - This will get the current status of AD Replication and alert if it's abnormal, as well as provide some diagnostic info. -.DESCRIPTION - This will get the current status of AD Replication and alert if it's abnormal, as well as provide some diagnostic info. - -.EXAMPLE - (No Parameters) - - WARNING: Replication has failed 100 or more times. See Diagnostic Info for more details - - ### Diagnostic Info ### - - Repadmin: running command /showrepl against full DC localhost - Default-First-Site-Name\SRV19-TEST - DSA Options: IS_GC - Site Options: (none) - DSA object GUID: ffe29454-2a68-4ba8-a877-d5a49b382d16 - DSA invocationID: ffe29454-2a68-4ba8-a877-d5a49b382d16 - -PARAMETER: -ErrorCount "99999999999999" - The number of errors until AD Replication is considered unhealthy. -.EXAMPLE - -ErrorCount "99999999999999" - - AD Replication appears to be healthy. Please check below to confirm. - - Destination DSA Last Success Time Failures Naming Context - --------------- ----------------- -------- -------------- - SRV19-TEST 2023-04-17 17:12:45 179 DC=test,DC=lan - SRV19-TEST 2023-04-17 16:51:45 21 CN=Configuration,DC=test,DC=lan - SRV19-TEST 2023-04-17 16:51:45 21 CN=Schema,CN=Configuration,DC=test,DC=lan - SRV19-TEST 2023-04-17 17:06:18 22 DC=DomainDnsZones,DC=test,DC=lan - SRV19-TEST 2023-04-17 17:06:15 22 DC=ForestDnsZones,DC=test,DC=lan - -PARAMETER: -EventLogStart "48" - Time in hours to search through event logs for possible issues. -.EXAMPLE - -EventLogStart "48" - - DsBindWithCred to localhost failed with status 5 - WARNING: Directory Service Log Event ID 1864 shows failure to replicate in > 1 week - - - TimeCreated Id LogName Level Message - ----------- -- ------- ----- ------- - 7/2/2028 8:34:33 AM 1864 Directory Service Error This is the replication status for the following directo... - 7/2/2028 8:34:33 AM 1864 Directory Service Error This is the replication status for the following directo... - -PARAMETER: -ErrorCustomField "ReplaceMeWithAnyIntegerCustomField" - Name of an integer custom field that contains your desired ErrorCount threshold. - ex. "AllowedADerrors" where you have entered in your desired ErrorCount limit in the "AllowedADerrors" custom field rather than in a parameter. -PARAMETER: -EventLogCustomField "ReplaceMeWithAnyIntegerCustomField" - Name of an integer custom field that contains your desired EventLogStart threshold. - ex. "ADeventsAgeLimit" where you have entered in your desired EventLogStart limit in the "ADeventsAgeLimit" custom field rather than in a parameter. -.EXAMPLE - -ErrorCustomField "ReplaceMeWithAnyIntegerCustomField" -EventLogCustomField "ReplaceMeWithAnyIntegerCustomField" - - DsBindWithCred to localhost failed with status 5 - WARNING: Directory Service Log Event ID 1864 shows failure to replicate in > 1 week - - TimeCreated Id LogName Level Message - ----------- -- ------- ----- ------- - 7/2/2028 8:34:33 AM 1864 Directory Service Error This is the replication status for the following directo... - 7/2/2028 8:34:33 AM 1864 Directory Service Error This is the replication status for the following directo... - -PARAMETER: -ExportCSV "ReplaceMeWithAnyMultiLineCustomField" - Name of a multi-line customfield you'd like to export the results to (in csv format). -.EXAMPLE - -ExportCSV "ReplaceMeWithAnyMultiLineCustomField" - - DsBindWithCred to localhost failed with status 5 - WARNING: Directory Service Log Event ID 1864 shows failure to replicate in > 1 week - - TimeCreated Id LogName Level Message - ----------- -- ------- ----- ------- - 7/2/2028 8:34:33 AM 1864 Directory Service Error This is the replication status for the following directo... - 7/2/2028 8:34:33 AM 1864 Directory Service Error This is the replication status for the following directo... - -PARAMETER: -ExportTXT "ReplaceMeWithAnyMultiLineCustomField" - Name of a multiline customfield you'd like to export the results to. -.EXAMPLE - -ExportTXT "ReplaceMeWithAnyMultiLineCustomField" - - DsBindWithCred to localhost failed with status 5 - WARNING: Directory Service Log Event ID 1864 shows failure to replicate in > 1 week - - - TimeCreated Id LogName Level Message - ----------- -- ------- ----- ------- - 7/2/2028 8:34:33 AM 1864 Directory Service Error This is the replication status for the following directo... - 7/2/2028 8:34:33 AM 1864 Directory Service Error This is the replication status for the following directo... -.OUTPUTS - -.NOTES - Minimum OS Architecture Supported: Server 2016+ - Release Notes: Renamed script and added Script Variable support -#> - -[CmdletBinding()] -param ( - [Parameter()] - [int]$EventLogStart = "24", - [Parameter()] - [int]$ErrorCount = "100", - [Parameter()] - [String]$EventLogCustomField, - [Parameter()] - [String]$ErrorCustomField, - [Parameter()] - [String]$ExportCSV, - [Parameter()] - [String]$ExportTXT -) -begin { - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - function Test-IsSystem { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - return $id.Name -like "NT AUTHORITY*" -or $id.IsSystem - } - - function Test-IsDomainController { - $OS = Get-CimInstance -ClassName Win32_OperatingSystem - - if ($OS.ProductType -eq "2") { - return $true - } - } - - if (!(Test-IsElevated) -and !(Test-IsSystem)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - if (!(Test-IsDomainController)) { - Write-Error "This is not a domain controller. Please run this script on a DC." - exit 1 - } - - # If script variables are used grab that and replace the static ones. - if ($env:hoursBackToSearchEventLog -and $env:hoursBackToSearchEventLog -notlike "null") { $EventLogStart = $env:hoursBackToSearchEventLog } - if ($env:errorCountToAlertOn -and $env:errorCountToAlertOn -notlike "null") { $ErrorCount = $env:errorCountToAlertOn } - if ($env:retrieveHoursBackFromCustomFieldNamed -and $env:retrieveHoursBackFromCustomFieldNamed -notlike "null") { $EventLogCustomField = $env:retrieveHoursBackFromCustomFieldNamed } - if ($env:retrieveErrorCountFromCustomFieldNamed -and $env:retrieveErrorCountFromCustomFieldNamed -notlike "null") { $ErrorCustomField = $env:retrieveErrorCountFromCustomFieldNamed } - if ($env:exportCsvResultsToThisCustomField -and $env:exportCsvResultsToThisCustomField -notlike "null") { $ExportCSV = $env:exportCsvResultsToThisCustomField } - if ($env:exportTextResultsToThisCustomField -and $env:exportTextResultsToThisCustomField -notlike "null") { $ExportTXT = $env:exportTextResultsToThisCustomField } - - # This function is to make it easier to set Ninja Custom Fields. - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields we can set. If no type is given we'll assume the input doesn't have to be changed in any way. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The below field requires additional information in order to set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If we received some sort of error it should have an exception property and we'll exit the function with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli is expecting the time to be representing as a Unix Epoch string. So we'll convert what we were given into that format. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw "Value is not present in dropdown" - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - - # Shortened Version from "Example - Get Ninja Property" - function Get-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter()] - [String]$DocumentName - ) - - # If we're requested to get the field value from a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # These two types require more information to parse. - $NeedsOptions = "DropDown", "MultiSelect" - - # Grabbing document values requires a slightly different command. - if ($DocumentName) { - # Secure fields are only readable when they're a device custom field - if ($Type -Like "Secure") { throw "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - Write-Host "Retrieving value from Ninja Document..." - $NinjaPropertyValue = Ninja-Property-Docs-Get -AttributeName $Name @DocumentationParams 2>&1 - - # Certain fields require more information to parse. - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - # We'll redirect error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1 - - # Certain fields require more information to parse. - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If we received some sort of error it should have an exception property and we'll exit the function with that error information. - if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # This switch will compare the type given with the quoted string. If it matches, it'll parse it further; otherwise, the default option will be selected. - switch ($Type) { - "Integer" { - # Cast's the Ninja provided string into an integer. - if (-not $NinjaPropertyValue) { - throw "CustomField $Name is empty!" - } - [int]$NinjaPropertyValue - } - default { - # If no type was given or not one that matches the above types just output what we retrieved. - $NinjaPropertyValue - } - } - } - - $ExitCode = 0 -}process { - - # Grabbing the information from custom fields (if any) - if ($ErrorCustomField) { - try { - $FieldCount = Get-NinjaProperty -Name $ErrorCustomField -Type "Integer" - if ($FieldCount) { $ErrorCount = $FieldCount } - } - catch { - Write-Error -Message $_.ToString() -Category InvalidOperation -Exception (New-Object System.Exception) - exit 1 - } - } - if ($EventLogCustomField) { - try { - $FieldStart = Get-NinjaProperty -Name $EventLogCustomField -Type "Integer" - if ($FieldStart) { $EventLogStart = $FieldStart } - } - catch { - Write-Error -Message $_.ToString() -Category InvalidOperation -Exception (New-Object System.Exception) - exit 1 - } - } - - $represult = (repadmin.exe /showrepl /csv | ConvertFrom-Csv) - - if ($ExportCSV) { - try { - Set-NinjaProperty -Name $ExportCSV -Value (repadmin.exe /showrepl /csv) - } - catch { - Write-Error -Message $_.ToString() -Category InvalidOperation -Exception (New-Object System.Exception) - $ExitCode = 1 - } - } - - if ($ExportTXT) { - $String = $represult | Format-Table -Property "Destination DSA", "Last Success Time", "Last Failure Status", "Number of Failures", "Naming Context" | Out-String - try { - Set-NinjaProperty -Name $ExportTXT -Value $String - } - catch { - Write-Error -Message $_.ToString() -Category InvalidOperation -Exception (New-Object System.Exception) - $ExitCode = 1 - } - } - - if ($represult."Number of Failures" -ge $ErrorCount) { - Write-Warning "Replication has failed $ErrorCount or more times. See Diagnostic Info for more details" - - # The Table version is a bit more to the point but the description gives you more of an idea of what's going wrong than in the non-table version. - Write-Host '### Diagnostic Info ###' - repadmin.exe /showrepl /errorsonly - $represult | Format-Table -Property "Destination DSA", "Last Success Time", @{Name = "Failures"; Expression = { $_."Number of Failures" } }, "Naming Context" | Out-String | Write-Host - - Exit 1 - } - else { - Write-Host "No errors found in repadmin /showrepl /csv" - } - - # Check Event Log for replication failure - $Date = (Get-Date).AddHours(-$EventLogStart) - - $Events = Get-WinEvent -FilterHashtable @{LogName = "Directory Service"; Id = 1864; StartTime = $Date } -ErrorAction SilentlyContinue | - Where-Object { ($_.Message -replace "`r`n", " ") -match "More than a week: [1-9]+.*" } - - if ($Events) { - Write-Warning "Directory Service Log Event ID 1864 shows failure to replicate in > 1 week" - $Events | Format-Table -Property TimeCreated, Id, LogName, @{Name = "Level"; Expression = { $_.LevelDisplayName } }, Message -AutoSize | Out-String | Write-Host - - Exit 1 - } - else { - Write-Host "No bad event viewer events found since $Date." - } - - # Check if Sysvol is present - $sysvol = (Get-CimInstance Win32_Share) | Where-Object { $_.name -eq "SYSVOL" } - if (!($sysvol.Path)) { - Write-Warning "SYSVOL is Missing!" - Get-CimInstance Win32_Share | Out-String | Write-Host - - Exit 1 - } - else { - Write-Host "SYSVOL appears to be present." - } - - Write-Host "AD Replication appears to be healthy. Please check script output and other sources to confirm." - $Report = $represult | Format-Table -Property "Destination DSA", "Last Success Time", @{Name = "Failures"; Expression = { $_."Number of Failures" } }, "Naming Context" | Out-String - - if ($Report) { - $Report | Write-Host - } - - exit $ExitCode - -}end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + This will get the current status of AD Replication and alert if it's abnormal, as well as provide some diagnostic info. +.DESCRIPTION + This will get the current status of AD Replication and alert if it's abnormal, as well as provide some diagnostic info. + +.EXAMPLE + (No Parameters) + + WARNING: Replication has failed 100 or more times. See Diagnostic Info for more details + + ### Diagnostic Info ### + + Repadmin: running command /showrepl against full DC localhost + Default-First-Site-Name\SRV19-TEST + DSA Options: IS_GC + Site Options: (none) + DSA object GUID: ffe29454-2a68-4ba8-a877-d5a49b382d16 + DSA invocationID: ffe29454-2a68-4ba8-a877-d5a49b382d16 + +PARAMETER: -ErrorCount "99999999999999" + The number of errors until AD Replication is considered unhealthy. +.EXAMPLE + -ErrorCount "99999999999999" + + AD Replication appears to be healthy. Please check below to confirm. + + Destination DSA Last Success Time Failures Naming Context + --------------- ----------------- -------- -------------- + SRV19-TEST 2023-04-17 17:12:45 179 DC=test,DC=lan + SRV19-TEST 2023-04-17 16:51:45 21 CN=Configuration,DC=test,DC=lan + SRV19-TEST 2023-04-17 16:51:45 21 CN=Schema,CN=Configuration,DC=test,DC=lan + SRV19-TEST 2023-04-17 17:06:18 22 DC=DomainDnsZones,DC=test,DC=lan + SRV19-TEST 2023-04-17 17:06:15 22 DC=ForestDnsZones,DC=test,DC=lan + +PARAMETER: -EventLogStart "48" + Time in hours to search through event logs for possible issues. +.EXAMPLE + -EventLogStart "48" + + DsBindWithCred to localhost failed with status 5 + WARNING: Directory Service Log Event ID 1864 shows failure to replicate in > 1 week + + TimeCreated Id LogName Level Message + ----------- -- ------- ----- ------- + 7/2/2028 8:34:33 AM 1864 Directory Service Error This is the replication status for the following directo... + 7/2/2028 8:34:33 AM 1864 Directory Service Error This is the replication status for the following directo... + +PARAMETER: -ErrorCustomField "ReplaceMeWithAnyIntegerCustomField" + Name of an integer custom field that contains your desired ErrorCount threshold. + ex. "AllowedADerrors" where you have entered in your desired ErrorCount limit in the "AllowedADerrors" custom field rather than in a parameter. +PARAMETER: -EventLogCustomField "ReplaceMeWithAnyIntegerCustomField" + Name of an integer custom field that contains your desired EventLogStart threshold. + ex. "ADeventsAgeLimit" where you have entered in your desired EventLogStart limit in the "ADeventsAgeLimit" custom field rather than in a parameter. +.EXAMPLE + -ErrorCustomField "ReplaceMeWithAnyIntegerCustomField" -EventLogCustomField "ReplaceMeWithAnyIntegerCustomField" + + DsBindWithCred to localhost failed with status 5 + WARNING: Directory Service Log Event ID 1864 shows failure to replicate in > 1 week + + TimeCreated Id LogName Level Message + ----------- -- ------- ----- ------- + 7/2/2028 8:34:33 AM 1864 Directory Service Error This is the replication status for the following directo... + 7/2/2028 8:34:33 AM 1864 Directory Service Error This is the replication status for the following directo... + +PARAMETER: -ExportCSV "ReplaceMeWithAnyMultiLineCustomField" + Name of a multi-line customfield you'd like to export the results to (in csv format). +.EXAMPLE + -ExportCSV "ReplaceMeWithAnyMultiLineCustomField" + + DsBindWithCred to localhost failed with status 5 + WARNING: Directory Service Log Event ID 1864 shows failure to replicate in > 1 week + + TimeCreated Id LogName Level Message + ----------- -- ------- ----- ------- + 7/2/2028 8:34:33 AM 1864 Directory Service Error This is the replication status for the following directo... + 7/2/2028 8:34:33 AM 1864 Directory Service Error This is the replication status for the following directo... + +PARAMETER: -ExportTXT "ReplaceMeWithAnyMultiLineCustomField" + Name of a multiline customfield you'd like to export the results to. +.EXAMPLE + -ExportTXT "ReplaceMeWithAnyMultiLineCustomField" + + DsBindWithCred to localhost failed with status 5 + WARNING: Directory Service Log Event ID 1864 shows failure to replicate in > 1 week + + TimeCreated Id LogName Level Message + ----------- -- ------- ----- ------- + 7/2/2028 8:34:33 AM 1864 Directory Service Error This is the replication status for the following directo... + 7/2/2028 8:34:33 AM 1864 Directory Service Error This is the replication status for the following directo... +.OUTPUTS + +.NOTES + Minimum OS Architecture Supported: Server 2016+ + Release Notes: Renamed script and added Script Variable support +#> + +[CmdletBinding()] +param ( + [Parameter()] + [int]$EventLogStart = "24", + [Parameter()] + [int]$ErrorCount = "100", + [Parameter()] + [String]$EventLogCustomField, + [Parameter()] + [String]$ErrorCustomField, + [Parameter()] + [String]$ExportCSV, + [Parameter()] + [String]$ExportTXT +) +begin { + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + function Test-IsSystem { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + return $id.Name -like "NT AUTHORITY*" -or $id.IsSystem + } + + function Test-IsDomainController { + $OS = Get-CimInstance -ClassName Win32_OperatingSystem + + if ($OS.ProductType -eq "2") { + return $true + } + } + + if (!(Test-IsElevated) -and !(Test-IsSystem)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + if (!(Test-IsDomainController)) { + Write-Error "This is not a domain controller. Please run this script on a DC." + exit 1 + } + + # If script variables are used grab that and replace the static ones. + if ($env:hoursBackToSearchEventLog -and $env:hoursBackToSearchEventLog -notlike "null") { $EventLogStart = $env:hoursBackToSearchEventLog } + if ($env:errorCountToAlertOn -and $env:errorCountToAlertOn -notlike "null") { $ErrorCount = $env:errorCountToAlertOn } + if ($env:retrieveHoursBackFromCustomFieldNamed -and $env:retrieveHoursBackFromCustomFieldNamed -notlike "null") { $EventLogCustomField = $env:retrieveHoursBackFromCustomFieldNamed } + if ($env:retrieveErrorCountFromCustomFieldNamed -and $env:retrieveErrorCountFromCustomFieldNamed -notlike "null") { $ErrorCustomField = $env:retrieveErrorCountFromCustomFieldNamed } + if ($env:exportCsvResultsToThisCustomField -and $env:exportCsvResultsToThisCustomField -notlike "null") { $ExportCSV = $env:exportCsvResultsToThisCustomField } + if ($env:exportTextResultsToThisCustomField -and $env:exportTextResultsToThisCustomField -notlike "null") { $ExportTXT = $env:exportTextResultsToThisCustomField } + + # This function is to make it easier to set Ninja Custom Fields. + + # Shortened Version from "Example - Get Ninja Property" + function Get-NinjaProperty { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [String]$Name, + [Parameter()] + [String]$Type, + [Parameter()] + [String]$DocumentName + ) + + # If we're requested to get the field value from a Ninja document we'll specify it here. + $DocumentationParams = @{} + if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } + + # These two types require more information to parse. + $NeedsOptions = "DropDown", "MultiSelect" + + # Grabbing document values requires a slightly different command. + if ($DocumentName) { + # Secure fields are only readable when they're a device custom field + if ($Type -Like "Secure") { throw "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } + + # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. + Write-Host "Retrieving value from Ninja Document..." + + # Certain fields require more information to parse. + if ($NeedsOptions -contains $Type) { + } + } + else { + # We'll redirect error output to the success stream to make it easier to error out if nothing was found or something else went wrong. + + # Certain fields require more information to parse. + if ($NeedsOptions -contains $Type) { + } + } + + # If we received some sort of error it should have an exception property and we'll exit the function with that error information. + if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } + if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } + + # This switch will compare the type given with the quoted string. If it matches, it'll parse it further; otherwise, the default option will be selected. + switch ($Type) { + "Integer" { + # Cast's the Ninja provided string into an integer. + if (-not $NinjaPropertyValue) { + throw "CustomField $Name is empty!" + } + [int]$NinjaPropertyValue + } + default { + # If no type was given or not one that matches the above types just output what we retrieved. + $NinjaPropertyValue + } + } + } + + $ExitCode = 0 +}process { + + # Grabbing the information from custom fields (if any) + if ($ErrorCustomField) { + try { + $FieldCount = Get-NinjaProperty -Name $ErrorCustomField -Type "Integer" + if ($FieldCount) { $ErrorCount = $FieldCount } + } + catch { + Write-Error -Message $_.ToString() -Category InvalidOperation -Exception (New-Object System.Exception) + exit 1 + } + } + if ($EventLogCustomField) { + try { + $FieldStart = Get-NinjaProperty -Name $EventLogCustomField -Type "Integer" + if ($FieldStart) { $EventLogStart = $FieldStart } + } + catch { + Write-Error -Message $_.ToString() -Category InvalidOperation -Exception (New-Object System.Exception) + exit 1 + } + } + + $represult = (repadmin.exe /showrepl /csv | ConvertFrom-Csv) + + if ($ExportCSV) { + try { + } + catch { + Write-Error -Message $_.ToString() -Category InvalidOperation -Exception (New-Object System.Exception) + $ExitCode = 1 + } + } + + if ($ExportTXT) { + $String = $represult | Format-Table -Property "Destination DSA", "Last Success Time", "Last Failure Status", "Number of Failures", "Naming Context" | Out-String + try { + } + catch { + Write-Error -Message $_.ToString() -Category InvalidOperation -Exception (New-Object System.Exception) + $ExitCode = 1 + } + } + + if ($represult."Number of Failures" -ge $ErrorCount) { + Write-Warning "Replication has failed $ErrorCount or more times. See Diagnostic Info for more details" + + # The Table version is a bit more to the point but the description gives you more of an idea of what's going wrong than in the non-table version. + Write-Host '### Diagnostic Info ###' + repadmin.exe /showrepl /errorsonly + $represult | Format-Table -Property "Destination DSA", "Last Success Time", @{Name = "Failures"; Expression = { $_."Number of Failures" } }, "Naming Context" | Out-String | Write-Host + + Exit 1 + } + else { + Write-Host "No errors found in repadmin /showrepl /csv" + } + + # Check Event Log for replication failure + $Date = (Get-Date).AddHours(-$EventLogStart) + + $Events = Get-WinEvent -FilterHashtable @{LogName = "Directory Service"; Id = 1864; StartTime = $Date } -ErrorAction SilentlyContinue | + Where-Object { ($_.Message -replace "`r`n", " ") -match "More than a week: [1-9]+.*" } + + if ($Events) { + Write-Warning "Directory Service Log Event ID 1864 shows failure to replicate in > 1 week" + $Events | Format-Table -Property TimeCreated, Id, LogName, @{Name = "Level"; Expression = { $_.LevelDisplayName } }, Message -AutoSize | Out-String | Write-Host + + Exit 1 + } + else { + Write-Host "No bad event viewer events found since $Date." + } + + # Check if Sysvol is present + $sysvol = (Get-CimInstance Win32_Share) | Where-Object { $_.name -eq "SYSVOL" } + if (!($sysvol.Path)) { + Write-Warning "SYSVOL is Missing!" + Get-CimInstance Win32_Share | Out-String | Write-Host + + Exit 1 + } + else { + Write-Host "SYSVOL appears to be present." + } + + Write-Host "AD Replication appears to be healthy. Please check script output and other sources to confirm." + $Report = $represult | Format-Table -Property "Destination DSA", "Last Success Time", @{Name = "Failures"; Expression = { $_."Number of Failures" } }, "Naming Context" | Out-String + + if ($Report) { + $Report | Write-Host + } + + exit $ExitCode + +}end { + +} + diff --git a/Powershell Scripts/Active Power Plan Report.ps1 b/Powershell Scripts/Active Power Plan Report.ps1 index f54c8d2..a101896 100644 --- a/Powershell Scripts/Active Power Plan Report.ps1 +++ b/Powershell Scripts/Active Power Plan Report.ps1 @@ -1,245 +1,243 @@ -# Reports the active power plan and active power settings. Outputs to activity log and optionally to structured output by default. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Reports the active power plan and active power settings. Outputs to activity log and optionally to structured output by default. -.DESCRIPTION - Reports the active power plan and active power settings. Outputs to activity log and optionally to structured output by default. -.EXAMPLE - (No Parameters) - - Active Power Plan: Balanced - - ### Current Power Settings For Balanced ### - - Name When Plugged In When On Battery Units - ---- --------------- --------------- ----- - Allow hybrid sleep Off Off N/A - Allow wake timers Important Wake Timers Only Disable N/A - Critical battery action Do nothing Do nothing N/A - Critical battery level 5 5 % - Critical battery notification On On N/A - Dimmed display brightness 50 50 % - Display brightness 100 40 % - Enable adaptive brightness Off Off N/A - Hibernate after 10800 10800 Seconds - JavaScript Timer Frequency Maximum Performance Maximum Power Savings N/A - Link State Power Management Maximum power savings Maximum power savings N/A - Low battery action Do nothing Do nothing N/A - Low battery level 10 10 % - Low battery notification On On N/A - Maximum processor state 100 100 % - Minimum processor state 5 5 % - Power Saving Mode Maximum Performance Medium Power Saving N/A - Reserve battery level 7 7 % - Sleep after 1800 900 Seconds - Slide show Available Paused N/A - Start menu power button Sleep Sleep N/A - System cooling policy Active Passive N/A - Turn off display after 600 300 Seconds - Turn off hard disk after 1200 600 Seconds - USB selective suspend setting Enabled Enabled N/A - Video playback quality bias Video playback performance bias Video playback power-saving bias N/A - When playing video Optimize video quality Balanced N/A - When sharing media Prevent idling to sleep Allow the computer to sleep N/A - -PARAMETER: -PowerPlanCustomFieldName "ReplaceMeWithAnyMultilineCustomField" - Replace the quoted text with any output identifier you'd like the script to write the active power plan to. -PARAMETER: -PowerSettingsCustomFieldName "ReplaceMeWithAnyTextCustomField" - Replace the quoted text with any output identifier you'd like the script to write the active power settings to. -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Renamed script and added Script Variable support -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$PowerPlanCustomFieldName = "activePowerPlan", - [Parameter()] - [String]$PowerSettingsCustomFieldName = "activePowerSettings" -) - -begin { - # No environment variable processing needed for cross-platform compatibility - - # Script will fail if not elevated (some setting values are hidden to non-admins) - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - # Get's the active power plan - function Get-PowerPlan { - [CmdletBinding()] - param( - [Parameter()] - [Switch]$Active, - [Parameter()] - [String]$Name - ) - if ($Active) { - $PowerPlan = powercfg.exe /getactivescheme - $PowerPlan = ($PowerPlan -replace "Power Scheme GUID:" -split "(?=\S{8}-\S{4}-\S{4}-\S{4}-\S{12})" -split '\(' -replace '\)') | Where-Object { $_ -ne " " } - $PowerPlan = @( - [PSCustomObject]@{ - Name = $($PowerPlan | Where-Object { $_ -notmatch "\S{8}-\S{4}-\S{4}-\S{4}-\S{12}" }) - GUID = $($PowerPlan | Where-Object { $_ -match "\S{8}-\S{4}-\S{4}-\S{4}-\S{12}" }) - } - ) - } - else { - $PowerPlan = powercfg.exe /L - $PowerPlan = $PowerPlan -replace '\s{2,}', ',' -replace ' \*', ',True' -replace "Existing Power Schemes \(\* Active\)", "GUID,Name,Active" -replace "-{2,}" -replace "Power Scheme GUID: " -replace '\(' -replace '\)' | Where-Object { $_ } | ConvertFrom-Csv - } - - if ($Name) { - $PowerPlan | Where-Object { $_.Name -like $Name } - } - else { - $PowerPlan - } - } - - # Gets all the powersettings for the current plan - function Get-PowerSettings { - [CmdletBinding()] - param() - process { - - # Grabs all the powersetting subroups first as that's require info to grab the actual setting values - $PowerSubgroups = powercfg.exe /Q | Select-String "Subgroup GUID:" - $PowerSubgroups = ($PowerSubgroups -replace "Subgroup GUID:" -replace '\(' -replace '\)').trim() | ForEach-Object { - @( - [PSCustomObject]@{ - SubName = $($_ -split "\s{2,}" | Where-Object { $_ -notmatch "(\S{8}-\S{4}-\S{4}-\S{4}-\S{12})" }) - SubGUID = $($_ -split "\s{2,}" | Where-Object { $_ -match "(\S{8}-\S{4}-\S{4}-\S{4}-\S{12})" }) - } - ) - } - - # From each subgroup we'll get a list of every power setting - $PowerSettings = ForEach ($Subgroup in $PowerSubgroups) { - $Settings = powercfg.exe /Q SCHEME_CURRENT $Subgroup.SubGUID | Select-String "Power Setting GUID:" - ($Settings -replace "Power Setting GUID:" -replace '\(' -replace '\)').trim() | ForEach-Object { - @( - [PSCustomObject]@{ - Name = $($_ -split "\s{2,}" | Where-Object { $_ -notmatch "(\S{8}-\S{4}-\S{4}-\S{4}-\S{12})" }) - GUID = $($_ -split "\s{2,}" | Where-Object { $_ -match "(\S{8}-\S{4}-\S{4}-\S{4}-\S{12})" }) - SubName = $Subgroup.SubName - SubGUID = $Subgroup.SubGUID - } - ) - } - } - - # Finally we'll parse out the actual power setting values based on the previously retrieved subgroup guid and setting guid - ForEach ($PowerSetting in $PowerSettings) { - # Windows has a different value/setting for both plugged in (AC) and battery (DC) - $ACValue = powercfg.exe /Q SCHEME_CURRENT $PowerSetting.SubGUID $PowerSetting.GUID | Select-String "Current AC Power Setting Index:" - $ACValue = ($ACValue -replace "Current AC Power Setting Index:" -replace '\(' -replace '\)').trim() - - $DCValue = powercfg.exe /Q SCHEME_CURRENT $PowerSetting.SubGUID $PowerSetting.GUID | Select-String "Current DC Power Setting Index:" - $DCValue = ($DCValue -replace "Current DC Power Setting Index:" -replace '\(' -replace '\)').trim() - - # The values are always in hex so we'll need to convert them into integers to make them easier to understand - $ACValue = [int32]$ACValue - $DCValue = [int32]$DCValue - - # Some settings correspond to an action rather than a certain percentage level or a number of seconds. These cases always have - # the pharse "Possible Setting Friendly Name" - $FriendlyName = powercfg.exe /Q SCHEME_CURRENT $PowerSetting.SubGUID $PowerSetting.GUID | Select-String "Possible Setting Friendly Name:" - if ($FriendlyName) { - # Since the friendly name, index and current value are stored seperately we'll have to parse them out individually - $Indexs = powercfg.exe /Q SCHEME_CURRENT $PowerSetting.SubGUID $PowerSetting.GUID | Select-String "Possible Setting Index:" - $Indexs = $Indexs | ForEach-Object { ($_ -replace "Possible Setting Index:").trim() } - - $FriendlyNames = powercfg.exe /Q SCHEME_CURRENT $PowerSetting.SubGUID $PowerSetting.GUID | Select-String "Possible Setting Friendly Name:" - $FriendlyNames = $FriendlyNames | ForEach-Object { ($_ -replace "Possible Setting Friendly Name:").trim() } - - # Once parsed the FriendlyNames and their index's should be the same number and one is always followed by the other. - # So to combine them we just need to loop through them in order, this'll give us a more Powershell friendly object - $FriendlyOptions = For ($i = 0; $i -lt $FriendlyNames.Count; $i++) { - [PSCustomObject]@{ - Name = $FriendlyNames[$i] - Index = $Indexs[$i] - } - } - - # Now that we have the object figuring out which action is active and what it does is a piece of cake. - # Though we'll have to convert the index to an integer to make everything match up easy. - $ACValue = $FriendlyOptions | Where-Object { [int32]$_.Index -eq $ACValue } | Select-Object Name -ExpandProperty Name - $DCValue = $FriendlyOptions | Where-Object { [int32]$_.Index -eq $DCValue } | Select-Object Name -ExpandProperty Name - - # There's no units to accompany these actions - $Units = "N/A" - } - else { - # Everything else is either a percent or a number of seconds we'll save that for later - $Units = powercfg.exe /Q SCHEME_CURRENT $PowerSetting.SubGUID $PowerSetting.GUID | Select-String "Possible Settings units:" - $Units = ($Units -replace "Possible Settings units:" -replace '\(' -replace '\)').trim() - } - - # Lastly we format our findings into a nice firendly PowerShell Object - [PSCustomObject]@{ - Name = $PowerSetting.Name - GUID = $PowerSetting.GUID - "When Plugged In" = $ACValue - "When On Battery" = $DCValue - Units = $Units - SubName = $PowerSetting.SubName - SubGUID = $PowerSetting.SubGUID - } - } - } - } -} -process { - # If not elevated exit the script. - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Retrieve's the Active Power Plan to be used for the report - $ActivePowerPlan = Get-PowerPlan -Active | Select-Object Name -ExpandProperty Name - - # If somehow we came up empty we should error out - if (-not $ActivePowerPlan) { - Write-Error "[Error] Unable to retrieve power plan!" - exit 1 - } - - # Retrieve's the current settings and formats them into a nice table. Organized by name for easy viewing. - $CurrentPowerSettings = Get-PowerSettings | Sort-Object Name | Format-Table -Property Name, 'When Plugged In', 'When On Battery', Units -AutoSize | Out-String - if (-not $CurrentPowerSettings) { - Write-Error "[Error] Unable to retrieve power settings!" - exit 1 - } - - # Constructing the actual report - $Report = New-Object System.Collections.Generic.List[string] - $Report.Add("Active Power Plan: $ActivePowerPlan") - $Report.Add("`n`n### Current Power Settings For $ActivePowerPlan ###") - $Report.Add("`n$CurrentPowerSettings") - - # Write to the activity log - Write-Host $Report # Output results to be captured by automation platforms or saved to files if needed - if ($PowerPlanCustomFieldName) { - Write-Output "PowerPlan:$ActivePowerPlan" - } - - if ($PowerSettingsCustomFieldName) { - Write-Output "PowerSettings:$CurrentPowerSettings" - } -} -end { - - - -} - +# Reports the active power plan and active power settings. Outputs to activity log and optionally to structured output by default. +#Requires -Version 5.1 + +<# +.SYNOPSIS + Reports the active power plan and active power settings. Outputs to activity log and optionally to structured output by default. +.DESCRIPTION + Reports the active power plan and active power settings. Outputs to activity log and optionally to structured output by default. +.EXAMPLE + (No Parameters) + + Active Power Plan: Balanced + + ### Current Power Settings For Balanced ### + + Name When Plugged In When On Battery Units + ---- --------------- --------------- ----- + Allow hybrid sleep Off Off N/A + Allow wake timers Important Wake Timers Only Disable N/A + Critical battery action Do nothing Do nothing N/A + Critical battery level 5 5 % + Critical battery notification On On N/A + Dimmed display brightness 50 50 % + Display brightness 100 40 % + Enable adaptive brightness Off Off N/A + Hibernate after 10800 10800 Seconds + JavaScript Timer Frequency Maximum Performance Maximum Power Savings N/A + Link State Power Management Maximum power savings Maximum power savings N/A + Low battery action Do nothing Do nothing N/A + Low battery level 10 10 % + Low battery notification On On N/A + Maximum processor state 100 100 % + Minimum processor state 5 5 % + Power Saving Mode Maximum Performance Medium Power Saving N/A + Reserve battery level 7 7 % + Sleep after 1800 900 Seconds + Slide show Available Paused N/A + Start menu power button Sleep Sleep N/A + System cooling policy Active Passive N/A + Turn off display after 600 300 Seconds + Turn off hard disk after 1200 600 Seconds + USB selective suspend setting Enabled Enabled N/A + Video playback quality bias Video playback performance bias Video playback power-saving bias N/A + When playing video Optimize video quality Balanced N/A + When sharing media Prevent idling to sleep Allow the computer to sleep N/A + +PARAMETER: -PowerPlanCustomFieldName "ReplaceMeWithAnyMultilineCustomField" + Replace the quoted text with any output identifier you'd like the script to write the active power plan to. +PARAMETER: -PowerSettingsCustomFieldName "ReplaceMeWithAnyTextCustomField" + Replace the quoted text with any output identifier you'd like the script to write the active power settings to. +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Renamed script and added Script Variable support +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$PowerPlanCustomFieldName = "activePowerPlan", + [Parameter()] + [String]$PowerSettingsCustomFieldName = "activePowerSettings" +) + +begin { + # No environment variable processing needed for cross-platform compatibility + + # Script will fail if not elevated (some setting values are hidden to non-admins) + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + # Get's the active power plan + function Get-PowerPlan { + [CmdletBinding()] + param( + [Parameter()] + [Switch]$Active, + [Parameter()] + [String]$Name + ) + if ($Active) { + $PowerPlan = powercfg.exe /getactivescheme + $PowerPlan = ($PowerPlan -replace "Power Scheme GUID:" -split "(?=\S{8}-\S{4}-\S{4}-\S{4}-\S{12})" -split '\(' -replace '\)') | Where-Object { $_ -ne " " } + $PowerPlan = @( + [PSCustomObject]@{ + Name = $($PowerPlan | Where-Object { $_ -notmatch "\S{8}-\S{4}-\S{4}-\S{4}-\S{12}" }) + GUID = $($PowerPlan | Where-Object { $_ -match "\S{8}-\S{4}-\S{4}-\S{4}-\S{12}" }) + } + ) + } + else { + $PowerPlan = powercfg.exe /L + $PowerPlan = $PowerPlan -replace '\s{2,}', ',' -replace ' \*', ',True' -replace "Existing Power Schemes \(\* Active\)", "GUID,Name,Active" -replace "-{2,}" -replace "Power Scheme GUID: " -replace '\(' -replace '\)' | Where-Object { $_ } | ConvertFrom-Csv + } + + if ($Name) { + $PowerPlan | Where-Object { $_.Name -like $Name } + } + else { + $PowerPlan + } + } + + # Gets all the powersettings for the current plan + function Get-PowerSettings { + [CmdletBinding()] + param() + process { + + # Grabs all the powersetting subroups first as that's require info to grab the actual setting values + $PowerSubgroups = powercfg.exe /Q | Select-String "Subgroup GUID:" + $PowerSubgroups = ($PowerSubgroups -replace "Subgroup GUID:" -replace '\(' -replace '\)').trim() | ForEach-Object { + @( + [PSCustomObject]@{ + SubName = $($_ -split "\s{2,}" | Where-Object { $_ -notmatch "(\S{8}-\S{4}-\S{4}-\S{4}-\S{12})" }) + SubGUID = $($_ -split "\s{2,}" | Where-Object { $_ -match "(\S{8}-\S{4}-\S{4}-\S{4}-\S{12})" }) + } + ) + } + + # From each subgroup we'll get a list of every power setting + $PowerSettings = ForEach ($Subgroup in $PowerSubgroups) { + $Settings = powercfg.exe /Q SCHEME_CURRENT $Subgroup.SubGUID | Select-String "Power Setting GUID:" + ($Settings -replace "Power Setting GUID:" -replace '\(' -replace '\)').trim() | ForEach-Object { + @( + [PSCustomObject]@{ + Name = $($_ -split "\s{2,}" | Where-Object { $_ -notmatch "(\S{8}-\S{4}-\S{4}-\S{4}-\S{12})" }) + GUID = $($_ -split "\s{2,}" | Where-Object { $_ -match "(\S{8}-\S{4}-\S{4}-\S{4}-\S{12})" }) + SubName = $Subgroup.SubName + SubGUID = $Subgroup.SubGUID + } + ) + } + } + + # Finally we'll parse out the actual power setting values based on the previously retrieved subgroup guid and setting guid + ForEach ($PowerSetting in $PowerSettings) { + # Windows has a different value/setting for both plugged in (AC) and battery (DC) + $ACValue = powercfg.exe /Q SCHEME_CURRENT $PowerSetting.SubGUID $PowerSetting.GUID | Select-String "Current AC Power Setting Index:" + $ACValue = ($ACValue -replace "Current AC Power Setting Index:" -replace '\(' -replace '\)').trim() + + $DCValue = powercfg.exe /Q SCHEME_CURRENT $PowerSetting.SubGUID $PowerSetting.GUID | Select-String "Current DC Power Setting Index:" + $DCValue = ($DCValue -replace "Current DC Power Setting Index:" -replace '\(' -replace '\)').trim() + + # The values are always in hex so we'll need to convert them into integers to make them easier to understand + $ACValue = [int32]$ACValue + $DCValue = [int32]$DCValue + + # Some settings correspond to an action rather than a certain percentage level or a number of seconds. These cases always have + # the pharse "Possible Setting Friendly Name" + $FriendlyName = powercfg.exe /Q SCHEME_CURRENT $PowerSetting.SubGUID $PowerSetting.GUID | Select-String "Possible Setting Friendly Name:" + if ($FriendlyName) { + # Since the friendly name, index and current value are stored seperately we'll have to parse them out individually + $Indexs = powercfg.exe /Q SCHEME_CURRENT $PowerSetting.SubGUID $PowerSetting.GUID | Select-String "Possible Setting Index:" + $Indexs = $Indexs | ForEach-Object { ($_ -replace "Possible Setting Index:").trim() } + + $FriendlyNames = powercfg.exe /Q SCHEME_CURRENT $PowerSetting.SubGUID $PowerSetting.GUID | Select-String "Possible Setting Friendly Name:" + $FriendlyNames = $FriendlyNames | ForEach-Object { ($_ -replace "Possible Setting Friendly Name:").trim() } + + # Once parsed the FriendlyNames and their index's should be the same number and one is always followed by the other. + # So to combine them we just need to loop through them in order, this'll give us a more Powershell friendly object + $FriendlyOptions = For ($i = 0; $i -lt $FriendlyNames.Count; $i++) { + [PSCustomObject]@{ + Name = $FriendlyNames[$i] + Index = $Indexs[$i] + } + } + + # Now that we have the object figuring out which action is active and what it does is a piece of cake. + # Though we'll have to convert the index to an integer to make everything match up easy. + $ACValue = $FriendlyOptions | Where-Object { [int32]$_.Index -eq $ACValue } | Select-Object Name -ExpandProperty Name + $DCValue = $FriendlyOptions | Where-Object { [int32]$_.Index -eq $DCValue } | Select-Object Name -ExpandProperty Name + + # There's no units to accompany these actions + $Units = "N/A" + } + else { + # Everything else is either a percent or a number of seconds we'll save that for later + $Units = powercfg.exe /Q SCHEME_CURRENT $PowerSetting.SubGUID $PowerSetting.GUID | Select-String "Possible Settings units:" + $Units = ($Units -replace "Possible Settings units:" -replace '\(' -replace '\)').trim() + } + + # Lastly we format our findings into a nice firendly PowerShell Object + [PSCustomObject]@{ + Name = $PowerSetting.Name + GUID = $PowerSetting.GUID + "When Plugged In" = $ACValue + "When On Battery" = $DCValue + Units = $Units + SubName = $PowerSetting.SubName + SubGUID = $PowerSetting.SubGUID + } + } + } + } +} +process { + # If not elevated exit the script. + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Retrieve's the Active Power Plan to be used for the report + $ActivePowerPlan = Get-PowerPlan -Active | Select-Object Name -ExpandProperty Name + + # If somehow we came up empty we should error out + if (-not $ActivePowerPlan) { + Write-Error "[Error] Unable to retrieve power plan!" + exit 1 + } + + # Retrieve's the current settings and formats them into a nice table. Organized by name for easy viewing. + $CurrentPowerSettings = Get-PowerSettings | Sort-Object Name | Format-Table -Property Name, 'When Plugged In', 'When On Battery', Units -AutoSize | Out-String + if (-not $CurrentPowerSettings) { + Write-Error "[Error] Unable to retrieve power settings!" + exit 1 + } + + # Constructing the actual report + $Report = New-Object System.Collections.Generic.List[string] + $Report.Add("Active Power Plan: $ActivePowerPlan") + $Report.Add("`n`n### Current Power Settings For $ActivePowerPlan ###") + $Report.Add("`n$CurrentPowerSettings") + + # Write to the activity log + Write-Host $Report # Output results to be captured by automation platforms or saved to files if needed + if ($PowerPlanCustomFieldName) { + Write-Output "PowerPlan:$ActivePowerPlan" + } + + if ($PowerSettingsCustomFieldName) { + Write-Output "PowerSettings:$CurrentPowerSettings" + } +} +end { + +} + diff --git a/Powershell Scripts/Add Network Printer.ps1 b/Powershell Scripts/Add Network Printer.ps1 index 53b1595..bc08dfa 100644 --- a/Powershell Scripts/Add Network Printer.ps1 +++ b/Powershell Scripts/Add Network Printer.ps1 @@ -1,414 +1,412 @@ # Adds or removes a shared network printer for all user profiles on this computer as a per computer connection. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Adds or removes a shared network printer for all user profiles on this computer as a per computer connection. -.DESCRIPTION - Adds or removes a shared network printer for all user profiles on this computer as a per computer connection. -.EXAMPLE - -PrinterSharePath '\\SRV22-DC1-TEST\Brother HL-L2395DW series' - - Verifying that the print server 'SRV22-DC1-TEST' is reachable via ping. - Print server 'SRV22-DC1-TEST' is reachable. - Verifying '\\SRV22-DC1-TEST\Brother HL-L2395DW series' is a valid printer share. - Adding the printer '\\SRV22-DC1-TEST\Brother HL-L2395DW series' to the system account. - Attempting to add the printer for all users. - Retrieving the printer driver. - Installing the printer driver. - Printer driver installed. - Restarting the print spooler. - The printer has been successfully added. - -PARAMETER: -PrinterSharePath '\\REPLACE-ME\My Printer Share' - Specify the path to the Windows server printer share you would like to add for all users on this computer. E.g., '\\PRNT-SRV\My Printer Share'. - -PARAMETER: -Remove - Removes the printer from this computer instead of adding it. - -PARAMETER: -Restart - A restart may be required for this script to take effect immediately. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Updated checkbox script variables. -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$PrinterSharePath, - [Parameter()] - [Switch]$Remove = [System.Convert]::ToBoolean($env:removePrinter), - [Parameter()] - [Switch]$Restart = [System.Convert]::ToBoolean($env:forceRestart) -) - -begin { - # If script form variables are used, replace the preset parameters with their value. - if ($env:printerSharePath -and $env:printerSharePath -notlike "null") { $PrinterSharePath = $env:printerSharePath } - - # If $PrinterSharePath exists, remove any leading double quotes and trim whitespace. - if ($PrinterSharePath) { - $PrinterSharePath = $PrinterSharePath -replace '^"' -replace '' - $PrinterSharePath = $PrinterSharePath.Trim() - } - - # Check if $PrinterSharePath is empty; if so, display an error and exit. - if (!$PrinterSharePath) { - Write-Host -Object "[Error] Please provide a valid printer share path to add. For example: '\\CONTOSO-PRNT-SRV\My Printer'." - exit 1 - } - - # Validate that $PrinterSharePath is in the correct UNC path format; if not, display an error and exit. - if ($PrinterSharePath -notmatch "^\\\\.+\\.+") { - Write-Host -Object "[Error] An invalid printer share path '$PrinterSharePath' was provided. Shared printer paths should be in the format \\Windows-Server-Name\Printer-Share-Name." - exit 1 - } - - # Extract the server name from $PrinterSharePath. - $Server = $PrinterSharePath -replace "\\[^\\]*$" -replace "^\\\\" - if ($Server) { - $Server = $Server.Trim() - } - - # Check if $Server is empty; if so, display an error and exit. - if (!$Server) { - Write-Host -Object "[Error] The server specified in the path '$PrinterSharePath' is invalid. Please provide a valid printer share path in the format '\\CONTOSO-PRNT-SRV\My_Printer'." - exit 1 - } - - # Validate that $Server contains only allowed characters; if not, display an error and exit. - if ($Server -match "[^a-zA-Z0-9:.-]") { - Write-Host -Object "[Error] The server '$Server' specified in the path '$PrinterSharePath' is invalid. Hostnames and IP addresses can only contain alphabetic characters, digits, colons, dots, and hyphens." - exit 1 - } - - # If $Server is an IP address, split it into octets and check that each is within the valid range (0–255). - if ($Server -match "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+") { - $Server -split '\.' | ForEach-Object { - if ([long]$_ -gt 255 -or [long]$_ -lt 0) { - Write-Host -Object "[Error] The server '$Server' specified in the path '$PrinterSharePath' is invalid. IP address octets cannot exceed 255 or be less than 0." - exit 1 - } - } - } - - # Validate the IP address format by casting $Server to [ipaddress]; if invalid, catch the exception and display an error. - if ($Server -match ":" -or $Server -match "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+") { - try { - $ErrorActionPreference = "Stop" - [ipaddress]$ipAddress = $Server - $ErrorActionPreference = "Continue" - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] The server '$Server' specified in the path '$PrinterSharePath' is invalid. The ip address given is invalid." - exit 1 - } - } - - # If not removing the printer, verify the server is reachable by pinging it; if not, display an error and exit. - if (!$Remove) { - try { - Write-Host -Object "Verifying that the print server '$Server' is reachable via ping." - Test-Connection -ComputerName $Server -ErrorAction Stop | Out-Null - Write-Host -Object "Print server '$Server' is reachable." - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] The device '$Server' at the path '$PrinterSharePath' is invalid. The print server is not reachable." - exit 1 - } - } - - # Extract the share name from $PrinterSharePath by removing the server name. - $ShareName = $PrinterSharePath -replace "^\\\\$Server\\" -replace "\\$" - if ($ShareName) { - $ShareName = $ShareName.Trim() - } - - # Check if $ShareName is empty; if so, display an error and exit. - if (!$ShareName) { - Write-Host -Object "[Error] The share name specified in the path '$PrinterSharePath' is invalid. Please provide a valid printer share in the format '\\CONTOSO-PRNT-SRV\My Printer'." - exit 1 - } - - # Validate that $ShareName does not contain any invalid characters (backslash, forward slash, double quote, or comma). - if ($ShareName -match '[/,"\\]') { - Write-Host -Object "[Error] The printer share '$ShareName' specified in the path '$PrinterSharePath' is invalid. Printer shares cannot contain a backslash, forward slash, double quote, or comma." - exit 1 - } - - # Attempt to verify the printer share's existence; check all printer shares on $Server or locally if removing. - try { - Write-Host -Object "Verifying '$PrinterSharePath' is a valid printer share." - - $CurrentPrinterShares = Get-Printer -ErrorAction Stop | Where-Object { $_.Type -eq "Connection" -and $_.Shared -eq $True -and $_.ComputerName -eq $Server } - - if (!$Remove) { - $AllPrinterShares = Get-Printer -ComputerName $Server -ErrorAction Stop | Where-Object { $_.Shared -eq $True } - } - else { - $AllPrinterShares = $CurrentPrinterShares - } - } - catch { - # Catch any errors while retrieving printer shares and display a relevant message. - Write-Host -Object "[Error] $($_.Exception.Message)" - if ($Remove) { - Write-Host -Object "[Error] Failed to retrieve shared printers from the device $env:ComputerName." - } - else { - Write-Host -Object "[Error] Failed to retrieve shared printers from the device $Server." - } - exit 1 - } - - # If no printer shares exist on the server, display an error. - if (!$AllPrinterShares -and !$Remove) { - Write-Host -Object "[Error] The printer share '$ShareName' specified in the path '$PrinterSharePath' is invalid. No printer shares exist on $Server." - exit 1 - } - - # If no all-user printer shares exist when removing, display an error. - if (!$AllPrinterShares -and $Remove) { - Write-Host -Object "[Error] The printer share '$ShareName' specified in the path '$PrinterSharePath' is invalid. No per computer printer shares are currently added to $env:ComputerName." - exit 1 - } - - # If $ShareName is not present in $AllPrinterShares, display an error listing the current shares. - if ($AllPrinterShares.ShareName -notcontains $ShareName) { - Write-Host -Object "[Error] The printer share '$ShareName' specified in the path '$PrinterSharePath' is invalid. The printer share does not exist." - - if (!$Remove) { - Write-Host -Object "### Current Printer Shares on $Server ###" - } - else { - Write-Host -Object "### Current Printer Shares on $env:ComputerName ###" - } - $AllPrinterShares | Format-Table ShareName, PortName, DriverName - exit 1 - } - - # Check if we are adding the printer (not removing) and if a printer share with the specified $ShareName already exists in the current printer shares on this computer. - if (!$Remove -and $CurrentPrinterShares.ShareName -contains $ShareName) { - Write-Host -Object "[Error] The printer share '$ShareName' specified in the path '$PrinterSharePath' is invalid. The printer share already exists on this computer." - exit 1 - } - - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - if ($ExitCode) { - $ExitCode = 0 - } -} -process { - if (!(Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Log the operation being performed (add or remove the printer from the system account). - if ($Remove) { - Write-Host -Object "Removing the printer '$PrinterSharePath' from the system account." - } - else { - Write-Host -Object "Adding the printer '$PrinterSharePath' to the system account." - } - - # Try to add or remove the printer connection based on the $Remove flag. - try { - if ($Remove) { - # Retrieve the specific printer connection to be removed. - $PrinterToRemove = Get-Printer -ErrorAction Stop | Where-Object { $_.ShareName -eq $ShareName -and $_.Type -eq "Connection" -and $_.Shared -eq $True } - - # Remove the retrieved printer connection. - Remove-Printer -InputObject $PrinterToRemove -ErrorAction Stop - } - else { - # Add the printer connection using the specified share path. - Add-Printer -ConnectionName $PrinterSharePath -ErrorAction Stop - } - } - catch { - # Handle any errors that occur during add or remove operations. - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to add or remove the printer from the system account." - exit 1 - } - - # Log the operation of adding or removing the printer for all users. - if ($Remove) { - Write-Host -Object "Attempting to remove the printer for all users." - } - else { - Write-Host -Object "Attempting to add the printer for all users." - } - - # Try to execute the global add/remove command for the printer using rundll32. - try { - # Capture the start time of the script to track operation duration. - $StartTime = Get-Date - - $ProcessTimeOut = 10 - - # Determine the print operation type ("/gd" for remove, "/ga" for add) based on the $Remove flag. - $AddOrRemove = if ($Remove) { "/gd" }else { "/ga" } - - # Start the process to add or remove the printer for all users. - $Process = Start-Process -FilePath "$env:SystemRoot\system32\rundll32.exe" -ArgumentList @( - "printui.dll,", "PrintUIEntry", $AddOrRemove, "/n`"$PrinterSharePath`"" - ) -PassThru -NoNewWindow - - # Wait for the process to complete or timeout. - while (!$Process.HasExited) { - if ($StartTime.AddMinutes($ProcessTimeOut) -lt $(Get-Date)) { - # Timeout reached; log an error and exit. - Write-Host -Object "[Error] $ProcessTimeOut minute timeout reached. Failed to add or remove the printer." - exit 1 - } - Start-Sleep -Milliseconds 100 - } - } - catch { - # Handle any errors that occur during the rundll32 operation. - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to add or remove the printer with the path '$PrinterSharePath'." - exit 1 - } - - # Restart the print spooler if removing the printer. - if ($Remove) { - Write-Host -Object "Restarting the print spooler." - try { - Restart-Service -Name Spooler -ErrorAction Stop - } - catch { - # Handle any errors that occur during spooler restart. - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to restart the print spooler." - exit 1 - } - - # If the $Restart flag is set, schedule a system restart. - if ($Restart) { - $RestartDate = (Get-Date).AddMinutes(1) - Write-Host -Object "Scheduling a restart for $($RestartDate.ToShortDateString()) at $($RestartDate.ToShortTimeString())." - try { - Start-Process shutdown.exe -ArgumentList "/r /t 60" -Wait -NoNewWindow - } - catch { - # Handle any errors that occur while scheduling the restart. - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to schedule restart." - exit 1 - } - } - - # Retrieve the current printer shares - $CurrentPrinterShares = Get-Printer -ErrorAction Stop | Where-Object { $_.Type -eq "Connection" -and $_.Shared -eq $True -and $_.ComputerName -eq $Server -and $_.ShareName -eq $ShareName } - - # If we are removing the printer and it does not exist in the current printer shares (meaning it was successfully removed), log a success message. - if (!$CurrentPrinterShares) { - Write-Host -Object "The printer is scheduled for removal." - if (!$Restart) { - Write-Warning -Message "A restart is required to complete the removal." - } - else { - Write-Warning -Message "The removal will complete after the restart." - } - } - else { - Write-Host -Object "[Error] The printer was found. Failed to remove the printer" - exit 1 - } - - exit - } - - # Retrieve the printer driver for the specified printer. - Write-Host -Object "Retrieving the printer driver." - try { - $ErrorActionPreference = "Stop" - - # Get the driver name for the specified printer. - $PrinterDriverName = Get-Printer -ComputerName $Server | Where-Object { $_.ShareName -eq $ShareName } | Select-Object -ExpandProperty "DriverName" - - # Retrieve the full printer driver object by name. - $PrinterDriver = Get-PrinterDriver -ComputerName $Server -Name $PrinterDriverName | Select-Object -ExpandProperty "Name" - $ErrorActionPreference = "Continue" - } - catch { - # Handle errors that occur during printer driver retrieval. - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to retrieve the printer driver." - exit 1 - } - - # Install the retrieved printer driver on the local system. - Write-Host -Object "Installing the printer driver." - try { - Add-PrinterDriver -Name $PrinterDriver -ErrorAction Stop - } - catch { - # Handle errors that occur during printer driver installation. - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to install the printer driver." - exit 1 - } - - # Log successful installation of the printer driver. - Write-Host -Object "Printer driver installed." - - # Restart the print spooler to apply the driver installation. - Write-Host -Object "Restarting the print spooler." - try { - Restart-Service -Name Spooler -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to restart the print spooler." - exit 1 - } - - # Schedule a system restart if the $Restart flag is set. - if ($Restart) { - # Set the restart time to one minute from now. - $RestartDate = (Get-Date).AddMinutes(1) - Write-Host -Object "Scheduling a restart for $($RestartDate.ToShortDateString()) at $($RestartDate.ToShortTimeString())." - try { - Start-Process shutdown.exe -ArgumentList "/r /t 60" -Wait -NoNewWindow - } - catch { - # Handle errors that occur while scheduling the restart. - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to schedule restart." - exit 1 - } - } - - # Retrieve the current printer shares - $CurrentPrinterShares = Get-Printer -ErrorAction Stop | Where-Object { $_.Type -eq "Connection" -and $_.Shared -eq $True -and $_.ComputerName -eq $Server -and $_.ShareName -eq $ShareName } - - # If we are not removing the printer and it exists in the current printer shares (meaning it was successfully added), log a success message. - if ($CurrentPrinterShares) { - Write-Host -Object "The printer has been successfully added." - } - else { - Write-Host -Object "[Error] The printer was not found. Failed to add the printer." - exit 1 - } - - exit $ExitCode -} -end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + Adds or removes a shared network printer for all user profiles on this computer as a per computer connection. +.DESCRIPTION + Adds or removes a shared network printer for all user profiles on this computer as a per computer connection. +.EXAMPLE + -PrinterSharePath '\\SRV22-DC1-TEST\Brother HL-L2395DW series' + + Verifying that the print server 'SRV22-DC1-TEST' is reachable via ping. + Print server 'SRV22-DC1-TEST' is reachable. + Verifying '\\SRV22-DC1-TEST\Brother HL-L2395DW series' is a valid printer share. + Adding the printer '\\SRV22-DC1-TEST\Brother HL-L2395DW series' to the system account. + Attempting to add the printer for all users. + Retrieving the printer driver. + Installing the printer driver. + Printer driver installed. + Restarting the print spooler. + The printer has been successfully added. + +PARAMETER: -PrinterSharePath '\\REPLACE-ME\My Printer Share' + Specify the path to the Windows server printer share you would like to add for all users on this computer. E.g., '\\PRNT-SRV\My Printer Share'. + +PARAMETER: -Remove + Removes the printer from this computer instead of adding it. + +PARAMETER: -Restart + A restart may be required for this script to take effect immediately. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Updated checkbox script variables. +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$PrinterSharePath, + [Parameter()] + [Switch]$Remove = [System.Convert]::ToBoolean($env:removePrinter), + [Parameter()] + [Switch]$Restart = [System.Convert]::ToBoolean($env:forceRestart) +) + +begin { + # If script form variables are used, replace the preset parameters with their value. + if ($env:printerSharePath -and $env:printerSharePath -notlike "null") { $PrinterSharePath = $env:printerSharePath } + + # If $PrinterSharePath exists, remove any leading double quotes and trim whitespace. + if ($PrinterSharePath) { + $PrinterSharePath = $PrinterSharePath -replace '^"' -replace '' + $PrinterSharePath = $PrinterSharePath.Trim() + } + + # Check if $PrinterSharePath is empty; if so, display an error and exit. + if (!$PrinterSharePath) { + Write-Host -Object "[Error] Please provide a valid printer share path to add. For example: '\\CONTOSO-PRNT-SRV\My Printer'." + exit 1 + } + + # Validate that $PrinterSharePath is in the correct UNC path format; if not, display an error and exit. + if ($PrinterSharePath -notmatch "^\\\\.+\\.+") { + Write-Host -Object "[Error] An invalid printer share path '$PrinterSharePath' was provided. Shared printer paths should be in the format \\Windows-Server-Name\Printer-Share-Name." + exit 1 + } + + # Extract the server name from $PrinterSharePath. + $Server = $PrinterSharePath -replace "\\[^\\]*$" -replace "^\\\\" + if ($Server) { + $Server = $Server.Trim() + } + + # Check if $Server is empty; if so, display an error and exit. + if (!$Server) { + Write-Host -Object "[Error] The server specified in the path '$PrinterSharePath' is invalid. Please provide a valid printer share path in the format '\\CONTOSO-PRNT-SRV\My_Printer'." + exit 1 + } + + # Validate that $Server contains only allowed characters; if not, display an error and exit. + if ($Server -match "[^a-zA-Z0-9:.-]") { + Write-Host -Object "[Error] The server '$Server' specified in the path '$PrinterSharePath' is invalid. Hostnames and IP addresses can only contain alphabetic characters, digits, colons, dots, and hyphens." + exit 1 + } + + # If $Server is an IP address, split it into octets and check that each is within the valid range (0–255). + if ($Server -match "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+") { + $Server -split '\.' | ForEach-Object { + if ([long]$_ -gt 255 -or [long]$_ -lt 0) { + Write-Host -Object "[Error] The server '$Server' specified in the path '$PrinterSharePath' is invalid. IP address octets cannot exceed 255 or be less than 0." + exit 1 + } + } + } + + # Validate the IP address format by casting $Server to [ipaddress]; if invalid, catch the exception and display an error. + if ($Server -match ":" -or $Server -match "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+") { + try { + $ErrorActionPreference = "Stop" + [ipaddress]$ipAddress = $Server + $ErrorActionPreference = "Continue" + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] The server '$Server' specified in the path '$PrinterSharePath' is invalid. The ip address given is invalid." + exit 1 + } + } + + # If not removing the printer, verify the server is reachable by pinging it; if not, display an error and exit. + if (!$Remove) { + try { + Write-Host -Object "Verifying that the print server '$Server' is reachable via ping." + Test-Connection -ComputerName $Server -ErrorAction Stop | Out-Null + Write-Host -Object "Print server '$Server' is reachable." + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] The device '$Server' at the path '$PrinterSharePath' is invalid. The print server is not reachable." + exit 1 + } + } + + # Extract the share name from $PrinterSharePath by removing the server name. + $ShareName = $PrinterSharePath -replace "^\\\\$Server\\" -replace "\\$" + if ($ShareName) { + $ShareName = $ShareName.Trim() + } + + # Check if $ShareName is empty; if so, display an error and exit. + if (!$ShareName) { + Write-Host -Object "[Error] The share name specified in the path '$PrinterSharePath' is invalid. Please provide a valid printer share in the format '\\CONTOSO-PRNT-SRV\My Printer'." + exit 1 + } + + # Validate that $ShareName does not contain any invalid characters (backslash, forward slash, double quote, or comma). + if ($ShareName -match '[/,"\\]') { + Write-Host -Object "[Error] The printer share '$ShareName' specified in the path '$PrinterSharePath' is invalid. Printer shares cannot contain a backslash, forward slash, double quote, or comma." + exit 1 + } + + # Attempt to verify the printer share's existence; check all printer shares on $Server or locally if removing. + try { + Write-Host -Object "Verifying '$PrinterSharePath' is a valid printer share." + + $CurrentPrinterShares = Get-Printer -ErrorAction Stop | Where-Object { $_.Type -eq "Connection" -and $_.Shared -eq $True -and $_.ComputerName -eq $Server } + + if (!$Remove) { + $AllPrinterShares = Get-Printer -ComputerName $Server -ErrorAction Stop | Where-Object { $_.Shared -eq $True } + } + else { + $AllPrinterShares = $CurrentPrinterShares + } + } + catch { + # Catch any errors while retrieving printer shares and display a relevant message. + Write-Host -Object "[Error] $($_.Exception.Message)" + if ($Remove) { + Write-Host -Object "[Error] Failed to retrieve shared printers from the device $env:ComputerName." + } + else { + Write-Host -Object "[Error] Failed to retrieve shared printers from the device $Server." + } + exit 1 + } + + # If no printer shares exist on the server, display an error. + if (!$AllPrinterShares -and !$Remove) { + Write-Host -Object "[Error] The printer share '$ShareName' specified in the path '$PrinterSharePath' is invalid. No printer shares exist on $Server." + exit 1 + } + + # If no all-user printer shares exist when removing, display an error. + if (!$AllPrinterShares -and $Remove) { + Write-Host -Object "[Error] The printer share '$ShareName' specified in the path '$PrinterSharePath' is invalid. No per computer printer shares are currently added to $env:ComputerName." + exit 1 + } + + # If $ShareName is not present in $AllPrinterShares, display an error listing the current shares. + if ($AllPrinterShares.ShareName -notcontains $ShareName) { + Write-Host -Object "[Error] The printer share '$ShareName' specified in the path '$PrinterSharePath' is invalid. The printer share does not exist." + + if (!$Remove) { + Write-Host -Object "### Current Printer Shares on $Server ###" + } + else { + Write-Host -Object "### Current Printer Shares on $env:ComputerName ###" + } + $AllPrinterShares | Format-Table ShareName, PortName, DriverName + exit 1 + } + + # Check if we are adding the printer (not removing) and if a printer share with the specified $ShareName already exists in the current printer shares on this computer. + if (!$Remove -and $CurrentPrinterShares.ShareName -contains $ShareName) { + Write-Host -Object "[Error] The printer share '$ShareName' specified in the path '$PrinterSharePath' is invalid. The printer share already exists on this computer." + exit 1 + } + + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if ($ExitCode) { + $ExitCode = 0 + } +} +process { + if (!(Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Log the operation being performed (add or remove the printer from the system account). + if ($Remove) { + Write-Host -Object "Removing the printer '$PrinterSharePath' from the system account." + } + else { + Write-Host -Object "Adding the printer '$PrinterSharePath' to the system account." + } + + # Try to add or remove the printer connection based on the $Remove flag. + try { + if ($Remove) { + # Retrieve the specific printer connection to be removed. + $PrinterToRemove = Get-Printer -ErrorAction Stop | Where-Object { $_.ShareName -eq $ShareName -and $_.Type -eq "Connection" -and $_.Shared -eq $True } + + # Remove the retrieved printer connection. + Remove-Printer -InputObject $PrinterToRemove -ErrorAction Stop + } + else { + # Add the printer connection using the specified share path. + Add-Printer -ConnectionName $PrinterSharePath -ErrorAction Stop + } + } + catch { + # Handle any errors that occur during add or remove operations. + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to add or remove the printer from the system account." + exit 1 + } + + # Log the operation of adding or removing the printer for all users. + if ($Remove) { + Write-Host -Object "Attempting to remove the printer for all users." + } + else { + Write-Host -Object "Attempting to add the printer for all users." + } + + # Try to execute the global add/remove command for the printer using rundll32. + try { + # Capture the start time of the script to track operation duration. + $StartTime = Get-Date + + $ProcessTimeOut = 10 + + # Determine the print operation type ("/gd" for remove, "/ga" for add) based on the $Remove flag. + $AddOrRemove = if ($Remove) { "/gd" }else { "/ga" } + + # Start the process to add or remove the printer for all users. + $Process = Start-Process -FilePath "$env:SystemRoot\system32\rundll32.exe" -ArgumentList @( + "printui.dll,", "PrintUIEntry", $AddOrRemove, "/n`"$PrinterSharePath`"" + ) -PassThru -NoNewWindow + + # Wait for the process to complete or timeout. + while (!$Process.HasExited) { + if ($StartTime.AddMinutes($ProcessTimeOut) -lt $(Get-Date)) { + # Timeout reached; log an error and exit. + Write-Host -Object "[Error] $ProcessTimeOut minute timeout reached. Failed to add or remove the printer." + exit 1 + } + Start-Sleep -Milliseconds 100 + } + } + catch { + # Handle any errors that occur during the rundll32 operation. + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to add or remove the printer with the path '$PrinterSharePath'." + exit 1 + } + + # Restart the print spooler if removing the printer. + if ($Remove) { + Write-Host -Object "Restarting the print spooler." + try { + Restart-Service -Name Spooler -ErrorAction Stop + } + catch { + # Handle any errors that occur during spooler restart. + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to restart the print spooler." + exit 1 + } + + # If the $Restart flag is set, schedule a system restart. + if ($Restart) { + $RestartDate = (Get-Date).AddMinutes(1) + Write-Host -Object "Scheduling a restart for $($RestartDate.ToShortDateString()) at $($RestartDate.ToShortTimeString())." + try { + Start-Process shutdown.exe -ArgumentList "/r /t 60" -Wait -NoNewWindow + } + catch { + # Handle any errors that occur while scheduling the restart. + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to schedule restart." + exit 1 + } + } + + # Retrieve the current printer shares + $CurrentPrinterShares = Get-Printer -ErrorAction Stop | Where-Object { $_.Type -eq "Connection" -and $_.Shared -eq $True -and $_.ComputerName -eq $Server -and $_.ShareName -eq $ShareName } + + # If we are removing the printer and it does not exist in the current printer shares (meaning it was successfully removed), log a success message. + if (!$CurrentPrinterShares) { + Write-Host -Object "The printer is scheduled for removal." + if (!$Restart) { + Write-Warning -Message "A restart is required to complete the removal." + } + else { + Write-Warning -Message "The removal will complete after the restart." + } + } + else { + Write-Host -Object "[Error] The printer was found. Failed to remove the printer" + exit 1 + } + + exit + } + + # Retrieve the printer driver for the specified printer. + Write-Host -Object "Retrieving the printer driver." + try { + $ErrorActionPreference = "Stop" + + # Get the driver name for the specified printer. + $PrinterDriverName = Get-Printer -ComputerName $Server | Where-Object { $_.ShareName -eq $ShareName } | Select-Object -ExpandProperty "DriverName" + + # Retrieve the full printer driver object by name. + $PrinterDriver = Get-PrinterDriver -ComputerName $Server -Name $PrinterDriverName | Select-Object -ExpandProperty "Name" + $ErrorActionPreference = "Continue" + } + catch { + # Handle errors that occur during printer driver retrieval. + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to retrieve the printer driver." + exit 1 + } + + # Install the retrieved printer driver on the local system. + Write-Host -Object "Installing the printer driver." + try { + Add-PrinterDriver -Name $PrinterDriver -ErrorAction Stop + } + catch { + # Handle errors that occur during printer driver installation. + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to install the printer driver." + exit 1 + } + + # Log successful installation of the printer driver. + Write-Host -Object "Printer driver installed." + + # Restart the print spooler to apply the driver installation. + Write-Host -Object "Restarting the print spooler." + try { + Restart-Service -Name Spooler -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to restart the print spooler." + exit 1 + } + + # Schedule a system restart if the $Restart flag is set. + if ($Restart) { + # Set the restart time to one minute from now. + $RestartDate = (Get-Date).AddMinutes(1) + Write-Host -Object "Scheduling a restart for $($RestartDate.ToShortDateString()) at $($RestartDate.ToShortTimeString())." + try { + Start-Process shutdown.exe -ArgumentList "/r /t 60" -Wait -NoNewWindow + } + catch { + # Handle errors that occur while scheduling the restart. + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to schedule restart." + exit 1 + } + } + + # Retrieve the current printer shares + $CurrentPrinterShares = Get-Printer -ErrorAction Stop | Where-Object { $_.Type -eq "Connection" -and $_.Shared -eq $True -and $_.ComputerName -eq $Server -and $_.ShareName -eq $ShareName } + + # If we are not removing the printer and it exists in the current printer shares (meaning it was successfully added), log a success message. + if ($CurrentPrinterShares) { + Write-Host -Object "The printer has been successfully added." + } + else { + Write-Host -Object "[Error] The printer was not found. Failed to add the printer." + exit 1 + } + + exit $ExitCode +} +end { + +} + diff --git a/Powershell Scripts/Alert on DHCP Lease Low.ps1 b/Powershell Scripts/Alert on DHCP Lease Low.ps1 index 083eaf2..0f9d8ec 100644 --- a/Powershell Scripts/Alert on DHCP Lease Low.ps1 +++ b/Powershell Scripts/Alert on DHCP Lease Low.ps1 @@ -1,211 +1,209 @@ # Checks the DHCP scopes for the number of leases used and alerts if the threshold is exceeded. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Checks the DHCP scopes for the number of leases used and alerts if the threshold is exceeded. -.DESCRIPTION - Checks the DHCP scopes for the number of leases used and alerts if the threshold is exceeded. - This script requires the DhcpServer module to be installed with the DHCP server feature installed. - The script will output the number of leases used, free, and total for each scope. - If the LeaseThreshold parameter is set, the script will alert if the number of free leases is less than the threshold. - If the ExcludeScope parameter is set, the script will exclude the specified scope from the output. - If the IncludeScope parameter is set, the script will only include the specified scope in the output. - -.PARAMETER LeaseThreshold - The number of free leases that will trigger an alert. If the number of free leases is less than the threshold, an alert will be triggered. -.PARAMETER ExcludeScope - The name of the scope to exclude from the output. -.PARAMETER IncludeScope - The name of the scope to include in the output. - -.EXAMPLE - (No Parameters) - ## EXAMPLE OUTPUT WITHOUT PARAMS ## - [Info] Scope: Test1 Leases Used(In Use/Total): 250/252 - [Info] Scope: Test2 Leases Used(In Use/Total): 220/252 - [Info] Scope: Test6 Leases Used(In Use/Total): 4954378/18446744073709551615 - -.EXAMPLE - PARAMETER: -LeaseThreshold 10 - ## EXAMPLE OUTPUT WITH LEASETHRESHOLD ## - [Alert] Scope: Test1 Leases Used(In Use/Free/Total): 220/2/252 - [Info] Scope: Test2 Leases Used(In Use/Free/Total): 150/102/252 - [Info] Scope: Test6 Leases Used(In Use/Free/Total): 0/18446744073709551615/18446744073709551615 - -.EXAMPLE - PARAMETER: -ExcludeScope "Test1" - ## EXAMPLE OUTPUT WITH EXCLUDESCOPE ## - [Info] Scope: Test2 Leases Used(In Use/Free/Total): 220/2/252 - [Info] Scope: Test6 Leases Used(In Use/Free/Total): 0/18446744073709551615/18446744073709551615 - -.EXAMPLE - PARAMETER: -IncludeScope "Test2" - ## EXAMPLE OUTPUT WITH INCLUDESCOPE ## - [Info] Scope: Test2 Leases Used(In Use/Free/Total): 220/2/252 -.NOTES - Minimum OS: Windows Server 2016 - Requires the DhcpServer module to be installed with the DHCP server feature installed. - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - $LeaseThreshold, - [string[]]$ExcludeScope, - [string[]]$IncludeScope -) - -begin { - function Test-IsElevated { - # check if running under a Pester test case - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - try { - if ($env:leaseThreshold -and $env:leaseThreshold -notlike "null") { - [int]$LeaseThreshold = $env:leaseThreshold - } - } - catch { - Write-Host "[Error] LeaseThreshold must be a number" - exit 2 - } - - if ($env:excludeScope -and $env:excludeScope -notlike "null") { - $ExcludeScope = $env:excludeScope - } - if ($env:includeScope -and $env:includeScope -notlike "null") { - $IncludeScope = $env:includeScope - } - - # Split the ExcludeScope and IncludeScope parameters into an array - if (-not [String]::IsNullOrWhiteSpace($ExcludeScope) -and $ExcludeScope -like '*,*') { - $ExcludeScope = $ExcludeScope -split ',' | ForEach-Object { $_.Trim() } | Where-Object { -not [String]::IsNullOrWhiteSpace($_) } | Sort-Object -Unique - } - if (-not [String]::IsNullOrWhiteSpace($IncludeScope) -and $IncludeScope -like '*,*') { - $IncludeScope = $IncludeScope -split ',' | ForEach-Object { $_.Trim() } | Where-Object { -not [String]::IsNullOrWhiteSpace($_) } | Sort-Object -Unique - } - - # Check if $ExcludeScope and $IncludeScope contain similar items - if (-not [String]::IsNullOrWhiteSpace($ExcludeScope) -and -not [String]::IsNullOrWhiteSpace($IncludeScope)) { - $SimilarItems = $ExcludeScope | Where-Object { $IncludeScope -contains $_ } - if ($SimilarItems) { - Write-Host "[Error] The following scopes are in both ExcludeScope and IncludeScope: $($SimilarItems -join ', ')" - exit 2 - } - } - - $ShouldAlert = $false -} -process { - if (-not (Test-IsElevated)) { - Write-Host "[Error] Access Denied. Please run with Administrator privileges." - exit 2 - } - - # Check if the DhcpServer module is installed - if (-not (Get-Module -ListAvailable -Name DhcpServer -ErrorAction SilentlyContinue)) { - Write-Host "[Error] The DhcpServer module is not installed. Please install the DHCP server feature and the DhcpServer module." - exit 2 - } - - # Get all DHCP scopes - $AllScopes = $( - Get-DhcpServerv4Scope | Select-Object -ExpandProperty Name - Get-DhcpServerv6Scope | Select-Object -ExpandProperty Name - ) - - # Output an error if the ExcludeScope or IncludeScope parameters contain invalid scope names - $( - if ($IncludeScope) { $IncludeScope } - if ($ExcludeScope) { $ExcludeScope } - ) | ForEach-Object { - if ($_ -notin $AllScopes) { - Write-Host "[Error] Scope: $_ does not exist in the DHCP server. Please check the scope name and try again." - } - } - - # IPv4 - # Get all DHCP scopes - $v4scopes = Get-DhcpServerv4Scope | Where-Object { $_.State } - - # Iterate through each scope - foreach ($scope in $v4scopes) { - # Get statistics for the scope - $Stats = Get-DhcpServerv4ScopeStatistics -ScopeId $scope.ScopeId - - # Get the name of the scope - $Name = (Get-DhcpServerv4Scope -ScopeId $scope.ScopeId).Name - - # Check if the scope should be excluded - if (-not [String]::IsNullOrWhiteSpace($ExcludeScope) -and $Name -in $ExcludeScope) { - continue - } - - # Check if the scope should be included - if (-not [String]::IsNullOrWhiteSpace($IncludeScope) -and $Name -notin $IncludeScope) { - continue - } - - # Check if the number of free leases is less than the threshold - if ($Stats.Free -lt $LeaseThreshold ) { - if ($ShouldAlert -eq $false) { - # Output once if this is the first scope to trigger an alert - Write-Host "[Alert] Available DHCP Leases Low. You may want to make modifications to one of the below scopes." - } - Write-Host "[Alert] Scope: $Name Leases Used(In Use/Free/Total): $($Stats.InUse)/$($Stats.Free)/$($Stats.InUse+$Stats.Free)" - $ShouldAlert = $true - } - else { - Write-Host "[Info] Scope: $Name Leases Used(In Use/Free/Total): $($Stats.InUse)/$($Stats.Free)/$($Stats.InUse+$Stats.Free)" - } - } - - # IPv6 - # Get all DHCP scopes - $v6Scopes = Get-DhcpServerv6Scope | Where-Object { $_.State } - - # Iterate through each scope - foreach ($scope in $v6Scopes) { - # Get statistics for the scope - $Stats = Get-DhcpServerv6ScopeStatistics -Prefix $scope.Prefix - - # Get the name of the scope - $Name = (Get-DhcpServerv6Scope -Prefix $scope.Prefix).Name - - # Check if the scope should be excluded - if (-not [String]::IsNullOrWhiteSpace($ExcludeScope) -and $Name -in $ExcludeScope) { - continue - } - - # Check if the scope should be included - if (-not [String]::IsNullOrWhiteSpace($IncludeScope) -and $Name -notin $IncludeScope) { - continue - } - - # Check if the number of free leases is less than the threshold - if ($Stats.Free -lt $LeaseThreshold ) { - if ($ShouldAlert -eq $false) { - # Output once if this is the first scope to trigger an alert - Write-Host "[Alert] Available DHCP Leases Low. You may want to make modifications to one of the below scopes." - } - Write-Host "[Alert] Scope: $Name Leases Used(In Use/Free/Total): $($Stats.InUse)/$($Stats.Free)/$($Stats.InUse+$Stats.Free)" - $ShouldAlert = $true - } - else { - Write-Host "[Info] Scope: $Name Leases Used(In Use/Free/Total): $($Stats.InUse)/$($Stats.Free)/$($Stats.InUse+$Stats.Free)" - } - } - - exit 0 - -} -end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + Checks the DHCP scopes for the number of leases used and alerts if the threshold is exceeded. +.DESCRIPTION + Checks the DHCP scopes for the number of leases used and alerts if the threshold is exceeded. + This script requires the DhcpServer module to be installed with the DHCP server feature installed. + The script will output the number of leases used, free, and total for each scope. + If the LeaseThreshold parameter is set, the script will alert if the number of free leases is less than the threshold. + If the ExcludeScope parameter is set, the script will exclude the specified scope from the output. + If the IncludeScope parameter is set, the script will only include the specified scope in the output. + +.PARAMETER LeaseThreshold + The number of free leases that will trigger an alert. If the number of free leases is less than the threshold, an alert will be triggered. +.PARAMETER ExcludeScope + The name of the scope to exclude from the output. +.PARAMETER IncludeScope + The name of the scope to include in the output. + +.EXAMPLE + (No Parameters) + ## EXAMPLE OUTPUT WITHOUT PARAMS ## + [Info] Scope: Test1 Leases Used(In Use/Total): 250/252 + [Info] Scope: Test2 Leases Used(In Use/Total): 220/252 + [Info] Scope: Test6 Leases Used(In Use/Total): 4954378/18446744073709551615 + +.EXAMPLE + PARAMETER: -LeaseThreshold 10 + ## EXAMPLE OUTPUT WITH LEASETHRESHOLD ## + [Alert] Scope: Test1 Leases Used(In Use/Free/Total): 220/2/252 + [Info] Scope: Test2 Leases Used(In Use/Free/Total): 150/102/252 + [Info] Scope: Test6 Leases Used(In Use/Free/Total): 0/18446744073709551615/18446744073709551615 + +.EXAMPLE + PARAMETER: -ExcludeScope "Test1" + ## EXAMPLE OUTPUT WITH EXCLUDESCOPE ## + [Info] Scope: Test2 Leases Used(In Use/Free/Total): 220/2/252 + [Info] Scope: Test6 Leases Used(In Use/Free/Total): 0/18446744073709551615/18446744073709551615 + +.EXAMPLE + PARAMETER: -IncludeScope "Test2" + ## EXAMPLE OUTPUT WITH INCLUDESCOPE ## + [Info] Scope: Test2 Leases Used(In Use/Free/Total): 220/2/252 +.NOTES + Minimum OS: Windows Server 2016 + Requires the DhcpServer module to be installed with the DHCP server feature installed. + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + $LeaseThreshold, + [string[]]$ExcludeScope, + [string[]]$IncludeScope +) + +begin { + function Test-IsElevated { + # check if running under a Pester test case + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + try { + if ($env:leaseThreshold -and $env:leaseThreshold -notlike "null") { + [int]$LeaseThreshold = $env:leaseThreshold + } + } + catch { + Write-Host "[Error] LeaseThreshold must be a number" + exit 2 + } + + if ($env:excludeScope -and $env:excludeScope -notlike "null") { + $ExcludeScope = $env:excludeScope + } + if ($env:includeScope -and $env:includeScope -notlike "null") { + $IncludeScope = $env:includeScope + } + + # Split the ExcludeScope and IncludeScope parameters into an array + if (-not [String]::IsNullOrWhiteSpace($ExcludeScope) -and $ExcludeScope -like '*,*') { + $ExcludeScope = $ExcludeScope -split ',' | ForEach-Object { $_.Trim() } | Where-Object { -not [String]::IsNullOrWhiteSpace($_) } | Sort-Object -Unique + } + if (-not [String]::IsNullOrWhiteSpace($IncludeScope) -and $IncludeScope -like '*,*') { + $IncludeScope = $IncludeScope -split ',' | ForEach-Object { $_.Trim() } | Where-Object { -not [String]::IsNullOrWhiteSpace($_) } | Sort-Object -Unique + } + + # Check if $ExcludeScope and $IncludeScope contain similar items + if (-not [String]::IsNullOrWhiteSpace($ExcludeScope) -and -not [String]::IsNullOrWhiteSpace($IncludeScope)) { + $SimilarItems = $ExcludeScope | Where-Object { $IncludeScope -contains $_ } + if ($SimilarItems) { + Write-Host "[Error] The following scopes are in both ExcludeScope and IncludeScope: $($SimilarItems -join ', ')" + exit 2 + } + } + + $ShouldAlert = $false +} +process { + if (-not (Test-IsElevated)) { + Write-Host "[Error] Access Denied. Please run with Administrator privileges." + exit 2 + } + + # Check if the DhcpServer module is installed + if (-not (Get-Module -ListAvailable -Name DhcpServer -ErrorAction SilentlyContinue)) { + Write-Host "[Error] The DhcpServer module is not installed. Please install the DHCP server feature and the DhcpServer module." + exit 2 + } + + # Get all DHCP scopes + $AllScopes = $( + Get-DhcpServerv4Scope | Select-Object -ExpandProperty Name + Get-DhcpServerv6Scope | Select-Object -ExpandProperty Name + ) + + # Output an error if the ExcludeScope or IncludeScope parameters contain invalid scope names + $( + if ($IncludeScope) { $IncludeScope } + if ($ExcludeScope) { $ExcludeScope } + ) | ForEach-Object { + if ($_ -notin $AllScopes) { + Write-Host "[Error] Scope: $_ does not exist in the DHCP server. Please check the scope name and try again." + } + } + + # IPv4 + # Get all DHCP scopes + $v4scopes = Get-DhcpServerv4Scope | Where-Object { $_.State } + + # Iterate through each scope + foreach ($scope in $v4scopes) { + # Get statistics for the scope + $Stats = Get-DhcpServerv4ScopeStatistics -ScopeId $scope.ScopeId + + # Get the name of the scope + $Name = (Get-DhcpServerv4Scope -ScopeId $scope.ScopeId).Name + + # Check if the scope should be excluded + if (-not [String]::IsNullOrWhiteSpace($ExcludeScope) -and $Name -in $ExcludeScope) { + continue + } + + # Check if the scope should be included + if (-not [String]::IsNullOrWhiteSpace($IncludeScope) -and $Name -notin $IncludeScope) { + continue + } + + # Check if the number of free leases is less than the threshold + if ($Stats.Free -lt $LeaseThreshold ) { + if ($ShouldAlert -eq $false) { + # Output once if this is the first scope to trigger an alert + Write-Host "[Alert] Available DHCP Leases Low. You may want to make modifications to one of the below scopes." + } + Write-Host "[Alert] Scope: $Name Leases Used(In Use/Free/Total): $($Stats.InUse)/$($Stats.Free)/$($Stats.InUse+$Stats.Free)" + $ShouldAlert = $true + } + else { + Write-Host "[Info] Scope: $Name Leases Used(In Use/Free/Total): $($Stats.InUse)/$($Stats.Free)/$($Stats.InUse+$Stats.Free)" + } + } + + # IPv6 + # Get all DHCP scopes + $v6Scopes = Get-DhcpServerv6Scope | Where-Object { $_.State } + + # Iterate through each scope + foreach ($scope in $v6Scopes) { + # Get statistics for the scope + $Stats = Get-DhcpServerv6ScopeStatistics -Prefix $scope.Prefix + + # Get the name of the scope + $Name = (Get-DhcpServerv6Scope -Prefix $scope.Prefix).Name + + # Check if the scope should be excluded + if (-not [String]::IsNullOrWhiteSpace($ExcludeScope) -and $Name -in $ExcludeScope) { + continue + } + + # Check if the scope should be included + if (-not [String]::IsNullOrWhiteSpace($IncludeScope) -and $Name -notin $IncludeScope) { + continue + } + + # Check if the number of free leases is less than the threshold + if ($Stats.Free -lt $LeaseThreshold ) { + if ($ShouldAlert -eq $false) { + # Output once if this is the first scope to trigger an alert + Write-Host "[Alert] Available DHCP Leases Low. You may want to make modifications to one of the below scopes." + } + Write-Host "[Alert] Scope: $Name Leases Used(In Use/Free/Total): $($Stats.InUse)/$($Stats.Free)/$($Stats.InUse+$Stats.Free)" + $ShouldAlert = $true + } + else { + Write-Host "[Info] Scope: $Name Leases Used(In Use/Free/Total): $($Stats.InUse)/$($Stats.Free)/$($Stats.InUse+$Stats.Free)" + } + } + + exit 0 + +} +end { + +} + diff --git a/Powershell Scripts/Audit Autopilot Hardware ID.ps1 b/Powershell Scripts/Audit Autopilot Hardware ID.ps1 index dc3a7f1..7d7e466 100644 --- a/Powershell Scripts/Audit Autopilot Hardware ID.ps1 +++ b/Powershell Scripts/Audit Autopilot Hardware ID.ps1 @@ -1,83 +1,80 @@ # Gets the hardware ID and saves it to a custom field. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Gets the hardware ID and saves it to a custom field. -.DESCRIPTION - Gets the hardware ID and saves it to a custom field. - Setup: - Create a Multi-line Custom Field, as the hardware ID is 4000 characters long. -.EXAMPLE - -CustomField "autopilothwid" - Uses the custom field named "autopilothwid" and saves the 4000 character long id to it. -.EXAMPLE - PS C:\> Get-HardwareId.ps1 -CustomField "autopilothwid" - Uses the custom field named "autopilothwid" and saves the 4000 character long id to it. -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2019 - Release Notes: Renamed script and added Script Variable support -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String] - $CustomField = "hardwareid" -) - -begin { - - if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - function Test-StringEmpty { - param([string]$Text) - # Returns true if string is empty, null, or whitespace - process { [string]::IsNullOrEmpty($Text) -or [string]::IsNullOrWhiteSpace($Text) } - } -} -process { - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - try { - $DeviceDetail = Get-CimInstance -Namespace "root/cimv2/mdm/dmmap" -Class "MDM_DevDetail_Ext01" -Filter "InstanceID='Ext' AND ParentID='./DevDetail'" -ErrorAction Stop - } - catch { - if ($_.Exception.Message -like "Invalid class") { - Write-Error -Message "root/cimv2/mdm/dmmap: MDM_DevDetail_Ext01 WMI class does not exist on system." - } - else { - Write-Error $_ - } - exit 1 - } - - if ( - $DeviceDetail -and - $DeviceDetail.DeviceHardwareData -and - -not $(Test-StringEmpty -Text $DeviceDetail.DeviceHardwareData) - ) { - Ninja-Property-Set -Name $CustomField -Value $DeviceDetail.DeviceHardwareData - Write-Host "HardwareID: $($DeviceDetail.DeviceHardwareData)" - } - else { - Write-Error "Unable to retrieve device details or DeviceHardwareData does not exist." - exit 1 - } - -} -end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + Gets the hardware ID and saves it to a custom field. +.DESCRIPTION + Gets the hardware ID and saves it to a custom field. + Setup: + Create a Multi-line Custom Field, as the hardware ID is 4000 characters long. +.EXAMPLE + -CustomField "autopilothwid" + Uses the custom field named "autopilothwid" and saves the 4000 character long id to it. +.EXAMPLE + PS C:\> Get-HardwareId.ps1 -CustomField "autopilothwid" + Uses the custom field named "autopilothwid" and saves the 4000 character long id to it. +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2019 + Release Notes: Renamed script and added Script Variable support +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String] + $CustomField = "hardwareid" +) + +begin { + + if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + function Test-StringEmpty { + param([string]$Text) + # Returns true if string is empty, null, or whitespace + process { [string]::IsNullOrEmpty($Text) -or [string]::IsNullOrWhiteSpace($Text) } + } +} +process { + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + try { + $DeviceDetail = Get-CimInstance -Namespace "root/cimv2/mdm/dmmap" -Class "MDM_DevDetail_Ext01" -Filter "InstanceID='Ext' AND ParentID='./DevDetail'" -ErrorAction Stop + } + catch { + if ($_.Exception.Message -like "Invalid class") { + Write-Error -Message "root/cimv2/mdm/dmmap: MDM_DevDetail_Ext01 WMI class does not exist on system." + } + else { + Write-Error $_ + } + exit 1 + } + + if ( + $DeviceDetail -and + $DeviceDetail.DeviceHardwareData -and + -not $(Test-StringEmpty -Text $DeviceDetail.DeviceHardwareData) + ) { + Write-Host "HardwareID: $($DeviceDetail.DeviceHardwareData)" + } + else { + Write-Error "Unable to retrieve device details or DeviceHardwareData does not exist." + exit 1 + } + +} +end { + +} + diff --git a/Powershell Scripts/Audit Powershell Version.ps1 b/Powershell Scripts/Audit Powershell Version.ps1 index b115664..3a3ae7a 100644 --- a/Powershell Scripts/Audit Powershell Version.ps1 +++ b/Powershell Scripts/Audit Powershell Version.ps1 @@ -1,106 +1,104 @@ -# Reports PowerShell Desktop and/or Core Version(s) to output for automation systems. - -<# -.SYNOPSIS - Reports PowerShell Desktop and/or Core Version(s) to output for automation systems. -.DESCRIPTION - Reports PowerShell Desktop and/or Core Version(s) to output for automation systems. Works on Windows, Linux, and macOS. - -.EXAMPLE - (No Parameters) - ## EXAMPLE OUTPUT WITHOUT PARAMS ## - PowerShell Desktop: 5.1.19041.3570 - PowerShell Core: 7.3.9 - -PARAMETER: -OutputPrefix "PowerShellVersion" - Prefix for the output to identify the version information in automation systems. -.EXAMPLE - -OutputPrefix "PowerShellVersion" - ## EXAMPLE OUTPUT WITH OutputPrefix ## - PowerShellVersion:PowerShell Desktop: 5.1.19041.3570 - PowerShell Core: 7.3.9 - -.OUTPUTS - String containing PowerShell version information -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2012 R2, Linux, macOS - Release Notes: Updated for cross-platform compatibility, removed RMM dependencies -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$OutputPrefix = "PowerShellVersion" -) - -begin { - # Cross-platform OS detection - function Get-OperatingSystem { - if ($IsWindows -or $env:OS -like "Windows*") { - return "Windows" - } - elseif ($IsLinux) { - return "Linux" - } - elseif ($IsMacOS) { - return "macOS" - } - else { - return "Unknown" - } - } - - $OS = Get-OperatingSystem -} -process { - # Get current PowerShell version information - $CurrentPSVersion = $PSVersionTable.PSVersion - $PSEdition = $PSVersionTable.PSEdition - - # Cross-platform PowerShell version reporting - if ($OS -eq "Windows") { - # Windows can have both Desktop and Core versions - if ($PSEdition -eq "Desktop") { - $PSDesktop = "PowerShell Desktop: $CurrentPSVersion" - } - else { - $PSDesktop = "PowerShell Core: $CurrentPSVersion" - } - - # Check for PowerShell Core on Windows - $PSVersionOutput = if (Get-Command -Name "pwsh" -ErrorAction SilentlyContinue) { - try { - $pwshVersion = (pwsh -version 2>$null) -replace 'PowerShell\s+', '' - if ($PSEdition -eq "Desktop") { - "$PSDesktop - PowerShell Core: $pwshVersion" - } - else { - $PSDesktop - } - } - catch { - $PSDesktop - } - } - else { - $PSDesktop - } - } - else { - # Linux and macOS typically use PowerShell Core - $PSVersionOutput = "PowerShell Core: $CurrentPSVersion" - } - - Write-Host "`n$PSVersionOutput`n" - - # Output for automation systems - if ($OutputPrefix) { - Write-Output "${OutputPrefix}:$PSVersionOutput" - } - else { - Write-Output $PSVersionOutput - } -} -end { - - - -} +# Reports PowerShell Desktop and/or Core Version(s) to output for automation systems. + +<# +.SYNOPSIS + Reports PowerShell Desktop and/or Core Version(s) to output for automation systems. +.DESCRIPTION + Reports PowerShell Desktop and/or Core Version(s) to output for automation systems. Works on Windows, Linux, and macOS. + +.EXAMPLE + (No Parameters) + ## EXAMPLE OUTPUT WITHOUT PARAMS ## + PowerShell Desktop: 5.1.19041.3570 - PowerShell Core: 7.3.9 + +PARAMETER: -OutputPrefix "PowerShellVersion" + Prefix for the output to identify the version information in automation systems. +.EXAMPLE + -OutputPrefix "PowerShellVersion" + ## EXAMPLE OUTPUT WITH OutputPrefix ## + PowerShellVersion:PowerShell Desktop: 5.1.19041.3570 - PowerShell Core: 7.3.9 + +.OUTPUTS + String containing PowerShell version information +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2012 R2, Linux, macOS + Release Notes: Updated for cross-platform compatibility, removed RMM dependencies +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$OutputPrefix = "PowerShellVersion" +) + +begin { + # Cross-platform OS detection + function Get-OperatingSystem { + if ($IsWindows -or $env:OS -like "Windows*") { + return "Windows" + } + elseif ($IsLinux) { + return "Linux" + } + elseif ($IsMacOS) { + return "macOS" + } + else { + return "Unknown" + } + } + + $OS = Get-OperatingSystem +} +process { + # Get current PowerShell version information + $CurrentPSVersion = $PSVersionTable.PSVersion + $PSEdition = $PSVersionTable.PSEdition + + # Cross-platform PowerShell version reporting + if ($OS -eq "Windows") { + # Windows can have both Desktop and Core versions + if ($PSEdition -eq "Desktop") { + $PSDesktop = "PowerShell Desktop: $CurrentPSVersion" + } + else { + $PSDesktop = "PowerShell Core: $CurrentPSVersion" + } + + # Check for PowerShell Core on Windows + $PSVersionOutput = if (Get-Command -Name "pwsh" -ErrorAction SilentlyContinue) { + try { + $pwshVersion = (pwsh -version 2>$null) -replace 'PowerShell\s+', '' + if ($PSEdition -eq "Desktop") { + "$PSDesktop - PowerShell Core: $pwshVersion" + } + else { + $PSDesktop + } + } + catch { + $PSDesktop + } + } + else { + $PSDesktop + } + } + else { + # Linux and macOS typically use PowerShell Core + $PSVersionOutput = "PowerShell Core: $CurrentPSVersion" + } + + Write-Host "`n$PSVersionOutput`n" + + # Output for automation systems + if ($OutputPrefix) { + Write-Output "${OutputPrefix}:$PSVersionOutput" + } + else { + Write-Output $PSVersionOutput + } +} +end { + +} diff --git a/Powershell Scripts/Audit UAC Level.ps1 b/Powershell Scripts/Audit UAC Level.ps1 index 34d50c0..54cb4ec 100644 --- a/Powershell Scripts/Audit UAC Level.ps1 +++ b/Powershell Scripts/Audit UAC Level.ps1 @@ -1,259 +1,178 @@ # Condition/Audit UAC Level. Can save the UAC Level to a custom field if specified. -#Requires -Version 2 - -<# -.SYNOPSIS - Condition/Audit UAC Level. Can save the UAC Level to a custom field if specified. -.DESCRIPTION - Condition/Audit UAC Level. Can save the UAC Level to a custom field if specified. - - Exit Code of 0 is that the UAC Level is set to the defaults or higher - Exit Code of 1 is that the UAC Level is to lower than defaults - Exit Code of 2 is when this fails to update a custom field -.EXAMPLE - -CustomField "uac" - Saves the UAC Level to a custom field. -.OUTPUTS - String[] -.NOTES - Minimum OS Architecture Supported: Windows 7, Windows Server 2012 - Release Notes: Renamed script and added Script Variable support -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$CustomField -) - -begin { - if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } - - # https://learn.microsoft.com/en-us/windows/security/identity-protection/user-account-control/user-account-control-group-policy-and-registry-key-settings#registry-key-settings - # Define the path in the registry - $Path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" - - # Define the values to check - $Values = "FilterAdministratorToken", - "EnableUIADesktopToggle", - "ConsentPromptBehaviorAdmin", - "ConsentPromptBehaviorUser", - "EnableInstallerDetection", - "ValidateAdminCodeSignatures", - "EnableSecureUIAPaths", - "EnableLUA", - "PromptOnSecureDesktop", - "EnableVirtualization" - - # This function is to make it easier to set Ninja Custom Fields. - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields we can set. If no type is given we'll assume the input doesn't have to be changed in any way. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The below field requires additional information in order to set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If we received some sort of error it should have an exception property and we'll exit the function with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli is expecting the time to be representing as a Unix Epoch string. So we'll convert what we were given into that format. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw "Value is not present in dropdown" - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } -} -process { - - $UacResults = [PSCustomObject]@{} - # Loop through each value and get each value and add them to $UacResults as a property - $Values | ForEach-Object { - $Value = $_ - $Result = $null - $Result = Get-ItemProperty -Path $Path -Name $Value -ErrorAction SilentlyContinue | Select-Object -ExpandProperty $Value - if ($null -eq $Result) { - switch ($Value) { - 'FilterAdministratorToken' { $Result = 0; break } - 'EnableUIADesktopToggle' { $Result = 0; break } - 'ConsentPromptBehaviorAdmin' { $Result = 5; break } - 'ConsentPromptBehaviorUser' { $Result = 3; break } - 'EnableInstallerDetection' { $Result = 0; break } # Assumes enterprise and not Home - 'ValidateAdminCodeSignatures' { $Result = 0; break } - 'EnableSecureUIAPaths' { $Result = 1; break } - 'EnableLUA' { $Result = 1; break } - 'PromptOnSecureDesktop' { $Result = 1; break } - 'EnableVirtualization' { $Result = 1; break } - Default { $Result = 1 } - } - } - $UacResults | Add-Member -MemberType NoteProperty -Name $Value -Value $Result - } - - # Is UAC enabled or disabled - if ( - $UacResults.ConsentPromptBehaviorAdmin -eq 5 -and - $UacResults.ConsentPromptBehaviorUser -eq 3 -and - $UacResults.EnableLUA -eq 1 -and - $UacResults.FilterAdministratorToken -eq 0 -and - $UacResults.EnableUIADesktopToggle -eq 0 -and - $UacResults.ConsentPromptBehaviorAdmin -eq 5 -and - $UacResults.ConsentPromptBehaviorUser -eq 3 -and - # Enterprise - ( - ( - (Get-CimInstance -ClassName Win32_OperatingSystem).Caption -notlike "*Home*" -and $UacResults.EnableInstallerDetection -eq 1 - ) -or - ( - (Get-CimInstance -ClassName Win32_OperatingSystem).Caption -like "*Home*" -and $UacResults.EnableInstallerDetection -eq 0 - ) - ) -and - $UacResults.ValidateAdminCodeSignatures -eq 0 -and - $UacResults.EnableSecureUIAPaths -eq 1 -and - $UacResults.EnableLUA -eq 1 -and - $UacResults.PromptOnSecureDesktop -eq 1 -and - $UacResults.EnableVirtualization -eq 1 - ) { - "UAC Enabled with defaults." | Write-Host - } - elseif ( - $UacResults.EnableLUA -eq 0 -or - $UacResults.ConsentPromptBehaviorAdmin -eq 0 -or - $UacResults.PromptOnSecureDesktop -eq 0 - ) { - "UAC Disabled." | Write-Host - } - - # Get the UAC Level - $UACLevel = if ( - $UacResults.EnableLUA -eq 0 - ) { - 0 - } - elseif ( - $UacResults.ConsentPromptBehaviorAdmin -eq 5 -and - $UacResults.PromptOnSecureDesktop -eq 0 -and - $UacResults.EnableLUA -eq 1 - ) { - 1 - } - elseif ( - $UacResults.ConsentPromptBehaviorAdmin -eq 5 -and - $UacResults.PromptOnSecureDesktop -eq 1 -and - $UacResults.EnableLUA -eq 1 - ) { - 2 - } - elseif ( - $UacResults.ConsentPromptBehaviorAdmin -eq 2 -and - $UacResults.PromptOnSecureDesktop -eq 1 -and - $UacResults.EnableLUA -eq 1 - ) { - 3 - } - - # Get the Text version of the UAC Level - $UACLevelText = switch ($UACLevel) { - 0 { "Never notify"; break } - 1 { "Notify me only (do not dim my desktop)"; break } - 2 { "Notify me only (default)"; break } - 3 { "Always notify"; break } - Default { "Unknown"; break } - } - - # Output the UAC Level - "UAC Level: $UACLevel = $UACLevelText" | Write-Host - - # Output the UAC settings - $UacResults | Out-String | Write-Host - - # When CustomField is used save the UAC Level to that custom field - if ($CustomField) { - try { - Set-NinjaProperty -Name $CustomField -Value "$UACLevel = $UACLevelText" -ErrorAction Stop - } - catch { - Write-Error "Failed to update Custom Field ($CustomField)" - exit 2 - } - } - - # Return and exit code of 0 if UAC is set to the default or higher, or 1 when not set to the default - if ($UACLevel -ge 2) { - exit 0 - } - elseif ($UACLevel -lt 2) { - exit 1 - } - else { - exit 1 - } -} -end { - - - -} - +#Requires -Version 2 + +<# +.SYNOPSIS + Condition/Audit UAC Level. Can save the UAC Level to a custom field if specified. +.DESCRIPTION + Condition/Audit UAC Level. Can save the UAC Level to a custom field if specified. + + Exit Code of 0 is that the UAC Level is set to the defaults or higher + Exit Code of 1 is that the UAC Level is to lower than defaults + Exit Code of 2 is when this fails to update a custom field +.EXAMPLE + -CustomField "uac" + Saves the UAC Level to a custom field. +.OUTPUTS + String[] +.NOTES + Minimum OS Architecture Supported: Windows 7, Windows Server 2012 + Release Notes: Renamed script and added Script Variable support +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$CustomField +) + +begin { + if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } + + # https://learn.microsoft.com/en-us/windows/security/identity-protection/user-account-control/user-account-control-group-policy-and-registry-key-settings#registry-key-settings + # Define the path in the registry + $Path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" + + # Define the values to check + $Values = "FilterAdministratorToken", + "EnableUIADesktopToggle", + "ConsentPromptBehaviorAdmin", + "ConsentPromptBehaviorUser", + "EnableInstallerDetection", + "ValidateAdminCodeSignatures", + "EnableSecureUIAPaths", + "EnableLUA", + "PromptOnSecureDesktop", + "EnableVirtualization" + + # This function is to make it easier to set Ninja Custom Fields. + +} +process { + + $UacResults = [PSCustomObject]@{} + # Loop through each value and get each value and add them to $UacResults as a property + $Values | ForEach-Object { + $Value = $_ + $Result = $null + $Result = Get-ItemProperty -Path $Path -Name $Value -ErrorAction SilentlyContinue | Select-Object -ExpandProperty $Value + if ($null -eq $Result) { + switch ($Value) { + 'FilterAdministratorToken' { $Result = 0; break } + 'EnableUIADesktopToggle' { $Result = 0; break } + 'ConsentPromptBehaviorAdmin' { $Result = 5; break } + 'ConsentPromptBehaviorUser' { $Result = 3; break } + 'EnableInstallerDetection' { $Result = 0; break } # Assumes enterprise and not Home + 'ValidateAdminCodeSignatures' { $Result = 0; break } + 'EnableSecureUIAPaths' { $Result = 1; break } + 'EnableLUA' { $Result = 1; break } + 'PromptOnSecureDesktop' { $Result = 1; break } + 'EnableVirtualization' { $Result = 1; break } + Default { $Result = 1 } + } + } + $UacResults | Add-Member -MemberType NoteProperty -Name $Value -Value $Result + } + + # Is UAC enabled or disabled + if ( + $UacResults.ConsentPromptBehaviorAdmin -eq 5 -and + $UacResults.ConsentPromptBehaviorUser -eq 3 -and + $UacResults.EnableLUA -eq 1 -and + $UacResults.FilterAdministratorToken -eq 0 -and + $UacResults.EnableUIADesktopToggle -eq 0 -and + $UacResults.ConsentPromptBehaviorAdmin -eq 5 -and + $UacResults.ConsentPromptBehaviorUser -eq 3 -and + # Enterprise + ( + ( + (Get-CimInstance -ClassName Win32_OperatingSystem).Caption -notlike "*Home*" -and $UacResults.EnableInstallerDetection -eq 1 + ) -or + ( + (Get-CimInstance -ClassName Win32_OperatingSystem).Caption -like "*Home*" -and $UacResults.EnableInstallerDetection -eq 0 + ) + ) -and + $UacResults.ValidateAdminCodeSignatures -eq 0 -and + $UacResults.EnableSecureUIAPaths -eq 1 -and + $UacResults.EnableLUA -eq 1 -and + $UacResults.PromptOnSecureDesktop -eq 1 -and + $UacResults.EnableVirtualization -eq 1 + ) { + "UAC Enabled with defaults." | Write-Host + } + elseif ( + $UacResults.EnableLUA -eq 0 -or + $UacResults.ConsentPromptBehaviorAdmin -eq 0 -or + $UacResults.PromptOnSecureDesktop -eq 0 + ) { + "UAC Disabled." | Write-Host + } + + # Get the UAC Level + $UACLevel = if ( + $UacResults.EnableLUA -eq 0 + ) { + 0 + } + elseif ( + $UacResults.ConsentPromptBehaviorAdmin -eq 5 -and + $UacResults.PromptOnSecureDesktop -eq 0 -and + $UacResults.EnableLUA -eq 1 + ) { + 1 + } + elseif ( + $UacResults.ConsentPromptBehaviorAdmin -eq 5 -and + $UacResults.PromptOnSecureDesktop -eq 1 -and + $UacResults.EnableLUA -eq 1 + ) { + 2 + } + elseif ( + $UacResults.ConsentPromptBehaviorAdmin -eq 2 -and + $UacResults.PromptOnSecureDesktop -eq 1 -and + $UacResults.EnableLUA -eq 1 + ) { + 3 + } + + # Get the Text version of the UAC Level + $UACLevelText = switch ($UACLevel) { + 0 { "Never notify"; break } + 1 { "Notify me only (do not dim my desktop)"; break } + 2 { "Notify me only (default)"; break } + 3 { "Always notify"; break } + Default { "Unknown"; break } + } + + # Output the UAC Level + "UAC Level: $UACLevel = $UACLevelText" | Write-Host + + # Output the UAC settings + $UacResults | Out-String | Write-Host + + # When CustomField is used save the UAC Level to that custom field + if ($CustomField) { + try { + } + catch { + Write-Error "Failed to update Custom Field ($CustomField)" + exit 2 + } + } + + # Return and exit code of 0 if UAC is set to the default or higher, or 1 when not set to the default + if ($UACLevel -ge 2) { + exit 0 + } + elseif ($UACLevel -lt 2) { + exit 1 + } + else { + exit 1 + } +} +end { + +} + diff --git a/Powershell Scripts/Backup Event Log to Local Disk.ps1 b/Powershell Scripts/Backup Event Log to Local Disk.ps1 index 2da20bb..c1350eb 100644 --- a/Powershell Scripts/Backup Event Log to Local Disk.ps1 +++ b/Powershell Scripts/Backup Event Log to Local Disk.ps1 @@ -1,328 +1,326 @@ # Exports the specified event logs to a specified location in a compressed zip file. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Exports the specified event logs to a specified location in a compressed zip file. -.DESCRIPTION - Exports the specified event logs to a specified location in a compressed zip file. - The event logs can be exported from a specific date range. - -PARAMETER: -EventLogs "System,Security" -BackupDestination "C:\Temp\EventLogs\" - Exports the specified event logs to a specified location in a compressed zip file. -.EXAMPLE - -EventLogs "System,Security" -BackupDestination "C:\Temp\EventLogs\" - ## EXAMPLE OUTPUT WITH EventLogs ## - [Info] Today is 2023-04-17 - [Info] EventLogs are System,Security - [Info] Backup Destination is C:\Temp\EventLogs\ - [Info] Start Date is null - [Info] End Date is null - [Info] Exporting Event Logs... - [Info] Exported Event Logs to C:\Temp\EventLogs\System.evtx - [Info] Exported Event Logs to C:\Temp\EventLogs\Security.evtx - [Info] Successfully exported Event Logs! - [Info] Compressing Event Logs... - [Info] Compressed Event Logs to C:\Temp\EventLogs\Backup-System-Security-2023-04-17.zip - [Info] Successfully compressed Event Logs! - [Info] Removing Temporary Event Logs... - [Info] Removed Temporary Event Logs! - -PARAMETER: -EventLogs "System,Security" -BackupDestination "C:\Temp\EventLogs\" -StartDate "2023-04-15" -EndDate "2023-04-15" - Exports the specified event logs to a specified location in a compressed zip file. - The event logs can be exported from a specific date range. -.EXAMPLE - -EventLogs "System,Security" -BackupDestination "C:\Temp\EventLogs\" -StartDate "2023-04-15" -EndDate "2023-04-15" - ## EXAMPLE OUTPUT WITH StartDate and EndDate ## - [Info] Today is 2023-04-17 - [Info] EventLogs are System,Security - [Info] Backup Destination is C:\Temp\EventLogs\ - [Info] Start Date is 2023-04-15 - [Info] End Date is 2023-04-16 - [Info] Exporting Event Logs... - [Info] Exported Event Logs to C:\Temp\EventLogs\System.evtx - [Info] Exported Event Logs to C:\Temp\EventLogs\Security.evtx - [Info] Successfully exported Event Logs! - [Info] Compressing Event Logs... - [Info] Compressed Event Logs to C:\Temp\EventLogs\Backup-System-Security-2023-04-17.zip - [Info] Successfully compressed Event Logs! - [Info] Removing Temporary Event Logs... - [Info] Removed Temporary Event Logs! -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [String]$EventLogs, - [String]$BackupDestination, - [DateTime]$StartDate, - [DateTime]$EndDate -) - -begin { - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } -} -process { - if (-not (Test-IsElevated)) { - Write-Host "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - if ($env:eventLogs -and $env:eventLogs -notlike "null") { - $EventLogs = $env:eventLogs - } - - $EventLogNames = $EventLogs -split "," | ForEach-Object { $_.Trim() } - if ($env:backupDestination -and $env:backupDestination -notlike "null") { - $BackupDestination = $env:backupDestination - } - if ($env:startDate -and $env:startDate -notlike "null") { - $StartDate = $env:startDate - } - if ($env:endDate -and $env:endDate -notlike "null") { - $EndDate = $env:endDate - } - - # Validate StartDate and EndDate - if ($StartDate) { - try { - $StartDate = Get-Date -Date $StartDate -ErrorAction Stop - } - catch { - Write-Host "[Error] The specified start date is not a valid date." - exit 1 - } - } - if ($EndDate) { - try { - $EndDate = Get-Date -Date $EndDate -ErrorAction Stop - } - catch { - Write-Host "[Error] The specified end date is not a valid date." - exit 1 - } - } - # Validate BackupDestination is a valid path to a folder - if ($(Test-Path -Path $BackupDestination -PathType Container -ErrorAction SilentlyContinue)) { - $BackupDestination = Get-Item -Path $BackupDestination - } - else { - try { - $BackupDestination = New-Item -Path $BackupDestination -ItemType Directory -ErrorAction Stop - } - catch { - Write-Host "[Error] The specified backup destination is not a valid path to a folder." - exit 1 - } - } - - Write-Host "[Info] Today is $(Get-Date -Format yyyy-MM-dd-HH-mm)" - - # Validate EventLogs are valid event logs - if ( - $( - wevtutil.exe el | ForEach-Object { - if ($EventLogNames -and $($EventLogNames -contains $_ -or $EventLogNames -like $_)) { $_ } - } - ).Count -eq 0 - ) { - Write-Host "[Error] No Event Logs matching: $EventLogNames" - } - - Write-Host "[Info] EventLogs are $EventLogNames" - if ($EventLogNames -and $EventLogNames.Count -gt 0) { - Write-Host "[Info] Backup Destination is $BackupDestination" - - # If the start date is specified, check if it's a valid date - if ($StartDate) { - try { - $StartDate = $(Get-Date -Date $StartDate).ToUniversalTime() - } - catch { - Write-Host "[Error] The specified start date is not a valid date." - exit 1 - } - Write-Host "[Info] Start Date is $(Get-Date -Date $StartDate -Format yyyy-MM-dd-HH-mm)" - } - else { - Write-Host "[Info] Start Date is null" - } - if ($EndDate) { - try { - $EndDate = $(Get-Date -Date $EndDate).ToUniversalTime() - } - catch { - Write-Host "[Error] The specified end date is not a valid date." - exit 1 - } - Write-Host "[Info] End Date is $(Get-Date -Date $EndDate -Format yyyy-MM-dd-HH-mm)" - } - else { - Write-Host "[Info] End Date is null" - } - - # Check if the start date after the end date - if ($StartDate -and $EndDate -and $StartDate -gt $EndDate) { - # Flip the dates if the start date is after the end date - $OldEndDate = $EndDate - $OldStartDate = $StartDate - $EndDate = $OldStartDate - $StartDate = $OldEndDate - Write-Host "[Info] Start Date is after the end date. Flipping dates." - } - - Write-Host "[Info] Exporting Event Logs..." - foreach ($EventLog in $EventLogNames) { - $EventLogPath = $(Join-Path -Path $BackupDestination -ChildPath "$EventLog.evtx") - try { - if ($StartDate -and $EndDate) { - wevtutil.exe epl "$EventLog" "$EventLogPath" /ow:true /query:"*[System[TimeCreated[@SystemTime>='$(Get-Date -Date $StartDate -UFormat "%Y-%m-%dT%H:%M:%S")' and @SystemTime<='$(Get-Date -Date $EndDate -UFormat "%Y-%m-%dT%H:%M:%S")']]]" 2>$null - } - elseif ($StartDate) { - wevtutil.exe epl "$EventLog" "$EventLogPath" /ow:true /query:"*[System[TimeCreated[@SystemTime>='$(Get-Date -Date $StartDate -UFormat "%Y-%m-%dT%H:%M:%S")']]]" 2>$null - } - elseif ($EndDate) { - wevtutil.exe epl "$EventLog" "$EventLogPath" /ow:true /query:"*[System[TimeCreated[@SystemTime<='$(Get-Date -Date $EndDate -UFormat "%Y-%m-%dT%H:%M:%S")']]]" 2>$null - } - else { - wevtutil.exe epl "$EventLog" "$EventLogPath" /ow:true /query:"*[System[TimeCreated[@SystemTime>='1970-01-01T00:00:00']]]" 2>$null - } - if ($(Test-Path -Path $EventLogPath -ErrorAction SilentlyContinue)) { - # Get the number of events in the log - $EventCount = $(Get-WinEvent -Path $EventLogPath -ErrorAction SilentlyContinue).Count - if ($EventCount -and $EventCount -gt 0) { - Write-Host "[Info] Found $EventCount events from $EventLog" - } - else { - Write-Host "[Warn] No events found in $EventLog" - continue - } - Write-Host "[Info] Exported Event Logs to $EventLogPath" - } - else { - throw - } - } - catch { - Write-Host "[Error] Failed to export event logs $EventLog" - continue - } - } - - Write-Host "[Info] Compressing Event Logs..." - - # Get the event log paths that where created - $JoinedPaths = foreach ($EventLog in $EventLogNames) { - # Join the Backup Destination and the Event Log Name - $JoinedPath = Join-Path -Path $BackupDestination -ChildPath "$EventLog.evtx" -ErrorAction SilentlyContinue - if ($(Test-Path -Path $JoinedPath -ErrorAction SilentlyContinue)) { - # Get the saved event log path - Get-Item -Path $JoinedPath -ErrorAction SilentlyContinue - } - } - $JoinedPaths = $JoinedPaths | Where-Object { $(Test-Path -Path $_ -ErrorAction SilentlyContinue) } - - try { - # Create a destination path to save the compressed file to - # Backup--.zip - $Destination = Join-Path -Path $($BackupDestination) -ChildPath $( - @( - "Backup-", - $($EventLogNames -join '-'), - "-", - $(Get-Date -Format yyyy-MM-dd-HH-mm), - ".zip" - ) -join '' - ) - - $CompressArchiveSplat = @{ - Path = $JoinedPaths - DestinationPath = $Destination - Update = $true - } - - # # If the destination path already exists, update the archive instead of creating a new one - # if ($(Test-Path -Path $Destination -ErrorAction SilentlyContinue)) { - # $CompressArchiveSplat.Add("Update", $true) - # } - - # Compress the Event Logs - $CompressError = $true - $ErrorCount = 0 - $SecondsToSleep = 1 - $TimeOut = 120 - while ($CompressError) { - try { - $CompressError = $false - Compress-Archive @CompressArchiveSplat -ErrorAction Stop - break - } - catch { - $CompressError = $true - } - - if ($CompressError) { - if ($ErrorCount -gt $TimeOut) { - Write-Host "[Warn] Skipping compression... Timed out." - } - if ($ErrorCount -eq 0) { - Write-Host "[Info] Waiting for wevtutil.exe to close file." - } - Start-Sleep -Seconds $SecondsToSleep - } - $ErrorCount++ - } - if ($CompressError) { - Write-Host "[Error] Failed to Compress Event Logs." - } - else { - Write-Host "[Info] Compressed Event Logs to $($Destination)" - } - } - catch { - Write-Host "[Error] Failed to compress event logs." - } - - if ($(Test-Path -Path $Destination -ErrorAction SilentlyContinue)) { - Write-Host "[Info] Removing Temporary Event Logs..." - foreach ($EventLogPath in $JoinedPaths) { - try { - Remove-Item -Path $EventLogPath -Force -ErrorAction SilentlyContinue - Write-Host "[Info] Removed Temporary Event Logs: $EventLogPath" - } - catch {} - } - } - else { - Write-Host "[Info] Renaming Event Logs..." - foreach ($EventLogPath in $JoinedPaths) { - if ($(Test-Path -Path $EventLogPath -ErrorAction SilentlyContinue)) { - try { - $NewPath = Rename-Item -Path $EventLogPath -NewName "$($EventLogPath.BaseName)-$(Get-Date -Format yyyy-MM-dd-HH-mm).evtx" -PassThru -ErrorAction Stop - Write-Host "[Info] Event Logs saved to: $NewPath" - } - catch { - Write-Host "[Info] Event Logs saved to: $EventLogPath" - } - } - else { - Write-Host "[Info] Event Logs saved to: $EventLogPath" - } - } - } - } - else { - Write-Host "[Error] No Event Logs were specified." - exit 1 - } -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Exports the specified event logs to a specified location in a compressed zip file. +.DESCRIPTION + Exports the specified event logs to a specified location in a compressed zip file. + The event logs can be exported from a specific date range. + +PARAMETER: -EventLogs "System,Security" -BackupDestination "C:\Temp\EventLogs\" + Exports the specified event logs to a specified location in a compressed zip file. +.EXAMPLE + -EventLogs "System,Security" -BackupDestination "C:\Temp\EventLogs\" + ## EXAMPLE OUTPUT WITH EventLogs ## + [Info] Today is 2023-04-17 + [Info] EventLogs are System,Security + [Info] Backup Destination is C:\Temp\EventLogs\ + [Info] Start Date is null + [Info] End Date is null + [Info] Exporting Event Logs... + [Info] Exported Event Logs to C:\Temp\EventLogs\System.evtx + [Info] Exported Event Logs to C:\Temp\EventLogs\Security.evtx + [Info] Successfully exported Event Logs! + [Info] Compressing Event Logs... + [Info] Compressed Event Logs to C:\Temp\EventLogs\Backup-System-Security-2023-04-17.zip + [Info] Successfully compressed Event Logs! + [Info] Removing Temporary Event Logs... + [Info] Removed Temporary Event Logs! + +PARAMETER: -EventLogs "System,Security" -BackupDestination "C:\Temp\EventLogs\" -StartDate "2023-04-15" -EndDate "2023-04-15" + Exports the specified event logs to a specified location in a compressed zip file. + The event logs can be exported from a specific date range. +.EXAMPLE + -EventLogs "System,Security" -BackupDestination "C:\Temp\EventLogs\" -StartDate "2023-04-15" -EndDate "2023-04-15" + ## EXAMPLE OUTPUT WITH StartDate and EndDate ## + [Info] Today is 2023-04-17 + [Info] EventLogs are System,Security + [Info] Backup Destination is C:\Temp\EventLogs\ + [Info] Start Date is 2023-04-15 + [Info] End Date is 2023-04-16 + [Info] Exporting Event Logs... + [Info] Exported Event Logs to C:\Temp\EventLogs\System.evtx + [Info] Exported Event Logs to C:\Temp\EventLogs\Security.evtx + [Info] Successfully exported Event Logs! + [Info] Compressing Event Logs... + [Info] Compressed Event Logs to C:\Temp\EventLogs\Backup-System-Security-2023-04-17.zip + [Info] Successfully compressed Event Logs! + [Info] Removing Temporary Event Logs... + [Info] Removed Temporary Event Logs! +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [String]$EventLogs, + [String]$BackupDestination, + [DateTime]$StartDate, + [DateTime]$EndDate +) + +begin { + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } +} +process { + if (-not (Test-IsElevated)) { + Write-Host "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + if ($env:eventLogs -and $env:eventLogs -notlike "null") { + $EventLogs = $env:eventLogs + } + + $EventLogNames = $EventLogs -split "," | ForEach-Object { $_.Trim() } + if ($env:backupDestination -and $env:backupDestination -notlike "null") { + $BackupDestination = $env:backupDestination + } + if ($env:startDate -and $env:startDate -notlike "null") { + $StartDate = $env:startDate + } + if ($env:endDate -and $env:endDate -notlike "null") { + $EndDate = $env:endDate + } + + # Validate StartDate and EndDate + if ($StartDate) { + try { + $StartDate = Get-Date -Date $StartDate -ErrorAction Stop + } + catch { + Write-Host "[Error] The specified start date is not a valid date." + exit 1 + } + } + if ($EndDate) { + try { + $EndDate = Get-Date -Date $EndDate -ErrorAction Stop + } + catch { + Write-Host "[Error] The specified end date is not a valid date." + exit 1 + } + } + # Validate BackupDestination is a valid path to a folder + if ($(Test-Path -Path $BackupDestination -PathType Container -ErrorAction SilentlyContinue)) { + $BackupDestination = Get-Item -Path $BackupDestination + } + else { + try { + $BackupDestination = New-Item -Path $BackupDestination -ItemType Directory -ErrorAction Stop + } + catch { + Write-Host "[Error] The specified backup destination is not a valid path to a folder." + exit 1 + } + } + + Write-Host "[Info] Today is $(Get-Date -Format yyyy-MM-dd-HH-mm)" + + # Validate EventLogs are valid event logs + if ( + $( + wevtutil.exe el | ForEach-Object { + if ($EventLogNames -and $($EventLogNames -contains $_ -or $EventLogNames -like $_)) { $_ } + } + ).Count -eq 0 + ) { + Write-Host "[Error] No Event Logs matching: $EventLogNames" + } + + Write-Host "[Info] EventLogs are $EventLogNames" + if ($EventLogNames -and $EventLogNames.Count -gt 0) { + Write-Host "[Info] Backup Destination is $BackupDestination" + + # If the start date is specified, check if it's a valid date + if ($StartDate) { + try { + $StartDate = $(Get-Date -Date $StartDate).ToUniversalTime() + } + catch { + Write-Host "[Error] The specified start date is not a valid date." + exit 1 + } + Write-Host "[Info] Start Date is $(Get-Date -Date $StartDate -Format yyyy-MM-dd-HH-mm)" + } + else { + Write-Host "[Info] Start Date is null" + } + if ($EndDate) { + try { + $EndDate = $(Get-Date -Date $EndDate).ToUniversalTime() + } + catch { + Write-Host "[Error] The specified end date is not a valid date." + exit 1 + } + Write-Host "[Info] End Date is $(Get-Date -Date $EndDate -Format yyyy-MM-dd-HH-mm)" + } + else { + Write-Host "[Info] End Date is null" + } + + # Check if the start date after the end date + if ($StartDate -and $EndDate -and $StartDate -gt $EndDate) { + # Flip the dates if the start date is after the end date + $OldEndDate = $EndDate + $OldStartDate = $StartDate + $EndDate = $OldStartDate + $StartDate = $OldEndDate + Write-Host "[Info] Start Date is after the end date. Flipping dates." + } + + Write-Host "[Info] Exporting Event Logs..." + foreach ($EventLog in $EventLogNames) { + $EventLogPath = $(Join-Path -Path $BackupDestination -ChildPath "$EventLog.evtx") + try { + if ($StartDate -and $EndDate) { + wevtutil.exe epl "$EventLog" "$EventLogPath" /ow:true /query:"*[System[TimeCreated[@SystemTime>='$(Get-Date -Date $StartDate -UFormat "%Y-%m-%dT%H:%M:%S")' and @SystemTime<='$(Get-Date -Date $EndDate -UFormat "%Y-%m-%dT%H:%M:%S")']]]" 2>$null + } + elseif ($StartDate) { + wevtutil.exe epl "$EventLog" "$EventLogPath" /ow:true /query:"*[System[TimeCreated[@SystemTime>='$(Get-Date -Date $StartDate -UFormat "%Y-%m-%dT%H:%M:%S")']]]" 2>$null + } + elseif ($EndDate) { + wevtutil.exe epl "$EventLog" "$EventLogPath" /ow:true /query:"*[System[TimeCreated[@SystemTime<='$(Get-Date -Date $EndDate -UFormat "%Y-%m-%dT%H:%M:%S")']]]" 2>$null + } + else { + wevtutil.exe epl "$EventLog" "$EventLogPath" /ow:true /query:"*[System[TimeCreated[@SystemTime>='1970-01-01T00:00:00']]]" 2>$null + } + if ($(Test-Path -Path $EventLogPath -ErrorAction SilentlyContinue)) { + # Get the number of events in the log + $EventCount = $(Get-WinEvent -Path $EventLogPath -ErrorAction SilentlyContinue).Count + if ($EventCount -and $EventCount -gt 0) { + Write-Host "[Info] Found $EventCount events from $EventLog" + } + else { + Write-Host "[Warn] No events found in $EventLog" + continue + } + Write-Host "[Info] Exported Event Logs to $EventLogPath" + } + else { + throw + } + } + catch { + Write-Host "[Error] Failed to export event logs $EventLog" + continue + } + } + + Write-Host "[Info] Compressing Event Logs..." + + # Get the event log paths that where created + $JoinedPaths = foreach ($EventLog in $EventLogNames) { + # Join the Backup Destination and the Event Log Name + $JoinedPath = Join-Path -Path $BackupDestination -ChildPath "$EventLog.evtx" -ErrorAction SilentlyContinue + if ($(Test-Path -Path $JoinedPath -ErrorAction SilentlyContinue)) { + # Get the saved event log path + Get-Item -Path $JoinedPath -ErrorAction SilentlyContinue + } + } + $JoinedPaths = $JoinedPaths | Where-Object { $(Test-Path -Path $_ -ErrorAction SilentlyContinue) } + + try { + # Create a destination path to save the compressed file to + # Backup--.zip + $Destination = Join-Path -Path $($BackupDestination) -ChildPath $( + @( + "Backup-", + $($EventLogNames -join '-'), + "-", + $(Get-Date -Format yyyy-MM-dd-HH-mm), + ".zip" + ) -join '' + ) + + $CompressArchiveSplat = @{ + Path = $JoinedPaths + DestinationPath = $Destination + Update = $true + } + + # # If the destination path already exists, update the archive instead of creating a new one + # if ($(Test-Path -Path $Destination -ErrorAction SilentlyContinue)) { + # $CompressArchiveSplat.Add("Update", $true) + # } + + # Compress the Event Logs + $CompressError = $true + $ErrorCount = 0 + $SecondsToSleep = 1 + $TimeOut = 120 + while ($CompressError) { + try { + $CompressError = $false + Compress-Archive @CompressArchiveSplat -ErrorAction Stop + break + } + catch { + $CompressError = $true + } + + if ($CompressError) { + if ($ErrorCount -gt $TimeOut) { + Write-Host "[Warn] Skipping compression... Timed out." + } + if ($ErrorCount -eq 0) { + Write-Host "[Info] Waiting for wevtutil.exe to close file." + } + Start-Sleep -Seconds $SecondsToSleep + } + $ErrorCount++ + } + if ($CompressError) { + Write-Host "[Error] Failed to Compress Event Logs." + } + else { + Write-Host "[Info] Compressed Event Logs to $($Destination)" + } + } + catch { + Write-Host "[Error] Failed to compress event logs." + } + + if ($(Test-Path -Path $Destination -ErrorAction SilentlyContinue)) { + Write-Host "[Info] Removing Temporary Event Logs..." + foreach ($EventLogPath in $JoinedPaths) { + try { + Remove-Item -Path $EventLogPath -Force -ErrorAction SilentlyContinue + Write-Host "[Info] Removed Temporary Event Logs: $EventLogPath" + } + catch {} + } + } + else { + Write-Host "[Info] Renaming Event Logs..." + foreach ($EventLogPath in $JoinedPaths) { + if ($(Test-Path -Path $EventLogPath -ErrorAction SilentlyContinue)) { + try { + $NewPath = Rename-Item -Path $EventLogPath -NewName "$($EventLogPath.BaseName)-$(Get-Date -Format yyyy-MM-dd-HH-mm).evtx" -PassThru -ErrorAction Stop + Write-Host "[Info] Event Logs saved to: $NewPath" + } + catch { + Write-Host "[Info] Event Logs saved to: $EventLogPath" + } + } + else { + Write-Host "[Info] Event Logs saved to: $EventLogPath" + } + } + } + } + else { + Write-Host "[Error] No Event Logs were specified." + exit 1 + } +} +end { + +} diff --git a/Powershell Scripts/Block Microsoft Account Creation.ps1 b/Powershell Scripts/Block Microsoft Account Creation.ps1 index 2a54e44..3414cd4 100644 --- a/Powershell Scripts/Block Microsoft Account Creation.ps1 +++ b/Powershell Scripts/Block Microsoft Account Creation.ps1 @@ -1,132 +1,130 @@ # Block or Allow the ability to create Microsoft Accounts. -#Requires -Version 5.1 -RunAsAdministrator - -<# -.SYNOPSIS - Block or Allow the ability to create Microsoft Accounts. -.DESCRIPTION - Block or Allow the ability to create Microsoft Accounts. -.EXAMPLE - PS C:\> Disable-MicrosoftAccountCreation.ps1 - Blocks creation of Microsoft Accounts. -PARAMETER: -Allow - Allows creation of Microsoft Accounts. -.EXAMPLE - PS C:\> Disable-MicrosoftAccountCreation.ps1 - Allows creation of Microsoft Accounts. -PARAMETER: -ForceReboot - Blocks creation of Microsoft Accounts and reboot after 2 minutes. -.EXAMPLE - PS C:\> Disable-MicrosoftAccountCreation.ps1 -ForceReboot - Blocks creation of Microsoft Accounts and reboot after 2 minutes. -.INPUTS - None -.OUTPUTS - String[] -.NOTES - Release Notes: Updated Calculated Name - Only usable on Windows 10, possible Windows 11(UNTESTED/UNVERIFIED). -.COMPONENT - LocalBuiltInAccountManagement -#> - -[CmdletBinding()] -param ( - [Parameter()] - [switch] - $Allow, - [switch] - $BlockLogin, - [switch] - $ForceReboot -) - -begin { - function Set-ItemProp { - param ( - $Path, - $Name, - $Value, - [ValidateSet("DWord", "QWord", "String", "ExpandedString", "Binary", "MultiString", "Unknown")] - $PropertyType = "DWord" - ) - if (-not $(Test-Path -Path $Path)) { - # Check if path does not exist and create the path - New-Item -Path $Path -Force | Out-Null - } - if ((Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue)) { - # Update property and print out what it was changed from and changed to - $CurrentValue = (Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue).$Name - try { - Set-ItemProperty -Path $Path -Name $Name -Value $Value -Force -Confirm:$false -ErrorAction Stop | Out-Null - } - catch { - Write-Error "[Error] Unable to Set registry key for $Name please see below error!" - Write-Error $_ - exit 1 - } - Write-Host "$Path\$Name changed from $CurrentValue to $($(Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue).$Name)" - } - else { - # Create property with value - try { - New-ItemProperty -Path $Path -Name $Name -Value $Value -PropertyType $PropertyType -Force -Confirm:$false -ErrorAction Stop | Out-Null - } - catch { - Write-Error "[Error] Unable to Set registry key for $Name please see below error!" - Write-Error $_ - exit 1 - } - Write-Host "Set $Path\$Name to $($(Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue).$Name)" - } - } - if ($env:allowOrBlockMicrosoftAccountCreation -like "Allow") { - $Allow = $true - } - elseif ($env:allowOrBlockMicrosoftAccountCreation -like "Block Creation" -or $env:allowOrBlockMicrosoftAccountCreation -like "Block Creation And Login") { - $Allow = $false - } - if ($env:forceReboot -like "true") { - $ForceReboot = $true - } -} -process { - if ($Allow) { - # Allow - Set-ItemProp -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "NoConnectedUser" -Value 0 - Set-ItemProp -Path "HKLM:\SOFTWARE\Microsoft\PolicyManager\default\Settings\AllowYourAccount" -Name "value" -Value 1 - Remove-ItemProperty -Path "HKLM:\Software\Policies\Microsoft\MicrosoftAccount" -Name "DisableUserAuth" -ErrorAction SilentlyContinue - - Write-Host "Allowing Microsoft accounts to be created." - } - else { - # Block - if ($env:allowOrBlockMicrosoftAccountCreation -like "Block Creation And Login" -or $BlockLogin) { - # Block MS Account Creation and Login - Set-ItemProp -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "NoConnectedUser" -Value 3 - Set-ItemProp -Path "HKLM:\Software\Policies\Microsoft\MicrosoftAccount" -Name "DisableUserAuth" -Value 1 - } - else { - # Block MS Account Creation - Set-ItemProp -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "NoConnectedUser" -Value 1 - } - Set-ItemProp -Path "HKLM:\SOFTWARE\Microsoft\PolicyManager\default\Settings\AllowYourAccount" -Name "value" -Value 0 - - Write-Host "Blocking Microsoft accounts from being created." - } - - if ($ForceReboot) { - # Reboot - shutdown.exe -r -t 60 - } - else { - # Do not reboot - Write-Host "Please restart $([System.Net.Dns]::GetHostName())" - } -} -end { - - - -} - +#Requires -Version 5.1 -RunAsAdministrator + +<# +.SYNOPSIS + Block or Allow the ability to create Microsoft Accounts. +.DESCRIPTION + Block or Allow the ability to create Microsoft Accounts. +.EXAMPLE + PS C:\> Disable-MicrosoftAccountCreation.ps1 + Blocks creation of Microsoft Accounts. +PARAMETER: -Allow + Allows creation of Microsoft Accounts. +.EXAMPLE + PS C:\> Disable-MicrosoftAccountCreation.ps1 + Allows creation of Microsoft Accounts. +PARAMETER: -ForceReboot + Blocks creation of Microsoft Accounts and reboot after 2 minutes. +.EXAMPLE + PS C:\> Disable-MicrosoftAccountCreation.ps1 -ForceReboot + Blocks creation of Microsoft Accounts and reboot after 2 minutes. +.INPUTS + None +.OUTPUTS + String[] +.NOTES + Release Notes: Updated Calculated Name + Only usable on Windows 10, possible Windows 11(UNTESTED/UNVERIFIED). +.COMPONENT + LocalBuiltInAccountManagement +#> + +[CmdletBinding()] +param ( + [Parameter()] + [switch] + $Allow, + [switch] + $BlockLogin, + [switch] + $ForceReboot +) + +begin { + function Set-ItemProp { + param ( + $Path, + $Name, + $Value, + [ValidateSet("DWord", "QWord", "String", "ExpandedString", "Binary", "MultiString", "Unknown")] + $PropertyType = "DWord" + ) + if (-not $(Test-Path -Path $Path)) { + # Check if path does not exist and create the path + New-Item -Path $Path -Force | Out-Null + } + if ((Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue)) { + # Update property and print out what it was changed from and changed to + $CurrentValue = (Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue).$Name + try { + Set-ItemProperty -Path $Path -Name $Name -Value $Value -Force -Confirm:$false -ErrorAction Stop | Out-Null + } + catch { + Write-Error "[Error] Unable to Set registry key for $Name please see below error!" + Write-Error $_ + exit 1 + } + Write-Host "$Path\$Name changed from $CurrentValue to $($(Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue).$Name)" + } + else { + # Create property with value + try { + New-ItemProperty -Path $Path -Name $Name -Value $Value -PropertyType $PropertyType -Force -Confirm:$false -ErrorAction Stop | Out-Null + } + catch { + Write-Error "[Error] Unable to Set registry key for $Name please see below error!" + Write-Error $_ + exit 1 + } + Write-Host "Set $Path\$Name to $($(Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue).$Name)" + } + } + if ($env:allowOrBlockMicrosoftAccountCreation -like "Allow") { + $Allow = $true + } + elseif ($env:allowOrBlockMicrosoftAccountCreation -like "Block Creation" -or $env:allowOrBlockMicrosoftAccountCreation -like "Block Creation And Login") { + $Allow = $false + } + if ($env:forceReboot -like "true") { + $ForceReboot = $true + } +} +process { + if ($Allow) { + # Allow + Set-ItemProp -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "NoConnectedUser" -Value 0 + Set-ItemProp -Path "HKLM:\SOFTWARE\Microsoft\PolicyManager\default\Settings\AllowYourAccount" -Name "value" -Value 1 + Remove-ItemProperty -Path "HKLM:\Software\Policies\Microsoft\MicrosoftAccount" -Name "DisableUserAuth" -ErrorAction SilentlyContinue + + Write-Host "Allowing Microsoft accounts to be created." + } + else { + # Block + if ($env:allowOrBlockMicrosoftAccountCreation -like "Block Creation And Login" -or $BlockLogin) { + # Block MS Account Creation and Login + Set-ItemProp -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "NoConnectedUser" -Value 3 + Set-ItemProp -Path "HKLM:\Software\Policies\Microsoft\MicrosoftAccount" -Name "DisableUserAuth" -Value 1 + } + else { + # Block MS Account Creation + Set-ItemProp -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "NoConnectedUser" -Value 1 + } + Set-ItemProp -Path "HKLM:\SOFTWARE\Microsoft\PolicyManager\default\Settings\AllowYourAccount" -Name "value" -Value 0 + + Write-Host "Blocking Microsoft accounts from being created." + } + + if ($ForceReboot) { + # Reboot + shutdown.exe -r -t 60 + } + else { + # Do not reboot + Write-Host "Please restart $([System.Net.Dns]::GetHostName())" + } +} +end { + +} + diff --git a/Powershell Scripts/Block Windows 10 to 11 Upgrade.ps1 b/Powershell Scripts/Block Windows 10 to 11 Upgrade.ps1 index 937eb1d..f732c35 100644 --- a/Powershell Scripts/Block Windows 10 to 11 Upgrade.ps1 +++ b/Powershell Scripts/Block Windows 10 to 11 Upgrade.ps1 @@ -1,114 +1,111 @@ # Disables Windows 11 upgrade by locking the TargetReleaseVersion and TargetReleaseVersionInfo to the currently installed version. - -<# -.SYNOPSIS - Disables Windows 11 upgrade by locking the TargetReleaseVersion and TargetReleaseVersionInfo to the currently installed version. -.DESCRIPTION - Disables Windows 11 upgrade by locking the TargetReleaseVersion and TargetReleaseVersionInfo to the currently installed version. -.EXAMPLE - -TargetReleaseVersion "22H2" - Disables Windows 11 upgrade by setting the TargetReleaseVersion to 22H2 -.EXAMPLE - -TargetReleaseVersion "22H1" - Disables Windows 11 upgrade by setting the TargetReleaseVersion to 22H1 -.EXAMPLE - -TargetReleaseVersion "2009" - Disables Windows 11 upgrade by setting the TargetReleaseVersion to 2009 -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 10 - Release Notes: Updated Calculated Name -#> -[CmdletBinding()] -param ( - [string] - $TargetReleaseVersion = "22H2" -) - -begin { - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - function Set-ItemProp { - param ( - $Path, - $Name, - $Value, - [ValidateSet("DWord", "QWord", "String", "ExpandedString", "Binary", "MultiString", "Unknown")] - $PropertyType = "DWord" - ) - # Do not output errors and continue - $ErrorActionPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue - if (-not $(Test-Path -Path $Path)) { - # Check if path does not exist and create the path - New-Item -Path $Path -Force | Out-Null - } - if ((Get-ItemProperty -Path $Path -Name $Name)) { - # Update property and print out what it was changed from and changed to - $CurrentValue = Get-ItemProperty -Path $Path -Name $Name - try { - Set-ItemProperty -Path $Path -Name $Name -Value $Value -Force -Confirm:$false -ErrorAction Stop | Out-Null - } - catch { - Write-Error $_ - } - Write-Host "$Path\$Name changed from $CurrentValue to $(Get-ItemProperty -Path $Path -Name $Name)" - } - else { - # Create property with value - try { - New-ItemProperty -Path $Path -Name $Name -Value $Value -PropertyType $PropertyType -Force -Confirm:$false -ErrorAction Stop | Out-Null - } - catch { - Write-Error $_ - } - Write-Host "Set $Path$Name to $(Get-ItemProperty -Path $Path -Name $Name)" - } - $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Continue - } -} -process { - if ([System.Environment]::OSVersion.Version.Build -lt 10240 -or [System.Environment]::OSVersion.Version.Build -gt 22000) { - Write-Error "OS Version is not Windows 10." - exit 1 - } - - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - if ($env:targetRelease -and $env:targetRelease -notlike "null") { - if ($env:targetRelease -like "Current") { - # Get Current Version - $release = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ReleaseId).ReleaseId - $ver = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name DisplayVersion).DisplayVersion - $TargetReleaseVersion = if ($release -eq '2009') { $ver } Else { $release } - } - else { - $TargetReleaseVersion = $env:targetRelease - } - } - - # Block Windows 11 Upgrade by changing the target release version to the current version - try { - Set-ItemProp -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name "TargetReleaseVersion" -Value 1 -PropertyType DWord - Set-ItemProp -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name "TargetReleaseVersionInfo" -Value "$TargetReleaseVersion" -PropertyType String - Set-ItemProp -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Name "SvOfferDeclined" -Value 1646085160366 -PropertyType QWord - } - catch { - Write-Error $_ - Write-Host "Failed to block Windows 11 Upgrade." - exit 1 - } - exit 0 -} -end { - - - -} - - + +<# +.SYNOPSIS + Disables Windows 11 upgrade by locking the TargetReleaseVersion and TargetReleaseVersionInfo to the currently installed version. +.DESCRIPTION + Disables Windows 11 upgrade by locking the TargetReleaseVersion and TargetReleaseVersionInfo to the currently installed version. +.EXAMPLE + -TargetReleaseVersion "22H2" + Disables Windows 11 upgrade by setting the TargetReleaseVersion to 22H2 +.EXAMPLE + -TargetReleaseVersion "22H1" + Disables Windows 11 upgrade by setting the TargetReleaseVersion to 22H1 +.EXAMPLE + -TargetReleaseVersion "2009" + Disables Windows 11 upgrade by setting the TargetReleaseVersion to 2009 +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 10 + Release Notes: Updated Calculated Name +#> +[CmdletBinding()] +param ( + [string] + $TargetReleaseVersion = "22H2" +) + +begin { + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + function Set-ItemProp { + param ( + $Path, + $Name, + $Value, + [ValidateSet("DWord", "QWord", "String", "ExpandedString", "Binary", "MultiString", "Unknown")] + $PropertyType = "DWord" + ) + # Do not output errors and continue + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue + if (-not $(Test-Path -Path $Path)) { + # Check if path does not exist and create the path + New-Item -Path $Path -Force | Out-Null + } + if ((Get-ItemProperty -Path $Path -Name $Name)) { + # Update property and print out what it was changed from and changed to + $CurrentValue = Get-ItemProperty -Path $Path -Name $Name + try { + Set-ItemProperty -Path $Path -Name $Name -Value $Value -Force -Confirm:$false -ErrorAction Stop | Out-Null + } + catch { + Write-Error $_ + } + Write-Host "$Path\$Name changed from $CurrentValue to $(Get-ItemProperty -Path $Path -Name $Name)" + } + else { + # Create property with value + try { + New-ItemProperty -Path $Path -Name $Name -Value $Value -PropertyType $PropertyType -Force -Confirm:$false -ErrorAction Stop | Out-Null + } + catch { + Write-Error $_ + } + Write-Host "Set $Path$Name to $(Get-ItemProperty -Path $Path -Name $Name)" + } + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Continue + } +} +process { + if ([System.Environment]::OSVersion.Version.Build -lt 10240 -or [System.Environment]::OSVersion.Version.Build -gt 22000) { + Write-Error "OS Version is not Windows 10." + exit 1 + } + + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + if ($env:targetRelease -and $env:targetRelease -notlike "null") { + if ($env:targetRelease -like "Current") { + # Get Current Version + $release = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ReleaseId).ReleaseId + $ver = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name DisplayVersion).DisplayVersion + $TargetReleaseVersion = if ($release -eq '2009') { $ver } Else { $release } + } + else { + $TargetReleaseVersion = $env:targetRelease + } + } + + # Block Windows 11 Upgrade by changing the target release version to the current version + try { + Set-ItemProp -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name "TargetReleaseVersion" -Value 1 -PropertyType DWord + Set-ItemProp -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name "TargetReleaseVersionInfo" -Value "$TargetReleaseVersion" -PropertyType String + Set-ItemProp -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Name "SvOfferDeclined" -Value 1646085160366 -PropertyType QWord + } + catch { + Write-Error $_ + Write-Host "Failed to block Windows 11 Upgrade." + exit 1 + } + exit 0 +} +end { + +} + diff --git a/Powershell Scripts/Boot Config Changed Alert.ps1 b/Powershell Scripts/Boot Config Changed Alert.ps1 index 0a7a1df..4cc25b8 100644 --- a/Powershell Scripts/Boot Config Changed Alert.ps1 +++ b/Powershell Scripts/Boot Config Changed Alert.ps1 @@ -1,96 +1,93 @@ # Checks if the BootConfig file was modified from last run. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Checks if the BootConfig file was modified from last run. -.DESCRIPTION - Checks if the BootConfig file was modified from last run. - On first run this will not produce an error, but will create a cache file for later comparison. -.EXAMPLE - No parameters needed. -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Updated Calculated Name -#> - -[CmdletBinding()] -param ( - # Path and file where the cache file will be saved for comparison - [string] - $CachePath = "C:\ProgramData\NinjaRMMAgent\scripting\Test-BootConfig.clixml" -) - -begin { - if ($env:CachePath) { - $CachePath = $env:CachePath - } - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - -} -process { - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Get content and create hash of BootConfig file - $BootConfigContent = bcdedit.exe /enum - $Stream = [IO.MemoryStream]::new([byte[]][char[]]"$BootConfigContent") - $BootConfigHash = Get-FileHash -InputStream $Stream -Algorithm SHA256 - - $Current = [PSCustomObject]@{ - Content = $BootConfigContent - Hash = $BootConfigHash - } - - # Check if this is first run or not - if ($(Test-Path -Path $CachePath)) { - # Compare last content and hash - $Cache = Import-Clixml -Path $CachePath - $ContentDifference = Compare-Object -ReferenceObject $Cache.Content -DifferenceObject $Current.Content -CaseSensitive - $HashDifference = $Cache.Hash -like $Current.Hash - $Current | Export-Clixml -Path $CachePath -Force -Confirm:$false - if (-not $HashDifference) { - Write-Host "BootConfig file has changed since last run!" - Write-Host "" - $ContentDifference | ForEach-Object { - if ($_.SideIndicator -like '=>') { - Write-Host "Added: $($_.InputObject)" - } - elseif ($_.SideIndicator -like '<=') { - Write-Host "Removed: $($_.InputObject)" - } - } - exit 1 - } - else { - Write-Host "No changes detected since last run." - } - } - else { - Write-Host "First run, saving comparison cache file." - - $FolderPath = $CachePath | Split-Path - if (-not $(Test-Path -Path $FolderPath)) { - Write-Host "$FolderPath does not exist creating..." - New-Item -ItemType Directory -Path $FolderPath | Out-Null - } - - $Current | Export-Clixml -Path $CachePath -Force -Confirm:$false - } - exit 0 -} -end { - - - -} - - +#Requires -Version 5.1 + +<# +.SYNOPSIS + Checks if the BootConfig file was modified from last run. +.DESCRIPTION + Checks if the BootConfig file was modified from last run. + On first run this will not produce an error, but will create a cache file for later comparison. +.EXAMPLE + No parameters needed. +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Updated Calculated Name +#> + +[CmdletBinding()] +param ( + # Path and file where the cache file will be saved for comparison + [string] + $CachePath = "C:\ProgramData\NinjaRMMAgent\scripting\Test-BootConfig.clixml" +) + +begin { + if ($env:CachePath) { + $CachePath = $env:CachePath + } + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + +} +process { + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Get content and create hash of BootConfig file + $BootConfigContent = bcdedit.exe /enum + $Stream = [IO.MemoryStream]::new([byte[]][char[]]"$BootConfigContent") + $BootConfigHash = Get-FileHash -InputStream $Stream -Algorithm SHA256 + + $Current = [PSCustomObject]@{ + Content = $BootConfigContent + Hash = $BootConfigHash + } + + # Check if this is first run or not + if ($(Test-Path -Path $CachePath)) { + # Compare last content and hash + $Cache = Import-Clixml -Path $CachePath + $ContentDifference = Compare-Object -ReferenceObject $Cache.Content -DifferenceObject $Current.Content -CaseSensitive + $HashDifference = $Cache.Hash -like $Current.Hash + $Current | Export-Clixml -Path $CachePath -Force -Confirm:$false + if (-not $HashDifference) { + Write-Host "BootConfig file has changed since last run!" + Write-Host "" + $ContentDifference | ForEach-Object { + if ($_.SideIndicator -like '=>') { + Write-Host "Added: $($_.InputObject)" + } + elseif ($_.SideIndicator -like '<=') { + Write-Host "Removed: $($_.InputObject)" + } + } + exit 1 + } + else { + Write-Host "No changes detected since last run." + } + } + else { + Write-Host "First run, saving comparison cache file." + + $FolderPath = $CachePath | Split-Path + if (-not $(Test-Path -Path $FolderPath)) { + Write-Host "$FolderPath does not exist creating..." + New-Item -ItemType Directory -Path $FolderPath | Out-Null + } + + $Current | Export-Clixml -Path $CachePath -Force -Confirm:$false + } + exit 0 +} +end { + +} + diff --git a/Powershell Scripts/Boot Time Alert.ps1 b/Powershell Scripts/Boot Time Alert.ps1 index 0ef73a1..b5fa6d7 100644 --- a/Powershell Scripts/Boot Time Alert.ps1 +++ b/Powershell Scripts/Boot Time Alert.ps1 @@ -1,220 +1,134 @@ # Gets the Last BIOS time from the startup section of task manager and alerts if it exceeds a threshold you specify. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Gets the Last BIOS time from the startup section of task manager and alerts if it exceeds a threshold you specify. -.DESCRIPTION - Gets the Last BIOS time from the startup section of task manager and alerts if it exceeds a threshold you specify. - Can save the result to a custom field. - -.EXAMPLE - (No Parameters) - ## EXAMPLE OUTPUT WITHOUT PARAMS ## - Last BIOS Time: 14.6s - -PARAMETER: -BootCustomField "BootTime" - Saves the boot time to this Text Custom Field. -.EXAMPLE - -BootCustomField "BootTime" - ## EXAMPLE OUTPUT WITH BootCustomField ## - Last BIOS Time: 14.6s - -PARAMETER: -Seconds 20 - Sets the threshold for when the boot time is greater than this number. - In this case the boot time is over the threshold. -.EXAMPLE - -Seconds 20 - ## EXAMPLE OUTPUT WITH Seconds ## - Last BIOS Time: 14.6s - [Error] Boot time exceeded threshold of 20s by 5.41s. Boot time: 14.6s - -PARAMETER: -Seconds 10 - Sets the threshold for when the boot time is greater than this number. - In this case the boot time is under the threshold. -.EXAMPLE - -Seconds 10 - ## EXAMPLE OUTPUT WITH Seconds ## - Last BIOS Time: 14.6s - [Info] Boot time under threshold of 10s by 4.59s. Boot time: 14.6s - -.OUTPUTS - String -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - $Seconds, - [String]$BootCustomField -) - -begin { - if ($env:bootCustomField -and $env:bootCustomField -notlike "null") { - $BootCustomField = $env:bootCustomField - } - if ($env:bootTimeThreshold -and $env:bootTimeThreshold -notlike "null") { - # Remove any non digits - [double]$Seconds = $env:bootTimeThreshold -replace '[^0-9.]+' - } - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 10000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") - } - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to be set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received it will have an exception property, the function will exit with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } -} -process { - $Ticks = try { - # Get boot time from performance event logs - $PerfTicks = Get-WinEvent -FilterHashtable @{LogName = "Microsoft-Windows-Diagnostics-Performance/Operational"; Id = 100 } -MaxEvents 1 -ErrorAction SilentlyContinue | ForEach-Object { - # Convert the event to XML and grab the Event node - $eventXml = ([xml]$_.ToXml()).Event - # Output boot time in ms - [int64]($eventXml.EventData.Data | Where-Object { $_.Name -eq 'BootTime' }).InnerXml - } - # Get the boot POST time from the firmware, when available - $FirmwareTicks = Get-ItemPropertyValue -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power" -Name "FwPOSTTime" -ErrorAction SilentlyContinue - # Get the boot POST time from Windows, used as fall back - $OsTicks = Get-ItemPropertyValue -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power" -Name "POSTTime" -ErrorAction SilentlyContinue - # Use most likely to be accurate to least accurate - if ($FirmwareTicks -gt 0) { - $FirmwareTicks - } - elseif ($OsTicks -gt 0) { - $OsTicks - } - elseif ($PerfTicks -and $PerfTicks -gt 0) { - $PerfTicks - } - else { - # Fall back to reading System event logs - $StartOfBoot = Get-WinEvent -FilterHashtable @{LogName = 'System'; Id = 12 } -MaxEvents 1 | Select-Object -ExpandProperty TimeCreated - $LastUpTime = Get-WmiObject Win32_OperatingSystem -ErrorAction Stop | Select-Object @{Label = 'LastBootUpTime'; Expression = { $_.ConvertToDateTime($_.LastBootUpTime) } } | Select-Object -ExpandProperty LastBootUpTime - New-TimeSpan -Start $LastUpTime -End $StartOfBoot -ErrorAction Stop | Select-Object -ExpandProperty TotalMilliseconds - } - } - catch { - Write-Host "[Error] Failed to get Last BIOS Time from registry." - exit 2 - } - - $TimeSpan = [TimeSpan]::FromMilliseconds($Ticks) - - $BootTime = if ($TimeSpan.Days -gt 0) { - "$($TimeSpan.Days)d, $($TimeSpan.Hours)h, $($TimeSpan.Minutes)m, $($TimeSpan.Seconds + [Math]::Round($TimeSpan.Milliseconds / 1000, 1))s" - } - elseif ($TimeSpan.Hours -gt 0) { - "$($TimeSpan.Hours)h, $($TimeSpan.Minutes)m, $($TimeSpan.Seconds + [Math]::Round($TimeSpan.Milliseconds / 1000, 1))s" - } - elseif ($TimeSpan.Minutes -gt 0) { - "$($TimeSpan.Minutes)m, $($TimeSpan.Seconds + [Math]::Round($TimeSpan.Milliseconds / 1000, 1))s" - } - elseif ($TimeSpan.Seconds -gt 0) { - "$($TimeSpan.Seconds + [Math]::Round($TimeSpan.Milliseconds / 1000, 1))s" - } - else { - # Fail safe output - "$($TimeSpan.Days)d, $($TimeSpan.Hours)h, $($TimeSpan.Minutes)m, $($TimeSpan.Seconds + [Math]::Round($TimeSpan.Milliseconds / 1000, 1))s" - } - - Write-Host "Last BIOS Time: $BootTime" - - if ($BootCustomField) { - Set-NinjaProperty -Name $BootCustomField -Type Text -Value $BootTime - } - - if ($Seconds -gt 0) { - if ($TimeSpan.TotalSeconds -gt $Seconds) { - Write-Host "[Error] Boot time exceeded threshold of $($Seconds)s by $($TimeSpan.TotalSeconds - $Seconds)s. Boot time: $BootTime" - exit 1 - } - Write-Host "[Info] Boot time under threshold of $($Seconds)s by $($Seconds - $TimeSpan.TotalSeconds)s. Boot time: $BootTime" - } - exit 0 -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Gets the Last BIOS time from the startup section of task manager and alerts if it exceeds a threshold you specify. +.DESCRIPTION + Gets the Last BIOS time from the startup section of task manager and alerts if it exceeds a threshold you specify. + Can save the result to a custom field. + +.EXAMPLE + (No Parameters) + ## EXAMPLE OUTPUT WITHOUT PARAMS ## + Last BIOS Time: 14.6s + +PARAMETER: -BootCustomField "BootTime" + Saves the boot time to this Text Custom Field. +.EXAMPLE + -BootCustomField "BootTime" + ## EXAMPLE OUTPUT WITH BootCustomField ## + Last BIOS Time: 14.6s + +PARAMETER: -Seconds 20 + Sets the threshold for when the boot time is greater than this number. + In this case the boot time is over the threshold. +.EXAMPLE + -Seconds 20 + ## EXAMPLE OUTPUT WITH Seconds ## + Last BIOS Time: 14.6s + [Error] Boot time exceeded threshold of 20s by 5.41s. Boot time: 14.6s + +PARAMETER: -Seconds 10 + Sets the threshold for when the boot time is greater than this number. + In this case the boot time is under the threshold. +.EXAMPLE + -Seconds 10 + ## EXAMPLE OUTPUT WITH Seconds ## + Last BIOS Time: 14.6s + [Info] Boot time under threshold of 10s by 4.59s. Boot time: 14.6s + +.OUTPUTS + String +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + $Seconds, + [String]$BootCustomField +) + +begin { + if ($env:bootCustomField -and $env:bootCustomField -notlike "null") { + $BootCustomField = $env:bootCustomField + } + if ($env:bootTimeThreshold -and $env:bootTimeThreshold -notlike "null") { + # Remove any non digits + [double]$Seconds = $env:bootTimeThreshold -replace '[^0-9.]+' + } + +} +process { + $Ticks = try { + # Get boot time from performance event logs + $PerfTicks = Get-WinEvent -FilterHashtable @{LogName = "Microsoft-Windows-Diagnostics-Performance/Operational"; Id = 100 } -MaxEvents 1 -ErrorAction SilentlyContinue | ForEach-Object { + # Convert the event to XML and grab the Event node + $eventXml = ([xml]$_.ToXml()).Event + # Output boot time in ms + [int64]($eventXml.EventData.Data | Where-Object { $_.Name -eq 'BootTime' }).InnerXml + } + # Get the boot POST time from the firmware, when available + $FirmwareTicks = Get-ItemPropertyValue -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power" -Name "FwPOSTTime" -ErrorAction SilentlyContinue + # Get the boot POST time from Windows, used as fall back + $OsTicks = Get-ItemPropertyValue -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power" -Name "POSTTime" -ErrorAction SilentlyContinue + # Use most likely to be accurate to least accurate + if ($FirmwareTicks -gt 0) { + $FirmwareTicks + } + elseif ($OsTicks -gt 0) { + $OsTicks + } + elseif ($PerfTicks -and $PerfTicks -gt 0) { + $PerfTicks + } + else { + # Fall back to reading System event logs + $StartOfBoot = Get-WinEvent -FilterHashtable @{LogName = 'System'; Id = 12 } -MaxEvents 1 | Select-Object -ExpandProperty TimeCreated + $LastUpTime = Get-WmiObject Win32_OperatingSystem -ErrorAction Stop | Select-Object @{Label = 'LastBootUpTime'; Expression = { $_.ConvertToDateTime($_.LastBootUpTime) } } | Select-Object -ExpandProperty LastBootUpTime + New-TimeSpan -Start $LastUpTime -End $StartOfBoot -ErrorAction Stop | Select-Object -ExpandProperty TotalMilliseconds + } + } + catch { + Write-Host "[Error] Failed to get Last BIOS Time from registry." + exit 2 + } + + $TimeSpan = [TimeSpan]::FromMilliseconds($Ticks) + + $BootTime = if ($TimeSpan.Days -gt 0) { + "$($TimeSpan.Days)d, $($TimeSpan.Hours)h, $($TimeSpan.Minutes)m, $($TimeSpan.Seconds + [Math]::Round($TimeSpan.Milliseconds / 1000, 1))s" + } + elseif ($TimeSpan.Hours -gt 0) { + "$($TimeSpan.Hours)h, $($TimeSpan.Minutes)m, $($TimeSpan.Seconds + [Math]::Round($TimeSpan.Milliseconds / 1000, 1))s" + } + elseif ($TimeSpan.Minutes -gt 0) { + "$($TimeSpan.Minutes)m, $($TimeSpan.Seconds + [Math]::Round($TimeSpan.Milliseconds / 1000, 1))s" + } + elseif ($TimeSpan.Seconds -gt 0) { + "$($TimeSpan.Seconds + [Math]::Round($TimeSpan.Milliseconds / 1000, 1))s" + } + else { + # Fail safe output + "$($TimeSpan.Days)d, $($TimeSpan.Hours)h, $($TimeSpan.Minutes)m, $($TimeSpan.Seconds + [Math]::Round($TimeSpan.Milliseconds / 1000, 1))s" + } + + Write-Host "Last BIOS Time: $BootTime" + + if ($BootCustomField) { + } + + if ($Seconds -gt 0) { + if ($TimeSpan.TotalSeconds -gt $Seconds) { + Write-Host "[Error] Boot time exceeded threshold of $($Seconds)s by $($TimeSpan.TotalSeconds - $Seconds)s. Boot time: $BootTime" + exit 1 + } + Write-Host "[Info] Boot time under threshold of $($Seconds)s by $($Seconds - $TimeSpan.TotalSeconds)s. Boot time: $BootTime" + } + exit 0 +} +end { + +} diff --git a/Powershell Scripts/CVE-2023-32019 Remediation.ps1 b/Powershell Scripts/CVE-2023-32019 Remediation.ps1 index e93fd36..02663fb 100644 --- a/Powershell Scripts/CVE-2023-32019 Remediation.ps1 +++ b/Powershell Scripts/CVE-2023-32019 Remediation.ps1 @@ -1,248 +1,246 @@ # This is an example script for remediating a vulnerability that requires a different registry key per OS Build. -#Requires -Version 5.1 - -<# -.SYNOPSIS - This is an example script for remediating a vulnerability that requires a different registry key per OS Build. -.DESCRIPTION - This script will apply the registry fix suggested by microsoft for CVE-2023-32019 for the particular OS the computer is run on. Please note not all OS's have a fix to apply! - https://support.microsoft.com/en-au/topic/kb5028407-how-to-manage-the-vulnerability-associated-with-cve-2023-32019-bd6ed35f-48b1-41f6-bd19-d2d97270f080 -.EXAMPLE - (No Parameters) - - Checking Windows Version.... - Desktop Windows Detected! - Windows 10 identified! - 22H2 Detected! - Set Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Policies\Microsoft\FeatureManagement\Overrides\4103588492 to 1 - Successfully set registry key! - -PARAMETER: -Undo - Removes the registry key set for this fix. Script will error out if that registry key is not present. -.EXAMPLE - -Undo - - Checking Windows Version.... - Desktop Windows Detected! - Windows 10 identified! - 22H2 Detected! - Undoing registry fix... - Successfully removed registry fix! - -.OUTPUTS - None -.NOTES - Release: Initial Release (6/15/2023) - General notes -#> - -[CmdletBinding()] -param ( - [Parameter()] - [switch]$Undo = [System.Convert]::ToBoolean($env:undo) -) - -begin { - # Tests that the script is elevated - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - # We want the script to check if its running on a workstation or something else - function Test-IsWorkstation { - $OS = Get-CimInstance -ClassName Win32_OperatingSystem - return $OS.ProductType -eq 1 - } - - function Get-OSBuild { - return [System.Environment]::OSVersion.Version.Build - } - - function Get-MajorWinVer { - return [System.Environment]::OSVersion.Version.Major - } - - # This will set the registry key and any preceding keys needed - function Set-RegKey { - param ( - $Path, - $Name, - $Value, - [ValidateSet("DWord", "QWord", "String", "ExpandedString", "Binary", "MultiString", "Unknown")] - $PropertyType = "DWord" - ) - if (-not $(Test-Path -Path $Path)) { - # Check if path does not exist and create the path - New-Item -Path $Path -Force | Out-Null - } - if ((Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore)) { - # Update property and print out what it was changed from and changed to - $CurrentValue = (Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore).$Name - try { - Set-ItemProperty -Path $Path -Name $Name -Value $Value -Force -Confirm:$false -ErrorAction Stop | Out-Null - } - catch { - Write-Error "[Error] Unable to Set registry key for $Name please see below error!" - Write-Error $_ - exit 1 - } - Write-Host "$Path\$Name changed from $CurrentValue to $($(Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore).$Name)" - } - else { - # Create property with value - try { - New-ItemProperty -Path $Path -Name $Name -Value $Value -PropertyType $PropertyType -Force -Confirm:$false -ErrorAction Stop | Out-Null - } - catch { - Write-Error "[Error] Unable to Set registry key for $Name please see below error!" - Write-Error $_ - exit 1 - } - Write-Host "Set $Path\$Name to $($(Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore).$Name)" - } - } - - # Is it Windows 10 or 11 or something else? - $WindowsVersion = Get-MajorWinVer - - # Current Build Number - $BuildNumber = Get-OSBuild -} -process { - - # If not elevated error out. Admin priveledges are required to create HKLM registry keys - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Keeping the end user updated on the status - Write-Host "Checking Windows Version...." - if (Test-IsWorkstation) { - Write-Host "Desktop Windows Detected!" - # Depending on the version we'll want to check on a different set of build numbers - switch ($WindowsVersion) { - "10" { - switch ($BuildNumber) { - "22621" { - Write-Host "Windows 11 identified!" - Write-Host "22H2 Detected!" - $key = "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Policies\Microsoft\FeatureManagement\Overrides" - $name = "4237806220" - $value = "1" - } - "22000" { - Write-Host "Windows 11 identified!" - Write-Host "21H2 Detected!" - $key = "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Policies\Microsoft\FeatureManagement\Overrides" - $name = "4204251788" - $value = "1" - } - "19045" { - # This sets us up to set the registry key depending on the current build and version. - Write-Host "Windows 10 identified!" - Write-Host "22H2 Detected!" - $key = "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Policies\Microsoft\FeatureManagement\Overrides" - $name = "4103588492" - $value = "1" - } - "19044" { - Write-Host "Windows 10 identified!" - Write-Host "21H2 Detected!" - $key = "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Policies\Microsoft\FeatureManagement\Overrides" - $name = "4103588492" - $value = "1" - } - "19042" { - Write-Host "Windows 10 identified!" - Write-Host "20H2 Detected!" - $key = "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Policies\Microsoft\FeatureManagement\Overrides" - $name = "4103588492" - $value = "1" - } - "17763" { - Write-Host "Windows 10 identified!" - Write-Host "1809 Detected!" - $key = "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Configuration Manager" - $name = "LazyRetryOnCommitFailure" - $value = "0" - } - "14393" { - Write-Host "Windows 10 identified!" - Write-Host "1607 Detected!" - $key = "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Configuration Manager" - $name = "LazyRetryOnCommitFailure" - $value = "0" - } - default { - Write-Warning "Looks like you're either on an unsupported windows build or one not supported by this script? (Only Win 11 22H2 and 21H1 and Win 10 22H2,21H2,20H2,1809 and 1607 has a fix out!)" - Write-Warning "https://en.wikipedia.org/wiki/Windows_10_version_history" - Write-Warning "https://en.wikipedia.org/wiki/Windows_11_version_history" - Write-Error "[Error] This version of windows cannot be remediated by this script? Please verify this https://support.microsoft.com/en-au/topic/kb5028407-how-to-manage-the-vulnerability-associated-with-cve-2023-32019-bd6ed35f-48b1-41f6-bd19-d2d97270f080" - exit 1 - } - } - } - default { - Write-Warning "Looks like you're on a version of windows not supported by this script? (Only Windows 10 and 11 have a fix out!)" - Write-Error "[Error] This version of windows appears to not be applicable or cannot be remediated by this script? Please verify this https://support.microsoft.com/en-au/topic/kb5028407-how-to-manage-the-vulnerability-associated-with-cve-2023-32019-bd6ed35f-48b1-41f6-bd19-d2d97270f080" - exit 1 - } - } - } - else { - Write-Host "Windows Server Detected!" - if (Get-ComputerInfo | Select-Object OSName | Where-Object { $_.OSName -like "*2022*" }) { - $key = "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Policies\Microsoft\FeatureManagement\Overrides" - $name = "4137142924" - $value = "1" - } - else { - Write-Warning "Looks like you're on a version of windows not supported by this script? (Only Server 2022 has a fix out!)" - Write-Error "[Error] This version of windows appears to not be applicable or cannot be remediated by this script? Please verify this https://support.microsoft.com/en-au/topic/kb5028407-how-to-manage-the-vulnerability-associated-with-cve-2023-32019-bd6ed35f-48b1-41f6-bd19-d2d97270f080" - exit 1 - } - } - - if ($key -and -not $Undo) { - Set-RegKey -Path $key -Name $name -Value $value -PropertyType DWord - if ((Get-ItemPropertyValue -Path $key -Name $name -ErrorAction Ignore) -ne $value) { - Write-Error "[Error] Unable to set registry key? Is something blocking the script?" - exit 1 - } - else { - Write-Host "Successfully set registry key!" - exit 0 - } - } - elseif ($Undo) { - if (Get-ItemProperty -Path $key -ErrorAction Ignore) { - Write-Host "Undoing registry fix..." - Remove-ItemProperty -Path $key -Name $name - if (Get-ItemProperty -Path $key -ErrorAction Ignore) { - Write-Error "[Error] Unable to undo registry fix!" - exit 1 - } - else { - Write-Host "Successfully removed registry fix!" - exit 0 - } - } - else { - Write-Error "[Error] Registry Key not found? Did you already undo it?" - exit 1 - } - } - else { - Write-Error "[Error] Unable to find registry key to set!" - exit 1 - } -} -end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + This is an example script for remediating a vulnerability that requires a different registry key per OS Build. +.DESCRIPTION + This script will apply the registry fix suggested by microsoft for CVE-2023-32019 for the particular OS the computer is run on. Please note not all OS's have a fix to apply! + https://support.microsoft.com/en-au/topic/kb5028407-how-to-manage-the-vulnerability-associated-with-cve-2023-32019-bd6ed35f-48b1-41f6-bd19-d2d97270f080 +.EXAMPLE + (No Parameters) + + Checking Windows Version.... + Desktop Windows Detected! + Windows 10 identified! + 22H2 Detected! + Set Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Policies\Microsoft\FeatureManagement\Overrides\4103588492 to 1 + Successfully set registry key! + +PARAMETER: -Undo + Removes the registry key set for this fix. Script will error out if that registry key is not present. +.EXAMPLE + -Undo + + Checking Windows Version.... + Desktop Windows Detected! + Windows 10 identified! + 22H2 Detected! + Undoing registry fix... + Successfully removed registry fix! + +.OUTPUTS + None +.NOTES + Release: Initial Release (6/15/2023) + General notes +#> + +[CmdletBinding()] +param ( + [Parameter()] + [switch]$Undo = [System.Convert]::ToBoolean($env:undo) +) + +begin { + # Tests that the script is elevated + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + # We want the script to check if its running on a workstation or something else + function Test-IsWorkstation { + $OS = Get-CimInstance -ClassName Win32_OperatingSystem + return $OS.ProductType -eq 1 + } + + function Get-OSBuild { + return [System.Environment]::OSVersion.Version.Build + } + + function Get-MajorWinVer { + return [System.Environment]::OSVersion.Version.Major + } + + # This will set the registry key and any preceding keys needed + function Set-RegKey { + param ( + $Path, + $Name, + $Value, + [ValidateSet("DWord", "QWord", "String", "ExpandedString", "Binary", "MultiString", "Unknown")] + $PropertyType = "DWord" + ) + if (-not $(Test-Path -Path $Path)) { + # Check if path does not exist and create the path + New-Item -Path $Path -Force | Out-Null + } + if ((Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore)) { + # Update property and print out what it was changed from and changed to + $CurrentValue = (Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore).$Name + try { + Set-ItemProperty -Path $Path -Name $Name -Value $Value -Force -Confirm:$false -ErrorAction Stop | Out-Null + } + catch { + Write-Error "[Error] Unable to Set registry key for $Name please see below error!" + Write-Error $_ + exit 1 + } + Write-Host "$Path\$Name changed from $CurrentValue to $($(Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore).$Name)" + } + else { + # Create property with value + try { + New-ItemProperty -Path $Path -Name $Name -Value $Value -PropertyType $PropertyType -Force -Confirm:$false -ErrorAction Stop | Out-Null + } + catch { + Write-Error "[Error] Unable to Set registry key for $Name please see below error!" + Write-Error $_ + exit 1 + } + Write-Host "Set $Path\$Name to $($(Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore).$Name)" + } + } + + # Is it Windows 10 or 11 or something else? + $WindowsVersion = Get-MajorWinVer + + # Current Build Number + $BuildNumber = Get-OSBuild +} +process { + + # If not elevated error out. Admin priveledges are required to create HKLM registry keys + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Keeping the end user updated on the status + Write-Host "Checking Windows Version...." + if (Test-IsWorkstation) { + Write-Host "Desktop Windows Detected!" + # Depending on the version we'll want to check on a different set of build numbers + switch ($WindowsVersion) { + "10" { + switch ($BuildNumber) { + "22621" { + Write-Host "Windows 11 identified!" + Write-Host "22H2 Detected!" + $key = "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Policies\Microsoft\FeatureManagement\Overrides" + $name = "4237806220" + $value = "1" + } + "22000" { + Write-Host "Windows 11 identified!" + Write-Host "21H2 Detected!" + $key = "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Policies\Microsoft\FeatureManagement\Overrides" + $name = "4204251788" + $value = "1" + } + "19045" { + # This sets us up to set the registry key depending on the current build and version. + Write-Host "Windows 10 identified!" + Write-Host "22H2 Detected!" + $key = "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Policies\Microsoft\FeatureManagement\Overrides" + $name = "4103588492" + $value = "1" + } + "19044" { + Write-Host "Windows 10 identified!" + Write-Host "21H2 Detected!" + $key = "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Policies\Microsoft\FeatureManagement\Overrides" + $name = "4103588492" + $value = "1" + } + "19042" { + Write-Host "Windows 10 identified!" + Write-Host "20H2 Detected!" + $key = "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Policies\Microsoft\FeatureManagement\Overrides" + $name = "4103588492" + $value = "1" + } + "17763" { + Write-Host "Windows 10 identified!" + Write-Host "1809 Detected!" + $key = "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Configuration Manager" + $name = "LazyRetryOnCommitFailure" + $value = "0" + } + "14393" { + Write-Host "Windows 10 identified!" + Write-Host "1607 Detected!" + $key = "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Configuration Manager" + $name = "LazyRetryOnCommitFailure" + $value = "0" + } + default { + Write-Warning "Looks like you're either on an unsupported windows build or one not supported by this script? (Only Win 11 22H2 and 21H1 and Win 10 22H2,21H2,20H2,1809 and 1607 has a fix out!)" + Write-Warning "https://en.wikipedia.org/wiki/Windows_10_version_history" + Write-Warning "https://en.wikipedia.org/wiki/Windows_11_version_history" + Write-Error "[Error] This version of windows cannot be remediated by this script? Please verify this https://support.microsoft.com/en-au/topic/kb5028407-how-to-manage-the-vulnerability-associated-with-cve-2023-32019-bd6ed35f-48b1-41f6-bd19-d2d97270f080" + exit 1 + } + } + } + default { + Write-Warning "Looks like you're on a version of windows not supported by this script? (Only Windows 10 and 11 have a fix out!)" + Write-Error "[Error] This version of windows appears to not be applicable or cannot be remediated by this script? Please verify this https://support.microsoft.com/en-au/topic/kb5028407-how-to-manage-the-vulnerability-associated-with-cve-2023-32019-bd6ed35f-48b1-41f6-bd19-d2d97270f080" + exit 1 + } + } + } + else { + Write-Host "Windows Server Detected!" + if (Get-ComputerInfo | Select-Object OSName | Where-Object { $_.OSName -like "*2022*" }) { + $key = "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Policies\Microsoft\FeatureManagement\Overrides" + $name = "4137142924" + $value = "1" + } + else { + Write-Warning "Looks like you're on a version of windows not supported by this script? (Only Server 2022 has a fix out!)" + Write-Error "[Error] This version of windows appears to not be applicable or cannot be remediated by this script? Please verify this https://support.microsoft.com/en-au/topic/kb5028407-how-to-manage-the-vulnerability-associated-with-cve-2023-32019-bd6ed35f-48b1-41f6-bd19-d2d97270f080" + exit 1 + } + } + + if ($key -and -not $Undo) { + Set-RegKey -Path $key -Name $name -Value $value -PropertyType DWord + if ((Get-ItemPropertyValue -Path $key -Name $name -ErrorAction Ignore) -ne $value) { + Write-Error "[Error] Unable to set registry key? Is something blocking the script?" + exit 1 + } + else { + Write-Host "Successfully set registry key!" + exit 0 + } + } + elseif ($Undo) { + if (Get-ItemProperty -Path $key -ErrorAction Ignore) { + Write-Host "Undoing registry fix..." + Remove-ItemProperty -Path $key -Name $name + if (Get-ItemProperty -Path $key -ErrorAction Ignore) { + Write-Error "[Error] Unable to undo registry fix!" + exit 1 + } + else { + Write-Host "Successfully removed registry fix!" + exit 0 + } + } + else { + Write-Error "[Error] Registry Key not found? Did you already undo it?" + exit 1 + } + } + else { + Write-Error "[Error] Unable to find registry key to set!" + exit 1 + } +} +end { + +} + diff --git a/Powershell Scripts/Change Power and Sleep Settings.ps1 b/Powershell Scripts/Change Power and Sleep Settings.ps1 index 95ec928..4e24f09 100644 --- a/Powershell Scripts/Change Power and Sleep Settings.ps1 +++ b/Powershell Scripts/Change Power and Sleep Settings.ps1 @@ -1,480 +1,478 @@ -# Set Power and Sleep Settings. It can adjust just the plugged in or battery settings if requested. +# Set Power and Sleep Settings. It can adjust just the plugged in or battery settings if requested. Please Note not all devices support all options. -<# -.SYNOPSIS - Set Power and Sleep Settings. It can adjust just the plugged in or battery settings if requested. - Please Note not all devices support all options. -.DESCRIPTION - Please Note not all devices support all options. - Options: ScreenTimeout, HibernateTimeout, SleepTimeout, Disk Timeout, PowerPlan, Lid Action, Wake Timers, USB Suspend, Critical Action - Low Action, Power Button Action, Critical Level, Low Level, Reserve Level, Critical Notification, and Low Notification. -.EXAMPLE - (No Parameters) - - By default The Script doesn't do anything without some parameters. -.LINK - https://learn.microsoft.com/en-us/windows/win32/power/power-policy-settings - -PARAMETER: -ScreenTimeout "60" - Replace 60 with any time in seconds to set the screen timeout. (0 for disabled) - -PARAMETER: -HibernateTimeout "28800" - Replace 28800 with any time in seconds. (0 for disabled) - -PARAMETER: -SleepTimeout "14400" - Replace 14400 with any time in seconds. (0 for disabled) - -PARAMETER: -DiskTimeout "0" - Replace 0 with your desired time in seconds. (0 for disabled) - -PARAMETER: -PowerPlan "High Performance" - Replace "High Performance" with your desired power plan. Keep in mind that most newer computers no longer have seperate power plans. - -PARAMETER: -LidAction "Nothing" - Replace Nothing with one of these three available options. Sleep, Shutdown, Nothing. - Will be skipped for non-laptops and this script cannot verify if the action was successfully set. - -PARAMETER: -AllowWakeTimers - Allows the ability for software to wake the computer from sleep at a later date. - -PARAMETER: -DisableWakeTimers - Disables the ability for software to wake the computer from sleep at a later date. - -PARAMETER: -EnableUSBSuspend - Allows the OS to suspend USB devices to conserve power. - -PARAMETER: -DisableUSBSuspend - Disable's the OS's ability to suspend USB devices to conserve power. - -PARAMETER: -CriticalAction "Hibernate" - Replace Hibernate with your desired action for when the machine is at a "Critical" batter level. - Valid Options: Hibernate, Sleep, Shutdown, Nothing - -PARAMETER: -LowAction "Hibernate" - Replace Hibernate with your desired action for when the machine is at a "Low" battery level. - Valid Options: Hibernate, Sleep, Shutdown, Nothing. - -PARAMETER: -CriticalLevel "7" - Replace 7 with your desired battery percent level to be considered critical (without the % symbol). - -PARAMETER: -LowLevel "10" - Replace 10 with your desired battery percent level to be considered low (without the % symbol). - -PARAMETER: -LowNotify - Allows the notification that comes in when the battery hits "low" levels. - -PARAMETER: -AC - Only applies your chosen battery settings to the "Plugged In" section of a power plan. - -PARAMETER: -DC - Only applies your chosen battery settings to the "Battery" section of a power plan. -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 7, Windows Server 2008 - General notes - Version: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$ScreenTimeout, - [Parameter()] - [String]$HibernateTimeout, - [Parameter()] - [String]$SleepTimeout, - [Parameter()] - [String]$DiskTimeout, - [Parameter()] - [String]$PowerPlan, - [Parameter()] - [String]$LidAction, - [Parameter()] - [Switch]$AllowWakeTimers, - [Parameter()] - [Switch]$DisableWakeTimers, - [Parameter()] - [Switch]$EnableUSBSuspend, - [Parameter()] - [Switch]$DisableUSBSuspend, - [Parameter()] - [String]$CriticalAction, - [Parameter()] - [String]$LowAction, - [Parameter()] - [String]$CriticalLevel, - [Parameter()] - [String]$LowLevel, - [Parameter()] - [Switch]$LowNotify, - [Parameter()] - [Switch]$LowNoNotify, - [Parameter()] - [Switch]$AC, - [Parameter()] - [Switch]$DC -) - -begin { - # Grab Script Variables if present - if ($env:powerSourceSetting -and $env:powerSourceSetting -notlike "null") { - switch ($env:powerSourceSetting) { - "Plugged In" { $AC = $True } - "On Battery" { $DC = $True } - default {} - } - } - if ($env:screenTimeoutInMinutes -and $env:screenTimeoutInMinutes -notlike "null") { $ScreenTimeout = $env:screenTimeoutInMinutes } - if ($env:hibernateTimeoutInMinutes -and $env:hibernateTimeoutInMinutes -notlike "null") { $HibernateTimeout = $env:hibernateTimeoutInMinutes } - if ($env:sleepTimeoutInMinutes -and $env:sleepTimeoutInMinutes -notlike "null") { $SleepTimeout = $env:sleepTimeoutInMinutes } - if ($env:diskTimeoutInMinutes -and $env:diskTimeoutInMinutes -notlike "null") { $DiskTimeout = $env:diskTimeoutInMinutes } - if ($env:powerPlan -and $env:powerPlan -notlike "null") { $PowerPlan = $env:powerPlan } - if ($env:lidAction -and $env:lidAction -notlike "null") { $LidAction = $env:lidAction } - if ($env:wakeTimers -and $env:wakeTimers -notlike "null") { - switch ($env:wakeTimers) { - "Enable" { $AllowWakeTimers = $True } - "Disable" { $DisableWakeTimers = $True } - } - } - if ($env:usbSuspend -and $env:usbSuspend -notlike "null") { - switch ($env:usbSuspend) { - "Enable" { $EnableUSBSuspend = $True } - "Disable" { $DisableUSBSuspend = $True } - } - } - if ($env:criticalBatteryAction -and $env:criticalBatteryAction -notlike "null") { $CriticalAction = $env:criticalBatteryAction } - if ($env:lowBatteryAction -and $env:lowBatteryAction -notlike "null") { $LowAction = $env:lowBatteryAction } - if ($env:criticalBatteryLevel -and $env:criticalBatteryLevel -notlike "null") { $CriticalLevel = $env:criticalBatteryLevel } - if ($env:lowBatteryLevel -and $env:lowBatteryLevel -notlike "null") { $LowLevel = $env:lowBatteryLevel } - if ($env:lowNotification -and $env:lowNotification -notlike "null") { - switch ($env:lowNotification) { - "Enable" { $LowNotify = $True } - "Disable" { $LowNoNotify = $True } - } - } - - if ($ScreenTimeout) { [int]$ScreenTimeoutValue = [int]$ScreenTimeout * 60 } - if ($HibernateTimeout) { [int]$HibernateTimeoutValue = [int]$HibernateTimeout * 60 } - if ($SleepTimeout) { [int]$SleepTimeoutValue = [int]$SleepTimeout * 60 } - if ($DiskTimeout) { [int]$DiskTimeoutValue = [int]$DiskTimeout * 60 } - if ($CriticalLevel) { [int]$CriticalLevelValue = [int]$CriticalLevel } - if ($LowLevel) { [int]$LowLevelValue = [int]$LowLevel } - - # Elevation Test - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - if ( - -not $ScreenTimeout -and -not $HibernateTimeout -and -not $SleepTimeout -and -not $DiskTimeout -and -not $PowerPlan -and - -not $LidAction -and -not $AllowWakeTimers -and -not $DisableWakeTimers -and -not $EnableUSBSuspend -and - -not $DisableUSBSuspend -and -not $CriticalAction -and -not $LowAction -and -not $CriticalLevel -and - -not $LowLevel -and -not $LowNotify -and -not $LowNoNotify - ) { - Write-Error -Message '[Error] No action given!' - exit 1 - } - - # Check for battery and whether or not the device is a laptop - if ($PSVersionTable.PSVersion.Major -lt 5) { - $BatteryCheck = @(Get-WmiObject -Class Win32_Battery).Count -gt 0 - $LaptopCheck = Get-WmiObject -Class win32_systemenclosure | Where-Object { $_.chassistypes -eq 9 -or $_.chassistypes -eq 10 -or $_.chassistypes -eq 14 } - } - else { - $BatteryCheck = @(Get-CimInstance -Class Win32_Battery).Count -gt 0 - $LaptopCheck = Get-CimInstance -Class win32_systemenclosure | Where-Object { $_.chassistypes -eq 9 -or $_.chassistypes -eq 10 -or $_.chassistypes -eq 14 } - } - - # Function to test if policy was set correctly - function Test-PowerValue { - [CmdletBinding()] - param( - [Parameter()] - [String]$GUID, - [Parameter()] - [String]$Index, - [Parameter()] - [int]$Value, - [Parameter()] - [String]$Setting - ) - - # LidAction shows no information so we'll issue a warning and then exit the test function. - if ($GUID -eq "SUB_BUTTONS" -and $Index -eq "5ca83367-6e45-459f-a27b-476b1d01c936") { - Write-Warning "Unable to verify LidAction via script." - break - } - - # Values are stored in hex so we'll need to convert it. - $Hex = "0x" + '{0:X8}' -f $Value - - $PowerQuery = powercfg.exe /QUERY SCHEME_CURRENT $GUID | Out-String - $RelevantSetting = $PowerQuery -split "Power Setting GUID:" | Where-Object { $_ -like "*$Index*" } - if (-not ($RelevantSetting)) { - Write-Warning "Unable to verify setting for $GUID $Index. This option may not exist for this machine." - break - } - - # Depending on how the script was ran we'll need to verify in different ways (ex. its on a laptop or only AC was specified) - if ($AC -or -not $BatteryCheck) { - $ACQuery = $RelevantSetting -split '\s{2}' | Where-Object { $_ -eq "Current AC Power Setting Index: $Hex" } - - # Actual check code is really similar though (only difference is AC vs DC). - if (-not $ACQuery) { - Write-Warning "AC Value of $Hex Not Found for $GUID $Index. You may want to verify the results." - break - } - } - elseif ($DC) { - $BatteryQuery = $RelevantSetting -split '\s{2}' | Where-Object { $_ -eq "Current DC Power Setting Index: $Hex" } - - if (-not $BatteryQuery) { - Write-Warning "DC Value of $Hex Not Found for $GUID $Index. You may want to verify the results." - break - } - } - else { - $BatteryQuery = $RelevantSetting -split '\s{2}' | Where-Object { $_ -eq "Current DC Power Setting Index: $Hex" } - if (-not $BatteryQuery) { - Write-Warning "DC Value of $Hex Not Found for $GUID $Index. You may want to verify the results." - break - } - - $ACQuery = $RelevantSetting -split '\s{2}' | Where-Object { $_ -eq "Current AC Power Setting Index: $Hex" } - if (-not $ACQuery) { - Write-Warning "AC Value of $Hex Not Found for $GUID $Index. You may want to verify the results." - break - } - } - - Write-Host "Successfully set power setting for $Setting!" - } - function Set-PowerAction { - [CmdletBinding()] - param( - [Parameter()] - [ValidateSet("Nothing", "Sleep", "Hibernate", "ShutDown")] - [String]$Action, - [Parameter()] - [String]$GUID, - [Parameter()] - [String]$Index, - [Parameter()] - [String]$Setting - ) - try { - switch ($Action) { - "Nothing" { - if ($AC -or -not $BatteryCheck) { - powercfg.exe /SETACVALUEINDEX SCHEME_CURRENT $GUID $Index 0 - } - elseif ($DC) { - powercfg.exe /SETDCVALUEINDEX SCHEME_CURRENT $GUID $Index 0 - } - else { - powercfg.exe /SETACVALUEINDEX SCHEME_CURRENT $GUID $Index 0 - powercfg.exe /SETDCVALUEINDEX SCHEME_CURRENT $GUID $Index 0 - } - - Test-PowerValue $GUID $Index 0 -Setting $Setting - } - "Sleep" { - if ($AC -or -not $BatteryCheck) { - powercfg.exe /SETACVALUEINDEX SCHEME_CURRENT $GUID $Index 1 - } - elseif ($DC) { - powercfg.exe /SETDCVALUEINDEX SCHEME_CURRENT $GUID $Index 1 - } - else { - powercfg.exe /SETACVALUEINDEX SCHEME_CURRENT $GUID $Index 1 - powercfg.exe /SETDCVALUEINDEX SCHEME_CURRENT $GUID $Index 1 - } - - Test-PowerValue $GUID $Index 1 -Setting $Setting - } - "Hibernate" { - if ($AC -or -not $BatteryCheck) { - powercfg.exe /SETACVALUEINDEX SCHEME_CURRENT $GUID $Index 2 - } - elseif ($DC) { - powercfg.exe /SETDCVALUEINDEX SCHEME_CURRENT $GUID $Index 2 - } - else { - powercfg.exe /SETACVALUEINDEX SCHEME_CURRENT $GUID $Index 2 - powercfg.exe /SETDCVALUEINDEX SCHEME_CURRENT $GUID $Index 2 - } - - Test-PowerValue $GUID $Index 2 -Setting $Setting - } - "Shutdown" { - if ($AC -or -not $BatteryCheck) { - powercfg.exe /SETACVALUEINDEX SCHEME_CURRENT $GUID $Index 3 - } - elseif ($DC) { - powercfg.exe /SETDCVALUEINDEX SCHEME_CURRENT $GUID $Index 3 - } - else { - powercfg.exe /SETACVALUEINDEX SCHEME_CURRENT $GUID $Index 3 - powercfg.exe /SETDCVALUEINDEX SCHEME_CURRENT $GUID $Index 3 - } - - Test-PowerValue $GUID $Index 3 -Setting $Setting - } - default { - throw "$Action is not a valid action for $Setting, valid actions are Shutdown, Hibernate, Sleep and Nothing." - } - } - } - catch { - Write-Warning $_.Exception.Message - Write-Warning "Failed to set power setting for $Setting" - } - } - - # The actual code required to set these values is very similar. - function Set-PowerValue { - [CmdletBinding()] - param( - [Parameter()] - [String]$Value, - [Parameter()] - [String]$GUID, - [Parameter()] - [String]$Index, - [Parameter()] - [String]$Setting - ) - - try { - if ($AC -or -not $BatteryCheck) { - powercfg.exe /SETACVALUEINDEX SCHEME_CURRENT $GUID $Index $Value - } - elseif ($DC) { - powercfg.exe /SETDCVALUEINDEX SCHEME_CURRENT $GUID $Index $Value - } - else { - powercfg.exe /SETACVALUEINDEX SCHEME_CURRENT $GUID $Index $Value - powercfg.exe /SETDCVALUEINDEX SCHEME_CURRENT $GUID $Index $Value - } - - Test-PowerValue $GUID $Index $Value -Setting $Setting - } - catch { - Write-Warning $_.Exception.Message - Write-Warning "Failed to set power setting for $Setting" - } - } - - # Before changing the power plan we should check if it exists and whether or not it is in use. - function Get-PowerPlan { - [CmdletBinding()] - param( - [Parameter()] - [Switch]$Active, - [Parameter()] - [String]$Name - ) - if ($Active) { - $PowerPlan = powercfg.exe /getactivescheme - $PowerPlan = ($PowerPlan -replace "Power Scheme GUID:" -split "(?=\S{8}-\S{4}-\S{4}-\S{17})" -split '\(' -replace '\)') | Where-Object { $_ -ne " " } - $PowerPlan = @( - [PSCustomObject]@{ - Name = $($PowerPlan | Where-Object { $_ -notmatch "\S{8}-\S{4}-\S{4}-\S{17}" }) - GUID = $($PowerPlan | Where-Object { $_ -match "\S{8}-\S{4}-\S{4}-\S{17}" }) - } - ) - } - else { - $PowerPlan = powercfg.exe /L - $PowerPlan = $PowerPlan -replace '\s{2,}', ',' -replace ' \*', ',True' -replace "Existing Power Schemes \(\* Active\)", "GUID,Name,Active" -replace "-{2,}" -replace "Power Scheme GUID: " -replace '\(' -replace '\)' | Where-Object { $_ } | ConvertFrom-Csv - } - - if ($Name) { - $PowerPlan | Where-Object { $_.Name -like $Name } - } - else { - $PowerPlan - } - } -} -process { - # If not elevated, exit the script. - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - # If a power plan was specified, let the user know that this might not actually be a changeable option (depending on computer age). - if ($PowerPlan) { - Write-Warning "Devices with modern standby might only have `"Balanced`" as an option." - Write-Warning "Link: https://learn.microsoft.com/en-us/windows/win32/power/power-policy-settings" - - $TargetPlan = Get-PowerPlan -Name $PowerPlan - # If totally not available we're going to exit as all these settings are tied to the active power plan. - # If we're not able to set it we'll want to give an opportunity to correct the error. - if ($null -eq $TargetPlan) { - Write-Error "Your targeted Power Plan is not available." - exit 1 - } - - if ($TargetPlan.Active -ne $True) { - powercfg.exe /setactive $TargetPlan.GUID - } - - $CurrentPlan = Get-PowerPlan -Active - if ($CurrentPlan.GUID -notlike "*$($TargetPlan.GUID)*") { - Write-Error "Failed to change power plan!" - exit 1 - } - else { - Write-Host "Successfully set Power Plan!" - } - } - - # We're going to run through all the various options and if requested we'll adjust them. - if ($ScreenTimeout) { Set-PowerValue -GUID "SUB_VIDEO" -Index "3c0bc021-c8a8-4e07-a973-6b14cbcb2b7e" -Value $ScreenTimeoutValue -Setting "Screen Timeout" } - if ($HibernateTimeout) { - Write-Warning "Hibernate timeout is not supported by all devices and may be ignored by the System." - Set-PowerValue -GUID "SUB_SLEEP" -Index "9d7815a6-7ee4-497e-8888-515a05f02364" -Value $HibernateTimeoutValue -Setting "Hibernate Timeout" - } - if ($SleepTimeout) { Set-PowerValue -GUID "SUB_SLEEP" -Index "29f6c1db-86da-48c5-9fdb-f2b67b1f44da" -Value $SleepTimeoutValue -Setting "Sleep Timeout" } - if ($DiskTimeout) { - Write-Warning "An HDD is required for this setting to display (has no effect on SSDs)." - Set-PowerValue -GUID "SUB_DISK" -Index "6738e2c4-e8a5-4a42-b16a-e040e769756e" -Value $DiskTimeoutValue -Setting "Disk Timeout" - } - if ($DisableUSBSuspend) { Set-PowerValue -GUID "2a737441-1930-4402-8d77-b2bebba308a3" -Index "48e6b7a6-50f5-4782-a5d4-53bb8f07e226" -Value 0 -Setting "Disable USB Auto-Suspend" } - if ($EnableUSBSuspend) { Set-PowerValue -GUID "2a737441-1930-4402-8d77-b2bebba308a3" -Index "48e6b7a6-50f5-4782-a5d4-53bb8f07e226" -Value 1 -Setting "Enable USB Auto-Suspend" } - if ($AllowWakeTimers) { Set-PowerValue -GUID "SUB_SLEEP" -Index "bd3b718a-0680-4d9d-8ab2-e1d2b4ac806d" -Value 1 -Setting "Allow Wake Timers" } - if ($DisableWakeTimers) { Set-PowerValue -GUID "SUB_SLEEP" -Index "bd3b718a-0680-4d9d-8ab2-e1d2b4ac806d" -Value 0 -Setting "Disable Wake Timers" } - # Lid Action has slightly different options than Set-PowerAction. - if ($LidAction -and $LaptopCheck) { - switch ($LidAction) { - "Nothing" { Set-PowerValue -GUID "SUB_BUTTONS" -Index "5ca83367-6e45-459f-a27b-476b1d01c936" -Value 0 -Setting "Lid Action" } - "Sleep" { Set-PowerValue -GUID "SUB_BUTTONS" -Index "5ca83367-6e45-459f-a27b-476b1d01c936" -Value 1 -Setting "Lid Action" } - "Shutdown" { Set-PowerValue -GUID "SUB_BUTTONS" -Index "5ca83367-6e45-459f-a27b-476b1d01c936" -Value 3 -Setting "Lid Action" } - default { - Write-Error "Invalid PowerButton Option. Only Sleep, Nothing and Shutdown are allowed!" - exit 1 - } - } - } - if ($CriticalAction) { - Set-PowerAction -GUID "SUB_BATTERY" -Index "637ea02f-bbcb-4015-8e2c-a1c7b9c0b546" -Action $CriticalAction -Setting "Critical Battery Action" - } - if ($LowAction) { - Set-PowerAction -GUID "SUB_BATTERY" -Index "d8742dcb-3e6a-4b3c-b3fe-374623cdcf06" -Action $LowAction -Setting "Low Battery Action" - } - if ($CriticalLevel) { Set-PowerValue -GUID "SUB_BATTERY" -Index "9a66d8d7-4ff7-4ef9-b5a2-5a326ca2a469" -Value $CriticalLevelValue -Setting "Critical Battery Level" } - if ($LowLevel) { Set-PowerValue -GUID "SUB_BATTERY" -Index "8183ba9a-e910-48da-8769-14ae6dc1170a" -Value $LowLevelValue -Setting "Low Battery Level" } - if ($LowNoNotify) { Set-PowerValue -GUID "SUB_BATTERY" -Index "bcded951-187b-4d05-bccc-f7e51960c258" -Value 0 -Setting "Disable Low Battery Notification" } - if ($LowNotify) { Set-PowerValue -GUID "SUB_BATTERY" -Index "bcded951-187b-4d05-bccc-f7e51960c258" -Value 1 -Setting "Enable Low Battery Notification" } -} -end { - - - -} - +<# +.SYNOPSIS + Set Power and Sleep Settings. It can adjust just the plugged in or battery settings if requested. + Please Note not all devices support all options. +.DESCRIPTION + Please Note not all devices support all options. + Options: ScreenTimeout, HibernateTimeout, SleepTimeout, Disk Timeout, PowerPlan, Lid Action, Wake Timers, USB Suspend, Critical Action + Low Action, Power Button Action, Critical Level, Low Level, Reserve Level, Critical Notification, and Low Notification. +.EXAMPLE + (No Parameters) + + By default The Script doesn't do anything without some parameters. +.LINK + https://learn.microsoft.com/en-us/windows/win32/power/power-policy-settings + +PARAMETER: -ScreenTimeout "60" + Replace 60 with any time in seconds to set the screen timeout. (0 for disabled) + +PARAMETER: -HibernateTimeout "28800" + Replace 28800 with any time in seconds. (0 for disabled) + +PARAMETER: -SleepTimeout "14400" + Replace 14400 with any time in seconds. (0 for disabled) + +PARAMETER: -DiskTimeout "0" + Replace 0 with your desired time in seconds. (0 for disabled) + +PARAMETER: -PowerPlan "High Performance" + Replace "High Performance" with your desired power plan. Keep in mind that most newer computers no longer have seperate power plans. + +PARAMETER: -LidAction "Nothing" + Replace Nothing with one of these three available options. Sleep, Shutdown, Nothing. + Will be skipped for non-laptops and this script cannot verify if the action was successfully set. + +PARAMETER: -AllowWakeTimers + Allows the ability for software to wake the computer from sleep at a later date. + +PARAMETER: -DisableWakeTimers + Disables the ability for software to wake the computer from sleep at a later date. + +PARAMETER: -EnableUSBSuspend + Allows the OS to suspend USB devices to conserve power. + +PARAMETER: -DisableUSBSuspend + Disable's the OS's ability to suspend USB devices to conserve power. + +PARAMETER: -CriticalAction "Hibernate" + Replace Hibernate with your desired action for when the machine is at a "Critical" batter level. + Valid Options: Hibernate, Sleep, Shutdown, Nothing + +PARAMETER: -LowAction "Hibernate" + Replace Hibernate with your desired action for when the machine is at a "Low" battery level. + Valid Options: Hibernate, Sleep, Shutdown, Nothing. + +PARAMETER: -CriticalLevel "7" + Replace 7 with your desired battery percent level to be considered critical (without the % symbol). + +PARAMETER: -LowLevel "10" + Replace 10 with your desired battery percent level to be considered low (without the % symbol). + +PARAMETER: -LowNotify + Allows the notification that comes in when the battery hits "low" levels. + +PARAMETER: -AC + Only applies your chosen battery settings to the "Plugged In" section of a power plan. + +PARAMETER: -DC + Only applies your chosen battery settings to the "Battery" section of a power plan. +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 7, Windows Server 2008 + General notes + Version: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$ScreenTimeout, + [Parameter()] + [String]$HibernateTimeout, + [Parameter()] + [String]$SleepTimeout, + [Parameter()] + [String]$DiskTimeout, + [Parameter()] + [String]$PowerPlan, + [Parameter()] + [String]$LidAction, + [Parameter()] + [Switch]$AllowWakeTimers, + [Parameter()] + [Switch]$DisableWakeTimers, + [Parameter()] + [Switch]$EnableUSBSuspend, + [Parameter()] + [Switch]$DisableUSBSuspend, + [Parameter()] + [String]$CriticalAction, + [Parameter()] + [String]$LowAction, + [Parameter()] + [String]$CriticalLevel, + [Parameter()] + [String]$LowLevel, + [Parameter()] + [Switch]$LowNotify, + [Parameter()] + [Switch]$LowNoNotify, + [Parameter()] + [Switch]$AC, + [Parameter()] + [Switch]$DC +) + +begin { + # Grab Script Variables if present + if ($env:powerSourceSetting -and $env:powerSourceSetting -notlike "null") { + switch ($env:powerSourceSetting) { + "Plugged In" { $AC = $True } + "On Battery" { $DC = $True } + default {} + } + } + if ($env:screenTimeoutInMinutes -and $env:screenTimeoutInMinutes -notlike "null") { $ScreenTimeout = $env:screenTimeoutInMinutes } + if ($env:hibernateTimeoutInMinutes -and $env:hibernateTimeoutInMinutes -notlike "null") { $HibernateTimeout = $env:hibernateTimeoutInMinutes } + if ($env:sleepTimeoutInMinutes -and $env:sleepTimeoutInMinutes -notlike "null") { $SleepTimeout = $env:sleepTimeoutInMinutes } + if ($env:diskTimeoutInMinutes -and $env:diskTimeoutInMinutes -notlike "null") { $DiskTimeout = $env:diskTimeoutInMinutes } + if ($env:powerPlan -and $env:powerPlan -notlike "null") { $PowerPlan = $env:powerPlan } + if ($env:lidAction -and $env:lidAction -notlike "null") { $LidAction = $env:lidAction } + if ($env:wakeTimers -and $env:wakeTimers -notlike "null") { + switch ($env:wakeTimers) { + "Enable" { $AllowWakeTimers = $True } + "Disable" { $DisableWakeTimers = $True } + } + } + if ($env:usbSuspend -and $env:usbSuspend -notlike "null") { + switch ($env:usbSuspend) { + "Enable" { $EnableUSBSuspend = $True } + "Disable" { $DisableUSBSuspend = $True } + } + } + if ($env:criticalBatteryAction -and $env:criticalBatteryAction -notlike "null") { $CriticalAction = $env:criticalBatteryAction } + if ($env:lowBatteryAction -and $env:lowBatteryAction -notlike "null") { $LowAction = $env:lowBatteryAction } + if ($env:criticalBatteryLevel -and $env:criticalBatteryLevel -notlike "null") { $CriticalLevel = $env:criticalBatteryLevel } + if ($env:lowBatteryLevel -and $env:lowBatteryLevel -notlike "null") { $LowLevel = $env:lowBatteryLevel } + if ($env:lowNotification -and $env:lowNotification -notlike "null") { + switch ($env:lowNotification) { + "Enable" { $LowNotify = $True } + "Disable" { $LowNoNotify = $True } + } + } + + if ($ScreenTimeout) { [int]$ScreenTimeoutValue = [int]$ScreenTimeout * 60 } + if ($HibernateTimeout) { [int]$HibernateTimeoutValue = [int]$HibernateTimeout * 60 } + if ($SleepTimeout) { [int]$SleepTimeoutValue = [int]$SleepTimeout * 60 } + if ($DiskTimeout) { [int]$DiskTimeoutValue = [int]$DiskTimeout * 60 } + if ($CriticalLevel) { [int]$CriticalLevelValue = [int]$CriticalLevel } + if ($LowLevel) { [int]$LowLevelValue = [int]$LowLevel } + + # Elevation Test + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if ( + -not $ScreenTimeout -and -not $HibernateTimeout -and -not $SleepTimeout -and -not $DiskTimeout -and -not $PowerPlan -and + -not $LidAction -and -not $AllowWakeTimers -and -not $DisableWakeTimers -and -not $EnableUSBSuspend -and + -not $DisableUSBSuspend -and -not $CriticalAction -and -not $LowAction -and -not $CriticalLevel -and + -not $LowLevel -and -not $LowNotify -and -not $LowNoNotify + ) { + Write-Error -Message '[Error] No action given!' + exit 1 + } + + # Check for battery and whether or not the device is a laptop + if ($PSVersionTable.PSVersion.Major -lt 5) { + $BatteryCheck = @(Get-WmiObject -Class Win32_Battery).Count -gt 0 + $LaptopCheck = Get-WmiObject -Class win32_systemenclosure | Where-Object { $_.chassistypes -eq 9 -or $_.chassistypes -eq 10 -or $_.chassistypes -eq 14 } + } + else { + $BatteryCheck = @(Get-CimInstance -Class Win32_Battery).Count -gt 0 + $LaptopCheck = Get-CimInstance -Class win32_systemenclosure | Where-Object { $_.chassistypes -eq 9 -or $_.chassistypes -eq 10 -or $_.chassistypes -eq 14 } + } + + # Function to test if policy was set correctly + function Test-PowerValue { + [CmdletBinding()] + param( + [Parameter()] + [String]$GUID, + [Parameter()] + [String]$Index, + [Parameter()] + [int]$Value, + [Parameter()] + [String]$Setting + ) + + # LidAction shows no information so we'll issue a warning and then exit the test function. + if ($GUID -eq "SUB_BUTTONS" -and $Index -eq "5ca83367-6e45-459f-a27b-476b1d01c936") { + Write-Warning "Unable to verify LidAction via script." + break + } + + # Values are stored in hex so we'll need to convert it. + $Hex = "0x" + '{0:X8}' -f $Value + + $PowerQuery = powercfg.exe /QUERY SCHEME_CURRENT $GUID | Out-String + $RelevantSetting = $PowerQuery -split "Power Setting GUID:" | Where-Object { $_ -like "*$Index*" } + if (-not ($RelevantSetting)) { + Write-Warning "Unable to verify setting for $GUID $Index. This option may not exist for this machine." + break + } + + # Depending on how the script was ran we'll need to verify in different ways (ex. its on a laptop or only AC was specified) + if ($AC -or -not $BatteryCheck) { + $ACQuery = $RelevantSetting -split '\s{2}' | Where-Object { $_ -eq "Current AC Power Setting Index: $Hex" } + + # Actual check code is really similar though (only difference is AC vs DC). + if (-not $ACQuery) { + Write-Warning "AC Value of $Hex Not Found for $GUID $Index. You may want to verify the results." + break + } + } + elseif ($DC) { + $BatteryQuery = $RelevantSetting -split '\s{2}' | Where-Object { $_ -eq "Current DC Power Setting Index: $Hex" } + + if (-not $BatteryQuery) { + Write-Warning "DC Value of $Hex Not Found for $GUID $Index. You may want to verify the results." + break + } + } + else { + $BatteryQuery = $RelevantSetting -split '\s{2}' | Where-Object { $_ -eq "Current DC Power Setting Index: $Hex" } + if (-not $BatteryQuery) { + Write-Warning "DC Value of $Hex Not Found for $GUID $Index. You may want to verify the results." + break + } + + $ACQuery = $RelevantSetting -split '\s{2}' | Where-Object { $_ -eq "Current AC Power Setting Index: $Hex" } + if (-not $ACQuery) { + Write-Warning "AC Value of $Hex Not Found for $GUID $Index. You may want to verify the results." + break + } + } + + Write-Host "Successfully set power setting for $Setting!" + } + function Set-PowerAction { + [CmdletBinding()] + param( + [Parameter()] + [ValidateSet("Nothing", "Sleep", "Hibernate", "ShutDown")] + [String]$Action, + [Parameter()] + [String]$GUID, + [Parameter()] + [String]$Index, + [Parameter()] + [String]$Setting + ) + try { + switch ($Action) { + "Nothing" { + if ($AC -or -not $BatteryCheck) { + powercfg.exe /SETACVALUEINDEX SCHEME_CURRENT $GUID $Index 0 + } + elseif ($DC) { + powercfg.exe /SETDCVALUEINDEX SCHEME_CURRENT $GUID $Index 0 + } + else { + powercfg.exe /SETACVALUEINDEX SCHEME_CURRENT $GUID $Index 0 + powercfg.exe /SETDCVALUEINDEX SCHEME_CURRENT $GUID $Index 0 + } + + Test-PowerValue $GUID $Index 0 -Setting $Setting + } + "Sleep" { + if ($AC -or -not $BatteryCheck) { + powercfg.exe /SETACVALUEINDEX SCHEME_CURRENT $GUID $Index 1 + } + elseif ($DC) { + powercfg.exe /SETDCVALUEINDEX SCHEME_CURRENT $GUID $Index 1 + } + else { + powercfg.exe /SETACVALUEINDEX SCHEME_CURRENT $GUID $Index 1 + powercfg.exe /SETDCVALUEINDEX SCHEME_CURRENT $GUID $Index 1 + } + + Test-PowerValue $GUID $Index 1 -Setting $Setting + } + "Hibernate" { + if ($AC -or -not $BatteryCheck) { + powercfg.exe /SETACVALUEINDEX SCHEME_CURRENT $GUID $Index 2 + } + elseif ($DC) { + powercfg.exe /SETDCVALUEINDEX SCHEME_CURRENT $GUID $Index 2 + } + else { + powercfg.exe /SETACVALUEINDEX SCHEME_CURRENT $GUID $Index 2 + powercfg.exe /SETDCVALUEINDEX SCHEME_CURRENT $GUID $Index 2 + } + + Test-PowerValue $GUID $Index 2 -Setting $Setting + } + "Shutdown" { + if ($AC -or -not $BatteryCheck) { + powercfg.exe /SETACVALUEINDEX SCHEME_CURRENT $GUID $Index 3 + } + elseif ($DC) { + powercfg.exe /SETDCVALUEINDEX SCHEME_CURRENT $GUID $Index 3 + } + else { + powercfg.exe /SETACVALUEINDEX SCHEME_CURRENT $GUID $Index 3 + powercfg.exe /SETDCVALUEINDEX SCHEME_CURRENT $GUID $Index 3 + } + + Test-PowerValue $GUID $Index 3 -Setting $Setting + } + default { + throw "$Action is not a valid action for $Setting, valid actions are Shutdown, Hibernate, Sleep and Nothing." + } + } + } + catch { + Write-Warning $_.Exception.Message + Write-Warning "Failed to set power setting for $Setting" + } + } + + # The actual code required to set these values is very similar. + function Set-PowerValue { + [CmdletBinding()] + param( + [Parameter()] + [String]$Value, + [Parameter()] + [String]$GUID, + [Parameter()] + [String]$Index, + [Parameter()] + [String]$Setting + ) + + try { + if ($AC -or -not $BatteryCheck) { + powercfg.exe /SETACVALUEINDEX SCHEME_CURRENT $GUID $Index $Value + } + elseif ($DC) { + powercfg.exe /SETDCVALUEINDEX SCHEME_CURRENT $GUID $Index $Value + } + else { + powercfg.exe /SETACVALUEINDEX SCHEME_CURRENT $GUID $Index $Value + powercfg.exe /SETDCVALUEINDEX SCHEME_CURRENT $GUID $Index $Value + } + + Test-PowerValue $GUID $Index $Value -Setting $Setting + } + catch { + Write-Warning $_.Exception.Message + Write-Warning "Failed to set power setting for $Setting" + } + } + + # Before changing the power plan we should check if it exists and whether or not it is in use. + function Get-PowerPlan { + [CmdletBinding()] + param( + [Parameter()] + [Switch]$Active, + [Parameter()] + [String]$Name + ) + if ($Active) { + $PowerPlan = powercfg.exe /getactivescheme + $PowerPlan = ($PowerPlan -replace "Power Scheme GUID:" -split "(?=\S{8}-\S{4}-\S{4}-\S{17})" -split '\(' -replace '\)') | Where-Object { $_ -ne " " } + $PowerPlan = @( + [PSCustomObject]@{ + Name = $($PowerPlan | Where-Object { $_ -notmatch "\S{8}-\S{4}-\S{4}-\S{17}" }) + GUID = $($PowerPlan | Where-Object { $_ -match "\S{8}-\S{4}-\S{4}-\S{17}" }) + } + ) + } + else { + $PowerPlan = powercfg.exe /L + $PowerPlan = $PowerPlan -replace '\s{2,}', ',' -replace ' \*', ',True' -replace "Existing Power Schemes \(\* Active\)", "GUID,Name,Active" -replace "-{2,}" -replace "Power Scheme GUID: " -replace '\(' -replace '\)' | Where-Object { $_ } | ConvertFrom-Csv + } + + if ($Name) { + $PowerPlan | Where-Object { $_.Name -like $Name } + } + else { + $PowerPlan + } + } +} +process { + # If not elevated, exit the script. + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + # If a power plan was specified, let the user know that this might not actually be a changeable option (depending on computer age). + if ($PowerPlan) { + Write-Warning "Devices with modern standby might only have `"Balanced`" as an option." + Write-Warning "Link: https://learn.microsoft.com/en-us/windows/win32/power/power-policy-settings" + + $TargetPlan = Get-PowerPlan -Name $PowerPlan + # If totally not available we're going to exit as all these settings are tied to the active power plan. + # If we're not able to set it we'll want to give an opportunity to correct the error. + if ($null -eq $TargetPlan) { + Write-Error "Your targeted Power Plan is not available." + exit 1 + } + + if ($TargetPlan.Active -ne $True) { + powercfg.exe /setactive $TargetPlan.GUID + } + + $CurrentPlan = Get-PowerPlan -Active + if ($CurrentPlan.GUID -notlike "*$($TargetPlan.GUID)*") { + Write-Error "Failed to change power plan!" + exit 1 + } + else { + Write-Host "Successfully set Power Plan!" + } + } + + # We're going to run through all the various options and if requested we'll adjust them. + if ($ScreenTimeout) { Set-PowerValue -GUID "SUB_VIDEO" -Index "3c0bc021-c8a8-4e07-a973-6b14cbcb2b7e" -Value $ScreenTimeoutValue -Setting "Screen Timeout" } + if ($HibernateTimeout) { + Write-Warning "Hibernate timeout is not supported by all devices and may be ignored by the System." + Set-PowerValue -GUID "SUB_SLEEP" -Index "9d7815a6-7ee4-497e-8888-515a05f02364" -Value $HibernateTimeoutValue -Setting "Hibernate Timeout" + } + if ($SleepTimeout) { Set-PowerValue -GUID "SUB_SLEEP" -Index "29f6c1db-86da-48c5-9fdb-f2b67b1f44da" -Value $SleepTimeoutValue -Setting "Sleep Timeout" } + if ($DiskTimeout) { + Write-Warning "An HDD is required for this setting to display (has no effect on SSDs)." + Set-PowerValue -GUID "SUB_DISK" -Index "6738e2c4-e8a5-4a42-b16a-e040e769756e" -Value $DiskTimeoutValue -Setting "Disk Timeout" + } + if ($DisableUSBSuspend) { Set-PowerValue -GUID "2a737441-1930-4402-8d77-b2bebba308a3" -Index "48e6b7a6-50f5-4782-a5d4-53bb8f07e226" -Value 0 -Setting "Disable USB Auto-Suspend" } + if ($EnableUSBSuspend) { Set-PowerValue -GUID "2a737441-1930-4402-8d77-b2bebba308a3" -Index "48e6b7a6-50f5-4782-a5d4-53bb8f07e226" -Value 1 -Setting "Enable USB Auto-Suspend" } + if ($AllowWakeTimers) { Set-PowerValue -GUID "SUB_SLEEP" -Index "bd3b718a-0680-4d9d-8ab2-e1d2b4ac806d" -Value 1 -Setting "Allow Wake Timers" } + if ($DisableWakeTimers) { Set-PowerValue -GUID "SUB_SLEEP" -Index "bd3b718a-0680-4d9d-8ab2-e1d2b4ac806d" -Value 0 -Setting "Disable Wake Timers" } + # Lid Action has slightly different options than Set-PowerAction. + if ($LidAction -and $LaptopCheck) { + switch ($LidAction) { + "Nothing" { Set-PowerValue -GUID "SUB_BUTTONS" -Index "5ca83367-6e45-459f-a27b-476b1d01c936" -Value 0 -Setting "Lid Action" } + "Sleep" { Set-PowerValue -GUID "SUB_BUTTONS" -Index "5ca83367-6e45-459f-a27b-476b1d01c936" -Value 1 -Setting "Lid Action" } + "Shutdown" { Set-PowerValue -GUID "SUB_BUTTONS" -Index "5ca83367-6e45-459f-a27b-476b1d01c936" -Value 3 -Setting "Lid Action" } + default { + Write-Error "Invalid PowerButton Option. Only Sleep, Nothing and Shutdown are allowed!" + exit 1 + } + } + } + if ($CriticalAction) { + Set-PowerAction -GUID "SUB_BATTERY" -Index "637ea02f-bbcb-4015-8e2c-a1c7b9c0b546" -Action $CriticalAction -Setting "Critical Battery Action" + } + if ($LowAction) { + Set-PowerAction -GUID "SUB_BATTERY" -Index "d8742dcb-3e6a-4b3c-b3fe-374623cdcf06" -Action $LowAction -Setting "Low Battery Action" + } + if ($CriticalLevel) { Set-PowerValue -GUID "SUB_BATTERY" -Index "9a66d8d7-4ff7-4ef9-b5a2-5a326ca2a469" -Value $CriticalLevelValue -Setting "Critical Battery Level" } + if ($LowLevel) { Set-PowerValue -GUID "SUB_BATTERY" -Index "8183ba9a-e910-48da-8769-14ae6dc1170a" -Value $LowLevelValue -Setting "Low Battery Level" } + if ($LowNoNotify) { Set-PowerValue -GUID "SUB_BATTERY" -Index "bcded951-187b-4d05-bccc-f7e51960c258" -Value 0 -Setting "Disable Low Battery Notification" } + if ($LowNotify) { Set-PowerValue -GUID "SUB_BATTERY" -Index "bcded951-187b-4d05-bccc-f7e51960c258" -Value 1 -Setting "Enable Low Battery Notification" } +} +end { + +} + diff --git a/Powershell Scripts/Check Battery Health.ps1 b/Powershell Scripts/Check Battery Health.ps1 index c608e20..5dba0e8 100644 --- a/Powershell Scripts/Check Battery Health.ps1 +++ b/Powershell Scripts/Check Battery Health.ps1 @@ -1,1244 +1,1124 @@ # Retrieves the overall battery health and optionally saves the results to a WYSIWYG custom field. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Retrieves the overall battery health and optionally saves the results to a WYSIWYG custom field. -.DESCRIPTION - Retrieves the overall battery health and optionally saves the results to a WYSIWYG custom field. -.EXAMPLE - -WYSIWYGCustomField "WYSIWYG" - - Creating the battery health report. - Battery life report saved to file path C:\Windows\Temp\batteryhealthreport.xml. - Created the battery health report. - Retrieving the report results. - Retrieved the results. - - Parsing the system information. - Parsing the battery specifications. - Parsing the battery capacity history. - Parsing the battery duration history. - Parsing the recent battery usage history. - - Formatting the battery capacity history to be human-readable. - Formatting the battery usage history to be human-readable. - Formatting the recent usage history to be human-readable. - - Formatting the results for the WYSIWYG Custom Field 'WYSIWYGCustomField'. - Creating the system information HTML card. - Creating the installed batteries HTML card. - Creating the battery capacity history HTML card. - Creating the battery usage HTML card. - Creating the recent usage HTML card. - Assembling the final WYSIWYG Value - Attempting to set the Custom Field 'WYSIWYG'. - Successfully set the Custom Field 'WYSIWYG'! - - ### System Information ### - ReportTime : 12/16/2024 4:12 PM - SystemProductName : Dell Inc. Precision 3571 - BIOS : 1.27.0 9/27/2024 - OSBuild : 26100.1.amd64fre.ge_release.240331-1435 - ConnectedStandby : Supported - - ### Installed Batteries ### - Name : DELL 0P3TJK9 - Manufacturer : SMP - SerialNumber : 1 - Chemistry : LiP - UsableBatteryPercentage : 70.29% - DesignCapacity : 64007 mWh - FullChargeCapacity : 44992 mWh - CycleCount : - - - ### Battery Capacity History ### - Date FullChargeCapacity DesignCapacity - ---- ------------------ -------------- - 12/15/2024 44992 mWh 64007 mWh - 12/9/2024 44992 mWh 64007 mWh - 5/26/2024 48929 mWh 64007 mWh - 2/4/2024 51565 mWh 64007 mWh - 1/7/2024 52850 mWh 64007 mWh - 12/24/2023 55681 mWh 64007 mWh - 12/10/2023 56057 mWh 64007 mWh - 10/1/2023 56787 mWh 64007 mWh - 8/27/2023 58532 mWh 64007 mWh - 7/30/2023 60709 mWh 64007 mWh - 7/16/2023 61052 mWh 64007 mWh - 4/30/2023 64007 mWh 64007 mWh - 4/2/2023 64007 mWh 64007 mWh - - ### Battery Usage ### - StartDate BatteryActive BatteryConnectedStandby ACActive ACConnectedStandby - --------- ------------- ----------------------- -------- ------------------ - 12/15/2024 - 14s - 21h 59m 12s - 12/14/2024 - 13s - 21h 59m 12s - 12/13/2024 30m - 7h 10m 27s 16h 19m 12s - 12/12/2024 - 1h 59m 59s - 21h 59m 17s - 12/11/2024 1h 58m 34s 13s 2h 1m 20s 22h 17m 16s - 12/10/2024 - 1h 59m 52s - 23h 28m 55s - 12/9/2024 - 1h 59m 39s - 21h 59m 22s - 12/8/2024 - 1h 59m 59s - 11h 59m 16s - - ### Recent Power Usage ### - StartTime State Source PercentageRemaining CapacityRemaining - --------- ----- ------ ------------------- ----------------- - 12/16/2024 9:37:35 AM Active AC 34.9% 15702 mWh - 12/16/2024 9:00:35 AM Active Battery 100% 44992 mWh - 12/16/2024 8:17:21 AM Active AC 100% 44992 mWh - 12/15/2024 11:01:15 AM ConnectedStandby AC 81.18% 36526 mWh - 12/15/2024 9:00:59 AM Suspend 98.78% 44445 mWh - 12/15/2024 9:00:54 AM ConnectedStandby Battery 98.95% 44521 mWh - 12/15/2024 9:00:44 AM Suspend 99.39% 44718 mWh - 12/15/2024 9:00:35 AM ConnectedStandby Battery 100% 44992 mWh - 12/15/2024 9:00:33 AM Suspend 100% 44992 mWh - 12/14/2024 11:01:13 AM ConnectedStandby AC 77.4% 34823 mWh - 12/14/2024 9:00:50 AM Suspend 98.99% 44536 mWh - 12/14/2024 9:00:45 AM ConnectedStandby Battery 99.16% 44612 mWh - 12/14/2024 9:00:40 AM Suspend 99.43% 44734 mWh - 12/14/2024 9:00:32 AM ConnectedStandby Battery 100% 44992 mWh - 12/14/2024 9:00:30 AM Suspend 100% 44992 mWh - 12/13/2024 4:26:48 PM ConnectedStandby AC 100% 44992 mWh - 12/13/2024 12:18:07 PM Active AC 98.85% 44475 mWh - 12/13/2024 11:53:51 AM ConnectedStandby AC 92.09% 41435 mWh - 12/13/2024 11:53:20 AM Active AC 91.86% 41329 mWh - 12/13/2024 11:44:16 AM ConnectedStandby AC 86.82% 39064 mWh - 12/13/2024 10:39:38 AM Active AC 32.91% 14805 mWh - 12/13/2024 10:37:50 AM ConnectedStandby AC 32.91% 14805 mWh - 12/13/2024 9:30:34 AM Active AC 32.94% 14820 mWh - 12/13/2024 9:00:32 AM Active Battery 100% 44992 mWh - 12/13/2024 8:11:03 AM Active AC 100% 44992 mWh - 12/12/2024 11:01:09 AM ConnectedStandby AC 81.99% 36890 mWh - 12/12/2024 11:00:31 AM Suspend 81.59% 36708 mWh - 12/12/2024 9:00:31 AM ConnectedStandby Battery 100% 44992 mWh - 12/11/2024 1:01:56 PM ConnectedStandby AC 100% 44992 mWh - 12/11/2024 11:00:33 AM Active AC 50.37% 22663 mWh - 12/11/2024 9:01:58 AM Active Battery 97.47% 43852 mWh - 12/11/2024 9:00:48 AM Suspend 99.43% 44734 mWh - 12/11/2024 9:00:34 AM ConnectedStandby Battery 100% 44992 mWh - 12/11/2024 9:00:33 AM Suspend 100% 44992 mWh - 12/11/2024 6:41:48 AM ConnectedStandby AC 100% 44992 mWh - 12/10/2024 11:01:19 AM ConnectedStandby AC 79.53% 35781 mWh - 12/10/2024 11:00:35 AM Suspend 78.99% 35538 mWh - 12/10/2024 9:00:54 AM ConnectedStandby Battery 99.26% 44658 mWh - 12/10/2024 9:00:49 AM Suspend 99.43% 44734 mWh - 12/10/2024 9:00:37 AM ConnectedStandby Battery 100% 44992 mWh - 12/10/2024 9:00:34 AM Suspend 100% 44992 mWh - 12/10/2024 7:30:51 AM ConnectedStandby AC 100% 44992 mWh - 12/9/2024 5:13:22 PM ConnectedStandby AC 100% 44992 mWh - -PARAMETER: -WYSIWYGCustomField "ReplaceMeWithTheNameOfAWysiwygCustomField" - Optionally, save the results to a WYSIWYG custom field. - -.NOTES - Minimum OS Architecture Supported: Windows 10 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$WYSIWYGCustomField -) - -begin { - # If script form variables are used, replace the commandline parameters with their value. - if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $WYSIWYGCustomField = $env:wysiwygCustomFieldName } - - # Attempt to retrieve the battery information using the appropriate command. - try { - if ($PSVersionTable.PSVersion.Major -lt 3) { - $CurrentBattery = Get-WmiObject -Class Win32_Battery -ErrorAction Stop - } - else { - $CurrentBattery = Get-CimInstance -ClassName Win32_Battery -ErrorAction Stop - } - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] No battery detected on the system." - exit 1 - } - - # Check if no battery information was retrieved. - if (!$CurrentBattery) { - # Inform the user that no battery was detected and exit with an error code. - Write-Host -Object "[Error] No battery detected on the system." - exit 1 - } - - function Test-IsServer { - # Determine the method to retrieve the operating system information based on PowerShell version - try { - $OS = if ($PSVersionTable.PSVersion.Major -lt 3) { - Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop - } - else { - Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop - } - } - catch { - Write-Host -Object "[Error] Unable to validate whether or not this device is a server." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Check if the ProductType is "2", which indicates that the system is a domain controller or is a server - if ($OS.ProductType -eq "2" -or $OS.ProductType -eq "3") { - return $true - } - } - - # Function to convert time spans into a human-friendly string. - function Get-FriendlyTimeSpan { - param( - [Parameter(Mandatory = $True)] - [TimeSpan]$TimeSpan - ) - - # Check if the provided TimeSpan is less than 1 second. - # Throw an exception if the TimeSpan is too short. - if ($TimeSpan -le [TimeSpan]::FromMilliseconds(999)) { - throw [System.ArgumentOutOfRangeException]::New("The provided time span is less than 0 seconds. Please specify a longer duration.") - } - - # Build a human-readable representation of the TimeSpan. - $FriendlyTimeSpan = $Null - if ($TimeSpan.Days) { $FriendlyTimeSpan = "$($TimeSpan.Days)d" } - if ($TimeSpan.Hours) { $FriendlyTimeSpan = "$FriendlyTimeSpan $($TimeSpan.Hours)h" } - if ($TimeSpan.Minutes) { $FriendlyTimeSpan = "$FriendlyTimeSpan $($TimeSpan.Minutes)m" } - if ($TimeSpan.Seconds) { $FriendlyTimeSpan = "$FriendlyTimeSpan $($TimeSpan.Seconds)s" } - - # Check if the conversion failed and no output was generated. - if (!$FriendlyTimeSpan) { - throw [System.FormatException]::New("Failed to convert the time span '$TimeSpan' into a human-friendly format.") - } - - # Return the trimmed friendly TimeSpan string (removes any leading or trailing whitespace). - $FriendlyTimeSpan.Trim() - } - - # Function to parse ISO 8601 duration strings and convert them into a TimeSpan object - function Get-ISO8601Duration { - param( - [Parameter()] - [String]$Duration - ) - - # Validate that the duration starts with the 'P' designator - if ($Duration -notmatch "^P") { - throw [System.IO.InvalidDataException]::New("An invalid duration of '$Duration' was given. ISO 8601 durations require durations to start with the P designator. https://en.wikipedia.org/wiki/ISO_8601#Durations") - } - - # Ensure the duration contains numeric characters - if ($Duration -notmatch "[0-9]") { - throw [System.IO.InvalidDataException]::New("An invalid duration of '$Duration' was given. ISO 8601 durations require numeric characters. https://en.wikipedia.org/wiki/ISO_8601#Durations") - } - - # Validate that only allowed characters are present in the duration string - if ($Duration -match "[^0-9PYMDTHS.,]") { - throw [System.IO.InvalidDataException]::New("An invalid duration of '$Duration' was given. ISO 8601 non-alternative duration format can only contain the following characters '0-9PYMDTHS.,'. https://en.wikipedia.org/wiki/ISO_8601#Durations") - } - - # Define patterns to match date and time components of ISO 8601 duration - $DateFormat = "P.*(([0-9]+Y)+|([0-9]+M)+|([0-9]+D)+)" - $TimeFormat = "P.*T(([0-9]+H)+|([0-9]+M)+|([0-9]+S)+)" - - # Ensure that the duration contains either valid date or time components - if ($Duration -notmatch $DateFormat -and $Duration -notmatch $TimeFormat) { - throw [System.IO.InvalidDataException]::New("An invalid duration of '$Duration' was given. The ISO 8601 non-alternative duration format should look like 'PnYnMnDTnHnMnS' where n is a number. https://en.wikipedia.org/wiki/ISO_8601#Durations") - } - - # Extract the date part of the duration (e.g., "PnYnMnD") - if ($Duration -match $DateFormat) { - $Date = $Duration -replace ',', '.' -replace 'T.*' - } - - # Extract the time part of the duration (e.g., "TnHnMnS") - if ($Duration -match $TimeFormat) { - $Time = $Duration -replace ',', '.' -replace '.*T' - } - - # If both date and time components are missing, throw an error - if (!$Date -and !$Time) { - throw [System.IO.InvalidDataException]::New("Failed to extract the date and time sections from '$Duration'.") - } - - # Parse date components: years, months, and days - if ($Date -match '[0-9.,]+Y') { $YearsGiven = $Matches[0] -replace "Y" } - if ($Date -match '[0-9.,]+M') { $MonthsGiven = $Matches[0] -replace "M" } - if ($Date -match '[0-9.,]+D') { $DaysGiven = $Matches[0] -replace "D" } - - # Parse time components: hours, minutes, and seconds - if ($Time -match '[0-9.,]+H') { $HoursGiven = $Matches[0] -replace "H" } - if ($Time -match '[0-9.,]+M') { $MinutesGiven = $Matches[0] -replace "M" } - if ($Time -match '[0-9.,]+S') { $SecondsGiven = $Matches[0] -replace "S" } - - # If no components were extracted, throw an error - if (!$YearsGiven -and !$MonthsGiven -and !$DaysGiven -and !$HoursGiven -and !$MinutesGiven -and !$SecondsGiven) { - throw [System.IO.InvalidDataException]::New("Failed to extract the years, months, days, hours, minutes, or seconds from '$Duration'.") - } - - try { - # Calculate the total duration in seconds - if ($YearsGiven) { $TotalSeconds = ([double]$YearsGiven * 31557600) } - if ($MonthsGiven) { $TotalSeconds = ([double]$TotalSeconds + ([double]$MonthsGiven * 2630016)) } - if ($DaysGiven) { $TotalSeconds = ([double]$TotalSeconds + ([double]$DaysGiven * 86400)) } - - if ($HoursGiven) { $TotalSeconds = ([double]$TotalSeconds + ([double]$HoursGiven * 3600)) } - if ($MinutesGiven) { $TotalSeconds = ([double]$TotalSeconds + ([double]$MinutesGiven * 60)) } - if ($SecondsGiven) { $TotalSeconds = ([double]$TotalSeconds + [double]$SecondsGiven) } - } - catch { - # Catch and re-throw any calculation errors - throw $_ - } - - try { - # Create and return a TimeSpan object representing the total duration - New-TimeSpan -Seconds $TotalSeconds -ErrorAction Stop - } - catch { - # Catch and re-throw any errors during TimeSpan creation - throw $_ - } - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName, - [Parameter()] - [Switch]$Piped - ) - # Remove the non-breaking space character - if ($Type -eq "WYSIWYG") { - $Value = $Value -replace ' ', ' ' - } - - # Measure the number of characters in the provided value - $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # Throw an error if the value exceeds the character limit of 200,000 characters - if ($Piped -and $Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - if (!$Piped -and $Characters -ge 45000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") - } - - # Initialize a hashtable for additional documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define a list of valid field types - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - - # Warn the user if the provided type is not valid - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # Define types that require options to be retrieved - $NeedsOptions = "Dropdown" - - # If the property is being set in a document or field and the type needs options, retrieve them - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an error if there was an issue retrieving the property options - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Process the property value based on its type - switch ($Type) { - "Checkbox" { - # Convert the value to a boolean for Checkbox type - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Convert the value to a Unix timestamp for Date or Date Time type - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Convert the dropdown value to its corresponding GUID - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - # Throw an error if the value is not present in the dropdown options - if (!($Selection)) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # For other types, use the value as is - $NinjaValue = $Value - } - } - - # Set the property value in the document if a document name is provided - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - try { - # Otherwise, set the standard property value - if ($Piped) { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - } - catch { - Write-Host -Object "[Error] Failed to set custom field." - throw $_.Exception.Message - } - } - - # Throw an error if setting the property failed - if ($CustomField.Exception) { - throw $CustomField - } - } - - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # Check if the current user is running the script with elevated privileges. - if (!(Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run this with Administrator privileges." - exit 1 - } - - # Check if this script is running on Windows Server. - if (Test-IsServer) { - Write-Host -Object "[Error] The powercfg battery report is not available on Windows Server. Please run this on a workstation." - exit 1 - } - - # Set file paths for the battery report and log files. - $BatteryReport = "$env:TEMP\batteryhealthreport.xml" - $StandardOutputLog = "$(New-Guid)-stdout-batteryreport.log" - $StandardErrorLog = "$(New-Guid)-stderr-batteryreport.log" - - # Define the arguments that will be passed to powercfg.exe to generate the battery report. - $PowerCfgArguments = @( - "/BATTERYREPORT" - "/XML" - "/OUTPUT" - $BatteryReport - ) - - # Define the parameters for starting the powercfg.exe process, including output redirection. - $PowerCfgProcessArguments = @{ - FilePath = "$env:SYSTEMROOT\System32\powercfg.exe" - ArgumentList = $PowerCfgArguments - RedirectStandardOutput = $StandardOutputLog - RedirectStandardError = $StandardErrorLog - PassThru = $True - NoNewWindow = $True - Wait = $True - } - - Write-Host -Object "Creating the battery health report." - # Attempt to run the powercfg.exe process with the specified arguments. - try { - $PowerCfgProcess = Start-Process @PowerCfgProcessArguments -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to generate battery health report." - exit 1 - } - - # Check the exit code of the powercfg.exe process. A non-zero exit code may indicate an error. - if ($PowerCfgProcess.ExitCode -ne 0) { - Write-Host -Object "Exit Code: $($PowerCfgProcess.ExitCode)" - Write-Host -Object "The exit code does not indicate success." - $ExitCode = $PowerCfgProcess.ExitCode - } - - # If the standard output log file exists, attempt to read it. - if (Test-Path -Path $StandardOutputLog -ErrorAction SilentlyContinue) { - try { - $StandardOutput = Get-Content -Path $StandardOutputLog -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to read the standard output log." - $ExitCode = 1 - } - - if ($StandardOutput) { - $StandardOutput | Write-Host - } - } - - # Attempt to remove the standard output log after reading it. - if (Test-Path -Path $StandardOutputLog -ErrorAction SilentlyContinue) { - try { - Remove-Item -Path $StandardOutputLog -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to remove the standard output log." - $ExitCode = 1 - } - } - - # If the standard error log file exists, attempt to read it. - if (Test-Path -Path $StandardErrorLog -ErrorAction SilentlyContinue) { - try { - $StandardError = Get-Content -Path $StandardErrorLog -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to read the standard error log." - $ExitCode = 1 - } - - # If there's any standard error content, print each line as an error and set exit code to 1. - if ($StandardError) { - $StandardError | ForEach-Object { - if ($_ -and $_.Trim()) { - Write-Host -Object "[Error] $_" - $ExitCode = 1 - } - } - } - } - - # Attempt to remove the standard error log after reading it. - if (Test-Path -Path $StandardErrorLog -ErrorAction SilentlyContinue) { - try { - Remove-Item -Path $StandardErrorLog -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to remove the standard error log." - $ExitCode = 1 - } - } - - # Check if the battery report XML file was created successfully. - if (!$(Test-Path -Path $BatteryReport)) { - Write-Host -Object "[Error] Failed to generate the battery health report at '$BatteryReport'." - exit 1 - } - else { - Write-Host -Object "Created the battery health report." - } - - Write-Host -Object "Retrieving the report results." - # Attempt to load the battery report XML content into a variable. - try { - [xml]$BatteryHealthReport = Get-Content -Path "$BatteryReport" -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to retrieve the report results." - exit 1 - } - - # Attempt to remove the battery report file after reading it. - try { - Remove-Item -Path $BatteryReport -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to remove the battery report at '$BatteryReport'." - $ExitCode = 1 - } - - # Check if the battery report is empty. - if (!$BatteryHealthReport) { - Write-Host -Object "[Error] The report was empty. Failed to retrieve the report results." - exit 1 - } - else { - Write-Host -Object "Retrieved the results." - } - - Write-Host -Object "`nParsing the system information." - # Extract system information from the battery health report XML. - $SystemManufacturer = $BatteryHealthReport.BatteryReport.SystemInformation.SystemManufacturer - $SystemProductName = $BatteryHealthReport.BatteryReport.SystemInformation.SystemProductName - $BIOSVersion = $BatteryHealthReport.BatteryReport.SystemInformation.BIOSVersion - $BIOSDate = Get-Date $BatteryHealthReport.BatteryReport.SystemInformation.BIOSDate -ErrorAction SilentlyContinue - - # Determine if Connected Standby is supported based on the report's data. - $ConnectedStandby = switch ($BatteryHealthReport.BatteryReport.SystemInformation.ConnectedStandby) { - 1 { "Supported" } - default { - "Not Supported" - } - } - - # Attempt to parse the report time and convert it to a readable format. - try { - $ReportTime = Get-Date $BatteryHealthReport.BatteryReport.ReportInformation.LocalScanTime - $ReportTime = "$($ReportTime.ToShortDateString()) $($ReportTime.ToShortTimeString())" - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to retrieve the timestamp for the report." - $ExitCode = 1 - } - - # Create a custom object to store the extracted system information - $SystemInformation = [PSCustomObject]@{ - ReportTime = $ReportTime - SystemProductName = "$SystemManufacturer $SystemProductName" - BIOS = if ($BIOSDate) { "$BIOSVersion $($BIOSDate.ToShortDateString())" }else { "$BIOSVersion" } - OSBuild = $BatteryHealthReport.BatteryReport.SystemInformation.OSBuild - ConnectedStandby = $ConnectedStandby - } - - Write-Host -Object "Parsing the battery specifications." - - # Create a list to hold battery information objects. - $Batteries = New-Object System.Collections.Generic.List[Object] - - # Iterate through each battery entry in the battery report. - $BatteryHealthReport.BatteryReport.Batteries.Battery | ForEach-Object { - # Calculate the usable battery percentage if both design capacity and full charge capacity are present. - $UsablePercent = if ($_.DesignCapacity -and $_.FullChargeCapacity) { - try { - # Perform a mathematical calculation to determine the percentage. - [math]::Round((($($_.FullChargeCapacity) / $($_.DesignCapacity) * 100)), 2) - } - catch { - Write-Host -Object "[Error] Failed to calculate usable battery percentage for the battery $($_.Id) $($_.SerialNumber)" - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - - # Add a custom object representing the current battery's details to the $Batteries list. - $Batteries.Add( - [PSCustomObject]@{ - Name = $_.Id - Manufacturer = $_.Manufacturer - SerialNumber = $_.SerialNumber - Chemistry = $_.Chemistry - UsableBatteryPercentage = if ($UsablePercent) { "$UsablePercent%" }else { " - " } - DesignCapacity = "$($_.DesignCapacity) mWh" - FullChargeCapacity = "$($_.FullChargeCapacity) mWh" - CycleCount = if ($_.CycleCount -eq 0) { " - " }else { $_.CycleCount } - } - ) - } - - # Print a message indicating the parsing of battery capacity history. - Write-Host -Object "Parsing the battery capacity history." - - # Create a list to hold battery capacity history objects. - $BatteryCapacityHistory = New-Object System.Collections.Generic.List[Object] - - # Iterate through each history entry, converting the date and capturing capacities. - $HistoryEntries = $BatteryHealthReport.BatteryReport.History.HistoryEntry | ForEach-Object { - try { - [PSCustomObject]@{ - Date = (Get-Date $_.LocalEndDate -ErrorAction Stop) - FullChargeCapacity = $_.FullChargeCapacity - DesignCapacity = $_.DesignCapacity - } - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to get the date for the history entry dated '$($_.LocalEndDate)'" - $ExitCode = 1 - return - } - } - - # Add unique history entries based on FullChargeCapacity to the battery capacity history list. - $HistoryEntries | Sort-Object -Property FullChargeCapacity -Unique | ForEach-Object { - $BatteryCapacityHistory.Add( - $_ - ) - } - - # If there are at least 3 entries, add the most recent one (sorted by date) to the list. - if (($HistoryEntries | Measure-Object | Select-Object -ExpandProperty Count) -ge 3) { - $BatteryCapacityHistory.Add(($HistoryEntries | Sort-Object Date | Select-Object -Last 1)) - } - - # Add the oldest entry to the list. - $BatteryCapacityHistory.Add(($HistoryEntries | Sort-Object Date | Select-Object -First 1)) - - Write-Host -Object "Parsing the battery duration history." - - # Iterate through each history entry to compute battery usage durations. - $BatteryUsageEntries = $BatteryHealthReport.BatteryReport.History.HistoryEntry | ForEach-Object { - # Convert the stored start date to a DateTime object. - try { - $HistoryStartDate = Get-Date $_.LocalStartDate -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to get the start date for the history entry dated '$($_.LocalStartDate)' with the end date of '$($_.LocalEndDate)'" - $ExitCode = 1 - return - } - - # Convert the stored end date to a DateTime object. - try { - $HistoryEndDate = Get-Date $_.LocalEndDate -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to get the end date for the history entry dated '$($_.LocalEndDate)' with the start date of '$($_.LocalStartDate)'" - $ExitCode = 1 - return - } - - # Calculate the timespan between the start and end dates. - try { - $HistoryTimeSpan = New-TimeSpan -Start $HistoryStartDate -End $HistoryEndDate -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to get time span for the history entry that started on '$HistoryStartDate' and ended on '$HistoryEndDate'." - $ExitCode = 1 - return - } - - # If the duration is more than one day, skip this entry as it's not needed. - if ($HistoryTimeSpan.TotalDays -gt 1) { - return - } - - # Convert ActiveDcTime to a PowerShell time span if possible. - try { - $BatteryActiveDuration = Get-ISO8601Duration -Duration $_.ActiveDcTime -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to translate the active battery duration for '$($_.ActiveDcTime)' on '$($HistoryStartDate.ToShortDateString()) $($HistoryStartDate.ToShortTimeString())'." - $ExitCode = 1 - } - - # Convert CsDcTime (Connected Standby on battery) to a PowerShell time span if possible. - try { - $BatteryConnectedDuration = Get-ISO8601Duration -Duration $_.CsDcTime -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to translate the battery connected standby duration for '$($_.CsDcTime)' on '$($HistoryStartDate.ToShortDateString()) $($HistoryStartDate.ToShortTimeString())'." - $ExitCode = 1 - } - - # Convert ActiveAcTime (Active time on AC) to a PowerShell time span if possible. - try { - $ACActiveDuration = Get-ISO8601Duration -Duration $_.ActiveAcTime -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to translate the active AC duration for '$($_.ActiveAcTime)' on '$($HistoryStartDate.ToShortDateString()) $($HistoryStartDate.ToShortTimeString())'." - $ExitCode = 1 - } - - # Convert CsAcTime (Connected Standby on AC) to a PowerShell time span if possible. - try { - $ACConnectedStandby = Get-ISO8601Duration -Duration $_.CsAcTime -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to translate the AC connected standby duration for '$($_.CsAcTime)' on '$($HistoryStartDate.ToShortDateString()) $($HistoryStartDate.ToShortTimeString())'." - $ExitCode = 1 - } - - # Return a custom object representing this history entry's duration details. - [PSCustomObject]@{ - StartDate = $HistoryStartDate - EndDate = $HistoryEndDate - BatteryActive = $BatteryActiveDuration - BatteryConnectedStandby = $BatteryConnectedDuration - ACActive = $ACActiveDuration - ACConnectedStandby = $ACConnectedStandby - } - } - - Write-Host -Object "Parsing the recent battery usage history." - - # Filter recent usage entries to exclude those with an EntryType of "ReportGenerated". - # For each entry, convert the timestamp, determine the power source, and calculate the percentage remaining. - $RecentUsageEntries = $BatteryHealthReport.BatteryReport.RecentUsage.UsageEntry | Where-Object { $_.EntryType -ne "ReportGenerated" } | ForEach-Object { - # Attempt to convert the LocalTimeStamp to a DateTime object. - try { - $StartTime = Get-Date $_.LocalTimeStamp - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to get the timestamp for the battery usage entry dated '$($_.LocalTimeStamp)'" - $ExitCode = 1 - return - } - - # Determine the power source: AC if 'AC' is 1, otherwise Battery. - $Source = switch ($_.AC) { - 1 { "AC" } - default { "Battery" } - } - - # If the entry type is "Suspend", there's no source (set to $Null). - if ($_.EntryType -eq "Suspend") { - $Source = $Null - } - - # Calculate the percentage of battery remaining. - try { - $PercentageRemaining = [math]::Round((($_.ChargeCapacity / $_.FullChargeCapacity) * 100), 2) - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to calculate battery percentage remaining from the battery usage entry dated '$StartTime'" - $ExitCode = 1 - } - - # Return a custom object with the processed information for this usage entry. - [PSCustomObject]@{ - StartTime = $StartTime - State = $_.EntryType - Source = $Source - PercentageRemaining = "$PercentageRemaining%" - CapacityRemaining = "$($_.ChargeCapacity) mWh" - } - } - - Write-Host -Object "`nFormatting the battery capacity history to be human-readable." - - # Convert each battery capacity history entry into a custom object with human-readable date and capacities. - $BatteryCapacityHistoryTable = $BatteryCapacityHistory | Sort-Object -Property Date -Descending | ForEach-Object { - [PSCustomObject]@{ - Date = $_.Date.ToShortDateString() - FullChargeCapacity = "$($_.FullChargeCapacity) mWh" - DesignCapacity = "$($_.DesignCapacity) mWh" - } - } - - Write-Host -Object "Formatting the battery usage history to be human-readable." - - # Convert each battery usage entry into a human-readable format. - $BatteryUsageTable = $BatteryUsageEntries | Sort-Object -Property StartDate -Descending | ForEach-Object { - # If BatteryActive is greater than ~1 second, attempt to convert it to a friendly time span. - if ($_.BatteryActive -and $_.BatteryActive -gt [TimeSpan]::FromMilliseconds(999)) { - try { - $BatteryActive = Get-FriendlyTimeSpan -TimeSpan $_.BatteryActive -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to get a human-readable string for the battery active duration of '$($_.BatteryActive)' for $($_.StartDate.ToShortDateString())." - $BatteryActive = " - " - $ExitCode = 1 - } - } - else { - $BatteryActive = " - " - } - - # If BatteryConnectedStandby is greater than ~1 second, convert it to a friendly time span. - if ($_.BatteryConnectedStandby -and $_.BatteryConnectedStandby -gt [TimeSpan]::FromMilliseconds(999)) { - try { - $BatteryConnectedStandby = Get-FriendlyTimeSpan -TimeSpan $_.BatteryConnectedStandby -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to get a human-readable string for the battery connected standby duration of '$($_.BatteryConnectedStandby)' for $($_.StartDate.ToShortDateString())." - $BatteryConnectedStandby = " - " - $ExitCode = 1 - } - } - else { - $BatteryConnectedStandby = " - " - } - - # If ACActive is greater than ~1 second, convert it to a friendly time span. - if ($_.ACActive -and $_.ACActive -gt [TimeSpan]::FromMilliseconds(999)) { - try { - $ACActive = Get-FriendlyTimeSpan -TimeSpan $_.ACActive -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to get a human-readable string for the AC active duration of '$($_.ACActive)' for $($_.StartDate.ToShortDateString())." - $ACActive = " - " - $ExitCode = 1 - } - } - else { - $ACActive = " - " - } - - # If ACConnectedStandby is greater than ~1 second, convert it to a friendly time span. - if ($_.ACConnectedStandby -and $_.ACConnectedStandby -gt [TimeSpan]::FromMilliseconds(999)) { - try { - $ACConnectedStandby = Get-FriendlyTimeSpan -TimeSpan $_.ACConnectedStandby -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to get a human-readable string for the AC connected standby duration of '$($_.ACConnectedStandby)' for $($_.StartDate.ToShortDateString())." - $ACActive = " - " - $ExitCode = 1 - } - } - else { - $ACConnectedStandby = " - " - } - - # Return a custom object representing the formatted usage information. - [PSCustomObject]@{ - StartDate = $_.StartDate.ToShortDateString() - BatteryActive = $BatteryActive - BatteryConnectedStandby = $BatteryConnectedStandby - ACActive = $ACActive - ACConnectedStandby = $ACConnectedStandby - } - } - - Write-Host -Object "Formatting the recent usage history to be human-readable." - # Convert each recent usage entry into a human-readable format, including date and time. - $RecentUsageEntryTable = $RecentUsageEntries | Sort-Object -Property StartTime -Descending | ForEach-Object { - [PSCustomObject]@{ - StartTime = "$($_.StartTime.ToShortDateString()) $($_.StartTime.ToLongTimeString())" - State = $_.State - Source = $_.Source - PercentageRemaining = $_.PercentageRemaining - CapacityRemaining = $_.CapacityRemaining - } - } - - if ($WYSIWYGCustomField) { - Write-Host -Object "`nFormatting the results for the WYSIWYG Custom Field 'WYSIWYGCustomField'." - - Write-Host -Object "Creating the system information HTML card." - - # Build an HTML card displaying system information details using the previously collected data. - $SystemInformationHTMLCard = "
-
-
  System Information
-
-
-

Report Time
$($SystemInformation.ReportTime)

-

System Product Name
$($SystemInformation.SystemProductName)

-

BIOS
$($SystemInformation.BIOS)

-

OS Build
$($SystemInformation.OSBuild)

-

Connected Standby
$($SystemInformation.ConnectedStandby)

-
-
" - - Write-Host -Object "Creating the installed batteries HTML card." - - # Initialize a counter to label each battery. - $i = 1 - # Build an HTML card that displays a table of all installed batteries and their properties. - $InstalledBatteriesHTMLCard = "
-
-
  Installed Batteries
-
-
-
- - - $($Batteries | ForEach-Object {"`n"}) - - - - - $($Batteries | ForEach-Object {"`n"; $i++}) - - - - - $($Batteries | ForEach-Object {"`n"}) - - - - $($Batteries | ForEach-Object {"`n"}) - - - - $($Batteries | ForEach-Object {"`n"}) - - - - $($Batteries | ForEach-Object {"`n"}) - - - - $($Batteries | ForEach-Object {"`n"}) - - - - $($Batteries | ForEach-Object {"`n"}) - - - - $($Batteries | ForEach-Object {"`n"}) - - - - $($Batteries | ForEach-Object {"`n"}) - -
Battery $i
Name$($_.Name)
Manufacturer$($_.Manufacturer)
Serial Number$($_.SerialNumber)
Chemistry$($_.Chemistry)
Usable Battery Percentage$($_.UsableBatteryPercentage)
Design Capacity$($_.DesignCapacity)
Full Charge Capacity$($_.FullChargeCapacity)
Cycle Count$($_.CycleCount)
- -" - - Write-Host -Object "Creating the battery capacity history HTML card." - # Convert the battery capacity history table into an HTML fragment and format table headers in bold. - $BatteryCapacityHistoryHTMLTable = $BatteryCapacityHistoryTable | ConvertTo-Html -Fragment - $BatteryCapacityHistoryHTMLTable = $BatteryCapacityHistoryHTMLTable -replace '', '' -replace '', '' - - # If battery capacity history data exists, create a detailed HTML card with a line chart. - if ($BatteryCapacityHistory) { - $BatteryCapacityHistoryHTMLCard = "
-
-
  Battery Capacity History
-
-
-
- - - $( - $PreviousCapacityPercentage = 0.99 - $BatteryCapacityHistory | Sort-Object Date | ForEach-Object { - try { - $CurrentCapacityPercentage = [math]::Round(($_.FullChargeCapacity / $_.DesignCapacity),2) - if($CurrentCapacityPercentage -eq 1){ - $CurrentCapacityPercentage = 0.99 - } - }catch{ - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to calculate the capacity percentage for '$($_.Date.ToShortDateString)'." - return - } - " - - - `n" - $PreviousCapacityPercentage = $CurrentCapacityPercentage - } - ) - -
$($_.Date.ToString("MM-dd yyyy"))$($_.FullChargeCapacity) mWh
-
-
- $BatteryCapacityHistoryHTMLTable -
-
-
" - } - - # If no battery capacity history data is available, show a message indicating unavailability. - if (!$BatteryCapacityHistory) { - $BatteryCapacityHistoryHTMLCard = "
-
-
  Battery Capacity History
-
-
-

Information not available or not found.

-
-
" - } - - Write-Host -Object "Creating the battery usage HTML card." - # If battery usage entries exist, generate a card that includes a chart and a table of usage information. - if ($BatteryUsageEntries) { - $BatteryUsageHTMLTable = $BatteryUsageTable | ConvertTo-Html -Fragment - - # Remove and replace parts of the generated HTML to create a custom table layout and headings. - $BatteryUsageHTMLTable = $BatteryUsageHTMLTable -replace ".*", "" - $BatteryUsageHTMLTable = $BatteryUsageHTMLTable -replace ".*", " - - - Battery Duration - AC Duration - - - Date - Active - Connected Standby - Active - Connected Standby - - -" - $BatteryUsageHTMLTable = $BatteryUsageHTMLTable -replace "", "`n" - - $BatteryUsageHTMLCard = "
-
-
  Battery Usage
-
-
-
- - - - $($BatteryUsageEntries | ForEach-Object { - # Initialize variables for duration calculations. - $TotalBatteryDuration = $null - $FriendlyTimeSpan = $null - $TotalBatteryPercent = $null - - try{ - # Calculate the total battery duration and its percentage of a 24-hour period. - $TotalBatteryDuration = ($_.BatteryActive + $_.BatteryConnectedStandby) - $TotalBatteryPercent = [math]::Round(($TotalBatteryDuration.TotalHours / 24), 2) - }catch{ - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to calculate the percentage of online hours for the battery duration graph for '$($_.StartDate.ToShortDateString())'." - $ExitCode = 1 - return - } - - try{ - # Convert the total battery duration to a friendly time span string if it's more than ~1 second. - if($TotalBatteryDuration -and $TotalBatteryDuration -gt [TimeSpan]::FromMilliseconds(999)){ - $FriendlyTimeSpan = Get-FriendlyTimeSpan -TimeSpan $TotalBatteryDuration - }else{ - $FriendlyTimeSpan = " - " - } - }catch{ - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to create a friendly time span for '$($_.StartDate.ToShortDateString())'." - $ExitCode = 1 - return - } - - # Create a table row for each usage entry with a visual representation of battery duration. - " - - - `n" - }) - -
Battery Duration
$($_.StartDate.ToString("MM-dd"))$FriendlyTimeSpan
-
-
- $BatteryUsageHTMLTable -
-
-
" - } - - # If there are no battery usage entries, display a message indicating no available information. - if (!$BatteryUsageEntries) { - $BatteryUsageHTMLCard = "
-
-
  Battery Usage
-
-
-

Information not available or not found.

-
-
" - } - - Write-Host -Object "Creating the recent usage HTML card." - - # Convert the recent usage entries into an HTML fragment, formatting headers in bold. - $RecentUsageEntryHTMLTable = $RecentUsageEntryTable | ConvertTo-Html -Fragment - $RecentUsageEntryHTMLTable = $RecentUsageEntryHTMLTable -replace '', '' -replace '', '' - - # Build an HTML card displaying the recent battery usage history in a table format. - $RecentUsageHTMLCard = "
-
-
  Recent Usage
-
-
- $RecentUsageEntryHTMLTable -
-
" - - Write-Host -Object "Assembling the final WYSIWYG Value" - - # Combine all the previously created HTML cards into a final WYSIWYG value. - $WYSIWYGValue = "
-
-
-
- $SystemInformationHTMLCard -
-
- $InstalledBatteriesHTMLCard -
-
-
-
-
- $BatteryCapacityHistoryHTMLCard - $BatteryUsageHTMLCard - $RecentUsageHTMLCard -
-
-
" - - try { - # Attempt to set the WYSIWYG custom field with the assembled HTML content. - Write-Host "Attempting to set the Custom Field '$WYSIWYGCustomField'." - Set-NinjaProperty -Name $WYSIWYGCustomField -Value $WYSIWYGValue - Write-Host "Successfully set the Custom Field '$WYSIWYGCustomField'!" - } - catch { - # If there's an error, print it and set the exit code to 1. - Write-Host "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - - # The following code block: - # 1. Prints headings and data in a human-readable format for: - # - System Information - # - Installed Batteries - # - Battery Capacity History - # - Battery Usage - # - Recent Power Usage - # 2. Each section is introduced with a heading (e.g., "### System Information ###"). - # 3. Data is formatted using Format-List or Format-Table, converted to a string via Out-String, - # trimmed to remove extra whitespace, and then printed using Write-Host. - Write-Host -Object "`n### System Information ###" - ($SystemInformation | Format-List | Out-String).Trim() | Write-Host - Write-Host -Object "`n### Installed Batteries ###" - ($Batteries | Format-List | Out-String).Trim() | Write-Host - Write-Host -Object "`n### Battery Capacity History ###" - ($BatteryCapacityHistoryTable | Format-Table | Out-String).Trim() | Write-Host - Write-Host -Object "`n### Battery Usage ###" - ($BatteryUsageTable | Format-Table | Out-String).Trim() | Write-Host - Write-Host -Object "`n### Recent Power Usage ###" - ($RecentUsageEntryTable | Format-Table | Out-String).Trim() | Write-Host - - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Retrieves the overall battery health and optionally saves the results to a WYSIWYG custom field. +.DESCRIPTION + Retrieves the overall battery health and optionally saves the results to a WYSIWYG custom field. +.EXAMPLE + -WYSIWYGCustomField "WYSIWYG" + + Creating the battery health report. + Battery life report saved to file path C:\Windows\Temp\batteryhealthreport.xml. + Created the battery health report. + Retrieving the report results. + Retrieved the results. + + Parsing the system information. + Parsing the battery specifications. + Parsing the battery capacity history. + Parsing the battery duration history. + Parsing the recent battery usage history. + + Formatting the battery capacity history to be human-readable. + Formatting the battery usage history to be human-readable. + Formatting the recent usage history to be human-readable. + + Formatting the results for the WYSIWYG Custom Field 'WYSIWYGCustomField'. + Creating the system information HTML card. + Creating the installed batteries HTML card. + Creating the battery capacity history HTML card. + Creating the battery usage HTML card. + Creating the recent usage HTML card. + Assembling the final WYSIWYG Value + Attempting to set the Custom Field 'WYSIWYG'. + Successfully set the Custom Field 'WYSIWYG'! + + ### System Information ### + ReportTime : 12/16/2024 4:12 PM + SystemProductName : Dell Inc. Precision 3571 + BIOS : 1.27.0 9/27/2024 + OSBuild : 26100.1.amd64fre.ge_release.240331-1435 + ConnectedStandby : Supported + + ### Installed Batteries ### + Name : DELL 0P3TJK9 + Manufacturer : SMP + SerialNumber : 1 + Chemistry : LiP + UsableBatteryPercentage : 70.29% + DesignCapacity : 64007 mWh + FullChargeCapacity : 44992 mWh + CycleCount : - + + ### Battery Capacity History ### + Date FullChargeCapacity DesignCapacity + ---- ------------------ -------------- + 12/15/2024 44992 mWh 64007 mWh + 12/9/2024 44992 mWh 64007 mWh + 5/26/2024 48929 mWh 64007 mWh + 2/4/2024 51565 mWh 64007 mWh + 1/7/2024 52850 mWh 64007 mWh + 12/24/2023 55681 mWh 64007 mWh + 12/10/2023 56057 mWh 64007 mWh + 10/1/2023 56787 mWh 64007 mWh + 8/27/2023 58532 mWh 64007 mWh + 7/30/2023 60709 mWh 64007 mWh + 7/16/2023 61052 mWh 64007 mWh + 4/30/2023 64007 mWh 64007 mWh + 4/2/2023 64007 mWh 64007 mWh + + ### Battery Usage ### + StartDate BatteryActive BatteryConnectedStandby ACActive ACConnectedStandby + --------- ------------- ----------------------- -------- ------------------ + 12/15/2024 - 14s - 21h 59m 12s + 12/14/2024 - 13s - 21h 59m 12s + 12/13/2024 30m - 7h 10m 27s 16h 19m 12s + 12/12/2024 - 1h 59m 59s - 21h 59m 17s + 12/11/2024 1h 58m 34s 13s 2h 1m 20s 22h 17m 16s + 12/10/2024 - 1h 59m 52s - 23h 28m 55s + 12/9/2024 - 1h 59m 39s - 21h 59m 22s + 12/8/2024 - 1h 59m 59s - 11h 59m 16s + + ### Recent Power Usage ### + StartTime State Source PercentageRemaining CapacityRemaining + --------- ----- ------ ------------------- ----------------- + 12/16/2024 9:37:35 AM Active AC 34.9% 15702 mWh + 12/16/2024 9:00:35 AM Active Battery 100% 44992 mWh + 12/16/2024 8:17:21 AM Active AC 100% 44992 mWh + 12/15/2024 11:01:15 AM ConnectedStandby AC 81.18% 36526 mWh + 12/15/2024 9:00:59 AM Suspend 98.78% 44445 mWh + 12/15/2024 9:00:54 AM ConnectedStandby Battery 98.95% 44521 mWh + 12/15/2024 9:00:44 AM Suspend 99.39% 44718 mWh + 12/15/2024 9:00:35 AM ConnectedStandby Battery 100% 44992 mWh + 12/15/2024 9:00:33 AM Suspend 100% 44992 mWh + 12/14/2024 11:01:13 AM ConnectedStandby AC 77.4% 34823 mWh + 12/14/2024 9:00:50 AM Suspend 98.99% 44536 mWh + 12/14/2024 9:00:45 AM ConnectedStandby Battery 99.16% 44612 mWh + 12/14/2024 9:00:40 AM Suspend 99.43% 44734 mWh + 12/14/2024 9:00:32 AM ConnectedStandby Battery 100% 44992 mWh + 12/14/2024 9:00:30 AM Suspend 100% 44992 mWh + 12/13/2024 4:26:48 PM ConnectedStandby AC 100% 44992 mWh + 12/13/2024 12:18:07 PM Active AC 98.85% 44475 mWh + 12/13/2024 11:53:51 AM ConnectedStandby AC 92.09% 41435 mWh + 12/13/2024 11:53:20 AM Active AC 91.86% 41329 mWh + 12/13/2024 11:44:16 AM ConnectedStandby AC 86.82% 39064 mWh + 12/13/2024 10:39:38 AM Active AC 32.91% 14805 mWh + 12/13/2024 10:37:50 AM ConnectedStandby AC 32.91% 14805 mWh + 12/13/2024 9:30:34 AM Active AC 32.94% 14820 mWh + 12/13/2024 9:00:32 AM Active Battery 100% 44992 mWh + 12/13/2024 8:11:03 AM Active AC 100% 44992 mWh + 12/12/2024 11:01:09 AM ConnectedStandby AC 81.99% 36890 mWh + 12/12/2024 11:00:31 AM Suspend 81.59% 36708 mWh + 12/12/2024 9:00:31 AM ConnectedStandby Battery 100% 44992 mWh + 12/11/2024 1:01:56 PM ConnectedStandby AC 100% 44992 mWh + 12/11/2024 11:00:33 AM Active AC 50.37% 22663 mWh + 12/11/2024 9:01:58 AM Active Battery 97.47% 43852 mWh + 12/11/2024 9:00:48 AM Suspend 99.43% 44734 mWh + 12/11/2024 9:00:34 AM ConnectedStandby Battery 100% 44992 mWh + 12/11/2024 9:00:33 AM Suspend 100% 44992 mWh + 12/11/2024 6:41:48 AM ConnectedStandby AC 100% 44992 mWh + 12/10/2024 11:01:19 AM ConnectedStandby AC 79.53% 35781 mWh + 12/10/2024 11:00:35 AM Suspend 78.99% 35538 mWh + 12/10/2024 9:00:54 AM ConnectedStandby Battery 99.26% 44658 mWh + 12/10/2024 9:00:49 AM Suspend 99.43% 44734 mWh + 12/10/2024 9:00:37 AM ConnectedStandby Battery 100% 44992 mWh + 12/10/2024 9:00:34 AM Suspend 100% 44992 mWh + 12/10/2024 7:30:51 AM ConnectedStandby AC 100% 44992 mWh + 12/9/2024 5:13:22 PM ConnectedStandby AC 100% 44992 mWh + +PARAMETER: -WYSIWYGCustomField "ReplaceMeWithTheNameOfAWysiwygCustomField" + Optionally, save the results to a WYSIWYG custom field. + +.NOTES + Minimum OS Architecture Supported: Windows 10 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$WYSIWYGCustomField +) + +begin { + # If script form variables are used, replace the commandline parameters with their value. + if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $WYSIWYGCustomField = $env:wysiwygCustomFieldName } + + # Attempt to retrieve the battery information using the appropriate command. + try { + if ($PSVersionTable.PSVersion.Major -lt 3) { + $CurrentBattery = Get-WmiObject -Class Win32_Battery -ErrorAction Stop + } + else { + $CurrentBattery = Get-CimInstance -ClassName Win32_Battery -ErrorAction Stop + } + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] No battery detected on the system." + exit 1 + } + + # Check if no battery information was retrieved. + if (!$CurrentBattery) { + # Inform the user that no battery was detected and exit with an error code. + Write-Host -Object "[Error] No battery detected on the system." + exit 1 + } + + function Test-IsServer { + # Determine the method to retrieve the operating system information based on PowerShell version + try { + $OS = if ($PSVersionTable.PSVersion.Major -lt 3) { + Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop + } + else { + Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop + } + } + catch { + Write-Host -Object "[Error] Unable to validate whether or not this device is a server." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Check if the ProductType is "2", which indicates that the system is a domain controller or is a server + if ($OS.ProductType -eq "2" -or $OS.ProductType -eq "3") { + return $true + } + } + + # Function to convert time spans into a human-friendly string. + function Get-FriendlyTimeSpan { + param( + [Parameter(Mandatory = $True)] + [TimeSpan]$TimeSpan + ) + + # Check if the provided TimeSpan is less than 1 second. + # Throw an exception if the TimeSpan is too short. + if ($TimeSpan -le [TimeSpan]::FromMilliseconds(999)) { + throw [System.ArgumentOutOfRangeException]::New("The provided time span is less than 0 seconds. Please specify a longer duration.") + } + + # Build a human-readable representation of the TimeSpan. + $FriendlyTimeSpan = $Null + if ($TimeSpan.Days) { $FriendlyTimeSpan = "$($TimeSpan.Days)d" } + if ($TimeSpan.Hours) { $FriendlyTimeSpan = "$FriendlyTimeSpan $($TimeSpan.Hours)h" } + if ($TimeSpan.Minutes) { $FriendlyTimeSpan = "$FriendlyTimeSpan $($TimeSpan.Minutes)m" } + if ($TimeSpan.Seconds) { $FriendlyTimeSpan = "$FriendlyTimeSpan $($TimeSpan.Seconds)s" } + + # Check if the conversion failed and no output was generated. + if (!$FriendlyTimeSpan) { + throw [System.FormatException]::New("Failed to convert the time span '$TimeSpan' into a human-friendly format.") + } + + # Return the trimmed friendly TimeSpan string (removes any leading or trailing whitespace). + $FriendlyTimeSpan.Trim() + } + + # Function to parse ISO 8601 duration strings and convert them into a TimeSpan object + function Get-ISO8601Duration { + param( + [Parameter()] + [String]$Duration + ) + + # Validate that the duration starts with the 'P' designator + if ($Duration -notmatch "^P") { + throw [System.IO.InvalidDataException]::New("An invalid duration of '$Duration' was given. ISO 8601 durations require durations to start with the P designator. https://en.wikipedia.org/wiki/ISO_8601#Durations") + } + + # Ensure the duration contains numeric characters + if ($Duration -notmatch "[0-9]") { + throw [System.IO.InvalidDataException]::New("An invalid duration of '$Duration' was given. ISO 8601 durations require numeric characters. https://en.wikipedia.org/wiki/ISO_8601#Durations") + } + + # Validate that only allowed characters are present in the duration string + if ($Duration -match "[^0-9PYMDTHS.,]") { + throw [System.IO.InvalidDataException]::New("An invalid duration of '$Duration' was given. ISO 8601 non-alternative duration format can only contain the following characters '0-9PYMDTHS.,'. https://en.wikipedia.org/wiki/ISO_8601#Durations") + } + + # Define patterns to match date and time components of ISO 8601 duration + $DateFormat = "P.*(([0-9]+Y)+|([0-9]+M)+|([0-9]+D)+)" + $TimeFormat = "P.*T(([0-9]+H)+|([0-9]+M)+|([0-9]+S)+)" + + # Ensure that the duration contains either valid date or time components + if ($Duration -notmatch $DateFormat -and $Duration -notmatch $TimeFormat) { + throw [System.IO.InvalidDataException]::New("An invalid duration of '$Duration' was given. The ISO 8601 non-alternative duration format should look like 'PnYnMnDTnHnMnS' where n is a number. https://en.wikipedia.org/wiki/ISO_8601#Durations") + } + + # Extract the date part of the duration (e.g., "PnYnMnD") + if ($Duration -match $DateFormat) { + $Date = $Duration -replace ',', '.' -replace 'T.*' + } + + # Extract the time part of the duration (e.g., "TnHnMnS") + if ($Duration -match $TimeFormat) { + $Time = $Duration -replace ',', '.' -replace '.*T' + } + + # If both date and time components are missing, throw an error + if (!$Date -and !$Time) { + throw [System.IO.InvalidDataException]::New("Failed to extract the date and time sections from '$Duration'.") + } + + # Parse date components: years, months, and days + if ($Date -match '[0-9.,]+Y') { $YearsGiven = $Matches[0] -replace "Y" } + if ($Date -match '[0-9.,]+M') { $MonthsGiven = $Matches[0] -replace "M" } + if ($Date -match '[0-9.,]+D') { $DaysGiven = $Matches[0] -replace "D" } + + # Parse time components: hours, minutes, and seconds + if ($Time -match '[0-9.,]+H') { $HoursGiven = $Matches[0] -replace "H" } + if ($Time -match '[0-9.,]+M') { $MinutesGiven = $Matches[0] -replace "M" } + if ($Time -match '[0-9.,]+S') { $SecondsGiven = $Matches[0] -replace "S" } + + # If no components were extracted, throw an error + if (!$YearsGiven -and !$MonthsGiven -and !$DaysGiven -and !$HoursGiven -and !$MinutesGiven -and !$SecondsGiven) { + throw [System.IO.InvalidDataException]::New("Failed to extract the years, months, days, hours, minutes, or seconds from '$Duration'.") + } + + try { + # Calculate the total duration in seconds + if ($YearsGiven) { $TotalSeconds = ([double]$YearsGiven * 31557600) } + if ($MonthsGiven) { $TotalSeconds = ([double]$TotalSeconds + ([double]$MonthsGiven * 2630016)) } + if ($DaysGiven) { $TotalSeconds = ([double]$TotalSeconds + ([double]$DaysGiven * 86400)) } + + if ($HoursGiven) { $TotalSeconds = ([double]$TotalSeconds + ([double]$HoursGiven * 3600)) } + if ($MinutesGiven) { $TotalSeconds = ([double]$TotalSeconds + ([double]$MinutesGiven * 60)) } + if ($SecondsGiven) { $TotalSeconds = ([double]$TotalSeconds + [double]$SecondsGiven) } + } + catch { + # Catch and re-throw any calculation errors + throw $_ + } + + try { + # Create and return a TimeSpan object representing the total duration + New-TimeSpan -Seconds $TotalSeconds -ErrorAction Stop + } + catch { + # Catch and re-throw any errors during TimeSpan creation + throw $_ + } + } + + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # Check if the current user is running the script with elevated privileges. + if (!(Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run this with Administrator privileges." + exit 1 + } + + # Check if this script is running on Windows Server. + if (Test-IsServer) { + Write-Host -Object "[Error] The powercfg battery report is not available on Windows Server. Please run this on a workstation." + exit 1 + } + + # Set file paths for the battery report and log files. + $BatteryReport = "$env:TEMP\batteryhealthreport.xml" + $StandardOutputLog = "$(New-Guid)-stdout-batteryreport.log" + $StandardErrorLog = "$(New-Guid)-stderr-batteryreport.log" + + # Define the arguments that will be passed to powercfg.exe to generate the battery report. + $PowerCfgArguments = @( + "/BATTERYREPORT" + "/XML" + "/OUTPUT" + $BatteryReport + ) + + # Define the parameters for starting the powercfg.exe process, including output redirection. + $PowerCfgProcessArguments = @{ + FilePath = "$env:SYSTEMROOT\System32\powercfg.exe" + ArgumentList = $PowerCfgArguments + RedirectStandardOutput = $StandardOutputLog + RedirectStandardError = $StandardErrorLog + PassThru = $True + NoNewWindow = $True + Wait = $True + } + + Write-Host -Object "Creating the battery health report." + # Attempt to run the powercfg.exe process with the specified arguments. + try { + $PowerCfgProcess = Start-Process @PowerCfgProcessArguments -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to generate battery health report." + exit 1 + } + + # Check the exit code of the powercfg.exe process. A non-zero exit code may indicate an error. + if ($PowerCfgProcess.ExitCode -ne 0) { + Write-Host -Object "Exit Code: $($PowerCfgProcess.ExitCode)" + Write-Host -Object "The exit code does not indicate success." + $ExitCode = $PowerCfgProcess.ExitCode + } + + # If the standard output log file exists, attempt to read it. + if (Test-Path -Path $StandardOutputLog -ErrorAction SilentlyContinue) { + try { + $StandardOutput = Get-Content -Path $StandardOutputLog -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to read the standard output log." + $ExitCode = 1 + } + + if ($StandardOutput) { + $StandardOutput | Write-Host + } + } + + # Attempt to remove the standard output log after reading it. + if (Test-Path -Path $StandardOutputLog -ErrorAction SilentlyContinue) { + try { + Remove-Item -Path $StandardOutputLog -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to remove the standard output log." + $ExitCode = 1 + } + } + + # If the standard error log file exists, attempt to read it. + if (Test-Path -Path $StandardErrorLog -ErrorAction SilentlyContinue) { + try { + $StandardError = Get-Content -Path $StandardErrorLog -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to read the standard error log." + $ExitCode = 1 + } + + # If there's any standard error content, print each line as an error and set exit code to 1. + if ($StandardError) { + $StandardError | ForEach-Object { + if ($_ -and $_.Trim()) { + Write-Host -Object "[Error] $_" + $ExitCode = 1 + } + } + } + } + + # Attempt to remove the standard error log after reading it. + if (Test-Path -Path $StandardErrorLog -ErrorAction SilentlyContinue) { + try { + Remove-Item -Path $StandardErrorLog -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to remove the standard error log." + $ExitCode = 1 + } + } + + # Check if the battery report XML file was created successfully. + if (!$(Test-Path -Path $BatteryReport)) { + Write-Host -Object "[Error] Failed to generate the battery health report at '$BatteryReport'." + exit 1 + } + else { + Write-Host -Object "Created the battery health report." + } + + Write-Host -Object "Retrieving the report results." + # Attempt to load the battery report XML content into a variable. + try { + [xml]$BatteryHealthReport = Get-Content -Path "$BatteryReport" -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to retrieve the report results." + exit 1 + } + + # Attempt to remove the battery report file after reading it. + try { + Remove-Item -Path $BatteryReport -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to remove the battery report at '$BatteryReport'." + $ExitCode = 1 + } + + # Check if the battery report is empty. + if (!$BatteryHealthReport) { + Write-Host -Object "[Error] The report was empty. Failed to retrieve the report results." + exit 1 + } + else { + Write-Host -Object "Retrieved the results." + } + + Write-Host -Object "`nParsing the system information." + # Extract system information from the battery health report XML. + $SystemManufacturer = $BatteryHealthReport.BatteryReport.SystemInformation.SystemManufacturer + $SystemProductName = $BatteryHealthReport.BatteryReport.SystemInformation.SystemProductName + $BIOSVersion = $BatteryHealthReport.BatteryReport.SystemInformation.BIOSVersion + $BIOSDate = Get-Date $BatteryHealthReport.BatteryReport.SystemInformation.BIOSDate -ErrorAction SilentlyContinue + + # Determine if Connected Standby is supported based on the report's data. + $ConnectedStandby = switch ($BatteryHealthReport.BatteryReport.SystemInformation.ConnectedStandby) { + 1 { "Supported" } + default { + "Not Supported" + } + } + + # Attempt to parse the report time and convert it to a readable format. + try { + $ReportTime = Get-Date $BatteryHealthReport.BatteryReport.ReportInformation.LocalScanTime + $ReportTime = "$($ReportTime.ToShortDateString()) $($ReportTime.ToShortTimeString())" + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to retrieve the timestamp for the report." + $ExitCode = 1 + } + + # Create a custom object to store the extracted system information + $SystemInformation = [PSCustomObject]@{ + ReportTime = $ReportTime + SystemProductName = "$SystemManufacturer $SystemProductName" + BIOS = if ($BIOSDate) { "$BIOSVersion $($BIOSDate.ToShortDateString())" }else { "$BIOSVersion" } + OSBuild = $BatteryHealthReport.BatteryReport.SystemInformation.OSBuild + ConnectedStandby = $ConnectedStandby + } + + Write-Host -Object "Parsing the battery specifications." + + # Create a list to hold battery information objects. + $Batteries = New-Object System.Collections.Generic.List[Object] + + # Iterate through each battery entry in the battery report. + $BatteryHealthReport.BatteryReport.Batteries.Battery | ForEach-Object { + # Calculate the usable battery percentage if both design capacity and full charge capacity are present. + $UsablePercent = if ($_.DesignCapacity -and $_.FullChargeCapacity) { + try { + # Perform a mathematical calculation to determine the percentage. + [math]::Round((($($_.FullChargeCapacity) / $($_.DesignCapacity) * 100)), 2) + } + catch { + Write-Host -Object "[Error] Failed to calculate usable battery percentage for the battery $($_.Id) $($_.SerialNumber)" + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + } + + # Add a custom object representing the current battery's details to the $Batteries list. + $Batteries.Add( + [PSCustomObject]@{ + Name = $_.Id + Manufacturer = $_.Manufacturer + SerialNumber = $_.SerialNumber + Chemistry = $_.Chemistry + UsableBatteryPercentage = if ($UsablePercent) { "$UsablePercent%" }else { " - " } + DesignCapacity = "$($_.DesignCapacity) mWh" + FullChargeCapacity = "$($_.FullChargeCapacity) mWh" + CycleCount = if ($_.CycleCount -eq 0) { " - " }else { $_.CycleCount } + } + ) + } + + # Print a message indicating the parsing of battery capacity history. + Write-Host -Object "Parsing the battery capacity history." + + # Create a list to hold battery capacity history objects. + $BatteryCapacityHistory = New-Object System.Collections.Generic.List[Object] + + # Iterate through each history entry, converting the date and capturing capacities. + $HistoryEntries = $BatteryHealthReport.BatteryReport.History.HistoryEntry | ForEach-Object { + try { + [PSCustomObject]@{ + Date = (Get-Date $_.LocalEndDate -ErrorAction Stop) + FullChargeCapacity = $_.FullChargeCapacity + DesignCapacity = $_.DesignCapacity + } + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to get the date for the history entry dated '$($_.LocalEndDate)'" + $ExitCode = 1 + return + } + } + + # Add unique history entries based on FullChargeCapacity to the battery capacity history list. + $HistoryEntries | Sort-Object -Property FullChargeCapacity -Unique | ForEach-Object { + $BatteryCapacityHistory.Add( + $_ + ) + } + + # If there are at least 3 entries, add the most recent one (sorted by date) to the list. + if (($HistoryEntries | Measure-Object | Select-Object -ExpandProperty Count) -ge 3) { + $BatteryCapacityHistory.Add(($HistoryEntries | Sort-Object Date | Select-Object -Last 1)) + } + + # Add the oldest entry to the list. + $BatteryCapacityHistory.Add(($HistoryEntries | Sort-Object Date | Select-Object -First 1)) + + Write-Host -Object "Parsing the battery duration history." + + # Iterate through each history entry to compute battery usage durations. + $BatteryUsageEntries = $BatteryHealthReport.BatteryReport.History.HistoryEntry | ForEach-Object { + # Convert the stored start date to a DateTime object. + try { + $HistoryStartDate = Get-Date $_.LocalStartDate -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to get the start date for the history entry dated '$($_.LocalStartDate)' with the end date of '$($_.LocalEndDate)'" + $ExitCode = 1 + return + } + + # Convert the stored end date to a DateTime object. + try { + $HistoryEndDate = Get-Date $_.LocalEndDate -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to get the end date for the history entry dated '$($_.LocalEndDate)' with the start date of '$($_.LocalStartDate)'" + $ExitCode = 1 + return + } + + # Calculate the timespan between the start and end dates. + try { + $HistoryTimeSpan = New-TimeSpan -Start $HistoryStartDate -End $HistoryEndDate -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to get time span for the history entry that started on '$HistoryStartDate' and ended on '$HistoryEndDate'." + $ExitCode = 1 + return + } + + # If the duration is more than one day, skip this entry as it's not needed. + if ($HistoryTimeSpan.TotalDays -gt 1) { + return + } + + # Convert ActiveDcTime to a PowerShell time span if possible. + try { + $BatteryActiveDuration = Get-ISO8601Duration -Duration $_.ActiveDcTime -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to translate the active battery duration for '$($_.ActiveDcTime)' on '$($HistoryStartDate.ToShortDateString()) $($HistoryStartDate.ToShortTimeString())'." + $ExitCode = 1 + } + + # Convert CsDcTime (Connected Standby on battery) to a PowerShell time span if possible. + try { + $BatteryConnectedDuration = Get-ISO8601Duration -Duration $_.CsDcTime -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to translate the battery connected standby duration for '$($_.CsDcTime)' on '$($HistoryStartDate.ToShortDateString()) $($HistoryStartDate.ToShortTimeString())'." + $ExitCode = 1 + } + + # Convert ActiveAcTime (Active time on AC) to a PowerShell time span if possible. + try { + $ACActiveDuration = Get-ISO8601Duration -Duration $_.ActiveAcTime -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to translate the active AC duration for '$($_.ActiveAcTime)' on '$($HistoryStartDate.ToShortDateString()) $($HistoryStartDate.ToShortTimeString())'." + $ExitCode = 1 + } + + # Convert CsAcTime (Connected Standby on AC) to a PowerShell time span if possible. + try { + $ACConnectedStandby = Get-ISO8601Duration -Duration $_.CsAcTime -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to translate the AC connected standby duration for '$($_.CsAcTime)' on '$($HistoryStartDate.ToShortDateString()) $($HistoryStartDate.ToShortTimeString())'." + $ExitCode = 1 + } + + # Return a custom object representing this history entry's duration details. + [PSCustomObject]@{ + StartDate = $HistoryStartDate + EndDate = $HistoryEndDate + BatteryActive = $BatteryActiveDuration + BatteryConnectedStandby = $BatteryConnectedDuration + ACActive = $ACActiveDuration + ACConnectedStandby = $ACConnectedStandby + } + } + + Write-Host -Object "Parsing the recent battery usage history." + + # Filter recent usage entries to exclude those with an EntryType of "ReportGenerated". + # For each entry, convert the timestamp, determine the power source, and calculate the percentage remaining. + $RecentUsageEntries = $BatteryHealthReport.BatteryReport.RecentUsage.UsageEntry | Where-Object { $_.EntryType -ne "ReportGenerated" } | ForEach-Object { + # Attempt to convert the LocalTimeStamp to a DateTime object. + try { + $StartTime = Get-Date $_.LocalTimeStamp + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to get the timestamp for the battery usage entry dated '$($_.LocalTimeStamp)'" + $ExitCode = 1 + return + } + + # Determine the power source: AC if 'AC' is 1, otherwise Battery. + $Source = switch ($_.AC) { + 1 { "AC" } + default { "Battery" } + } + + # If the entry type is "Suspend", there's no source (set to $Null). + if ($_.EntryType -eq "Suspend") { + $Source = $Null + } + + # Calculate the percentage of battery remaining. + try { + $PercentageRemaining = [math]::Round((($_.ChargeCapacity / $_.FullChargeCapacity) * 100), 2) + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to calculate battery percentage remaining from the battery usage entry dated '$StartTime'" + $ExitCode = 1 + } + + # Return a custom object with the processed information for this usage entry. + [PSCustomObject]@{ + StartTime = $StartTime + State = $_.EntryType + Source = $Source + PercentageRemaining = "$PercentageRemaining%" + CapacityRemaining = "$($_.ChargeCapacity) mWh" + } + } + + Write-Host -Object "`nFormatting the battery capacity history to be human-readable." + + # Convert each battery capacity history entry into a custom object with human-readable date and capacities. + $BatteryCapacityHistoryTable = $BatteryCapacityHistory | Sort-Object -Property Date -Descending | ForEach-Object { + [PSCustomObject]@{ + Date = $_.Date.ToShortDateString() + FullChargeCapacity = "$($_.FullChargeCapacity) mWh" + DesignCapacity = "$($_.DesignCapacity) mWh" + } + } + + Write-Host -Object "Formatting the battery usage history to be human-readable." + + # Convert each battery usage entry into a human-readable format. + $BatteryUsageTable = $BatteryUsageEntries | Sort-Object -Property StartDate -Descending | ForEach-Object { + # If BatteryActive is greater than ~1 second, attempt to convert it to a friendly time span. + if ($_.BatteryActive -and $_.BatteryActive -gt [TimeSpan]::FromMilliseconds(999)) { + try { + $BatteryActive = Get-FriendlyTimeSpan -TimeSpan $_.BatteryActive -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to get a human-readable string for the battery active duration of '$($_.BatteryActive)' for $($_.StartDate.ToShortDateString())." + $BatteryActive = " - " + $ExitCode = 1 + } + } + else { + $BatteryActive = " - " + } + + # If BatteryConnectedStandby is greater than ~1 second, convert it to a friendly time span. + if ($_.BatteryConnectedStandby -and $_.BatteryConnectedStandby -gt [TimeSpan]::FromMilliseconds(999)) { + try { + $BatteryConnectedStandby = Get-FriendlyTimeSpan -TimeSpan $_.BatteryConnectedStandby -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to get a human-readable string for the battery connected standby duration of '$($_.BatteryConnectedStandby)' for $($_.StartDate.ToShortDateString())." + $BatteryConnectedStandby = " - " + $ExitCode = 1 + } + } + else { + $BatteryConnectedStandby = " - " + } + + # If ACActive is greater than ~1 second, convert it to a friendly time span. + if ($_.ACActive -and $_.ACActive -gt [TimeSpan]::FromMilliseconds(999)) { + try { + $ACActive = Get-FriendlyTimeSpan -TimeSpan $_.ACActive -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to get a human-readable string for the AC active duration of '$($_.ACActive)' for $($_.StartDate.ToShortDateString())." + $ACActive = " - " + $ExitCode = 1 + } + } + else { + $ACActive = " - " + } + + # If ACConnectedStandby is greater than ~1 second, convert it to a friendly time span. + if ($_.ACConnectedStandby -and $_.ACConnectedStandby -gt [TimeSpan]::FromMilliseconds(999)) { + try { + $ACConnectedStandby = Get-FriendlyTimeSpan -TimeSpan $_.ACConnectedStandby -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to get a human-readable string for the AC connected standby duration of '$($_.ACConnectedStandby)' for $($_.StartDate.ToShortDateString())." + $ACActive = " - " + $ExitCode = 1 + } + } + else { + $ACConnectedStandby = " - " + } + + # Return a custom object representing the formatted usage information. + [PSCustomObject]@{ + StartDate = $_.StartDate.ToShortDateString() + BatteryActive = $BatteryActive + BatteryConnectedStandby = $BatteryConnectedStandby + ACActive = $ACActive + ACConnectedStandby = $ACConnectedStandby + } + } + + Write-Host -Object "Formatting the recent usage history to be human-readable." + # Convert each recent usage entry into a human-readable format, including date and time. + $RecentUsageEntryTable = $RecentUsageEntries | Sort-Object -Property StartTime -Descending | ForEach-Object { + [PSCustomObject]@{ + StartTime = "$($_.StartTime.ToShortDateString()) $($_.StartTime.ToLongTimeString())" + State = $_.State + Source = $_.Source + PercentageRemaining = $_.PercentageRemaining + CapacityRemaining = $_.CapacityRemaining + } + } + + if ($WYSIWYGCustomField) { + Write-Host -Object "`nFormatting the results for the WYSIWYG Custom Field 'WYSIWYGCustomField'." + + Write-Host -Object "Creating the system information HTML card." + + # Build an HTML card displaying system information details using the previously collected data. + $SystemInformationHTMLCard = "
+
+
  System Information
+
+
+

Report Time
$($SystemInformation.ReportTime)

+

System Product Name
$($SystemInformation.SystemProductName)

+

BIOS
$($SystemInformation.BIOS)

+

OS Build
$($SystemInformation.OSBuild)

+

Connected Standby
$($SystemInformation.ConnectedStandby)

+
+
" + + Write-Host -Object "Creating the installed batteries HTML card." + + # Initialize a counter to label each battery. + $i = 1 + # Build an HTML card that displays a table of all installed batteries and their properties. + $InstalledBatteriesHTMLCard = "
+
+
  Installed Batteries
+
+
+ + + + $($Batteries | ForEach-Object {"`n"}) + + + + + $($Batteries | ForEach-Object {"`n"; $i++}) + + + + + $($Batteries | ForEach-Object {"`n"}) + + + + $($Batteries | ForEach-Object {"`n"}) + + + + $($Batteries | ForEach-Object {"`n"}) + + + + $($Batteries | ForEach-Object {"`n"}) + + + + $($Batteries | ForEach-Object {"`n"}) + + + + $($Batteries | ForEach-Object {"`n"}) + + + + $($Batteries | ForEach-Object {"`n"}) + + + + $($Batteries | ForEach-Object {"`n"}) + +
Battery $i
Name$($_.Name)
Manufacturer$($_.Manufacturer)
Serial Number$($_.SerialNumber)
Chemistry$($_.Chemistry)
Usable Battery Percentage$($_.UsableBatteryPercentage)
Design Capacity$($_.DesignCapacity)
Full Charge Capacity$($_.FullChargeCapacity)
Cycle Count$($_.CycleCount)
+
+
" + + Write-Host -Object "Creating the battery capacity history HTML card." + # Convert the battery capacity history table into an HTML fragment and format table headers in bold. + $BatteryCapacityHistoryHTMLTable = $BatteryCapacityHistoryTable | ConvertTo-Html -Fragment + $BatteryCapacityHistoryHTMLTable = $BatteryCapacityHistoryHTMLTable -replace '', '' -replace '', '' + + # If battery capacity history data exists, create a detailed HTML card with a line chart. + if ($BatteryCapacityHistory) { + $BatteryCapacityHistoryHTMLCard = "
+
+
  Battery Capacity History
+
+
+
+ + + $( + $PreviousCapacityPercentage = 0.99 + $BatteryCapacityHistory | Sort-Object Date | ForEach-Object { + try { + $CurrentCapacityPercentage = [math]::Round(($_.FullChargeCapacity / $_.DesignCapacity),2) + if($CurrentCapacityPercentage -eq 1){ + $CurrentCapacityPercentage = 0.99 + } + }catch{ + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to calculate the capacity percentage for '$($_.Date.ToShortDateString)'." + return + } + " + + + `n" + $PreviousCapacityPercentage = $CurrentCapacityPercentage + } + ) + +
$($_.Date.ToString("MM-dd yyyy"))$($_.FullChargeCapacity) mWh
+
+
+ $BatteryCapacityHistoryHTMLTable +
+
+
" + } + + # If no battery capacity history data is available, show a message indicating unavailability. + if (!$BatteryCapacityHistory) { + $BatteryCapacityHistoryHTMLCard = "
+
+
  Battery Capacity History
+
+
+

Information not available or not found.

+
+
" + } + + Write-Host -Object "Creating the battery usage HTML card." + # If battery usage entries exist, generate a card that includes a chart and a table of usage information. + if ($BatteryUsageEntries) { + $BatteryUsageHTMLTable = $BatteryUsageTable | ConvertTo-Html -Fragment + + # Remove and replace parts of the generated HTML to create a custom table layout and headings. + $BatteryUsageHTMLTable = $BatteryUsageHTMLTable -replace ".*", "" + $BatteryUsageHTMLTable = $BatteryUsageHTMLTable -replace ".*", " + + + Battery Duration + AC Duration + + + Date + Active + Connected Standby + Active + Connected Standby + + +" + $BatteryUsageHTMLTable = $BatteryUsageHTMLTable -replace "", "`n" + + $BatteryUsageHTMLCard = "
+
+
  Battery Usage
+
+
+
+ + + + $($BatteryUsageEntries | ForEach-Object { + # Initialize variables for duration calculations. + $TotalBatteryDuration = $null + $FriendlyTimeSpan = $null + $TotalBatteryPercent = $null + + try{ + # Calculate the total battery duration and its percentage of a 24-hour period. + $TotalBatteryDuration = ($_.BatteryActive + $_.BatteryConnectedStandby) + $TotalBatteryPercent = [math]::Round(($TotalBatteryDuration.TotalHours / 24), 2) + }catch{ + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to calculate the percentage of online hours for the battery duration graph for '$($_.StartDate.ToShortDateString())'." + $ExitCode = 1 + return + } + + try{ + # Convert the total battery duration to a friendly time span string if it's more than ~1 second. + if($TotalBatteryDuration -and $TotalBatteryDuration -gt [TimeSpan]::FromMilliseconds(999)){ + $FriendlyTimeSpan = Get-FriendlyTimeSpan -TimeSpan $TotalBatteryDuration + }else{ + $FriendlyTimeSpan = " - " + } + }catch{ + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to create a friendly time span for '$($_.StartDate.ToShortDateString())'." + $ExitCode = 1 + return + } + + # Create a table row for each usage entry with a visual representation of battery duration. + " + + + `n" + }) + +
Battery Duration
$($_.StartDate.ToString("MM-dd"))$FriendlyTimeSpan
+
+
+ $BatteryUsageHTMLTable +
+
+
" + } + + # If there are no battery usage entries, display a message indicating no available information. + if (!$BatteryUsageEntries) { + $BatteryUsageHTMLCard = "
+
+
  Battery Usage
+
+
+

Information not available or not found.

+
+
" + } + + Write-Host -Object "Creating the recent usage HTML card." + + # Convert the recent usage entries into an HTML fragment, formatting headers in bold. + $RecentUsageEntryHTMLTable = $RecentUsageEntryTable | ConvertTo-Html -Fragment + $RecentUsageEntryHTMLTable = $RecentUsageEntryHTMLTable -replace '', '' -replace '', '' + + # Build an HTML card displaying the recent battery usage history in a table format. + $RecentUsageHTMLCard = "
+
+
  Recent Usage
+
+
+ $RecentUsageEntryHTMLTable +
+
" + + Write-Host -Object "Assembling the final WYSIWYG Value" + + # Combine all the previously created HTML cards into a final WYSIWYG value. + $WYSIWYGValue = "
+
+
+
+ $SystemInformationHTMLCard +
+
+ $InstalledBatteriesHTMLCard +
+
+
+
+
+ $BatteryCapacityHistoryHTMLCard + $BatteryUsageHTMLCard + $RecentUsageHTMLCard +
+
+
" + + try { + # Attempt to set the WYSIWYG custom field with the assembled HTML content. + Write-Host "Attempting to set the Custom Field '$WYSIWYGCustomField'." + Write-Host "Successfully set the Custom Field '$WYSIWYGCustomField'!" + } + catch { + # If there's an error, print it and set the exit code to 1. + Write-Host "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + } + + # The following code block: + # 1. Prints headings and data in a human-readable format for: + # - System Information + # - Installed Batteries + # - Battery Capacity History + # - Battery Usage + # - Recent Power Usage + # 2. Each section is introduced with a heading (e.g., "### System Information ###"). + # 3. Data is formatted using Format-List or Format-Table, converted to a string via Out-String, + # trimmed to remove extra whitespace, and then printed using Write-Host. + Write-Host -Object "`n### System Information ###" + ($SystemInformation | Format-List | Out-String).Trim() | Write-Host + Write-Host -Object "`n### Installed Batteries ###" + ($Batteries | Format-List | Out-String).Trim() | Write-Host + Write-Host -Object "`n### Battery Capacity History ###" + ($BatteryCapacityHistoryTable | Format-Table | Out-String).Trim() | Write-Host + Write-Host -Object "`n### Battery Usage ###" + ($BatteryUsageTable | Format-Table | Out-String).Trim() | Write-Host + Write-Host -Object "`n### Recent Power Usage ###" + ($RecentUsageEntryTable | Format-Table | Out-String).Trim() | Write-Host + + exit $ExitCode +} +end { + +} diff --git a/Powershell Scripts/Check Windows 11 Upgrade Compatibility.ps1 b/Powershell Scripts/Check Windows 11 Upgrade Compatibility.ps1 index be82afa..2a96712 100644 --- a/Powershell Scripts/Check Windows 11 Upgrade Compatibility.ps1 +++ b/Powershell Scripts/Check Windows 11 Upgrade Compatibility.ps1 @@ -1,737 +1,611 @@ # Checks if the computer is capable of upgrading to Windows 11. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Checks if the computer is capable of upgrading to Windows 11. -.DESCRIPTION - Checks if the computer is capable of upgrading to Windows 11. -.EXAMPLE - (No Parameters) - - Verifying Windows 11 compatibility. - Successfully retrieved Windows 11 compatibility results. - - Result: [Alert] Not Capable - Storage - -PARAMETER: -CustomFieldName "ReplaceMeWithNameOfACustomField" - Optionally specify the name of a custom field to save the results to. - -.NOTES - Minimum OS Architecture Supported: Windows 10 - Release Notes: Made the script more verbose, added the reason the device was listed as incompatible, and improved error handling. -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$CustomFieldName -) - -begin { - if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomFieldName = $env:customFieldName } - - if ($CustomFieldName) { - $CustomFieldName = $CustomFieldName.Trim() - } - - # Determine the method to retrieve the operating system information based on PowerShell version - try { - $OS = if ($PSVersionTable.PSVersion.Major -lt 3) { - Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop - } - else { - Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop - } - } - catch { - Write-Host -Object "[Error] Unable to retrieve information about the current operating system." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # If the device is already running Windows 11 exit with an error message. - if ($OS.Caption -match "Windows 11") { - Write-Host -Object "[Error] This device is already running Windows 11." - exit 1 - } - - function Get-HardwareReadiness() { - # Modified copy of https://aka.ms/HWReadinessScript minus the signature, as of 7/26/2023. - # Only modification was replacing Get-WmiObject with Get-CimInstance for PowerShell 7 compatibility - # Source Microsoft article: https://techcommunity.microsoft.com/t5/microsoft-endpoint-manager-blog/understanding-readiness-for-windows-11-with-microsoft-endpoint/ba-p/2770866 - - #============================================================================================================================= - # - # Script Name: HardwareReadiness.ps1 - # Description: Verifies the hardware compliance. Return code 0 for success. - # In case of failure, returns non zero error code along with error message. - - # This script is not supported under any Microsoft standard support program or service and is distributed under the MIT license - - # Copyright (C) 2021 Microsoft Corporation - - # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation - # files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, - # modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software - # is furnished to do so, subject to the following conditions: - - # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE - # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, - # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - #============================================================================================================================= - - $exitCode = 0 - - [int]$MinOSDiskSizeGB = 64 - [int]$MinMemoryGB = 4 - [Uint32]$MinClockSpeedMHz = 1000 - [Uint32]$MinLogicalCores = 2 - [Uint16]$RequiredAddressWidth = 64 - - $PASS_STRING = "PASS" - $FAIL_STRING = "FAIL" - $FAILED_TO_RUN_STRING = "FAILED TO RUN" - $UNDETERMINED_CAPS_STRING = "UNDETERMINED" - $UNDETERMINED_STRING = "Undetermined" - $CAPABLE_STRING = "Capable" - $NOT_CAPABLE_STRING = "Not capable" - $CAPABLE_CAPS_STRING = "CAPABLE" - $NOT_CAPABLE_CAPS_STRING = "NOT CAPABLE" - $STORAGE_STRING = "Storage" - $OS_DISK_SIZE_STRING = "OSDiskSize" - $MEMORY_STRING = "Memory" - $SYSTEM_MEMORY_STRING = "System_Memory" - $GB_UNIT_STRING = "GB" - $TPM_STRING = "TPM" - $TPM_VERSION_STRING = "TPMVersion" - $PROCESSOR_STRING = "Processor" - $SECUREBOOT_STRING = "SecureBoot" - $I7_7820HQ_CPU_STRING = "i7-7820hq CPU" - - # 0=name of check, 1=attribute checked, 2=value, 3=PASS/FAIL/UNDETERMINED - $logFormat = '{0}: {1}={2}. {3}; ' - - # 0=name of check, 1=attribute checked, 2=value, 3=unit of the value, 4=PASS/FAIL/UNDETERMINED - $logFormatWithUnit = '{0}: {1}={2}{3}. {4}; ' - - # 0=name of check. - $logFormatReturnReason = '{0}, ' - - # 0=exception. - $logFormatException = '{0}; ' - - # 0=name of check, 1= attribute checked and its value, 2=PASS/FAIL/UNDETERMINED - $logFormatWithBlob = '{0}: {1}. {2}; ' - - # return returnCode is -1 when an exception is thrown. 1 if the value does not meet requirements. 0 if successful. -2 default, script didn't run. - $outObject = @{ returnCode = -2; returnResult = $FAILED_TO_RUN_STRING; returnReason = ""; logging = "" } - - # NOT CAPABLE(1) state takes precedence over UNDETERMINED(-1) state - function Private:UpdateReturnCode { - param( - [Parameter(Mandatory = $true)] - [ValidateRange(-2, 1)] - [int] $ReturnCode - ) - - Switch ($ReturnCode) { - - 0 { - if ($outObject.returnCode -eq -2) { - $outObject.returnCode = $ReturnCode - } - } - 1 { - $outObject.returnCode = $ReturnCode - } - -1 { - if ($outObject.returnCode -ne 1) { - $outObject.returnCode = $ReturnCode - } - } - } - } - - $Source = @" -using Microsoft.Win32; -using System; -using System.Runtime.InteropServices; - - public class CpuFamilyResult - { - public bool IsValid { get; set; } - public string Message { get; set; } - } - - public class CpuFamily - { - [StructLayout(LayoutKind.Sequential)] - public struct SYSTEM_INFO - { - public ushort ProcessorArchitecture; - ushort Reserved; - public uint PageSize; - public IntPtr MinimumApplicationAddress; - public IntPtr MaximumApplicationAddress; - public IntPtr ActiveProcessorMask; - public uint NumberOfProcessors; - public uint ProcessorType; - public uint AllocationGranularity; - public ushort ProcessorLevel; - public ushort ProcessorRevision; - } - - [DllImport("kernel32.dll")] - internal static extern void GetNativeSystemInfo(ref SYSTEM_INFO lpSystemInfo); - - public enum ProcessorFeature : uint - { - ARM_SUPPORTED_INSTRUCTIONS = 34 - } - - [DllImport("kernel32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - static extern bool IsProcessorFeaturePresent(ProcessorFeature processorFeature); - - private const ushort PROCESSOR_ARCHITECTURE_X86 = 0; - private const ushort PROCESSOR_ARCHITECTURE_ARM64 = 12; - private const ushort PROCESSOR_ARCHITECTURE_X64 = 9; - - private const string INTEL_MANUFACTURER = "GenuineIntel"; - private const string AMD_MANUFACTURER = "AuthenticAMD"; - private const string QUALCOMM_MANUFACTURER = "Qualcomm Technologies Inc"; - - public static CpuFamilyResult Validate(string manufacturer, ushort processorArchitecture) - { - CpuFamilyResult cpuFamilyResult = new CpuFamilyResult(); - - if (string.IsNullOrWhiteSpace(manufacturer)) - { - cpuFamilyResult.IsValid = false; - cpuFamilyResult.Message = "Manufacturer is null or empty"; - return cpuFamilyResult; - } - - string registryPath = "HKEY_LOCAL_MACHINE\\Hardware\\Description\\System\\CentralProcessor\\0"; - SYSTEM_INFO sysInfo = new SYSTEM_INFO(); - GetNativeSystemInfo(ref sysInfo); - - switch (processorArchitecture) - { - case PROCESSOR_ARCHITECTURE_ARM64: - - if (manufacturer.Equals(QUALCOMM_MANUFACTURER, StringComparison.OrdinalIgnoreCase)) - { - bool isArmv81Supported = IsProcessorFeaturePresent(ProcessorFeature.ARM_SUPPORTED_INSTRUCTIONS); - - if (!isArmv81Supported) - { - string registryName = "CP 4030"; - long registryValue = (long)Registry.GetValue(registryPath, registryName, -1); - long atomicResult = (registryValue >> 20) & 0xF; - - if (atomicResult >= 2) - { - isArmv81Supported = true; - } - } - - cpuFamilyResult.IsValid = isArmv81Supported; - cpuFamilyResult.Message = isArmv81Supported ? "" : "Processor does not implement ARM v8.1 atomic instruction"; - } - else - { - cpuFamilyResult.IsValid = false; - cpuFamilyResult.Message = "The processor isn't currently supported for Windows 11"; - } - - break; - - case PROCESSOR_ARCHITECTURE_X64: - case PROCESSOR_ARCHITECTURE_X86: - - int cpuFamily = sysInfo.ProcessorLevel; - int cpuModel = (sysInfo.ProcessorRevision >> 8) & 0xFF; - int cpuStepping = sysInfo.ProcessorRevision & 0xFF; - - if (manufacturer.Equals(INTEL_MANUFACTURER, StringComparison.OrdinalIgnoreCase)) - { - try - { - cpuFamilyResult.IsValid = true; - cpuFamilyResult.Message = ""; - - if (cpuFamily >= 6 && cpuModel <= 95 && !(cpuFamily == 6 && cpuModel == 85)) - { - cpuFamilyResult.IsValid = false; - cpuFamilyResult.Message = ""; - } - else if (cpuFamily == 6 && (cpuModel == 142 || cpuModel == 158) && cpuStepping == 9) - { - string registryName = "Platform Specific Field 1"; - int registryValue = (int)Registry.GetValue(registryPath, registryName, -1); - - if ((cpuModel == 142 && registryValue != 16) || (cpuModel == 158 && registryValue != 8)) - { - cpuFamilyResult.IsValid = false; - } - cpuFamilyResult.Message = "PlatformId " + registryValue; - } - } - catch (Exception ex) - { - cpuFamilyResult.IsValid = false; - cpuFamilyResult.Message = "Exception:" + ex.GetType().Name; - } - } - else if (manufacturer.Equals(AMD_MANUFACTURER, StringComparison.OrdinalIgnoreCase)) - { - cpuFamilyResult.IsValid = true; - cpuFamilyResult.Message = ""; - - if (cpuFamily < 23 || (cpuFamily == 23 && (cpuModel == 1 || cpuModel == 17))) - { - cpuFamilyResult.IsValid = false; - } - } - else - { - cpuFamilyResult.IsValid = false; - cpuFamilyResult.Message = "Unsupported Manufacturer: " + manufacturer + ", Architecture: " + processorArchitecture + ", CPUFamily: " + sysInfo.ProcessorLevel + ", ProcessorRevision: " + sysInfo.ProcessorRevision; - } - - break; - - default: - cpuFamilyResult.IsValid = false; - cpuFamilyResult.Message = "Unsupported CPU category. Manufacturer: " + manufacturer + ", Architecture: " + processorArchitecture + ", CPUFamily: " + sysInfo.ProcessorLevel + ", ProcessorRevision: " + sysInfo.ProcessorRevision; - break; - } - return cpuFamilyResult; - } - } -"@ - - # Storage - try { - $osDrive = Get-CimInstance -Class Win32_OperatingSystem | Select-Object -Property SystemDrive - $osDriveSize = Get-CimInstance -Class Win32_LogicalDisk -Filter "DeviceID='$($osDrive.SystemDrive)'" | Select-Object @{Name = "SizeGB"; Expression = { $_.Size / 1GB -as [int] } } - - if ($null -eq $osDriveSize) { - UpdateReturnCode -ReturnCode 1 - $outObject.returnReason += $logFormatReturnReason -f $STORAGE_STRING - $outObject.logging += $logFormatWithBlob -f $STORAGE_STRING, "Storage is null", $FAIL_STRING - $exitCode = 1 - } - elseif ($osDriveSize.SizeGB -lt $MinOSDiskSizeGB) { - UpdateReturnCode -ReturnCode 1 - $outObject.returnReason += $logFormatReturnReason -f $STORAGE_STRING - $outObject.logging += $logFormatWithUnit -f $STORAGE_STRING, $OS_DISK_SIZE_STRING, ($osDriveSize.SizeGB), $GB_UNIT_STRING, $FAIL_STRING - $exitCode = 1 - } - else { - $outObject.logging += $logFormatWithUnit -f $STORAGE_STRING, $OS_DISK_SIZE_STRING, ($osDriveSize.SizeGB), $GB_UNIT_STRING, $PASS_STRING - UpdateReturnCode -ReturnCode 0 - } - } - catch { - UpdateReturnCode -ReturnCode -1 - $outObject.logging += $logFormat -f $STORAGE_STRING, $OS_DISK_SIZE_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING - $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" - $exitCode = 1 - } - - # Memory (bytes) - try { - $memory = Get-CimInstance Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum | Select-Object @{Name = "SizeGB"; Expression = { $_.Sum / 1GB -as [int] } } - - if ($null -eq $memory) { - UpdateReturnCode -ReturnCode 1 - $outObject.returnReason += $logFormatReturnReason -f $MEMORY_STRING - $outObject.logging += $logFormatWithBlob -f $MEMORY_STRING, "Memory is null", $FAIL_STRING - $exitCode = 1 - } - elseif ($memory.SizeGB -lt $MinMemoryGB) { - UpdateReturnCode -ReturnCode 1 - $outObject.returnReason += $logFormatReturnReason -f $MEMORY_STRING - $outObject.logging += $logFormatWithUnit -f $MEMORY_STRING, $SYSTEM_MEMORY_STRING, ($memory.SizeGB), $GB_UNIT_STRING, $FAIL_STRING - $exitCode = 1 - } - else { - $outObject.logging += $logFormatWithUnit -f $MEMORY_STRING, $SYSTEM_MEMORY_STRING, ($memory.SizeGB), $GB_UNIT_STRING, $PASS_STRING - UpdateReturnCode -ReturnCode 0 - } - } - catch { - UpdateReturnCode -ReturnCode -1 - $outObject.logging += $logFormat -f $MEMORY_STRING, $SYSTEM_MEMORY_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING - $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" - $exitCode = 1 - } - - # TPM - try { - $tpm = Get-Tpm - - if ($null -eq $tpm) { - UpdateReturnCode -ReturnCode 1 - $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING - $outObject.logging += $logFormatWithBlob -f $TPM_STRING, "TPM is null", $FAIL_STRING - $exitCode = 1 - } - elseif ($tpm.TpmPresent) { - $tpmVersion = Get-CimInstance -Class Win32_Tpm -Namespace root\CIMV2\Security\MicrosoftTpm | Select-Object -Property SpecVersion - - if ($null -eq $tpmVersion.SpecVersion) { - UpdateReturnCode -ReturnCode 1 - $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING - $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, "null", $FAIL_STRING - $exitCode = 1 - } - - $majorVersion = $tpmVersion.SpecVersion.Split(",")[0] -as [int] - if ($majorVersion -lt 2) { - UpdateReturnCode -ReturnCode 1 - $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING - $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, ($tpmVersion.SpecVersion), $FAIL_STRING - $exitCode = 1 - } - else { - $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, ($tpmVersion.SpecVersion), $PASS_STRING - UpdateReturnCode -ReturnCode 0 - } - } - else { - if ($tpm.GetType().Name -eq "String") { - UpdateReturnCode -ReturnCode -1 - $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING - $outObject.logging += $logFormatException -f $tpm - } - else { - UpdateReturnCode -ReturnCode 1 - $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING - $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, ($tpm.TpmPresent), $FAIL_STRING - } - $exitCode = 1 - } - } - catch { - UpdateReturnCode -ReturnCode -1 - $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING - $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" - $exitCode = 1 - } - - # CPU Details - $cpuDetails; - try { - $cpuDetails = @(Get-CimInstance -Class Win32_Processor)[0] - - if ($null -eq $cpuDetails) { - UpdateReturnCode -ReturnCode 1 - $exitCode = 1 - $outObject.returnReason += $logFormatReturnReason -f $PROCESSOR_STRING - $outObject.logging += $logFormatWithBlob -f $PROCESSOR_STRING, "CpuDetails is null", $FAIL_STRING - } - else { - $processorCheckFailed = $false - - # AddressWidth - if ($null -eq $cpuDetails.AddressWidth -or $cpuDetails.AddressWidth -ne $RequiredAddressWidth) { - UpdateReturnCode -ReturnCode 1 - $processorCheckFailed = $true - $exitCode = 1 - } - - # ClockSpeed is in MHz - if ($null -eq $cpuDetails.MaxClockSpeed -or $cpuDetails.MaxClockSpeed -le $MinClockSpeedMHz) { - UpdateReturnCode -ReturnCode 1; - $processorCheckFailed = $true - $exitCode = 1 - } - - # Number of Logical Cores - if ($null -eq $cpuDetails.NumberOfLogicalProcessors -or $cpuDetails.NumberOfLogicalProcessors -lt $MinLogicalCores) { - UpdateReturnCode -ReturnCode 1 - $processorCheckFailed = $true - $exitCode = 1 - } - - # CPU Family - Add-Type -TypeDefinition $Source - $cpuFamilyResult = [CpuFamily]::Validate([String]$cpuDetails.Manufacturer, [uint16]$cpuDetails.Architecture) - - $cpuDetailsLog = "{AddressWidth=$($cpuDetails.AddressWidth); MaxClockSpeed=$($cpuDetails.MaxClockSpeed); NumberOfLogicalCores=$($cpuDetails.NumberOfLogicalProcessors); Manufacturer=$($cpuDetails.Manufacturer); Caption=$($cpuDetails.Caption); $($cpuFamilyResult.Message)}" - - if (!$cpuFamilyResult.IsValid) { - UpdateReturnCode -ReturnCode 1 - $processorCheckFailed = $true - $exitCode = 1 - } - - if ($processorCheckFailed) { - $outObject.returnReason += $logFormatReturnReason -f $PROCESSOR_STRING - $outObject.logging += $logFormatWithBlob -f $PROCESSOR_STRING, ($cpuDetailsLog), $FAIL_STRING - } - else { - $outObject.logging += $logFormatWithBlob -f $PROCESSOR_STRING, ($cpuDetailsLog), $PASS_STRING - UpdateReturnCode -ReturnCode 0 - } - } - } - catch { - UpdateReturnCode -ReturnCode -1 - $outObject.logging += $logFormat -f $PROCESSOR_STRING, $PROCESSOR_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING - $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" - $exitCode = 1 - } - - # SecureBoot - try { - $isSecureBootEnabled = Confirm-SecureBootUEFI - $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $CAPABLE_STRING, $PASS_STRING - UpdateReturnCode -ReturnCode 0 - } - catch [System.PlatformNotSupportedException] { - # PlatformNotSupportedException "Cmdlet not supported on this platform." - SecureBoot is not supported or is non-UEFI computer. - UpdateReturnCode -ReturnCode 1 - $outObject.returnReason += $logFormatReturnReason -f $SECUREBOOT_STRING - $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $NOT_CAPABLE_STRING, $FAIL_STRING - $exitCode = 1 - } - catch [System.UnauthorizedAccessException] { - UpdateReturnCode -ReturnCode -1 - $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING - $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" - $exitCode = 1 - } - catch { - UpdateReturnCode -ReturnCode -1 - $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING - $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" - $exitCode = 1 - } - - # i7-7820hq CPU - try { - $supportedDevices = @('surface studio 2', 'precision 5520') - $systemInfo = @(Get-CimInstance -Class Win32_ComputerSystem)[0] - - if ($null -ne $cpuDetails) { - if ($cpuDetails.Name -match 'i7-7820hq cpu @ 2.90ghz') { - $modelOrSKUCheckLog = $systemInfo.Model.Trim() - if ($supportedDevices -contains $modelOrSKUCheckLog) { - $outObject.logging += $logFormatWithBlob -f $I7_7820HQ_CPU_STRING, $modelOrSKUCheckLog, $PASS_STRING - $outObject.returnCode = 0 - $exitCode = 0 - } - } - } - } - catch { - if ($outObject.returnCode -ne 0) { - UpdateReturnCode -ReturnCode -1 - $outObject.logging += $logFormatWithBlob -f $I7_7820HQ_CPU_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING - $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" - $exitCode = 1 - } - } - - Switch ($outObject.returnCode) { - - 0 { $outObject.returnResult = $CAPABLE_CAPS_STRING } - 1 { $outObject.returnResult = $NOT_CAPABLE_CAPS_STRING } - -1 { $outObject.returnResult = $UNDETERMINED_CAPS_STRING } - -2 { $outObject.returnResult = $FAILED_TO_RUN_STRING } - } - - $outObject | ConvertTo-Json -Compress - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName, - [Parameter()] - [Switch]$Piped - ) - # Remove the non-breaking space character - if ($Type -eq "WYSIWYG") { - $Value = $Value -replace ' ', ' ' - } - - # Measure the number of characters in the provided value - $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # Throw an error if the value exceeds the character limit of 200,000 characters - if ($Piped -and $Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - if (!$Piped -and $Characters -ge 45000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") - } - - # Initialize a hashtable for additional documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define a list of valid field types - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - - # Warn the user if the provided type is not valid - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # Define types that require options to be retrieved - $NeedsOptions = "Dropdown" - - # If the property is being set in a document or field and the type needs options, retrieve them - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an error if there was an issue retrieving the property options - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Process the property value based on its type - switch ($Type) { - "Checkbox" { - # Convert the value to a boolean for Checkbox type - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Convert the value to a Unix timestamp for Date or Date Time type - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Convert the dropdown value to its corresponding GUID - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - # Throw an error if the value is not present in the dropdown options - if (!($Selection)) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # For other types, use the value as is - $NinjaValue = $Value - } - } - - # Set the property value in the document if a document name is provided - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - try { - # Otherwise, set the standard property value - if ($Piped) { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - } - catch { - Write-Host -Object "[Error] Failed to set custom field." - throw $_.Exception.Message - } - } - - # Throw an error if setting the property failed - if ($CustomField.Exception) { - throw $CustomField - } - } - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - try { - $ErrorActionPreference = "Stop" - - Write-Host -Object "Verifying Windows 11 compatibility." - $Result = Get-HardwareReadiness | Select-Object -Unique | ConvertFrom-Json - Write-Host -Object "Successfully retrieved Windows 11 compatibility results.`n" - - $ErrorActionPreference = "Continue" - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to retrieve Windows 11 compatibility results." - exit 1 - } - - switch ($Result.returnCode) { - 0 { - $ResultString = "Capable" - } - 1 { - $ResultString = "[Alert] Not Capable" - } - -2 { - $ResultString = "[Error] Failed To Run" - $ExitCode = 1 - } - default { - $ResultString = "[Error] Undetermined" - $ExitCode = 1 - } - } - - if ($Result.returnReason) { - $ResultString = "$ResultString - $($Result.returnReason)" - $ResultString = $ResultString -replace ",\s*$" - } - - if ($CustomFieldName) { - try { - Write-Host -Object "Attempting to set Custom Field '$CustomFieldName'." - Set-NinjaProperty -Name $CustomFieldName -Value $ResultString - Write-Host -Object "Successfully set Custom Field '$CustomFieldName'!`n" - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "" - $ExitCode = 1 - } - } - - # Print Return Result - Write-Host -Object "Result: $ResultString" - exit $ExitCode -} -end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + Checks if the computer is capable of upgrading to Windows 11. +.DESCRIPTION + Checks if the computer is capable of upgrading to Windows 11. +.EXAMPLE + (No Parameters) + + Verifying Windows 11 compatibility. + Successfully retrieved Windows 11 compatibility results. + + Result: [Alert] Not Capable - Storage + +PARAMETER: -CustomFieldName "ReplaceMeWithNameOfACustomField" + Optionally specify the name of a custom field to save the results to. + +.NOTES + Minimum OS Architecture Supported: Windows 10 + Release Notes: Made the script more verbose, added the reason the device was listed as incompatible, and improved error handling. +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$CustomFieldName +) + +begin { + if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomFieldName = $env:customFieldName } + + if ($CustomFieldName) { + $CustomFieldName = $CustomFieldName.Trim() + } + + # Determine the method to retrieve the operating system information based on PowerShell version + try { + $OS = if ($PSVersionTable.PSVersion.Major -lt 3) { + Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop + } + else { + Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop + } + } + catch { + Write-Host -Object "[Error] Unable to retrieve information about the current operating system." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # If the device is already running Windows 11 exit with an error message. + if ($OS.Caption -match "Windows 11") { + Write-Host -Object "[Error] This device is already running Windows 11." + exit 1 + } + + function Get-HardwareReadiness() { + # Modified copy of https://aka.ms/HWReadinessScript minus the signature, as of 7/26/2023. + # Only modification was replacing Get-WmiObject with Get-CimInstance for PowerShell 7 compatibility + # Source Microsoft article: https://techcommunity.microsoft.com/t5/microsoft-endpoint-manager-blog/understanding-readiness-for-windows-11-with-microsoft-endpoint/ba-p/2770866 + + #============================================================================================================================= + # + # Script Name: HardwareReadiness.ps1 + # Description: Verifies the hardware compliance. Return code 0 for success. + # In case of failure, returns non zero error code along with error message. + + # This script is not supported under any Microsoft standard support program or service and is distributed under the MIT license + + # Copyright (C) 2021 Microsoft Corporation + + # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation + # files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, + # modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software + # is furnished to do so, subject to the following conditions: + + # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + #============================================================================================================================= + + $exitCode = 0 + + [int]$MinOSDiskSizeGB = 64 + [int]$MinMemoryGB = 4 + [Uint32]$MinClockSpeedMHz = 1000 + [Uint32]$MinLogicalCores = 2 + [Uint16]$RequiredAddressWidth = 64 + + $PASS_STRING = "PASS" + $FAIL_STRING = "FAIL" + $FAILED_TO_RUN_STRING = "FAILED TO RUN" + $UNDETERMINED_CAPS_STRING = "UNDETERMINED" + $UNDETERMINED_STRING = "Undetermined" + $CAPABLE_STRING = "Capable" + $NOT_CAPABLE_STRING = "Not capable" + $CAPABLE_CAPS_STRING = "CAPABLE" + $NOT_CAPABLE_CAPS_STRING = "NOT CAPABLE" + $STORAGE_STRING = "Storage" + $OS_DISK_SIZE_STRING = "OSDiskSize" + $MEMORY_STRING = "Memory" + $SYSTEM_MEMORY_STRING = "System_Memory" + $GB_UNIT_STRING = "GB" + $TPM_STRING = "TPM" + $TPM_VERSION_STRING = "TPMVersion" + $PROCESSOR_STRING = "Processor" + $SECUREBOOT_STRING = "SecureBoot" + $I7_7820HQ_CPU_STRING = "i7-7820hq CPU" + + # 0=name of check, 1=attribute checked, 2=value, 3=PASS/FAIL/UNDETERMINED + $logFormat = '{0}: {1}={2}. {3}; ' + + # 0=name of check, 1=attribute checked, 2=value, 3=unit of the value, 4=PASS/FAIL/UNDETERMINED + $logFormatWithUnit = '{0}: {1}={2}{3}. {4}; ' + + # 0=name of check. + $logFormatReturnReason = '{0}, ' + + # 0=exception. + $logFormatException = '{0}; ' + + # 0=name of check, 1= attribute checked and its value, 2=PASS/FAIL/UNDETERMINED + $logFormatWithBlob = '{0}: {1}. {2}; ' + + # return returnCode is -1 when an exception is thrown. 1 if the value does not meet requirements. 0 if successful. -2 default, script didn't run. + $outObject = @{ returnCode = -2; returnResult = $FAILED_TO_RUN_STRING; returnReason = ""; logging = "" } + + # NOT CAPABLE(1) state takes precedence over UNDETERMINED(-1) state + function Private:UpdateReturnCode { + param( + [Parameter(Mandatory = $true)] + [ValidateRange(-2, 1)] + [int] $ReturnCode + ) + + Switch ($ReturnCode) { + + 0 { + if ($outObject.returnCode -eq -2) { + $outObject.returnCode = $ReturnCode + } + } + 1 { + $outObject.returnCode = $ReturnCode + } + -1 { + if ($outObject.returnCode -ne 1) { + $outObject.returnCode = $ReturnCode + } + } + } + } + + $Source = @" +using Microsoft.Win32; +using System; +using System.Runtime.InteropServices; + + public class CpuFamilyResult + { + public bool IsValid { get; set; } + public string Message { get; set; } + } + + public class CpuFamily + { + [StructLayout(LayoutKind.Sequential)] + public struct SYSTEM_INFO + { + public ushort ProcessorArchitecture; + ushort Reserved; + public uint PageSize; + public IntPtr MinimumApplicationAddress; + public IntPtr MaximumApplicationAddress; + public IntPtr ActiveProcessorMask; + public uint NumberOfProcessors; + public uint ProcessorType; + public uint AllocationGranularity; + public ushort ProcessorLevel; + public ushort ProcessorRevision; + } + + [DllImport("kernel32.dll")] + internal static extern void GetNativeSystemInfo(ref SYSTEM_INFO lpSystemInfo); + + public enum ProcessorFeature : uint + { + ARM_SUPPORTED_INSTRUCTIONS = 34 + } + + [DllImport("kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool IsProcessorFeaturePresent(ProcessorFeature processorFeature); + + private const ushort PROCESSOR_ARCHITECTURE_X86 = 0; + private const ushort PROCESSOR_ARCHITECTURE_ARM64 = 12; + private const ushort PROCESSOR_ARCHITECTURE_X64 = 9; + + private const string INTEL_MANUFACTURER = "GenuineIntel"; + private const string AMD_MANUFACTURER = "AuthenticAMD"; + private const string QUALCOMM_MANUFACTURER = "Qualcomm Technologies Inc"; + + public static CpuFamilyResult Validate(string manufacturer, ushort processorArchitecture) + { + CpuFamilyResult cpuFamilyResult = new CpuFamilyResult(); + + if (string.IsNullOrWhiteSpace(manufacturer)) + { + cpuFamilyResult.IsValid = false; + cpuFamilyResult.Message = "Manufacturer is null or empty"; + return cpuFamilyResult; + } + + string registryPath = "HKEY_LOCAL_MACHINE\\Hardware\\Description\\System\\CentralProcessor\\0"; + SYSTEM_INFO sysInfo = new SYSTEM_INFO(); + GetNativeSystemInfo(ref sysInfo); + + switch (processorArchitecture) + { + case PROCESSOR_ARCHITECTURE_ARM64: + + if (manufacturer.Equals(QUALCOMM_MANUFACTURER, StringComparison.OrdinalIgnoreCase)) + { + bool isArmv81Supported = IsProcessorFeaturePresent(ProcessorFeature.ARM_SUPPORTED_INSTRUCTIONS); + + if (!isArmv81Supported) + { + string registryName = "CP 4030"; + long registryValue = (long)Registry.GetValue(registryPath, registryName, -1); + long atomicResult = (registryValue >> 20) & 0xF; + + if (atomicResult >= 2) + { + isArmv81Supported = true; + } + } + + cpuFamilyResult.IsValid = isArmv81Supported; + cpuFamilyResult.Message = isArmv81Supported ? "" : "Processor does not implement ARM v8.1 atomic instruction"; + } + else + { + cpuFamilyResult.IsValid = false; + cpuFamilyResult.Message = "The processor isn't currently supported for Windows 11"; + } + + break; + + case PROCESSOR_ARCHITECTURE_X64: + case PROCESSOR_ARCHITECTURE_X86: + + int cpuFamily = sysInfo.ProcessorLevel; + int cpuModel = (sysInfo.ProcessorRevision >> 8) & 0xFF; + int cpuStepping = sysInfo.ProcessorRevision & 0xFF; + + if (manufacturer.Equals(INTEL_MANUFACTURER, StringComparison.OrdinalIgnoreCase)) + { + try + { + cpuFamilyResult.IsValid = true; + cpuFamilyResult.Message = ""; + + if (cpuFamily >= 6 && cpuModel <= 95 && !(cpuFamily == 6 && cpuModel == 85)) + { + cpuFamilyResult.IsValid = false; + cpuFamilyResult.Message = ""; + } + else if (cpuFamily == 6 && (cpuModel == 142 || cpuModel == 158) && cpuStepping == 9) + { + string registryName = "Platform Specific Field 1"; + int registryValue = (int)Registry.GetValue(registryPath, registryName, -1); + + if ((cpuModel == 142 && registryValue != 16) || (cpuModel == 158 && registryValue != 8)) + { + cpuFamilyResult.IsValid = false; + } + cpuFamilyResult.Message = "PlatformId " + registryValue; + } + } + catch (Exception ex) + { + cpuFamilyResult.IsValid = false; + cpuFamilyResult.Message = "Exception:" + ex.GetType().Name; + } + } + else if (manufacturer.Equals(AMD_MANUFACTURER, StringComparison.OrdinalIgnoreCase)) + { + cpuFamilyResult.IsValid = true; + cpuFamilyResult.Message = ""; + + if (cpuFamily < 23 || (cpuFamily == 23 && (cpuModel == 1 || cpuModel == 17))) + { + cpuFamilyResult.IsValid = false; + } + } + else + { + cpuFamilyResult.IsValid = false; + cpuFamilyResult.Message = "Unsupported Manufacturer: " + manufacturer + ", Architecture: " + processorArchitecture + ", CPUFamily: " + sysInfo.ProcessorLevel + ", ProcessorRevision: " + sysInfo.ProcessorRevision; + } + + break; + + default: + cpuFamilyResult.IsValid = false; + cpuFamilyResult.Message = "Unsupported CPU category. Manufacturer: " + manufacturer + ", Architecture: " + processorArchitecture + ", CPUFamily: " + sysInfo.ProcessorLevel + ", ProcessorRevision: " + sysInfo.ProcessorRevision; + break; + } + return cpuFamilyResult; + } + } +"@ + + # Storage + try { + $osDrive = Get-CimInstance -Class Win32_OperatingSystem | Select-Object -Property SystemDrive + $osDriveSize = Get-CimInstance -Class Win32_LogicalDisk -Filter "DeviceID='$($osDrive.SystemDrive)'" | Select-Object @{Name = "SizeGB"; Expression = { $_.Size / 1GB -as [int] } } + + if ($null -eq $osDriveSize) { + UpdateReturnCode -ReturnCode 1 + $outObject.returnReason += $logFormatReturnReason -f $STORAGE_STRING + $outObject.logging += $logFormatWithBlob -f $STORAGE_STRING, "Storage is null", $FAIL_STRING + $exitCode = 1 + } + elseif ($osDriveSize.SizeGB -lt $MinOSDiskSizeGB) { + UpdateReturnCode -ReturnCode 1 + $outObject.returnReason += $logFormatReturnReason -f $STORAGE_STRING + $outObject.logging += $logFormatWithUnit -f $STORAGE_STRING, $OS_DISK_SIZE_STRING, ($osDriveSize.SizeGB), $GB_UNIT_STRING, $FAIL_STRING + $exitCode = 1 + } + else { + $outObject.logging += $logFormatWithUnit -f $STORAGE_STRING, $OS_DISK_SIZE_STRING, ($osDriveSize.SizeGB), $GB_UNIT_STRING, $PASS_STRING + UpdateReturnCode -ReturnCode 0 + } + } + catch { + UpdateReturnCode -ReturnCode -1 + $outObject.logging += $logFormat -f $STORAGE_STRING, $OS_DISK_SIZE_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING + $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" + $exitCode = 1 + } + + # Memory (bytes) + try { + $memory = Get-CimInstance Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum | Select-Object @{Name = "SizeGB"; Expression = { $_.Sum / 1GB -as [int] } } + + if ($null -eq $memory) { + UpdateReturnCode -ReturnCode 1 + $outObject.returnReason += $logFormatReturnReason -f $MEMORY_STRING + $outObject.logging += $logFormatWithBlob -f $MEMORY_STRING, "Memory is null", $FAIL_STRING + $exitCode = 1 + } + elseif ($memory.SizeGB -lt $MinMemoryGB) { + UpdateReturnCode -ReturnCode 1 + $outObject.returnReason += $logFormatReturnReason -f $MEMORY_STRING + $outObject.logging += $logFormatWithUnit -f $MEMORY_STRING, $SYSTEM_MEMORY_STRING, ($memory.SizeGB), $GB_UNIT_STRING, $FAIL_STRING + $exitCode = 1 + } + else { + $outObject.logging += $logFormatWithUnit -f $MEMORY_STRING, $SYSTEM_MEMORY_STRING, ($memory.SizeGB), $GB_UNIT_STRING, $PASS_STRING + UpdateReturnCode -ReturnCode 0 + } + } + catch { + UpdateReturnCode -ReturnCode -1 + $outObject.logging += $logFormat -f $MEMORY_STRING, $SYSTEM_MEMORY_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING + $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" + $exitCode = 1 + } + + # TPM + try { + $tpm = Get-Tpm + + if ($null -eq $tpm) { + UpdateReturnCode -ReturnCode 1 + $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING + $outObject.logging += $logFormatWithBlob -f $TPM_STRING, "TPM is null", $FAIL_STRING + $exitCode = 1 + } + elseif ($tpm.TpmPresent) { + $tpmVersion = Get-CimInstance -Class Win32_Tpm -Namespace root\CIMV2\Security\MicrosoftTpm | Select-Object -Property SpecVersion + + if ($null -eq $tpmVersion.SpecVersion) { + UpdateReturnCode -ReturnCode 1 + $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING + $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, "null", $FAIL_STRING + $exitCode = 1 + } + + $majorVersion = $tpmVersion.SpecVersion.Split(",")[0] -as [int] + if ($majorVersion -lt 2) { + UpdateReturnCode -ReturnCode 1 + $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING + $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, ($tpmVersion.SpecVersion), $FAIL_STRING + $exitCode = 1 + } + else { + $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, ($tpmVersion.SpecVersion), $PASS_STRING + UpdateReturnCode -ReturnCode 0 + } + } + else { + if ($tpm.GetType().Name -eq "String") { + UpdateReturnCode -ReturnCode -1 + $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING + $outObject.logging += $logFormatException -f $tpm + } + else { + UpdateReturnCode -ReturnCode 1 + $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING + $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, ($tpm.TpmPresent), $FAIL_STRING + } + $exitCode = 1 + } + } + catch { + UpdateReturnCode -ReturnCode -1 + $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING + $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" + $exitCode = 1 + } + + # CPU Details + $cpuDetails; + try { + $cpuDetails = @(Get-CimInstance -Class Win32_Processor)[0] + + if ($null -eq $cpuDetails) { + UpdateReturnCode -ReturnCode 1 + $exitCode = 1 + $outObject.returnReason += $logFormatReturnReason -f $PROCESSOR_STRING + $outObject.logging += $logFormatWithBlob -f $PROCESSOR_STRING, "CpuDetails is null", $FAIL_STRING + } + else { + $processorCheckFailed = $false + + # AddressWidth + if ($null -eq $cpuDetails.AddressWidth -or $cpuDetails.AddressWidth -ne $RequiredAddressWidth) { + UpdateReturnCode -ReturnCode 1 + $processorCheckFailed = $true + $exitCode = 1 + } + + # ClockSpeed is in MHz + if ($null -eq $cpuDetails.MaxClockSpeed -or $cpuDetails.MaxClockSpeed -le $MinClockSpeedMHz) { + UpdateReturnCode -ReturnCode 1; + $processorCheckFailed = $true + $exitCode = 1 + } + + # Number of Logical Cores + if ($null -eq $cpuDetails.NumberOfLogicalProcessors -or $cpuDetails.NumberOfLogicalProcessors -lt $MinLogicalCores) { + UpdateReturnCode -ReturnCode 1 + $processorCheckFailed = $true + $exitCode = 1 + } + + # CPU Family + Add-Type -TypeDefinition $Source + $cpuFamilyResult = [CpuFamily]::Validate([String]$cpuDetails.Manufacturer, [uint16]$cpuDetails.Architecture) + + $cpuDetailsLog = "{AddressWidth=$($cpuDetails.AddressWidth); MaxClockSpeed=$($cpuDetails.MaxClockSpeed); NumberOfLogicalCores=$($cpuDetails.NumberOfLogicalProcessors); Manufacturer=$($cpuDetails.Manufacturer); Caption=$($cpuDetails.Caption); $($cpuFamilyResult.Message)}" + + if (!$cpuFamilyResult.IsValid) { + UpdateReturnCode -ReturnCode 1 + $processorCheckFailed = $true + $exitCode = 1 + } + + if ($processorCheckFailed) { + $outObject.returnReason += $logFormatReturnReason -f $PROCESSOR_STRING + $outObject.logging += $logFormatWithBlob -f $PROCESSOR_STRING, ($cpuDetailsLog), $FAIL_STRING + } + else { + $outObject.logging += $logFormatWithBlob -f $PROCESSOR_STRING, ($cpuDetailsLog), $PASS_STRING + UpdateReturnCode -ReturnCode 0 + } + } + } + catch { + UpdateReturnCode -ReturnCode -1 + $outObject.logging += $logFormat -f $PROCESSOR_STRING, $PROCESSOR_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING + $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" + $exitCode = 1 + } + + # SecureBoot + try { + $isSecureBootEnabled = Confirm-SecureBootUEFI + $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $CAPABLE_STRING, $PASS_STRING + UpdateReturnCode -ReturnCode 0 + } + catch [System.PlatformNotSupportedException] { + # PlatformNotSupportedException "Cmdlet not supported on this platform." - SecureBoot is not supported or is non-UEFI computer. + UpdateReturnCode -ReturnCode 1 + $outObject.returnReason += $logFormatReturnReason -f $SECUREBOOT_STRING + $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $NOT_CAPABLE_STRING, $FAIL_STRING + $exitCode = 1 + } + catch [System.UnauthorizedAccessException] { + UpdateReturnCode -ReturnCode -1 + $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING + $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" + $exitCode = 1 + } + catch { + UpdateReturnCode -ReturnCode -1 + $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING + $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" + $exitCode = 1 + } + + # i7-7820hq CPU + try { + $supportedDevices = @('surface studio 2', 'precision 5520') + $systemInfo = @(Get-CimInstance -Class Win32_ComputerSystem)[0] + + if ($null -ne $cpuDetails) { + if ($cpuDetails.Name -match 'i7-7820hq cpu @ 2.90ghz') { + $modelOrSKUCheckLog = $systemInfo.Model.Trim() + if ($supportedDevices -contains $modelOrSKUCheckLog) { + $outObject.logging += $logFormatWithBlob -f $I7_7820HQ_CPU_STRING, $modelOrSKUCheckLog, $PASS_STRING + $outObject.returnCode = 0 + $exitCode = 0 + } + } + } + } + catch { + if ($outObject.returnCode -ne 0) { + UpdateReturnCode -ReturnCode -1 + $outObject.logging += $logFormatWithBlob -f $I7_7820HQ_CPU_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING + $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" + $exitCode = 1 + } + } + + Switch ($outObject.returnCode) { + + 0 { $outObject.returnResult = $CAPABLE_CAPS_STRING } + 1 { $outObject.returnResult = $NOT_CAPABLE_CAPS_STRING } + -1 { $outObject.returnResult = $UNDETERMINED_CAPS_STRING } + -2 { $outObject.returnResult = $FAILED_TO_RUN_STRING } + } + + $outObject | ConvertTo-Json -Compress + } + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + try { + $ErrorActionPreference = "Stop" + + Write-Host -Object "Verifying Windows 11 compatibility." + $Result = Get-HardwareReadiness | Select-Object -Unique | ConvertFrom-Json + Write-Host -Object "Successfully retrieved Windows 11 compatibility results.`n" + + $ErrorActionPreference = "Continue" + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to retrieve Windows 11 compatibility results." + exit 1 + } + + switch ($Result.returnCode) { + 0 { + $ResultString = "Capable" + } + 1 { + $ResultString = "[Alert] Not Capable" + } + -2 { + $ResultString = "[Error] Failed To Run" + $ExitCode = 1 + } + default { + $ResultString = "[Error] Undetermined" + $ExitCode = 1 + } + } + + if ($Result.returnReason) { + $ResultString = "$ResultString - $($Result.returnReason)" + $ResultString = $ResultString -replace ",\s*$" + } + + if ($CustomFieldName) { + Write-Host "" + Write-Host "Note: Custom field '$CustomFieldName' was specified but NinjaOne integration has been removed." + Write-Host "All output has been displayed above." + } + + # Print Return Result + Write-Host -Object "Result: $ResultString" + exit $ExitCode +} +end { + +} + diff --git a/Powershell Scripts/Check for Brute Force login attempts.ps1 b/Powershell Scripts/Check for Brute Force login attempts.ps1 index 0fbba53..41f0a6c 100644 --- a/Powershell Scripts/Check for Brute Force login attempts.ps1 +++ b/Powershell Scripts/Check for Brute Force login attempts.ps1 @@ -1,148 +1,146 @@ # Condition for helping detect brute force login attempts. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Condition for helping detect brute force login attempts. -.DESCRIPTION - Condition for helping detect brute force login attempts. -.EXAMPLE - -Hours 10 - Number of hours back in time to look through in the event log. - Default is 1 hour. -.EXAMPLE - -Attempts 100 - Number of login attempts to trigger at or above this number. - Default is 8 attempts. -.OUTPUTS - PSCustomObject[] -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Renamed script, added Script Variable support, added more verbose output. -#> - -[CmdletBinding()] -param ( - [Parameter()] - [int]$Hours = 1, - [Parameter()] - [int]$Attempts = 8 -) - -begin { - if ($env:hours -and $env:hours -notlike "null") { - $Hours = $env:hours - } - - if ($env:attempts -and $env:attempts -notlike "null") { - $Attempts = $env:attempts - } - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } -} -process { - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - if ($(auditpol.exe /get /category:* | Where-Object { $_ -like "*Logon*Success and Failure" })) { - Write-Information "Audit Policy for Logon is set to: Success and Failure" - } - else { - Write-Error "Audit Policy for Logon is NOT set to: Success and Failure" - exit 1 - # Write-Host "Setting Logon to: Success and Failure" - # auditpol.exe /set /subcategory:"Logon" /success:enable /failure:enable - # Write-Host "Future failed login attempts will be captured." - } - - $StartTime = (Get-Date).AddHours(0 - $Hours) - $EventId = 4625 - - # Get failed login attempts - try { - $Events = Get-WinEvent -FilterHashtable @{LogName = "Security"; ID = $EventId; StartTime = $StartTime } -ErrorAction Stop | ForEach-Object { - $Message = $_.Message -split [System.Environment]::NewLine - $Account = $($Message | Where-Object { $_ -Like "*Account Name:*" }) -split '\s+' | Select-Object -Last 1 - [int]$LogonType = $($Message | Where-Object { $_ -Like "Logon Type:*" }) -split '\s+' | Select-Object -Last 1 - $SourceNetworkAddress = $($Message | Where-Object { $_ -Like "*Source Network Address:*" }) -split '\s+' | Select-Object -Last 1 - [PSCustomObject]@{ - Account = $Account - LogonType = $LogonType - SourceNetworkAddress = $SourceNetworkAddress - } - } | Where-Object { $_.LogonType -in @(2, 7, 10) } - } - catch { - if ($_.Exception.Message -like "No events were found that match the specified selection criteria.") { - Write-Host "No failed logins found in the past $Hours hour(s)." - exit 0 - } - else { - Write-Error $_ - exit 1 - } - } - - # Build a list of accounts - $UsersAccounts = [System.Collections.Generic.List[String]]::new() - try { - $ErrorActionPreference = "Stop" - Get-LocalUser | Select-Object -ExpandProperty Name | ForEach-Object { $UsersAccounts.Add($_) } - $ErrorActionPreference = "Continue" - } - catch { - $NetUser = net.exe user - $( - $NetUser | Select-Object -Skip 4 | Select-Object -SkipLast 2 - # Join each line with a "," - # Replace and spaces with a "," - # Split everything by "," - ) -join ',' -replace '\s+', ',' -split ',' | - # Sort and remove any duplicates - Sort-Object -Descending -Unique | - # Filter out empty strings - Where-Object { -not [string]::IsNullOrEmpty($_) -and -not [string]::IsNullOrWhiteSpace($_) } | - ForEach-Object { - $UsersAccounts.Add($_) - } - } - $Events | Select-Object -ExpandProperty Account | ForEach-Object { $UsersAccounts.Add($_) } - - $Results = $UsersAccounts | Select-Object -Unique | ForEach-Object { - $Account = $_ - $AccountEvents = $Events | Where-Object { $_.Account -like $Account } - $AttemptCount = $AccountEvents.Count - $SourceNetworkAddress = $AccountEvents | Select-Object -ExpandProperty SourceNetworkAddress -Unique - if ($AttemptCount -gt 0) { - [PSCustomObject]@{ - Account = $Account - Attempts = $AttemptCount - SourceNetworkAddress = $SourceNetworkAddress - } - } - } - - # Get only the accounts with fail login attempts at or over $Attempts - $BruteForceAttempts = $Results | Where-Object { $_.Attempts -ge $Attempts } - if ($BruteForceAttempts) { - Write-Warning "Possible brute force attempts detected!" - $BruteForceAttempts | Out-String | Write-Host - exit 1 - } - - Write-Host "No brute force attempts were detected." - $Results | Out-String | Write-Host - - exit 0 -} -end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + Condition for helping detect brute force login attempts. +.DESCRIPTION + Condition for helping detect brute force login attempts. +.EXAMPLE + -Hours 10 + Number of hours back in time to look through in the event log. + Default is 1 hour. +.EXAMPLE + -Attempts 100 + Number of login attempts to trigger at or above this number. + Default is 8 attempts. +.OUTPUTS + PSCustomObject[] +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Renamed script, added Script Variable support, added more verbose output. +#> + +[CmdletBinding()] +param ( + [Parameter()] + [int]$Hours = 1, + [Parameter()] + [int]$Attempts = 8 +) + +begin { + if ($env:hours -and $env:hours -notlike "null") { + $Hours = $env:hours + } + + if ($env:attempts -and $env:attempts -notlike "null") { + $Attempts = $env:attempts + } + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } +} +process { + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + if ($(auditpol.exe /get /category:* | Where-Object { $_ -like "*Logon*Success and Failure" })) { + Write-Information "Audit Policy for Logon is set to: Success and Failure" + } + else { + Write-Error "Audit Policy for Logon is NOT set to: Success and Failure" + exit 1 + # Write-Host "Setting Logon to: Success and Failure" + # auditpol.exe /set /subcategory:"Logon" /success:enable /failure:enable + # Write-Host "Future failed login attempts will be captured." + } + + $StartTime = (Get-Date).AddHours(0 - $Hours) + $EventId = 4625 + + # Get failed login attempts + try { + $Events = Get-WinEvent -FilterHashtable @{LogName = "Security"; ID = $EventId; StartTime = $StartTime } -ErrorAction Stop | ForEach-Object { + $Message = $_.Message -split [System.Environment]::NewLine + $Account = $($Message | Where-Object { $_ -Like "*Account Name:*" }) -split '\s+' | Select-Object -Last 1 + [int]$LogonType = $($Message | Where-Object { $_ -Like "Logon Type:*" }) -split '\s+' | Select-Object -Last 1 + $SourceNetworkAddress = $($Message | Where-Object { $_ -Like "*Source Network Address:*" }) -split '\s+' | Select-Object -Last 1 + [PSCustomObject]@{ + Account = $Account + LogonType = $LogonType + SourceNetworkAddress = $SourceNetworkAddress + } + } | Where-Object { $_.LogonType -in @(2, 7, 10) } + } + catch { + if ($_.Exception.Message -like "No events were found that match the specified selection criteria.") { + Write-Host "No failed logins found in the past $Hours hour(s)." + exit 0 + } + else { + Write-Error $_ + exit 1 + } + } + + # Build a list of accounts + $UsersAccounts = [System.Collections.Generic.List[String]]::new() + try { + $ErrorActionPreference = "Stop" + Get-LocalUser | Select-Object -ExpandProperty Name | ForEach-Object { $UsersAccounts.Add($_) } + $ErrorActionPreference = "Continue" + } + catch { + $NetUser = net.exe user + $( + $NetUser | Select-Object -Skip 4 | Select-Object -SkipLast 2 + # Join each line with a "," + # Replace and spaces with a "," + # Split everything by "," + ) -join ',' -replace '\s+', ',' -split ',' | + # Sort and remove any duplicates + Sort-Object -Descending -Unique | + # Filter out empty strings + Where-Object { -not [string]::IsNullOrEmpty($_) -and -not [string]::IsNullOrWhiteSpace($_) } | + ForEach-Object { + $UsersAccounts.Add($_) + } + } + $Events | Select-Object -ExpandProperty Account | ForEach-Object { $UsersAccounts.Add($_) } + + $Results = $UsersAccounts | Select-Object -Unique | ForEach-Object { + $Account = $_ + $AccountEvents = $Events | Where-Object { $_.Account -like $Account } + $AttemptCount = $AccountEvents.Count + $SourceNetworkAddress = $AccountEvents | Select-Object -ExpandProperty SourceNetworkAddress -Unique + if ($AttemptCount -gt 0) { + [PSCustomObject]@{ + Account = $Account + Attempts = $AttemptCount + SourceNetworkAddress = $SourceNetworkAddress + } + } + } + + # Get only the accounts with fail login attempts at or over $Attempts + $BruteForceAttempts = $Results | Where-Object { $_.Attempts -ge $Attempts } + if ($BruteForceAttempts) { + Write-Warning "Possible brute force attempts detected!" + $BruteForceAttempts | Out-String | Write-Host + exit 1 + } + + Write-Host "No brute force attempts were detected." + $Results | Out-String | Write-Host + + exit 0 +} +end { + +} + diff --git a/Powershell Scripts/Check for Stopped Automatic Services.ps1 b/Powershell Scripts/Check for Stopped Automatic Services.ps1 index 0fa5f40..4f11cb8 100644 --- a/Powershell Scripts/Check for Stopped Automatic Services.ps1 +++ b/Powershell Scripts/Check for Stopped Automatic Services.ps1 @@ -1,208 +1,206 @@ # Reports on or starts services for Automatic Services that are not currently running. Services set as Delayed Start or Trigger Start are ignored. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Reports on or starts services for Automatic Services that are not currently running. Services set as 'Delayed Start' or 'Trigger Start' are ignored. -.DESCRIPTION - Reports on or starts services for Automatic Services that are not currently running. Services set as 'Delayed Start' or 'Trigger Start' are ignored. -.EXAMPLE - (No Parameters) - - Matching Services found! - - Name Description - ---- ----------- - SysMain Maintains and improves system performance over time. - -PARAMETER: -IgnoreServices "ExampleServiceName" - A comma separated list of service names to ignore. - -PARAMETER: -StartFoundServices - Attempts to start any services found matching the criteria. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$IgnoreServices, - [Parameter()] - [Switch]$StartFoundServices = [System.Convert]::ToBoolean($env:startFoundServices) -) - -begin { - # Replace script parameters with form variables - if($env:servicesToExclude -and $env:servicesToExclude -notlike "null"){ $IgnoreServices = $env:servicesToExclude } - - # Get the last startup time of the operating system. - $LastBootDateTime = Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -ExpandProperty LastBootUpTime - if ($LastBootDateTime -gt $(Get-Date).AddMinutes(-15)) { - $Uptime = New-TimeSpan $LastBootDateTime (Get-Date) | Select-Object -ExpandProperty TotalMinutes - Write-Host "Current uptime is $([math]::Round($Uptime)) minutes." - Write-Host "[Error] Please wait at least 15 minutes after startup before running this script." - exit 1 - } - - # Define a function to test if the current user has elevated (administrator) privileges. - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - $ExitCode = 0 -} -process { - # Check if the script is running with Administrator privileges. - if (!(Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Define a string of characters that are invalid for service names. - $InvalidServiceNameCharacters = "\\|/|:" - # Create a list to hold the names of services to ignore. - $ServicesToIgnore = New-Object System.Collections.Generic.List[string] - - # If there are services to ignore and they are separated by commas, split the string into individual service names. - if ($IgnoreServices -and $IgnoreServices -match ",") { - $IgnoreServices -split "," | ForEach-Object { - # Check each service name for invalid characters or excessive length. - if ($_.Trim() -match $InvalidServiceNameCharacters) { - Write-Host "[Error] Service Name contains one of the invalid characters '\/:'. $_ is not a valid service to ignore." - $ExitCode = 1 - return - } - - if (($_.Trim()).Length -gt 256) { - Write-Host "[Error] Service Name is greater than 256 characters. $_ is not a valid service to ignore. " - $ExitCode = 1 - return - } - - # Add valid services to the ignore list. - $ServicesToIgnore.Add($_.Trim()) - } - } - elseif ($IgnoreServices) { - # For a single service name, perform similar validation and add if valid. - $ValidService = $True - - if ($IgnoreServices.Trim() -match $InvalidServiceNameCharacters) { - Write-Host "[Error] Service Name contains one of the invalid characters '\/:'. '$IgnoreServices' is not a valid service to ignore. " - $ExitCode = 1 - $ValidService = $False - } - - if (($IgnoreServices.Trim()).Length -gt 256) { - Write-Host "[Error] Service Name is greater than 256 characters. '$IgnoreServices' is not a valid service to ignore. " - $ExitCode = 1 - $ValidService = $False - } - - if ($ValidService) { - $ServicesToIgnore.Add($IgnoreServices.Trim()) - } - } - - # Create a list to hold non-running services that are set to start automatically. - $NonRunningAutoServices = New-Object System.Collections.Generic.List[object] - Get-Service | Where-Object { $_.StartType -like "Automatic" -and $_.Status -ne "Running" } | ForEach-Object { - $NonRunningAutoServices.Add($_) - } - - # Remove services from the list that have triggers or are set to delayed start, - if ($NonRunningAutoServices.Count -gt 0) { - $TriggerServices = Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\*\*" -ErrorAction SilentlyContinue | Where-Object { $_.Name -match "TriggerInfo" } - $TriggerServices = $TriggerServices | Select-Object -ExpandProperty PSParentPath | Split-Path -Leaf - foreach ($TriggerService in $TriggerServices) { - $NonRunningAutoServices.Remove(($NonRunningAutoServices | Where-Object { $_.ServiceName -match $TriggerService })) | Out-Null - } - - $DelayedStartServices = Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\*" | Where-Object { $_.DelayedAutoStart -eq 1 } - $DelayedStartServices = $DelayedStartServices | Select-Object -ExpandProperty PSChildName - foreach ($DelayedStartService in $DelayedStartServices) { - $NonRunningAutoServices.Remove(($NonRunningAutoServices | Where-Object { $_.ServiceName -match $DelayedStartService })) | Out-Null - } - } - - # Remove explicitly ignored services from the list of non-running automatic services. - if ($ServicesToIgnore.Count -gt 0 -and $NonRunningAutoServices.Count -gt 0) { - foreach ($ServiceToIgnore in $ServicesToIgnore) { - if ($NonRunningAutoServices.ServiceName -contains $ServiceToIgnore) { - $NonRunningAutoServices.Remove(($NonRunningAutoServices | Where-Object { $_.ServiceName -match [Regex]::Escape($ServiceToIgnore) })) | Out-Null - } - } - } - - # If there are still non-running automatic services left, display their names. - # Otherwise, indicate no stopped automatic services were detected. - if ($NonRunningAutoServices.Count -gt 0) { - Write-Host "Matching Services found!" - - # Add Description to report. - $ServicesReport = New-Object System.Collections.Generic.List[object] - $NonRunningAutoServices | ForEach-Object { - $Description = Get-CimInstance -ClassName Win32_Service -Filter "Name = '$($_.ServiceName)'" | Select-Object @{ - Name = "Description" - Expression = { - $Characters = $_.Description | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -gt 100) { - "$(($_.Description).SubString(0,100))..." - } - else { - $_.Description - } - } - } - $ServicesReport.Add( - [PSCustomObject]@{ - Name = $_.ServiceName - Description = $Description | Select-Object -ExpandProperty Description - } - ) - } - - # Output report to activity log. - $ServicesReport | Sort-Object Name | Format-Table -Property Name,Description -AutoSize | Out-String | Write-Host - } - else { - Write-Host "No stopped automatic services detected!" - } - - # Exit the script if there are no services to start or if starting services is not requested. - if (!$StartFoundServices -or !($NonRunningAutoServices.Count -gt 0)) { - exit $ExitCode - } - - # Attempt to start each non-running automatic service up to three times. - # Log success or error messages accordingly. - $NonRunningAutoServices | ForEach-Object { - Write-Host "`nAttempting to start $($_.ServiceName)." - $Attempt = 1 - while ($Attempt -le 3) { - Write-Host -Object "Attempt: $Attempt" - try { - $_ | Start-Service -ErrorAction Stop - Write-Host -Object "Successfully started $($_.ServiceName)." - $Attempt = 4 - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - if ($Attempt -eq 3) { $ExitCode = 1 } - } - $Attempt++ - } - } - - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Reports on or starts services for Automatic Services that are not currently running. Services set as 'Delayed Start' or 'Trigger Start' are ignored. +.DESCRIPTION + Reports on or starts services for Automatic Services that are not currently running. Services set as 'Delayed Start' or 'Trigger Start' are ignored. +.EXAMPLE + (No Parameters) + + Matching Services found! + + Name Description + ---- ----------- + SysMain Maintains and improves system performance over time. + +PARAMETER: -IgnoreServices "ExampleServiceName" + A comma separated list of service names to ignore. + +PARAMETER: -StartFoundServices + Attempts to start any services found matching the criteria. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$IgnoreServices, + [Parameter()] + [Switch]$StartFoundServices = [System.Convert]::ToBoolean($env:startFoundServices) +) + +begin { + # Replace script parameters with form variables + if($env:servicesToExclude -and $env:servicesToExclude -notlike "null"){ $IgnoreServices = $env:servicesToExclude } + + # Get the last startup time of the operating system. + $LastBootDateTime = Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -ExpandProperty LastBootUpTime + if ($LastBootDateTime -gt $(Get-Date).AddMinutes(-15)) { + $Uptime = New-TimeSpan $LastBootDateTime (Get-Date) | Select-Object -ExpandProperty TotalMinutes + Write-Host "Current uptime is $([math]::Round($Uptime)) minutes." + Write-Host "[Error] Please wait at least 15 minutes after startup before running this script." + exit 1 + } + + # Define a function to test if the current user has elevated (administrator) privileges. + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + $ExitCode = 0 +} +process { + # Check if the script is running with Administrator privileges. + if (!(Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Define a string of characters that are invalid for service names. + $InvalidServiceNameCharacters = "\\|/|:" + # Create a list to hold the names of services to ignore. + $ServicesToIgnore = New-Object System.Collections.Generic.List[string] + + # If there are services to ignore and they are separated by commas, split the string into individual service names. + if ($IgnoreServices -and $IgnoreServices -match ",") { + $IgnoreServices -split "," | ForEach-Object { + # Check each service name for invalid characters or excessive length. + if ($_.Trim() -match $InvalidServiceNameCharacters) { + Write-Host "[Error] Service Name contains one of the invalid characters '\/:'. $_ is not a valid service to ignore." + $ExitCode = 1 + return + } + + if (($_.Trim()).Length -gt 256) { + Write-Host "[Error] Service Name is greater than 256 characters. $_ is not a valid service to ignore. " + $ExitCode = 1 + return + } + + # Add valid services to the ignore list. + $ServicesToIgnore.Add($_.Trim()) + } + } + elseif ($IgnoreServices) { + # For a single service name, perform similar validation and add if valid. + $ValidService = $True + + if ($IgnoreServices.Trim() -match $InvalidServiceNameCharacters) { + Write-Host "[Error] Service Name contains one of the invalid characters '\/:'. '$IgnoreServices' is not a valid service to ignore. " + $ExitCode = 1 + $ValidService = $False + } + + if (($IgnoreServices.Trim()).Length -gt 256) { + Write-Host "[Error] Service Name is greater than 256 characters. '$IgnoreServices' is not a valid service to ignore. " + $ExitCode = 1 + $ValidService = $False + } + + if ($ValidService) { + $ServicesToIgnore.Add($IgnoreServices.Trim()) + } + } + + # Create a list to hold non-running services that are set to start automatically. + $NonRunningAutoServices = New-Object System.Collections.Generic.List[object] + Get-Service | Where-Object { $_.StartType -like "Automatic" -and $_.Status -ne "Running" } | ForEach-Object { + $NonRunningAutoServices.Add($_) + } + + # Remove services from the list that have triggers or are set to delayed start, + if ($NonRunningAutoServices.Count -gt 0) { + $TriggerServices = Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\*\*" -ErrorAction SilentlyContinue | Where-Object { $_.Name -match "TriggerInfo" } + $TriggerServices = $TriggerServices | Select-Object -ExpandProperty PSParentPath | Split-Path -Leaf + foreach ($TriggerService in $TriggerServices) { + $NonRunningAutoServices.Remove(($NonRunningAutoServices | Where-Object { $_.ServiceName -match $TriggerService })) | Out-Null + } + + $DelayedStartServices = Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\*" | Where-Object { $_.DelayedAutoStart -eq 1 } + $DelayedStartServices = $DelayedStartServices | Select-Object -ExpandProperty PSChildName + foreach ($DelayedStartService in $DelayedStartServices) { + $NonRunningAutoServices.Remove(($NonRunningAutoServices | Where-Object { $_.ServiceName -match $DelayedStartService })) | Out-Null + } + } + + # Remove explicitly ignored services from the list of non-running automatic services. + if ($ServicesToIgnore.Count -gt 0 -and $NonRunningAutoServices.Count -gt 0) { + foreach ($ServiceToIgnore in $ServicesToIgnore) { + if ($NonRunningAutoServices.ServiceName -contains $ServiceToIgnore) { + $NonRunningAutoServices.Remove(($NonRunningAutoServices | Where-Object { $_.ServiceName -match [Regex]::Escape($ServiceToIgnore) })) | Out-Null + } + } + } + + # If there are still non-running automatic services left, display their names. + # Otherwise, indicate no stopped automatic services were detected. + if ($NonRunningAutoServices.Count -gt 0) { + Write-Host "Matching Services found!" + + # Add Description to report. + $ServicesReport = New-Object System.Collections.Generic.List[object] + $NonRunningAutoServices | ForEach-Object { + $Description = Get-CimInstance -ClassName Win32_Service -Filter "Name = '$($_.ServiceName)'" | Select-Object @{ + Name = "Description" + Expression = { + $Characters = $_.Description | Measure-Object -Character | Select-Object -ExpandProperty Characters + if ($Characters -gt 100) { + "$(($_.Description).SubString(0,100))..." + } + else { + $_.Description + } + } + } + $ServicesReport.Add( + [PSCustomObject]@{ + Name = $_.ServiceName + Description = $Description | Select-Object -ExpandProperty Description + } + ) + } + + # Output report to activity log. + $ServicesReport | Sort-Object Name | Format-Table -Property Name,Description -AutoSize | Out-String | Write-Host + } + else { + Write-Host "No stopped automatic services detected!" + } + + # Exit the script if there are no services to start or if starting services is not requested. + if (!$StartFoundServices -or !($NonRunningAutoServices.Count -gt 0)) { + exit $ExitCode + } + + # Attempt to start each non-running automatic service up to three times. + # Log success or error messages accordingly. + $NonRunningAutoServices | ForEach-Object { + Write-Host "`nAttempting to start $($_.ServiceName)." + $Attempt = 1 + while ($Attempt -le 3) { + Write-Host -Object "Attempt: $Attempt" + try { + $_ | Start-Service -ErrorAction Stop + Write-Host -Object "Successfully started $($_.ServiceName)." + $Attempt = 4 + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + if ($Attempt -eq 3) { $ExitCode = 1 } + } + $Attempt++ + } + } + + exit $ExitCode +} +end { + +} diff --git a/Powershell Scripts/Chocolatey Install, Upgrade and Uninstall.ps1 b/Powershell Scripts/Chocolatey Install, Upgrade and Uninstall.ps1 index 54265e8..5ca2a4f 100644 --- a/Powershell Scripts/Chocolatey Install, Upgrade and Uninstall.ps1 +++ b/Powershell Scripts/Chocolatey Install, Upgrade and Uninstall.ps1 @@ -1,583 +1,579 @@ # This script allows you to install, uninstall, or upgrade an application using Chocolatey. If Chocolatey is not installed or is outdated, options are available to install or upgrade it before proceeding with the application action. -#Requires -Version 4.0 - -<# -.SYNOPSIS - This script allows you to install, uninstall, or upgrade an application using Chocolatey. If Chocolatey is not installed or is outdated, options are available to install or upgrade it before proceeding with the application action. -.DESCRIPTION - This script allows you to install, uninstall, or upgrade an application using Chocolatey. If Chocolatey is not installed or is outdated, options are available to install or upgrade it before proceeding with the application action. -.EXAMPLE - -Action "Install" -Name "vlc" -InstallChocolateyIfMissing -SkipSleep - Chocolatey is not installed. - Downloading Chocolatey's install script and installing. - URL 'https://community.chocolatey.org/install.ps1' was given. - Downloading the file... - Download Attempt 1 - Forcing web requests to allow TLS v1.2 (Required for requests to Chocolatey.org) - Getting latest version of the Chocolatey package for download. - Not using proxy. - Getting Chocolatey from https://community.chocolatey.org/api/v2/package/chocolatey/2.3.0. - Downloading https://community.chocolatey.org/api/v2/package/chocolatey/2.3.0 to C:\Windows\TEMP\chocolatey\chocoInstall\chocolatey.zip - Not using proxy. - Extracting C:\Windows\TEMP\chocolatey\chocoInstall\chocolatey.zip to C:\Windows\TEMP\chocolatey\chocoInstall - Downloading 7-Zip commandline tool prior to extraction. - Downloading https://community.chocolatey.org/7za.exe to C:\Windows\TEMP\chocolatey\chocoInstall\7za.exe - Not using proxy. - Installing Chocolatey on the local machine - WARNING: It's very likely you will need to close and reopen your shell - before you can use choco. - PATH environment variable does not have C:\ProgramData\chocolatey\bin in it. Adding... - WARNING: Not setting tab completion: Current user is SYSTEM user. - Ensuring Chocolatey commands are on the path - Ensuring chocolatey.nupkg is in the lib folder - Creating ChocolateyInstall as an environment variable (targeting 'Machine') - Setting ChocolateyInstall to 'C:\ProgramData\chocolatey' - Restricting write permissions to Administrators - We are setting up the Chocolatey package repository. - The packages themselves go to 'C:\ProgramData\chocolatey\lib' - (i.e. C:\ProgramData\chocolatey\lib\yourPackageName). - A shim file for the command line goes to 'C:\ProgramData\chocolatey\bin' - and points to an executable in 'C:\ProgramData\chocolatey\lib\yourPackageName'. - - Creating Chocolatey CLI folders if they do not already exist. - - chocolatey.nupkg file not installed in lib. - Attempting to locate it from bootstrapper. - Chocolatey CLI (choco.exe) is now ready. - You can call choco from anywhere, command line or powershell by typing choco. - Run choco /? for a list of functions. - You may need to shut down and restart powershell and/or consoles - first prior to using choco. - Installing the following packages: - vlc - By installing, you accept licenses for the packages. - Downloading package from source 'https://community.chocolatey.org/api/v2/' - - chocolatey-compatibility.extension v1.0.0 [Approved] - chocolatey-compatibility.extension package files install completed. Performing other installation steps. - Installed/updated chocolatey-compatibility extensions. - The install of chocolatey-compatibility.extension was successful. - Deployed to 'C:\ProgramData\chocolatey\extensions\chocolatey-compatibility' - Downloading package from source 'https://community.chocolatey.org/api/v2/' - - chocolatey-core.extension v1.4.0 [Approved] - chocolatey-core.extension package files install completed. Performing other installation steps. - Installed/updated chocolatey-core extensions. - The install of chocolatey-core.extension was successful. - Deployed to 'C:\ProgramData\chocolatey\extensions\chocolatey-core' - Downloading package from source 'https://community.chocolatey.org/api/v2/' - - vlc.install v3.0.21 [Approved] - vlc.install package files install completed. Performing other installation steps. - Installing 64-bit vlc.install... - vlc.install has been installed. - WARNING: No registry key found based on 'vlc.install' - WARNING: Can't find vlc.install install location - vlc.install may be able to be automatically uninstalled. - The install of vlc.install was successful. - Deployed to 'C:\Program Files\VideoLAN\VLC' - Downloading package from source 'https://community.chocolatey.org/api/v2/' - - vlc v3.0.21 [Approved] - vlc package files install completed. Performing other installation steps. - The install of vlc was successful. - Deployed to 'C:\ProgramData\chocolatey\lib\vlc' - - Chocolatey installed 4/4 packages. - See the log for details (C:\ProgramData\chocolatey\logs\chocolatey.log). - Exit Code: 0 - Successfully completed the action 'Install' for package 'vlc'. - -PARAMETER: -Action "ReplaceMeWithValidAction" - Valid actions are 'Install', 'Upgrade', or 'Uninstall' for your desired package. - -PARAMETER: -Name "NameOfApplication" - Name of the application you would like to uninstall, upgrade, or install. - https://community.chocolatey.org/packages is a good resource to find this. - -PARAMETER: -Version "DesiredVersion" - Optionally, specify a version to install. - -PARAMETER: -AllowDowngrades - Allows downgrading existing installations to the specified version. - -PARAMETER: -InstallChocolateyIfMissing - If Chocolatey isn't installed, this option installs it before starting your action. - -PARAMETER: -UpgradeChocolatey - If an update for Chocolatey itself is available, this option upgrades it to the latest version. - -PARAMETER: -SkipSleep - The script waits for a random interval between 1 and 15 minutes before performing an action with Chocolatey to help avoid rate limiting. - Use this option to skip the wait. For more information, see https://docs.chocolatey.org/en-us/community-repository/community-packages-disclaimer#excessive-use. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2012 R2 - Release Notes: Updated functions, removed writing to the error stream, removed update environment variables, added signature validation, updated comments, added the option to specify a version, and added data validation. -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$Action, - [Parameter()] - [String]$Name, - [Parameter()] - [String]$Version, - [Parameter()] - [Switch]$AllowDowngrades = [System.Convert]::ToBoolean($env:allowDowngrades), - [Parameter()] - [Switch]$InstallChocolateyIfMissing = [System.Convert]::ToBoolean($env:installChocolateyIfNecessary), - [Parameter()] - [Switch]$UpgradeChocolatey = [System.Convert]::ToBoolean($env:upgradeChocolatey), - [Parameter()] - [Switch]$SkipSleep = [System.Convert]::ToBoolean($env:skipSleep) -) -# Helper functions and input validation -begin { - # URL to Chocolatey installation script. Feel free to replace this with your own link. - $InstallUri = "https://community.chocolatey.org/install.ps1" - - # If script form variables are used, replace the command line parameters with their value. - if ($env:action -and $env:action -notlike "null") { $Action = $env:action } - if ($env:packageName -and $env:packageName -notlike "null") { $Name = $env:packageName } - if ($env:version -and $env:version -notlike "null") { $Version = $env:version } - - # Trim whitespace from the action if it's defined - if ($Action) { - $Action = $Action.Trim() - } - - # Trim whitespace from the package name if it's defined - if ($Name) { - $Name = $Name.Trim() - } - - # Trim whitespace from the version if it's defined - if ($Version) { - $Version = $Version.Trim() - } - - # Ensure that both a package name and action are provided - # If not, display an error message and exit with status code 1 - if (!($Name) -or !($Action)) { - Write-Host -Object "[Error] You must provide a valid package name and action." - exit 1 - } - - # Validate the package name format; it should only contain lowercase letters, hyphens, and dots - if ($Name -cmatch "[^a-z0-9.-]") { - Write-Host -Object "[Error] An invalid package name '$Name' was given. Chocolatey package names can only contain lowercase letters, numbers, hyphens, and dots." - Write-Host -Object "[Error] https://docs.chocolatey.org/en-us/create/create-packages/#naming-your-package" - exit 1 - } - - # Define a list of valid actions - $ValidActions = "Install", "Upgrade", "Uninstall" - - # Check if the action is in the list of valid actions - # If not, display an error message and exit with status code 1 - if ($ValidActions -notcontains $Action) { - Write-Host -Object "[Error] An invalid action '$Action' was given. Only the following actions are supported: 'Install', 'Uninstall', 'Upgrade'." - exit 1 - } - - # Check if the name is "All" and the action is not "Upgrade" - # Display an error message and exit if an attempt is made to install or uninstall all packages at once - if ($Name -like "All" -and $Action -ne "Upgrade") { - Write-Host -Object "[Error] Installing or uninstalling all packages at once is not supported!" - exit 1 - } - - # Check if a specific version is provided but the action is not "Install". - if ($Version -and $Action -ne "Install") { - Write-Host -Object "[Error] To install a specific version, you must specify 'Install', even if you're changing the version of an existing application." - exit 1 - } - - # Validate the format of the version number. - # If the version contains characters other than numbers or dots, print an error message and a reference URL, then exit. - if ($Version -match "[^0-9.]") { - Write-Host -Object "[Error] An invalid version '$Version' was given. Chocolatey version numbers can only contain numbers and dots." - Write-Host -Object "[Error] https://docs.chocolatey.org/en-us/create/create-packages/#versioning-recommendations" - exit 1 - } - - # Check if the user has allowed downgrades without specifying a version. - # Print an error message and exit if downgrades are allowed but no version is specified. - if ($AllowDowngrades -and !$Version) { - Write-Host -Object "[Error] You must specify a version to allow downgrades to an older version." - exit 1 - } - - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - return $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - if (!(Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Local Administrator privileges or run as SYSTEM. https://ninjarmm.zendesk.com/hc/en-us/articles/360016094532-Credential-Exchange" - exit 1 - } - - function Test-ChocolateyInstalled { - [CmdletBinding()] - param() - - # Try to retrieve the 'choco' command. If it exists, assign it to $Command, suppressing errors. - $Command = Get-Command choco -ErrorAction SilentlyContinue - - # Check if the 'choco' command path is found and if it exists on the filesystem - if ($Command.Path -and (Test-Path -Path $Command.Path -ErrorAction SilentlyContinue)) { - return $true - } - - # If 'choco' command was not found, check if 'Chocolatey\bin' is in the system PATH - if (([Environment]::GetEnvironmentVariable('Path', [System.EnvironmentVariableTarget]::Machine)) -like "*chocolatey\bin*") { - # Update the current session's PATH with the system PATH containing 'Chocolatey\bin' - $env:Path = [Environment]::GetEnvironmentVariable('Path', [System.EnvironmentVariableTarget]::Machine); - - # Re-check for the 'choco' command after updating the PATH - $Command = Get-Command choco -ErrorAction SilentlyContinue - if ($Command.Path -and (Test-Path -Path $Command.Path -ErrorAction SilentlyContinue)) { - return $true - } - else { - return $false - } - } - - # Check if the 'ChocolateyInstall' environment variable is set - # Verify if 'choco' exists in the 'ChocolateyInstall\bin' directory - if ($env:ChocolateyInstall -and (Test-Path -Path "$env:ChocolateyInstall\bin\choco" -ErrorAction SilentlyContinue)) { - # Update the current session's PATH with the Chocolatey installation path - $Env:Path = "$Env:Path;$env:ChocolateyInstall\bin" - return $true - } - - # As a last check, look for 'choco' in the default ProgramData path for Chocolatey - if (Test-Path -Path "$env:ProgramData\chocolatey\bin\choco" -ErrorAction SilentlyContinue) { - # Update the PATH to include the default Chocolatey ProgramData path - $Env:Path = "$Env:Path;$env:ChocolateyInstall\bin" - return $true - } - } - - function Test-ChocolateyInstallVariable { - [CmdletBinding()] - param() - - # Check if the 'ChocolateyInstall' environment variable is set - if ($env:ChocolateyInstall) { - return $True - } - - # Check if the 'ChocolateyInstall' environment variable is set at the machine level. - if (([Environment]::GetEnvironmentVariable('ChocolateyInstall', [System.EnvironmentVariableTarget]::Machine))) { - $env:ChocolateyInstall = [Environment]::GetEnvironmentVariable('ChocolateyInstall', [System.EnvironmentVariableTarget]::Machine); - return $true - } - - # Try to retrieve the 'choco' command. If it exists, assign it to $Command, suppressing errors. - $Command = Get-Command choco -ErrorAction SilentlyContinue - - # Verify that the 'choco' command path exists, is valid, and matches the typical path pattern for Chocolatey installations. - if ($Command.Path -and (Test-Path -Path $Command.Path -ErrorAction SilentlyContinue) -and $Command.Path -like "*\bin\choco.exe") { - # Set the 'ChocolateyInstall' environment variable based on the retrieved path. - $env:ChocolateyInstall = $Command.Path -replace "\\bin\\choco.exe.*" - return $true - } - - # As a last check, look for 'choco' in the default ProgramData path for Chocolatey - if (Test-Path -Path "$env:ProgramData\chocolatey\bin\choco.exe" -ErrorAction SilentlyContinue) { - # Update the ChocolateyInstall variable to include the default Chocolatey ProgramData path - $env:ChocolateyInstall = "$env:ProgramData\chocolatey" - $Env:Path = "$Env:Path;$env:ChocolateyInstall\bin" - return $true - } - } - - # Utility function for downloading files. - function Invoke-Download { - param( - [Parameter()] - [String]$URL, - [Parameter()] - [String]$Path, - [Parameter()] - [int]$Attempts = 3, - [Parameter()] - [Switch]$SkipSleep - ) - - # Display the URL being used for the download - Write-Host -Object "URL '$URL' was given." - Write-Host -Object "Downloading the file..." - - # Initialize the attempt counter - $i = 1 - While ($i -le $Attempts) { - # If SkipSleep is not set, wait for a random time between 3 and 15 seconds before each attempt - if (!($SkipSleep)) { - $SleepTime = Get-Random -Minimum 3 -Maximum 15 - Write-Host "Waiting for $SleepTime seconds." - Start-Sleep -Seconds $SleepTime - } - - # Provide a visual break between attempts - if ($i -ne 1) { Write-Host "" } - Write-Host "Download Attempt $i" - - # Temporarily disable progress reporting to speed up script performance - $PreviousProgressPreference = $ProgressPreference - $ProgressPreference = 'SilentlyContinue' - try { - if ($PSVersionTable.PSVersion.Major -lt 4) { - # For older versions of PowerShell, use WebClient to download the file - $WebClient = New-Object System.Net.WebClient - $WebClient.DownloadFile($URL, $Path) - } - else { - # For PowerShell 4.0 and above, use Invoke-WebRequest with specified arguments - $WebRequestArgs = @{ - Uri = $URL - OutFile = $Path - MaximumRedirection = 10 - UseBasicParsing = $true - } - - Invoke-WebRequest @WebRequestArgs - } - - # Verify if the file was successfully downloaded - $File = Test-Path -Path $Path -ErrorAction SilentlyContinue - } - catch { - # Handle any errors that occur during the download attempt - Write-Warning "An error has occurred while downloading!" - Write-Warning $_.Exception.Message - - # If the file partially downloaded, delete it to avoid corruption - if (Test-Path -Path $Path -ErrorAction SilentlyContinue) { - Remove-Item $Path -Force -Confirm:$false -ErrorAction SilentlyContinue - } - - $File = $False - } - - # Restore the original progress preference setting - $ProgressPreference = $PreviousProgressPreference - # If the file was successfully downloaded, exit the loop - if ($File) { - $i = $Attempts - } - else { - # Warn the user if the download attempt failed - Write-Warning "File failed to download." - Write-Host "" - } - - # Increment the attempt counter - $i++ - } - - # Final check: if the file still doesn't exist, report an error and exit - if (!(Test-Path $Path)) { - Write-Host -Object "[Error] Failed to download file." - Write-Host -Object "Please verify the URL of '$URL'." - exit 1 - } - else { - # If the download succeeded, return the path to the downloaded file - return $Path - } - } - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # Check if Chocolatey is installed and if InstallChocolateyIfMissing is false - # If Chocolatey is not installed and installation is not allowed, exit with an error - if (!$(Test-ChocolateyInstalled) -and !$InstallChocolateyIfMissing) { - Write-Host -Object "[Error] Install Chocolatey If Necessary is not selected and chocolatey was not installed. Unable to continue." - exit 1 - } - - # Determine the supported TLS versions and set the appropriate security protocol - $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType') - if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12 - } - elseif ( $SupportedTLSversions -contains 'Tls12' ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 - } - else { - # Warn the user if TLS 1.2 and 1.3 are not supported, which may cause the script to fail - Write-Warning "TLS 1.2 and/or TLS 1.3 are not supported on this system. This script may fail. https://blog.chocolatey.org/2020/01/remove-support-for-old-tls-versions/" - if ($PSVersionTable.PSVersion.Major -lt 3) { - Write-Warning "PowerShell 2 / .NET 2.0 doesn't support TLS 1.2." - } - } - - if (!($SkipSleep)) { - $SleepTime = Get-Random -Minimum 60 -Maximum 900 - $SleepTimeMinutes = [math]::Round($($SleepTime / 60)) - Write-Host "Waiting for $SleepTimeMinutes minutes." - Start-Sleep -Seconds $SleepTime - } - - # Check if Chocolatey is not installed but installation is allowed - if (!$(Test-ChocolateyInstalled) -and $InstallChocolateyIfMissing) { - Write-Host "Chocolatey is not installed." - Write-Host "Downloading Chocolatey's install script and installing." - - # Define download arguments for Chocolatey installation script - $DownloadArguments = @{ - Path = "$env:TEMP\install.ps1" - URL = $InstallUri - } - - # Optionally add SkipSleep to download arguments if specified - if ($SkipSleep) { $DownloadArguments["SkipSleep"] = $True } - - # Download and create the Chocolatey installation script - $ChocolateyScriptFilePath = Invoke-Download @DownloadArguments - - # Validating signature of the installation script - $ScriptSignature = Get-AuthenticodeSignature -FilePath $ChocolateyScriptFilePath -ErrorAction SilentlyContinue - if (!$ScriptSignature) { - Write-Host -Object "[Error] A signature was not found on the script file." - exit 1 - } - if ($ScriptSignature.Status -ne "Valid" -and $ScriptSignature.SignerCertificate.Subject -notlike "*Chocolatey Software, Inc*") { - Write-Host -Object "[Error] The script file's signature is '$($ScriptSignature.Status)' with the subject '$($ScriptSignature.SignerCertificate.Subject)'." - Write-Host -Object "[Error] Expected the signature to be valid and contain 'Chocolatey Software, Inc'" - exit 1 - } - - # Convert the script into a script block - $ChocolateyScript = [scriptblock]::Create($ChocolateyScriptFilePath) - try { - # Run the installation script - $ChocolateyScript.Invoke() - if (!(Test-ChocolateyInstalled)) { - throw "Chocolatey is missing but the script didn't throw any terminating errors?" - } - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host "Failed to install Chocolatey." - exit 1 - } - - # Remove the installation script from the TEMP folder if it exists - if (Test-Path "$env:TEMP\install.ps1" -ErrorAction SilentlyContinue) { - try { - Remove-Item -Path "$env:TEMP\install.ps1" -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] Failed to remove installation script at $env:TEMP\install.ps1" - $ExitCode = 1 - } - } - } - - if (!(Test-ChocolateyInstallVariable) -and $UpgradeChocolatey) { - Write-Host -Object "[Error] The environment variable 'ChocolateyInstall' is missing. You may need to restart or reinstall Chocolatey." - exit 1 - } - - - # Check for outdated Chocolatey version if upgrades are allowed - $ChocolateyOutdated = & choco outdated --limitoutput - if ($ChocolateyOutdated -match "chocolatey\|" -and $UpgradeChocolatey) { - Write-Host "The current installation of Chocolatey is outdated." - Write-Host "" - Write-Host "Installed Package | Installed Version | Current Version | Pinned?" - $ChocolateyOutdated | Write-Host - Write-Host "" - Write-Host "Upgrading..." - - # Define arguments to run Chocolatey upgrade command for Chocolatey itself - $ChocoUpdateArgs = New-Object System.Collections.Generic.List[string] - $ChocoUpdateArgs.Add("upgrade") - $ChocoUpdateArgs.Add("chocolatey") - $ChocoUpdateArgs.Add("--yes") - $ChocoUpdateArgs.Add("--nocolor") - $ChocoUpdateArgs.Add("--no-progress") - $ChocoUpdateArgs.Add("--limitoutput") - - # Start the Chocolatey upgrade process and wait for completion - $chocoupdate = Start-Process "choco" -ArgumentList $ChocoUpdateArgs -Wait -PassThru -NoNewWindow - Write-Host "Exit Code: $($chocoupdate.ExitCode)" - - # Check the exit code of the upgrade process - switch ($chocoupdate.ExitCode) { - 0 { } - default { - Write-Host -Object "[Error] The exit code does not indicate success." - exit $chocoupdate.ExitCode - } - } - - # If Chocolatey fails to update, log an error and exit. - $ChocolateyOutdated = & choco outdated --limitoutput - if ($ChocolateyOutdated -match "chocolatey\|") { - Write-Host -Object "[Error] Failed to update Chocolatey." - exit 1 - } - else { - Write-Host -Object "Chocolatey updated successfully.`n" - } - } - elseif ($UpgradeChocolatey) { - Write-Host -Object "`nChocolatey is already up-to-date.`n" - } - - # Define arguments for the specified action (Install, Uninstall, Upgrade) on a package - $ChocoArguments = New-Object System.Collections.Generic.List[string] - switch ($Action) { - "Install" { $ChocoArguments.Add("install") } - "Uninstall" { $ChocoArguments.Add("uninstall") } - "Upgrade" { $ChocoArguments.Add("upgrade") } - } - - # Add the package name and other required options to the arguments list - $ChocoArguments.Add($Name) - if ($Version) { - $ChocoArguments.Add("--version") - $ChocoArguments.Add($Version) - } - if ($AllowDowngrades) { - $ChocoArguments.Add("--allow-downgrade") - } - $ChocoArguments.Add("--yes") - $ChocoArguments.Add("--nocolor") - $ChocoArguments.Add("--no-progress") - $ChocoArguments.Add("--limitoutput") - - # Start the specified Chocolatey action process (install, uninstall, or upgrade) and wait for completion - $chocolatey = Start-Process "choco" -ArgumentList $ChocoArguments -Wait -PassThru -NoNewWindow - - # Display the exit code from the Chocolatey action process - Write-Host "Exit Code: $($chocolatey.ExitCode)" - switch ($chocolatey.ExitCode) { - 0 { Write-Host "Successfully completed the action '$Action' for package '$Name'." } - default { - Write-Host -Object "[Error] The exit code does not indicate success." - exit $($chocolatey.ExitCode) - } - } - - exit $ExitCode -} -end { - - - -} - - +#Requires -Version 4.0 + +<# +.SYNOPSIS + This script allows you to install, uninstall, or upgrade an application using Chocolatey. If Chocolatey is not installed or is outdated, options are available to install or upgrade it before proceeding with the application action. +.DESCRIPTION + This script allows you to install, uninstall, or upgrade an application using Chocolatey. If Chocolatey is not installed or is outdated, options are available to install or upgrade it before proceeding with the application action. +.EXAMPLE + -Action "Install" -Name "vlc" -InstallChocolateyIfMissing -SkipSleep + Chocolatey is not installed. + Downloading Chocolatey's install script and installing. + URL 'https://community.chocolatey.org/install.ps1' was given. + Downloading the file... + Download Attempt 1 + Forcing web requests to allow TLS v1.2 (Required for requests to Chocolatey.org) + Getting latest version of the Chocolatey package for download. + Not using proxy. + Getting Chocolatey from https://community.chocolatey.org/api/v2/package/chocolatey/2.3.0. + Downloading https://community.chocolatey.org/api/v2/package/chocolatey/2.3.0 to C:\Windows\TEMP\chocolatey\chocoInstall\chocolatey.zip + Not using proxy. + Extracting C:\Windows\TEMP\chocolatey\chocoInstall\chocolatey.zip to C:\Windows\TEMP\chocolatey\chocoInstall + Downloading 7-Zip commandline tool prior to extraction. + Downloading https://community.chocolatey.org/7za.exe to C:\Windows\TEMP\chocolatey\chocoInstall\7za.exe + Not using proxy. + Installing Chocolatey on the local machine + WARNING: It's very likely you will need to close and reopen your shell + before you can use choco. + PATH environment variable does not have C:\ProgramData\chocolatey\bin in it. Adding... + WARNING: Not setting tab completion: Current user is SYSTEM user. + Ensuring Chocolatey commands are on the path + Ensuring chocolatey.nupkg is in the lib folder + Creating ChocolateyInstall as an environment variable (targeting 'Machine') + Setting ChocolateyInstall to 'C:\ProgramData\chocolatey' + Restricting write permissions to Administrators + We are setting up the Chocolatey package repository. + The packages themselves go to 'C:\ProgramData\chocolatey\lib' + (i.e. C:\ProgramData\chocolatey\lib\yourPackageName). + A shim file for the command line goes to 'C:\ProgramData\chocolatey\bin' + and points to an executable in 'C:\ProgramData\chocolatey\lib\yourPackageName'. + + Creating Chocolatey CLI folders if they do not already exist. + + chocolatey.nupkg file not installed in lib. + Attempting to locate it from bootstrapper. + Chocolatey CLI (choco.exe) is now ready. + You can call choco from anywhere, command line or powershell by typing choco. + Run choco /? for a list of functions. + You may need to shut down and restart powershell and/or consoles + first prior to using choco. + Installing the following packages: + vlc + By installing, you accept licenses for the packages. + Downloading package from source 'https://community.chocolatey.org/api/v2/' + + chocolatey-compatibility.extension v1.0.0 [Approved] + chocolatey-compatibility.extension package files install completed. Performing other installation steps. + Installed/updated chocolatey-compatibility extensions. + The install of chocolatey-compatibility.extension was successful. + Deployed to 'C:\ProgramData\chocolatey\extensions\chocolatey-compatibility' + Downloading package from source 'https://community.chocolatey.org/api/v2/' + + chocolatey-core.extension v1.4.0 [Approved] + chocolatey-core.extension package files install completed. Performing other installation steps. + Installed/updated chocolatey-core extensions. + The install of chocolatey-core.extension was successful. + Deployed to 'C:\ProgramData\chocolatey\extensions\chocolatey-core' + Downloading package from source 'https://community.chocolatey.org/api/v2/' + + vlc.install v3.0.21 [Approved] + vlc.install package files install completed. Performing other installation steps. + Installing 64-bit vlc.install... + vlc.install has been installed. + WARNING: No registry key found based on 'vlc.install' + WARNING: Can't find vlc.install install location + vlc.install may be able to be automatically uninstalled. + The install of vlc.install was successful. + Deployed to 'C:\Program Files\VideoLAN\VLC' + Downloading package from source 'https://community.chocolatey.org/api/v2/' + + vlc v3.0.21 [Approved] + vlc package files install completed. Performing other installation steps. + The install of vlc was successful. + Deployed to 'C:\ProgramData\chocolatey\lib\vlc' + + Chocolatey installed 4/4 packages. + See the log for details (C:\ProgramData\chocolatey\logs\chocolatey.log). + Exit Code: 0 + Successfully completed the action 'Install' for package 'vlc'. + +PARAMETER: -Action "ReplaceMeWithValidAction" + Valid actions are 'Install', 'Upgrade', or 'Uninstall' for your desired package. + +PARAMETER: -Name "NameOfApplication" + Name of the application you would like to uninstall, upgrade, or install. + https://community.chocolatey.org/packages is a good resource to find this. + +PARAMETER: -Version "DesiredVersion" + Optionally, specify a version to install. + +PARAMETER: -AllowDowngrades + Allows downgrading existing installations to the specified version. + +PARAMETER: -InstallChocolateyIfMissing + If Chocolatey isn't installed, this option installs it before starting your action. + +PARAMETER: -UpgradeChocolatey + If an update for Chocolatey itself is available, this option upgrades it to the latest version. + +PARAMETER: -SkipSleep + The script waits for a random interval between 1 and 15 minutes before performing an action with Chocolatey to help avoid rate limiting. + Use this option to skip the wait. For more information, see https://docs.chocolatey.org/en-us/community-repository/community-packages-disclaimer#excessive-use. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2012 R2 + Release Notes: Updated functions, removed writing to the error stream, removed update environment variables, added signature validation, updated comments, added the option to specify a version, and added data validation. +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$Action, + [Parameter()] + [String]$Name, + [Parameter()] + [String]$Version, + [Parameter()] + [Switch]$AllowDowngrades = [System.Convert]::ToBoolean($env:allowDowngrades), + [Parameter()] + [Switch]$InstallChocolateyIfMissing = [System.Convert]::ToBoolean($env:installChocolateyIfNecessary), + [Parameter()] + [Switch]$UpgradeChocolatey = [System.Convert]::ToBoolean($env:upgradeChocolatey), + [Parameter()] + [Switch]$SkipSleep = [System.Convert]::ToBoolean($env:skipSleep) +) +# Helper functions and input validation +begin { + # URL to Chocolatey installation script. Feel free to replace this with your own link. + $InstallUri = "https://community.chocolatey.org/install.ps1" + + # If script form variables are used, replace the command line parameters with their value. + if ($env:action -and $env:action -notlike "null") { $Action = $env:action } + if ($env:packageName -and $env:packageName -notlike "null") { $Name = $env:packageName } + if ($env:version -and $env:version -notlike "null") { $Version = $env:version } + + # Trim whitespace from the action if it's defined + if ($Action) { + $Action = $Action.Trim() + } + + # Trim whitespace from the package name if it's defined + if ($Name) { + $Name = $Name.Trim() + } + + # Trim whitespace from the version if it's defined + if ($Version) { + $Version = $Version.Trim() + } + + # Ensure that both a package name and action are provided + # If not, display an error message and exit with status code 1 + if (!($Name) -or !($Action)) { + Write-Host -Object "[Error] You must provide a valid package name and action." + exit 1 + } + + # Validate the package name format; it should only contain lowercase letters, hyphens, and dots + if ($Name -cmatch "[^a-z0-9.-]") { + Write-Host -Object "[Error] An invalid package name '$Name' was given. Chocolatey package names can only contain lowercase letters, numbers, hyphens, and dots." + Write-Host -Object "[Error] https://docs.chocolatey.org/en-us/create/create-packages/#naming-your-package" + exit 1 + } + + # Define a list of valid actions + $ValidActions = "Install", "Upgrade", "Uninstall" + + # Check if the action is in the list of valid actions + # If not, display an error message and exit with status code 1 + if ($ValidActions -notcontains $Action) { + Write-Host -Object "[Error] An invalid action '$Action' was given. Only the following actions are supported: 'Install', 'Uninstall', 'Upgrade'." + exit 1 + } + + # Check if the name is "All" and the action is not "Upgrade" + # Display an error message and exit if an attempt is made to install or uninstall all packages at once + if ($Name -like "All" -and $Action -ne "Upgrade") { + Write-Host -Object "[Error] Installing or uninstalling all packages at once is not supported!" + exit 1 + } + + # Check if a specific version is provided but the action is not "Install". + if ($Version -and $Action -ne "Install") { + Write-Host -Object "[Error] To install a specific version, you must specify 'Install', even if you're changing the version of an existing application." + exit 1 + } + + # Validate the format of the version number. + # If the version contains characters other than numbers or dots, print an error message and a reference URL, then exit. + if ($Version -match "[^0-9.]") { + Write-Host -Object "[Error] An invalid version '$Version' was given. Chocolatey version numbers can only contain numbers and dots." + Write-Host -Object "[Error] https://docs.chocolatey.org/en-us/create/create-packages/#versioning-recommendations" + exit 1 + } + + # Check if the user has allowed downgrades without specifying a version. + # Print an error message and exit if downgrades are allowed but no version is specified. + if ($AllowDowngrades -and !$Version) { + Write-Host -Object "[Error] You must specify a version to allow downgrades to an older version." + exit 1 + } + + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + return $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if (!(Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Local Administrator privileges or run as SYSTEM. https://ninjarmm.zendesk.com/hc/en-us/articles/360016094532-Credential-Exchange" + exit 1 + } + + function Test-ChocolateyInstalled { + [CmdletBinding()] + param() + + # Try to retrieve the 'choco' command. If it exists, assign it to $Command, suppressing errors. + $Command = Get-Command choco -ErrorAction SilentlyContinue + + # Check if the 'choco' command path is found and if it exists on the filesystem + if ($Command.Path -and (Test-Path -Path $Command.Path -ErrorAction SilentlyContinue)) { + return $true + } + + # If 'choco' command was not found, check if 'Chocolatey\bin' is in the system PATH + if (([Environment]::GetEnvironmentVariable('Path', [System.EnvironmentVariableTarget]::Machine)) -like "*chocolatey\bin*") { + # Update the current session's PATH with the system PATH containing 'Chocolatey\bin' + $env:Path = [Environment]::GetEnvironmentVariable('Path', [System.EnvironmentVariableTarget]::Machine); + + # Re-check for the 'choco' command after updating the PATH + $Command = Get-Command choco -ErrorAction SilentlyContinue + if ($Command.Path -and (Test-Path -Path $Command.Path -ErrorAction SilentlyContinue)) { + return $true + } + else { + return $false + } + } + + # Check if the 'ChocolateyInstall' environment variable is set + # Verify if 'choco' exists in the 'ChocolateyInstall\bin' directory + if ($env:ChocolateyInstall -and (Test-Path -Path "$env:ChocolateyInstall\bin\choco" -ErrorAction SilentlyContinue)) { + # Update the current session's PATH with the Chocolatey installation path + $Env:Path = "$Env:Path;$env:ChocolateyInstall\bin" + return $true + } + + # As a last check, look for 'choco' in the default ProgramData path for Chocolatey + if (Test-Path -Path "$env:ProgramData\chocolatey\bin\choco" -ErrorAction SilentlyContinue) { + # Update the PATH to include the default Chocolatey ProgramData path + $Env:Path = "$Env:Path;$env:ChocolateyInstall\bin" + return $true + } + } + + function Test-ChocolateyInstallVariable { + [CmdletBinding()] + param() + + # Check if the 'ChocolateyInstall' environment variable is set + if ($env:ChocolateyInstall) { + return $True + } + + # Check if the 'ChocolateyInstall' environment variable is set at the machine level. + if (([Environment]::GetEnvironmentVariable('ChocolateyInstall', [System.EnvironmentVariableTarget]::Machine))) { + $env:ChocolateyInstall = [Environment]::GetEnvironmentVariable('ChocolateyInstall', [System.EnvironmentVariableTarget]::Machine); + return $true + } + + # Try to retrieve the 'choco' command. If it exists, assign it to $Command, suppressing errors. + $Command = Get-Command choco -ErrorAction SilentlyContinue + + # Verify that the 'choco' command path exists, is valid, and matches the typical path pattern for Chocolatey installations. + if ($Command.Path -and (Test-Path -Path $Command.Path -ErrorAction SilentlyContinue) -and $Command.Path -like "*\bin\choco.exe") { + # Set the 'ChocolateyInstall' environment variable based on the retrieved path. + $env:ChocolateyInstall = $Command.Path -replace "\\bin\\choco.exe.*" + return $true + } + + # As a last check, look for 'choco' in the default ProgramData path for Chocolatey + if (Test-Path -Path "$env:ProgramData\chocolatey\bin\choco.exe" -ErrorAction SilentlyContinue) { + # Update the ChocolateyInstall variable to include the default Chocolatey ProgramData path + $env:ChocolateyInstall = "$env:ProgramData\chocolatey" + $Env:Path = "$Env:Path;$env:ChocolateyInstall\bin" + return $true + } + } + + # Utility function for downloading files. + function Invoke-Download { + param( + [Parameter()] + [String]$URL, + [Parameter()] + [String]$Path, + [Parameter()] + [int]$Attempts = 3, + [Parameter()] + [Switch]$SkipSleep + ) + + # Display the URL being used for the download + Write-Host -Object "URL '$URL' was given." + Write-Host -Object "Downloading the file..." + + # Initialize the attempt counter + $i = 1 + While ($i -le $Attempts) { + # If SkipSleep is not set, wait for a random time between 3 and 15 seconds before each attempt + if (!($SkipSleep)) { + $SleepTime = Get-Random -Minimum 3 -Maximum 15 + Write-Host "Waiting for $SleepTime seconds." + Start-Sleep -Seconds $SleepTime + } + + # Provide a visual break between attempts + if ($i -ne 1) { Write-Host "" } + Write-Host "Download Attempt $i" + + # Temporarily disable progress reporting to speed up script performance + $PreviousProgressPreference = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + try { + if ($PSVersionTable.PSVersion.Major -lt 4) { + # For older versions of PowerShell, use WebClient to download the file + $WebClient = New-Object System.Net.WebClient + $WebClient.DownloadFile($URL, $Path) + } + else { + # For PowerShell 4.0 and above, use Invoke-WebRequest with specified arguments + $WebRequestArgs = @{ + Uri = $URL + OutFile = $Path + MaximumRedirection = 10 + UseBasicParsing = $true + } + + Invoke-WebRequest @WebRequestArgs + } + + # Verify if the file was successfully downloaded + $File = Test-Path -Path $Path -ErrorAction SilentlyContinue + } + catch { + # Handle any errors that occur during the download attempt + Write-Warning "An error has occurred while downloading!" + Write-Warning $_.Exception.Message + + # If the file partially downloaded, delete it to avoid corruption + if (Test-Path -Path $Path -ErrorAction SilentlyContinue) { + Remove-Item $Path -Force -Confirm:$false -ErrorAction SilentlyContinue + } + + $File = $False + } + + # Restore the original progress preference setting + $ProgressPreference = $PreviousProgressPreference + # If the file was successfully downloaded, exit the loop + if ($File) { + $i = $Attempts + } + else { + # Warn the user if the download attempt failed + Write-Warning "File failed to download." + Write-Host "" + } + + # Increment the attempt counter + $i++ + } + + # Final check: if the file still doesn't exist, report an error and exit + if (!(Test-Path $Path)) { + Write-Host -Object "[Error] Failed to download file." + Write-Host -Object "Please verify the URL of '$URL'." + exit 1 + } + else { + # If the download succeeded, return the path to the downloaded file + return $Path + } + } + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # Check if Chocolatey is installed and if InstallChocolateyIfMissing is false + # If Chocolatey is not installed and installation is not allowed, exit with an error + if (!$(Test-ChocolateyInstalled) -and !$InstallChocolateyIfMissing) { + Write-Host -Object "[Error] Install Chocolatey If Necessary is not selected and chocolatey was not installed. Unable to continue." + exit 1 + } + + # Determine the supported TLS versions and set the appropriate security protocol + $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType') + if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12 + } + elseif ( $SupportedTLSversions -contains 'Tls12' ) { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 + } + else { + # Warn the user if TLS 1.2 and 1.3 are not supported, which may cause the script to fail + Write-Warning "TLS 1.2 and/or TLS 1.3 are not supported on this system. This script may fail. https://blog.chocolatey.org/2020/01/remove-support-for-old-tls-versions/" + if ($PSVersionTable.PSVersion.Major -lt 3) { + Write-Warning "PowerShell 2 / .NET 2.0 doesn't support TLS 1.2." + } + } + + if (!($SkipSleep)) { + $SleepTime = Get-Random -Minimum 60 -Maximum 900 + $SleepTimeMinutes = [math]::Round($($SleepTime / 60)) + Write-Host "Waiting for $SleepTimeMinutes minutes." + Start-Sleep -Seconds $SleepTime + } + + # Check if Chocolatey is not installed but installation is allowed + if (!$(Test-ChocolateyInstalled) -and $InstallChocolateyIfMissing) { + Write-Host "Chocolatey is not installed." + Write-Host "Downloading Chocolatey's install script and installing." + + # Define download arguments for Chocolatey installation script + $DownloadArguments = @{ + Path = "$env:TEMP\install.ps1" + URL = $InstallUri + } + + # Optionally add SkipSleep to download arguments if specified + if ($SkipSleep) { $DownloadArguments["SkipSleep"] = $True } + + # Download and create the Chocolatey installation script + $ChocolateyScriptFilePath = Invoke-Download @DownloadArguments + + # Validating signature of the installation script + $ScriptSignature = Get-AuthenticodeSignature -FilePath $ChocolateyScriptFilePath -ErrorAction SilentlyContinue + if (!$ScriptSignature) { + Write-Host -Object "[Error] A signature was not found on the script file." + exit 1 + } + if ($ScriptSignature.Status -ne "Valid" -and $ScriptSignature.SignerCertificate.Subject -notlike "*Chocolatey Software, Inc*") { + Write-Host -Object "[Error] The script file's signature is '$($ScriptSignature.Status)' with the subject '$($ScriptSignature.SignerCertificate.Subject)'." + Write-Host -Object "[Error] Expected the signature to be valid and contain 'Chocolatey Software, Inc'" + exit 1 + } + + # Convert the script into a script block + $ChocolateyScript = [scriptblock]::Create($ChocolateyScriptFilePath) + try { + # Run the installation script + $ChocolateyScript.Invoke() + if (!(Test-ChocolateyInstalled)) { + throw "Chocolatey is missing but the script didn't throw any terminating errors?" + } + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host "Failed to install Chocolatey." + exit 1 + } + + # Remove the installation script from the TEMP folder if it exists + if (Test-Path "$env:TEMP\install.ps1" -ErrorAction SilentlyContinue) { + try { + Remove-Item -Path "$env:TEMP\install.ps1" -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] Failed to remove installation script at $env:TEMP\install.ps1" + $ExitCode = 1 + } + } + } + + if (!(Test-ChocolateyInstallVariable) -and $UpgradeChocolatey) { + Write-Host -Object "[Error] The environment variable 'ChocolateyInstall' is missing. You may need to restart or reinstall Chocolatey." + exit 1 + } + + # Check for outdated Chocolatey version if upgrades are allowed + $ChocolateyOutdated = & choco outdated --limitoutput + if ($ChocolateyOutdated -match "chocolatey\|" -and $UpgradeChocolatey) { + Write-Host "The current installation of Chocolatey is outdated." + Write-Host "" + Write-Host "Installed Package | Installed Version | Current Version | Pinned?" + $ChocolateyOutdated | Write-Host + Write-Host "" + Write-Host "Upgrading..." + + # Define arguments to run Chocolatey upgrade command for Chocolatey itself + $ChocoUpdateArgs = New-Object System.Collections.Generic.List[string] + $ChocoUpdateArgs.Add("upgrade") + $ChocoUpdateArgs.Add("chocolatey") + $ChocoUpdateArgs.Add("--yes") + $ChocoUpdateArgs.Add("--nocolor") + $ChocoUpdateArgs.Add("--no-progress") + $ChocoUpdateArgs.Add("--limitoutput") + + # Start the Chocolatey upgrade process and wait for completion + $chocoupdate = Start-Process "choco" -ArgumentList $ChocoUpdateArgs -Wait -PassThru -NoNewWindow + Write-Host "Exit Code: $($chocoupdate.ExitCode)" + + # Check the exit code of the upgrade process + switch ($chocoupdate.ExitCode) { + 0 { } + default { + Write-Host -Object "[Error] The exit code does not indicate success." + exit $chocoupdate.ExitCode + } + } + + # If Chocolatey fails to update, log an error and exit. + $ChocolateyOutdated = & choco outdated --limitoutput + if ($ChocolateyOutdated -match "chocolatey\|") { + Write-Host -Object "[Error] Failed to update Chocolatey." + exit 1 + } + else { + Write-Host -Object "Chocolatey updated successfully.`n" + } + } + elseif ($UpgradeChocolatey) { + Write-Host -Object "`nChocolatey is already up-to-date.`n" + } + + # Define arguments for the specified action (Install, Uninstall, Upgrade) on a package + $ChocoArguments = New-Object System.Collections.Generic.List[string] + switch ($Action) { + "Install" { $ChocoArguments.Add("install") } + "Uninstall" { $ChocoArguments.Add("uninstall") } + "Upgrade" { $ChocoArguments.Add("upgrade") } + } + + # Add the package name and other required options to the arguments list + $ChocoArguments.Add($Name) + if ($Version) { + $ChocoArguments.Add("--version") + $ChocoArguments.Add($Version) + } + if ($AllowDowngrades) { + $ChocoArguments.Add("--allow-downgrade") + } + $ChocoArguments.Add("--yes") + $ChocoArguments.Add("--nocolor") + $ChocoArguments.Add("--no-progress") + $ChocoArguments.Add("--limitoutput") + + # Start the specified Chocolatey action process (install, uninstall, or upgrade) and wait for completion + $chocolatey = Start-Process "choco" -ArgumentList $ChocoArguments -Wait -PassThru -NoNewWindow + + # Display the exit code from the Chocolatey action process + Write-Host "Exit Code: $($chocolatey.ExitCode)" + switch ($chocolatey.ExitCode) { + 0 { Write-Host "Successfully completed the action '$Action' for package '$Name'." } + default { + Write-Host -Object "[Error] The exit code does not indicate success." + exit $($chocolatey.ExitCode) + } + } + + exit $ExitCode +} +end { + +} + diff --git a/Powershell Scripts/Clear Browser Cache.ps1 b/Powershell Scripts/Clear Browser Cache.ps1 index 59a2796..7287fbe 100644 --- a/Powershell Scripts/Clear Browser Cache.ps1 +++ b/Powershell Scripts/Clear Browser Cache.ps1 @@ -1,433 +1,430 @@ # Clear the browser cache for all users (when run as system), some users (when a user is specified), or just the current user (when run as current user) depending on how the script is run. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Clear the browser cache for all users (when run as system), some users (when a user is specified), or just the current user (when run as current user) depending on how the script is run. -.DESCRIPTION - Clear the browser cache for all users (when run as system), some users (when a user is specified), or just the current user (when run as current user) depending on how the script is run. -.EXAMPLE - -Firefox -ForceCloseBrowsers - - WARNING: Running Mozilla Firefox processess detected. - - Clearing browser cache for tuser1 - Closing Mozilla Firefox processes for tuser1 as requested. - Clearing Mozilla Firefox's browser cache for tuser1. - - - Clearing browser cache for cheart - Closing Mozilla Firefox processes for cheart as requested. - Clearing Mozilla Firefox's browser cache for cheart. - -PARAMETER: -Usernames "ReplaceWithYourDesiredUsername" - Clear the browser cache for only this comma-separated list of users. - -PARAMETER: -Firefox - Clear the browser cache for Mozilla Firefox. - -PARAMETER: -Chrome - Clear the browser cache for Google Chrome. - -PARAMETER: -Edge - Clear the browser cache for Microsoft Edge. - -PARAMETER: -ForceCloseBrowsers - Force close the browser prior to clearing the browser cache. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$Usernames, - [Parameter()] - [Switch]$Firefox = [System.Convert]::ToBoolean($env:mozillaFirefox), - [Parameter()] - [Switch]$Chrome = [System.Convert]::ToBoolean($env:googleChrome), - [Parameter()] - [Switch]$Edge = [System.Convert]::ToBoolean($env:microsoftEdge), - [Parameter()] - [Switch]$ForceCloseBrowsers = [System.Convert]::ToBoolean($env:forceCloseBrowsers) -) - -begin { - if ($env:usernames -and $env:usernames -notlike "null") { $Usernames = $env:usernames } - - # Check if none of the browser checkboxes (Firefox, Chrome, Edge) are selected - if (!$Firefox -and !$Chrome -and !$Edge) { - # Output an error message and exit the script if no browser is selected - Write-Host -Object "[Error] You must select a checkbox for a browser whose cache you would like to clear." - exit 1 - } - - # Define a function to get the list of logged-in users - function Get-LoggedInUsers { - # Run the 'quser.exe' command to get the list of logged-in users - $quser = quser.exe - # Replace multiple spaces with a comma, then convert the output to CSV format - $quser -replace '\s{2,}', ',' -replace '>' | ConvertFrom-Csv - } - - function Get-UserHives { - param ( - [Parameter()] - [ValidateSet('AzureAD', 'DomainAndLocal', 'All')] - [String]$Type = "All", - [Parameter()] - [String[]]$ExcludedUsers, - [Parameter()] - [switch]$IncludeDefault - ) - - # User account SIDs follow specific patterns depending on if they are Azure AD, Domain, or local accounts. - $Patterns = switch ($Type) { - "AzureAD" { "S-1-12-1-(\d+-?){4}$" } - "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" } - "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" } - } - - # Retrieve user profiles by matching account SIDs to the defined patterns. - $UserProfiles = Foreach ($Pattern in $Patterns) { - Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | - Where-Object { $_.PSChildName -match $Pattern } | - Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, - @{Name = "UserName"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } }, - @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, - @{Name = "Path"; Expression = { $_.ProfileImagePath } } - } - - # Optionally include the .Default user profile if requested. - switch ($IncludeDefault) { - $True { - $DefaultProfile = "" | Select-Object UserName, SID, UserHive, Path - $DefaultProfile.UserName = "Default" - $DefaultProfile.SID = "DefaultProfile" - $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT" - $DefaultProfile.Path = "C:\Users\Default" - - # Add default profile to the list if it's not in the excluded users list - $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.UserName } - } - } - - # Filter out the excluded users from the user profiles list and return the result. - $UserProfiles | Where-Object { $ExcludedUsers -notcontains $_.UserName } - } - - # Function to find installation keys based on the display name - function Find-InstallKey { - [CmdletBinding()] - param ( - [Parameter(ValueFromPipeline = $True)] - [String]$DisplayName, - [Parameter()] - [Switch]$UninstallString, - [Parameter()] - [String]$UserBaseKey - ) - process { - # Initialize an empty list to hold installation objects - $InstallList = New-Object System.Collections.Generic.List[Object] - - # Search for programs in 32-bit and 64-bit system locations. Then add them to the list if they match the display name - $Result = Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } - if ($Result) { $InstallList.Add($Result) } - - $Result = Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } - if ($Result) { $InstallList.Add($Result) } - - # If a user base key is specified, search in the user-specified 64-bit and 32-bit paths. - if ($UserBaseKey) { - $Result = Get-ChildItem -Path "$UserBaseKey\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } - if ($Result) { $InstallList.Add($Result) } - - $Result = Get-ChildItem -Path "$UserBaseKey\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } - if ($Result) { $InstallList.Add($Result) } - } - - # If the UninstallString switch is specified, return only the uninstall strings; otherwise, return the full installation objects. - if ($UninstallString) { - $InstallList | Select-Object -ExpandProperty UninstallString -ErrorAction SilentlyContinue - } - else { - $InstallList - } - } - } - - function Test-IsSystem { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - return $id.Name -like "NT AUTHORITY*" -or $id.IsSystem - } - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # Initialize a list to store users whose cache needs to be cleared - $UsersToClear = New-Object System.Collections.Generic.List[object] - - # Get all user profiles - $AllUserProfiles = Get-UserHives - - # If specific usernames are provided - if ($Usernames) { - $Usernames -split "," | ForEach-Object { - $User = $_.Trim() - - # Check if different user to existing user - if (!(Test-IsSystem) -and $User -ne $env:username) { - Write-Host -Object "[Error] Unable to clear cache for $User." - Write-Host -Object "[Error] Please run as 'System' to clear the cache for users other than the currently logged-on user." - $ExitCode = 1 - return - } - - # Ensure username does not contain illegal characters. - if ($_.Trim() -match '\[|\]|:|;|\||=|\+|\*|\?|<|>|/|\\|"|@') { - Write-Host -Object ("[Error] '$User' contains one of the following invalid characters." + ' " [ ] : ; | = + * ? < > / \ @') - $ExitCode = 1 - return - } - - # Ensure the username does not contain spaces. - if ($User -match '\s') { - Write-Host -Object ("[Error] '$User' is an invalid username because it contains a space.") - $ExitCode = 1 - return - } - - # Ensure the username is not longer than 20 characters. - $UserNameCharacters = $User | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($UserNameCharacters -gt 20) { - Write-Host -Object "[Error] '$($_.Trim())' is an invalid username because it is too long. The username needs to be less than or equal to 20 characters." - $ExitCode = 1 - return - } - - # Check if the user exists in the user profiles. - if ($($AllUserProfiles.Username) -notcontains $User) { - Write-Host "[Error] User '$User' either does not exist or has not signed in yet. Please see the table below for initialized profiles." - $AllUserProfiles | Format-Table Username, Path | Out-String | Write-Host - $ExitCode = 1 - return - } - - # Add the user profile to the list of users to clear. - $UsersToClear.Add(( $AllUserProfiles | Where-Object { $_.Username -eq $User } )) - } - - # Check if no valid usernames were given. - if ($UsersToClear.Count -eq 0) { - Write-Host -Object "[Error] No valid username was given." - exit 1 - } - } - elseif (Test-IsSystem) { - # If running as System, add all user profiles to the list - $AllUserProfiles | ForEach-Object { - $UsersToClear.Add($_) - } - } - else { - # Otherwise, add the currently logged-in user to the list - $UsersToClear.Add(( $AllUserProfiles | Where-Object { $_.Username -eq $env:USERNAME } )) - } - - $LoadedProfiles = New-Object System.Collections.Generic.List[string] - - # Iterate over each user in the list of users to clear - $UsersToClear | ForEach-Object { - # Load the user's registry hive (ntuser.dat) if it's not already loaded - if ((Test-Path Registry::HKEY_USERS\$($_.SID)) -eq $false) { - $LoadedProfiles.Add("$($_.SID)") - - Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe LOAD HKU\$($_.SID) `"$($_.UserHive)`"" -Wait -WindowStyle Hidden - } - - # Find the Firefox installation for the user - if ($Firefox) { - $FirefoxInstallation = Find-InstallKey -DisplayName "Mozilla Firefox" -UserBaseKey "Registry::HKEY_USERS\$($_.SID)" - } - - # Find the Chrome installation for the user - if ($Chrome) { - $ChromeInstallation = Find-InstallKey -DisplayName "Google Chrome" -UserBaseKey "Registry::HKEY_USERS\$($_.SID)" - } - - # Find the Edge installation for the user - if ($Edge) { - $EdgeInstallation = Find-InstallKey -DisplayName "Microsoft Edge" -UserBaseKey "Registry::HKEY_USERS\$($_.SID)" - } - } - - # If force closing browsers is requested - if ($ForceCloseBrowsers) { - # Check and handle running Firefox processes - if ($Firefox -and (Get-Process -Name "firefox" -ErrorAction SilentlyContinue)) { - Write-Warning -Message "Running Mozilla Firefox processess detected." - $FirefoxProcesses = Get-Process -Name "firefox" -ErrorAction SilentlyContinue - } - - # Check and handle running Chrome processes - if ($Chrome -and (Get-Process -Name "chrome" -ErrorAction SilentlyContinue)) { - Write-Warning -Message "Running Google Chrome processess detected." - $ChromeProcesses = Get-Process -Name "chrome" -ErrorAction SilentlyContinue - } - - # Check and handle running Edge processes - if ($Edge -and (Get-Process -Name "msedge" -ErrorAction SilentlyContinue)) { - Write-Warning -Message "Running Microsoft Edge processess detected." - $EdgeProcesses = Get-Process -Name "msedge" -ErrorAction SilentlyContinue - } - } - - # Iterate over each user in the list of users to clear - $UsersToClear | ForEach-Object { - Write-Host -Object "`nClearing browser cache for $($_.Username)" - - # Handle Firefox cache clearing - if ($Firefox -and !$FirefoxInstallation) { - Write-Warning -Message "Mozilla Firefox is not installed!" - } - elseif ($Firefox) { - if (Test-Path -Path "$($_.Path)\AppData\Local\Mozilla\Firefox\Profiles") { - - if ($ForceCloseBrowsers -and $FirefoxProcesses) { - Write-Host -Object "Closing Mozilla Firefox processes for $($_.Username) as requested." - - $User = $_.Username - $RelevantAccount = Get-LoggedInUsers | Where-Object { $User -match $_.USERNAME } - $RelevantProcess = $FirefoxProcesses | Where-Object { $RelevantAccount.ID -contains $_.SI } - - try { - $RelevantProcess | ForEach-Object { $_ | Stop-Process -Force -ErrorAction Stop } - } - catch { - Write-Host -Object "[Error] Failed to close one of Mozilla Firefox's processes." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - - Write-Host -Object "Clearing Mozilla Firefox's browser cache for $($_.Username)." - - try { - Get-ChildItem -Path "$($_.Path)\AppData\Local\Mozilla\Firefox\Profiles\*\cache2" -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_.PSIsContainer -eq $False } | Remove-Item -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] Unable to clear Mozilla Firefox's cache." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - else { - Write-Host -Object "[Error] Mozilla Firefox's local appdata folder is not at '$($_.Path)\AppData\Local\Mozilla\Firefox\Profiles'. Unable to clear cache." - $ExitCode = 1 - } - } - - # Handle Chrome cache clearing - if ($Chrome -and !$ChromeInstallation) { - Write-Warning -Message "Google Chrome is not installed!" - } - elseif ($Chrome) { - if (Test-Path -Path "$($_.Path)\AppData\Local\Google") { - - if ($ForceCloseBrowsers -and $ChromeProcesses) { - Write-Host -Object "Closing Google Chrome processes for $($_.Username) as requested." - - $User = $_.Username - $RelevantAccount = Get-LoggedInUsers | Where-Object { $User -match $_.USERNAME } - $RelevantProcess = $ChromeProcesses | Where-Object { $RelevantAccount.ID -contains $_.SI } - - try { - $RelevantProcess | ForEach-Object { $_ | Stop-Process -Force -ErrorAction Stop } - } - catch { - Write-Host -Object "[Error] Failed to close one of Google Chrome's processes." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - - Write-Host -Object "Clearing Google Chrome's browser cache for $($_.Username)." - - try{ - Get-ChildItem -Path "$($_.Path)\AppData\Local\Google\Chrome\User Data\*\Cache" -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_.PSIsContainer -eq $False } | Remove-Item -ErrorAction Stop - Get-ChildItem -Path "$($_.Path)\AppData\Local\Google\Chrome\User Data\*\Code Cache" -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_.PSIsContainer -eq $False } | Remove-Item -ErrorAction Stop - Get-ChildItem -Path "$($_.Path)\AppData\Local\Google\Chrome\User Data\*\GPUCache" -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_.PSIsContainer -eq $False } | Remove-Item -ErrorAction Stop - }catch{ - Write-Host -Object "[Error] Unable to clear Google Chrome's cache." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - }else { - Write-Host -Object "[Error] Chrome's local appdata folder is not at '$($_.Path)\AppData\Local\Google'. Unable to clear cache." - $ExitCode = 1 - } - } - - # Handle Edge cache clearing - if ($Edge -and !$EdgeInstallation) { - Write-Warning -Message "Microsoft Edge is not installed!" - } - elseif ($Edge) { - if (Test-Path -Path "$($_.Path)\AppData\Local\Microsoft\Edge") { - if ($ForceCloseBrowsers -and $ChromeProcesses) { - Write-Host -Object "Closing Microsoft Edge processes for $($_.Username) as requested." - - $User = $_.Username - $RelevantAccount = Get-LoggedInUsers | Where-Object { $User -match $_.USERNAME } - $RelevantProcess = $EdgeProcesses | Where-Object { $RelevantAccount.ID -contains $_.SI } - - try { - $RelevantProcess | ForEach-Object { $_ | Stop-Process -Force -ErrorAction Stop } - } - catch { - Write-Host -Object "[Error] Failed to close one of Microsoft Edge's processes." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - - Write-Host -Object "Clearing Microsoft Edge's browser cache for $($_.Username)." - - try{ - Get-ChildItem -Path "$($_.Path)\AppData\Local\Microsoft\Edge\User Data\*\Cache" -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_.PSIsContainer -eq $False } | Remove-Item -ErrorAction Stop - Get-ChildItem -Path "$($_.Path)\AppData\Local\Microsoft\Edge\User Data\*\Code Cache" -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_.PSIsContainer -eq $False } | Remove-Item -ErrorAction Stop - Get-ChildItem -Path "$($_.Path)\AppData\Local\Microsoft\Edge\User Data\*\GPUCache" -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_.PSIsContainer -eq $False } | Remove-Item -ErrorAction Stop - }catch{ - Write-Host -Object "[Error] Unable to clear Microsoft Edge's cache." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - else { - Write-Host -Object "[Error] Microsoft Edge's local appdata folder is not at '$($_.Path)\AppData\Local\Microsoft\Edge'. Unable to clear cache." - $ExitCode = 1 - } - } - - Write-Host "" - } - - # Iterate over each loaded profile - Foreach ($LoadedProfile in $LoadedProfiles) { - [gc]::Collect() - Start-Sleep -Seconds 1 - Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe UNLOAD HKU\$($LoadedProfile)" -Wait -WindowStyle Hidden | Out-Null - } - - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Clear the browser cache for all users (when run as system), some users (when a user is specified), or just the current user (when run as current user) depending on how the script is run. +.DESCRIPTION + Clear the browser cache for all users (when run as system), some users (when a user is specified), or just the current user (when run as current user) depending on how the script is run. +.EXAMPLE + -Firefox -ForceCloseBrowsers + + WARNING: Running Mozilla Firefox processess detected. + + Clearing browser cache for tuser1 + Closing Mozilla Firefox processes for tuser1 as requested. + Clearing Mozilla Firefox's browser cache for tuser1. + + Clearing browser cache for cheart + Closing Mozilla Firefox processes for cheart as requested. + Clearing Mozilla Firefox's browser cache for cheart. + +PARAMETER: -Usernames "ReplaceWithYourDesiredUsername" + Clear the browser cache for only this comma-separated list of users. + +PARAMETER: -Firefox + Clear the browser cache for Mozilla Firefox. + +PARAMETER: -Chrome + Clear the browser cache for Google Chrome. + +PARAMETER: -Edge + Clear the browser cache for Microsoft Edge. + +PARAMETER: -ForceCloseBrowsers + Force close the browser prior to clearing the browser cache. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$Usernames, + [Parameter()] + [Switch]$Firefox = [System.Convert]::ToBoolean($env:mozillaFirefox), + [Parameter()] + [Switch]$Chrome = [System.Convert]::ToBoolean($env:googleChrome), + [Parameter()] + [Switch]$Edge = [System.Convert]::ToBoolean($env:microsoftEdge), + [Parameter()] + [Switch]$ForceCloseBrowsers = [System.Convert]::ToBoolean($env:forceCloseBrowsers) +) + +begin { + if ($env:usernames -and $env:usernames -notlike "null") { $Usernames = $env:usernames } + + # Check if none of the browser checkboxes (Firefox, Chrome, Edge) are selected + if (!$Firefox -and !$Chrome -and !$Edge) { + # Output an error message and exit the script if no browser is selected + Write-Host -Object "[Error] You must select a checkbox for a browser whose cache you would like to clear." + exit 1 + } + + # Define a function to get the list of logged-in users + function Get-LoggedInUsers { + # Run the 'quser.exe' command to get the list of logged-in users + $quser = quser.exe + # Replace multiple spaces with a comma, then convert the output to CSV format + $quser -replace '\s{2,}', ',' -replace '>' | ConvertFrom-Csv + } + + function Get-UserHives { + param ( + [Parameter()] + [ValidateSet('AzureAD', 'DomainAndLocal', 'All')] + [String]$Type = "All", + [Parameter()] + [String[]]$ExcludedUsers, + [Parameter()] + [switch]$IncludeDefault + ) + + # User account SIDs follow specific patterns depending on if they are Azure AD, Domain, or local accounts. + $Patterns = switch ($Type) { + "AzureAD" { "S-1-12-1-(\d+-?){4}$" } + "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" } + "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" } + } + + # Retrieve user profiles by matching account SIDs to the defined patterns. + $UserProfiles = Foreach ($Pattern in $Patterns) { + Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | + Where-Object { $_.PSChildName -match $Pattern } | + Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, + @{Name = "UserName"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } }, + @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, + @{Name = "Path"; Expression = { $_.ProfileImagePath } } + } + + # Optionally include the .Default user profile if requested. + switch ($IncludeDefault) { + $True { + $DefaultProfile = "" | Select-Object UserName, SID, UserHive, Path + $DefaultProfile.UserName = "Default" + $DefaultProfile.SID = "DefaultProfile" + $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT" + $DefaultProfile.Path = "C:\Users\Default" + + # Add default profile to the list if it's not in the excluded users list + $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.UserName } + } + } + + # Filter out the excluded users from the user profiles list and return the result. + $UserProfiles | Where-Object { $ExcludedUsers -notcontains $_.UserName } + } + + # Function to find installation keys based on the display name + function Find-InstallKey { + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $True)] + [String]$DisplayName, + [Parameter()] + [Switch]$UninstallString, + [Parameter()] + [String]$UserBaseKey + ) + process { + # Initialize an empty list to hold installation objects + $InstallList = New-Object System.Collections.Generic.List[Object] + + # Search for programs in 32-bit and 64-bit system locations. Then add them to the list if they match the display name + $Result = Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } + if ($Result) { $InstallList.Add($Result) } + + $Result = Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } + if ($Result) { $InstallList.Add($Result) } + + # If a user base key is specified, search in the user-specified 64-bit and 32-bit paths. + if ($UserBaseKey) { + $Result = Get-ChildItem -Path "$UserBaseKey\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } + if ($Result) { $InstallList.Add($Result) } + + $Result = Get-ChildItem -Path "$UserBaseKey\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } + if ($Result) { $InstallList.Add($Result) } + } + + # If the UninstallString switch is specified, return only the uninstall strings; otherwise, return the full installation objects. + if ($UninstallString) { + $InstallList | Select-Object -ExpandProperty UninstallString -ErrorAction SilentlyContinue + } + else { + $InstallList + } + } + } + + function Test-IsSystem { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + return $id.Name -like "NT AUTHORITY*" -or $id.IsSystem + } + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # Initialize a list to store users whose cache needs to be cleared + $UsersToClear = New-Object System.Collections.Generic.List[object] + + # Get all user profiles + $AllUserProfiles = Get-UserHives + + # If specific usernames are provided + if ($Usernames) { + $Usernames -split "," | ForEach-Object { + $User = $_.Trim() + + # Check if different user to existing user + if (!(Test-IsSystem) -and $User -ne $env:username) { + Write-Host -Object "[Error] Unable to clear cache for $User." + Write-Host -Object "[Error] Please run as 'System' to clear the cache for users other than the currently logged-on user." + $ExitCode = 1 + return + } + + # Ensure username does not contain illegal characters. + if ($_.Trim() -match '\[|\]|:|;|\||=|\+|\*|\?|<|>|/|\\|"|@') { + Write-Host -Object ("[Error] '$User' contains one of the following invalid characters." + ' " [ ] : ; | = + * ? < > / \ @') + $ExitCode = 1 + return + } + + # Ensure the username does not contain spaces. + if ($User -match '\s') { + Write-Host -Object ("[Error] '$User' is an invalid username because it contains a space.") + $ExitCode = 1 + return + } + + # Ensure the username is not longer than 20 characters. + $UserNameCharacters = $User | Measure-Object -Character | Select-Object -ExpandProperty Characters + if ($UserNameCharacters -gt 20) { + Write-Host -Object "[Error] '$($_.Trim())' is an invalid username because it is too long. The username needs to be less than or equal to 20 characters." + $ExitCode = 1 + return + } + + # Check if the user exists in the user profiles. + if ($($AllUserProfiles.Username) -notcontains $User) { + Write-Host "[Error] User '$User' either does not exist or has not signed in yet. Please see the table below for initialized profiles." + $AllUserProfiles | Format-Table Username, Path | Out-String | Write-Host + $ExitCode = 1 + return + } + + # Add the user profile to the list of users to clear. + $UsersToClear.Add(( $AllUserProfiles | Where-Object { $_.Username -eq $User } )) + } + + # Check if no valid usernames were given. + if ($UsersToClear.Count -eq 0) { + Write-Host -Object "[Error] No valid username was given." + exit 1 + } + } + elseif (Test-IsSystem) { + # If running as System, add all user profiles to the list + $AllUserProfiles | ForEach-Object { + $UsersToClear.Add($_) + } + } + else { + # Otherwise, add the currently logged-in user to the list + $UsersToClear.Add(( $AllUserProfiles | Where-Object { $_.Username -eq $env:USERNAME } )) + } + + $LoadedProfiles = New-Object System.Collections.Generic.List[string] + + # Iterate over each user in the list of users to clear + $UsersToClear | ForEach-Object { + # Load the user's registry hive (ntuser.dat) if it's not already loaded + if ((Test-Path Registry::HKEY_USERS\$($_.SID)) -eq $false) { + $LoadedProfiles.Add("$($_.SID)") + + Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe LOAD HKU\$($_.SID) `"$($_.UserHive)`"" -Wait -WindowStyle Hidden + } + + # Find the Firefox installation for the user + if ($Firefox) { + $FirefoxInstallation = Find-InstallKey -DisplayName "Mozilla Firefox" -UserBaseKey "Registry::HKEY_USERS\$($_.SID)" + } + + # Find the Chrome installation for the user + if ($Chrome) { + $ChromeInstallation = Find-InstallKey -DisplayName "Google Chrome" -UserBaseKey "Registry::HKEY_USERS\$($_.SID)" + } + + # Find the Edge installation for the user + if ($Edge) { + $EdgeInstallation = Find-InstallKey -DisplayName "Microsoft Edge" -UserBaseKey "Registry::HKEY_USERS\$($_.SID)" + } + } + + # If force closing browsers is requested + if ($ForceCloseBrowsers) { + # Check and handle running Firefox processes + if ($Firefox -and (Get-Process -Name "firefox" -ErrorAction SilentlyContinue)) { + Write-Warning -Message "Running Mozilla Firefox processess detected." + $FirefoxProcesses = Get-Process -Name "firefox" -ErrorAction SilentlyContinue + } + + # Check and handle running Chrome processes + if ($Chrome -and (Get-Process -Name "chrome" -ErrorAction SilentlyContinue)) { + Write-Warning -Message "Running Google Chrome processess detected." + $ChromeProcesses = Get-Process -Name "chrome" -ErrorAction SilentlyContinue + } + + # Check and handle running Edge processes + if ($Edge -and (Get-Process -Name "msedge" -ErrorAction SilentlyContinue)) { + Write-Warning -Message "Running Microsoft Edge processess detected." + $EdgeProcesses = Get-Process -Name "msedge" -ErrorAction SilentlyContinue + } + } + + # Iterate over each user in the list of users to clear + $UsersToClear | ForEach-Object { + Write-Host -Object "`nClearing browser cache for $($_.Username)" + + # Handle Firefox cache clearing + if ($Firefox -and !$FirefoxInstallation) { + Write-Warning -Message "Mozilla Firefox is not installed!" + } + elseif ($Firefox) { + if (Test-Path -Path "$($_.Path)\AppData\Local\Mozilla\Firefox\Profiles") { + + if ($ForceCloseBrowsers -and $FirefoxProcesses) { + Write-Host -Object "Closing Mozilla Firefox processes for $($_.Username) as requested." + + $User = $_.Username + $RelevantAccount = Get-LoggedInUsers | Where-Object { $User -match $_.USERNAME } + $RelevantProcess = $FirefoxProcesses | Where-Object { $RelevantAccount.ID -contains $_.SI } + + try { + $RelevantProcess | ForEach-Object { $_ | Stop-Process -Force -ErrorAction Stop } + } + catch { + Write-Host -Object "[Error] Failed to close one of Mozilla Firefox's processes." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + } + + Write-Host -Object "Clearing Mozilla Firefox's browser cache for $($_.Username)." + + try { + Get-ChildItem -Path "$($_.Path)\AppData\Local\Mozilla\Firefox\Profiles\*\cache2" -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_.PSIsContainer -eq $False } | Remove-Item -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] Unable to clear Mozilla Firefox's cache." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + } + else { + Write-Host -Object "[Error] Mozilla Firefox's local appdata folder is not at '$($_.Path)\AppData\Local\Mozilla\Firefox\Profiles'. Unable to clear cache." + $ExitCode = 1 + } + } + + # Handle Chrome cache clearing + if ($Chrome -and !$ChromeInstallation) { + Write-Warning -Message "Google Chrome is not installed!" + } + elseif ($Chrome) { + if (Test-Path -Path "$($_.Path)\AppData\Local\Google") { + + if ($ForceCloseBrowsers -and $ChromeProcesses) { + Write-Host -Object "Closing Google Chrome processes for $($_.Username) as requested." + + $User = $_.Username + $RelevantAccount = Get-LoggedInUsers | Where-Object { $User -match $_.USERNAME } + $RelevantProcess = $ChromeProcesses | Where-Object { $RelevantAccount.ID -contains $_.SI } + + try { + $RelevantProcess | ForEach-Object { $_ | Stop-Process -Force -ErrorAction Stop } + } + catch { + Write-Host -Object "[Error] Failed to close one of Google Chrome's processes." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + } + + Write-Host -Object "Clearing Google Chrome's browser cache for $($_.Username)." + + try{ + Get-ChildItem -Path "$($_.Path)\AppData\Local\Google\Chrome\User Data\*\Cache" -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_.PSIsContainer -eq $False } | Remove-Item -ErrorAction Stop + Get-ChildItem -Path "$($_.Path)\AppData\Local\Google\Chrome\User Data\*\Code Cache" -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_.PSIsContainer -eq $False } | Remove-Item -ErrorAction Stop + Get-ChildItem -Path "$($_.Path)\AppData\Local\Google\Chrome\User Data\*\GPUCache" -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_.PSIsContainer -eq $False } | Remove-Item -ErrorAction Stop + }catch{ + Write-Host -Object "[Error] Unable to clear Google Chrome's cache." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + }else { + Write-Host -Object "[Error] Chrome's local appdata folder is not at '$($_.Path)\AppData\Local\Google'. Unable to clear cache." + $ExitCode = 1 + } + } + + # Handle Edge cache clearing + if ($Edge -and !$EdgeInstallation) { + Write-Warning -Message "Microsoft Edge is not installed!" + } + elseif ($Edge) { + if (Test-Path -Path "$($_.Path)\AppData\Local\Microsoft\Edge") { + if ($ForceCloseBrowsers -and $ChromeProcesses) { + Write-Host -Object "Closing Microsoft Edge processes for $($_.Username) as requested." + + $User = $_.Username + $RelevantAccount = Get-LoggedInUsers | Where-Object { $User -match $_.USERNAME } + $RelevantProcess = $EdgeProcesses | Where-Object { $RelevantAccount.ID -contains $_.SI } + + try { + $RelevantProcess | ForEach-Object { $_ | Stop-Process -Force -ErrorAction Stop } + } + catch { + Write-Host -Object "[Error] Failed to close one of Microsoft Edge's processes." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + } + + Write-Host -Object "Clearing Microsoft Edge's browser cache for $($_.Username)." + + try{ + Get-ChildItem -Path "$($_.Path)\AppData\Local\Microsoft\Edge\User Data\*\Cache" -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_.PSIsContainer -eq $False } | Remove-Item -ErrorAction Stop + Get-ChildItem -Path "$($_.Path)\AppData\Local\Microsoft\Edge\User Data\*\Code Cache" -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_.PSIsContainer -eq $False } | Remove-Item -ErrorAction Stop + Get-ChildItem -Path "$($_.Path)\AppData\Local\Microsoft\Edge\User Data\*\GPUCache" -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_.PSIsContainer -eq $False } | Remove-Item -ErrorAction Stop + }catch{ + Write-Host -Object "[Error] Unable to clear Microsoft Edge's cache." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + } + else { + Write-Host -Object "[Error] Microsoft Edge's local appdata folder is not at '$($_.Path)\AppData\Local\Microsoft\Edge'. Unable to clear cache." + $ExitCode = 1 + } + } + + Write-Host "" + } + + # Iterate over each loaded profile + Foreach ($LoadedProfile in $LoadedProfiles) { + [gc]::Collect() + Start-Sleep -Seconds 1 + Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe UNLOAD HKU\$($LoadedProfile)" -Wait -WindowStyle Hidden | Out-Null + } + + exit $ExitCode +} +end { + +} diff --git a/Powershell Scripts/Clear DNS Cache.ps1 b/Powershell Scripts/Clear DNS Cache.ps1 index ef62539..5fbc0a2 100644 --- a/Powershell Scripts/Clear DNS Cache.ps1 +++ b/Powershell Scripts/Clear DNS Cache.ps1 @@ -1,101 +1,99 @@ # Clears the DNS Cache the number of times you specify (defaults to 3). - -<# -.SYNOPSIS - Clear's the DNS Cache the number of times you specify (defaults to 3). -.DESCRIPTION - Clear's the DNS Cache the number of times you specify (defaults to 3). -.EXAMPLE - (No Parameters) - - DNS Cache clearing attempt 1. - DNS Cache cleared successfully! - - DNS Cache clearing attempt 2. - DNS Cache cleared successfully! - - DNS Cache clearing attempt 3. - DNS Cache cleared successfully! - -PARAMETER: -Attempts "1" - Replace 1 with the number of times you'd like to clear the dns cache. - -.EXAMPLE - -Attempts "1" - - DNS Cache clearing attempt 1. - DNS Cache cleared successfully! - -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Renamed script and added Script Variable support -#> - -[CmdletBinding()] -param ( - [Parameter()] - [int]$Attempts = 3 -) - -begin { - # If script form is used overwrite the parameter - if ($env:numberOfTimesToClearCache -and $env:numberOfTimesToClearCache -notlike "null") { $Attempts = $env:numberOfTimesToClearCache } -} -process { - - try { - # Settiing $i to 1 for readability purposes - $i = 1 - - # Adding 1 to attempts again for readability purposes - if ($Attempts -ne 0) { $Attempts = $Attempts + 1 } - - # Loop through flush dns command - For ($i; $i -lt $Attempts; $i++) { - Start-Sleep -Seconds 1 - Write-Host "DNS Cache clearing attempt $i." - if ((Get-Command Clear-DNSClientCache -ErrorAction SilentlyContinue)) { - Clear-DnsClientCache -ErrorAction Stop - Write-Host "DNS Cache cleared successfully!`n" - } - else { - $dnsflush = ipconfig.exe /flushdns | Where-Object { $_ } | Out-String - Write-Host "$dnsflush" - - if ($dnsflush -like "*Could not flush the DNS Resolver Cache*") { - throw "Could not flush the DNS Resolver Cache." - } - } - } - } - catch { - Write-Error "Failed to clear DNS Cache?" - exit 1 - } - - # Write out the current dns cache - Write-Host "### Current DNS Cache ###" - - # Get-DNSClientCache isn't a thing in PowerShell 2.0 - if ((Get-Command Get-DNSClientCache -ErrorAction SilentlyContinue)) { - $currentcache = Get-DnsClientCache | Format-Table Entry, TimeToLive, Data | Out-String - } - else { - $currentcache = ipconfig.exe /displaydns - $currentcache = $currentcache -replace "Windows IP Configuration" | Where-Object { $_ } | Out-String - } - if (-not $currentcache -or $currentcache -like "*Could not display the DNS Resolver Cache.*") { - Write-Warning "DNS Cache is currently empty." - } - else { - Write-Host $currentcache - } -} -end { - - - -} - + +<# +.SYNOPSIS + Clear's the DNS Cache the number of times you specify (defaults to 3). +.DESCRIPTION + Clear's the DNS Cache the number of times you specify (defaults to 3). +.EXAMPLE + (No Parameters) + + DNS Cache clearing attempt 1. + DNS Cache cleared successfully! + + DNS Cache clearing attempt 2. + DNS Cache cleared successfully! + + DNS Cache clearing attempt 3. + DNS Cache cleared successfully! + +PARAMETER: -Attempts "1" + Replace 1 with the number of times you'd like to clear the dns cache. + +.EXAMPLE + -Attempts "1" + + DNS Cache clearing attempt 1. + DNS Cache cleared successfully! + +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Renamed script and added Script Variable support +#> + +[CmdletBinding()] +param ( + [Parameter()] + [int]$Attempts = 3 +) + +begin { + # If script form is used overwrite the parameter + if ($env:numberOfTimesToClearCache -and $env:numberOfTimesToClearCache -notlike "null") { $Attempts = $env:numberOfTimesToClearCache } +} +process { + + try { + # Settiing $i to 1 for readability purposes + $i = 1 + + # Adding 1 to attempts again for readability purposes + if ($Attempts -ne 0) { $Attempts = $Attempts + 1 } + + # Loop through flush dns command + For ($i; $i -lt $Attempts; $i++) { + Start-Sleep -Seconds 1 + Write-Host "DNS Cache clearing attempt $i." + if ((Get-Command Clear-DNSClientCache -ErrorAction SilentlyContinue)) { + Clear-DnsClientCache -ErrorAction Stop + Write-Host "DNS Cache cleared successfully!`n" + } + else { + $dnsflush = ipconfig.exe /flushdns | Where-Object { $_ } | Out-String + Write-Host "$dnsflush" + + if ($dnsflush -like "*Could not flush the DNS Resolver Cache*") { + throw "Could not flush the DNS Resolver Cache." + } + } + } + } + catch { + Write-Error "Failed to clear DNS Cache?" + exit 1 + } + + # Write out the current dns cache + Write-Host "### Current DNS Cache ###" + + # Get-DNSClientCache isn't a thing in PowerShell 2.0 + if ((Get-Command Get-DNSClientCache -ErrorAction SilentlyContinue)) { + $currentcache = Get-DnsClientCache | Format-Table Entry, TimeToLive, Data | Out-String + } + else { + $currentcache = ipconfig.exe /displaydns + $currentcache = $currentcache -replace "Windows IP Configuration" | Where-Object { $_ } | Out-String + } + if (-not $currentcache -or $currentcache -like "*Could not display the DNS Resolver Cache.*") { + Write-Warning "DNS Cache is currently empty." + } + else { + Write-Host $currentcache + } +} +end { + +} + diff --git a/Powershell Scripts/Collect MSSQL Instances.ps1 b/Powershell Scripts/Collect MSSQL Instances.ps1 index 067aff0..b4d281b 100644 --- a/Powershell Scripts/Collect MSSQL Instances.ps1 +++ b/Powershell Scripts/Collect MSSQL Instances.ps1 @@ -1,223 +1,120 @@ # Gets a list of MSSQL server instances and optionally save the results to a custom field. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Gets a list of MSSQL server instances and optionally save the results to a custom field. -.DESCRIPTION - Gets a list of MSSQL server instances and optionally save the results to a custom field. - The custom field can be either/both a multi-line or WYSIWYG custom field. - - SQL Server, SQL Server Developer and SQL Express are supported. - - SQL "Local" that are built into an application are not supported as they aren't an SQL Server instance. - - SQL service name that don't start with "MSSQL$" will not get detected. - - PS > Get-Service -Name "MSSQL`$*" - Status Name DisplayName - ------ ---- ----------- - Running MSSQL$DB SQL Server (DB) - Running MSSQL$DB01 SQL Server (DB01) - Running MSSQL$DB02 SQL Server (DB02) - -.EXAMPLE - (No Parameters) - ## EXAMPLE OUTPUT WITHOUT PARAMS ## - Status Name Instance Path - ------ ---- -------- ---- - Running SQL Server (DB01) DB01 C:\Program Files\Microsoft SQL Server\MSSQL16.DB01\MSSQL - Running SQL Server (DB02) DB02 C:\Program Files\Microsoft SQL Server\MSSQL16.DB02\MSSQL - -PARAMETER: -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" - Saves an text table to a multi-line Custom Field with a list of SQL instances. -.EXAMPLE - -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" - ## EXAMPLE OUTPUT WITH CustomFieldName ## - Status Name Instance Path - ------ ---- -------- ---- - Running SQL Server (DB01) DB01 C:\Program Files\Microsoft SQL Server\MSSQL16.DB01\MSSQL - Running SQL Server (DB02) DB02 C:\Program Files\Microsoft SQL Server\MSSQL16.DB02\MSSQL - -PARAMETER: -CustomFieldParam "ReplaceMeWithAnyWysiwygCustomField" - Saves an html table to a Wysiwyg Custom Field with a list of SQL instances. -.EXAMPLE - -WysiwygCustomFieldName "ReplaceMeWithAnyWysiwygCustomField" - ## EXAMPLE OUTPUT WITH WysiwygCustomFieldName ## - Status Name Instance Path - ------ ---- -------- ---- - Running SQL Server (DB01) DB01 C:\Program Files\Microsoft SQL Server\MSSQL16.DB01\MSSQL - Running SQL Server (DB02) DB02 C:\Program Files\Microsoft SQL Server\MSSQL16.DB02\MSSQL -.OUTPUTS - None -.NOTES - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [String]$CustomFieldName, - [String]$WysiwygCustomFieldName -) - -begin { - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 10000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") - } - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to be set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received it will have an exception property, the function will exit with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - if ($env:multilineCustomFieldName -and $env:multilineCustomFieldName -notlike "null") { - $CustomFieldName = $env:multilineCustomFieldName - } - if ($env:WysiwygCustomFieldName -and $env:WysiwygCustomFieldName -notlike "null") { - $WysiwygCustomFieldName = $env:WysiwygCustomFieldName - } -} -process { - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - try { - $InstanceNames = $(Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\" -ErrorAction Stop).InstalledInstances - $SqlInstances = $InstanceNames | ForEach-Object { - $SqlPath = $(Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$_\Setup" -ErrorAction Stop).SQLPath - $SqlServices = Get-Service -Name "MSSQL`$$_" -ErrorAction Stop - $SqlService = $SqlServices | Where-Object { $_.Name -notlike $SqlServices.DependentServices.Name -and $_.Name -notlike "SQLTelemetry*" } - [PSCustomObject]@{ - Status = $SqlService.Status - Service = $SqlService.DisplayName - Instance = $_ - Path = $SqlPath - } - } - } - catch { - Write-Host "[Error] $($_.Message)" - Write-Host "[Info] Likely no MSSQL instance found." - exit 1 - } - - $SqlInstances | Out-String | Write-Host - - if ($CustomFieldName) { - Write-Host "Attempting to set Custom Field '$CustomFieldName'." - Set-NinjaProperty -Name $CustomFieldName -Value ($SqlInstances | Out-String) - Write-Host "Successfully set Custom Field '$CustomFieldName'!" - } - - if ($WysiwygCustomFieldName) { - try { - Write-Host "Attempting to set Custom Field '$WysiwygCustomFieldName'." - $htmlReport = New-Object System.Collections.Generic.List[String] - $htmlReport.Add("

SQL Server Instances

") - $htmlTable = $SqlInstances | ConvertTo-Html -Fragment - $htmlTable = $htmlTable -replace "Running", 'Running' - $htmlTable = $htmlTable -replace "StartPending", 'StartPending' - $htmlTable = $htmlTable -replace "ContinuePending", 'ContinuePending' - $htmlTable = $htmlTable -replace "Paused", 'Paused' - $htmlTable = $htmlTable -replace "PausePending", 'PausePending' - $htmlTable = $htmlTable -replace "Stopped", 'Stopped' - $htmlTable = $htmlTable -replace "StopPending", 'StopPending' - $htmlTable | ForEach-Object { $htmlReport.Add($_) } - Set-NinjaProperty -Name $WysiwygCustomFieldName -Value ($htmlReport | Out-String) - Write-Host "Successfully set Custom Field '$WysiwygCustomFieldName'!" - } - catch { - Write-Error $_ - Write-Host "[Error] $($_.Message)" - exit 1 - } - } - exit 0 -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Gets a list of MSSQL server instances and optionally save the results to a custom field. +.DESCRIPTION + Gets a list of MSSQL server instances and optionally save the results to a custom field. + The custom field can be either/both a multi-line or WYSIWYG custom field. + + SQL Server, SQL Server Developer and SQL Express are supported. + + SQL "Local" that are built into an application are not supported as they aren't an SQL Server instance. + + SQL service name that don't start with "MSSQL$" will not get detected. + + PS > Get-Service -Name "MSSQL`$*" + Status Name DisplayName + ------ ---- ----------- + Running MSSQL$DB SQL Server (DB) + Running MSSQL$DB01 SQL Server (DB01) + Running MSSQL$DB02 SQL Server (DB02) + +.EXAMPLE + (No Parameters) + ## EXAMPLE OUTPUT WITHOUT PARAMS ## + Status Name Instance Path + ------ ---- -------- ---- + Running SQL Server (DB01) DB01 C:\Program Files\Microsoft SQL Server\MSSQL16.DB01\MSSQL + Running SQL Server (DB02) DB02 C:\Program Files\Microsoft SQL Server\MSSQL16.DB02\MSSQL + +PARAMETER: -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" + Saves an text table to a multi-line Custom Field with a list of SQL instances. +.EXAMPLE + -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" + ## EXAMPLE OUTPUT WITH CustomFieldName ## + Status Name Instance Path + ------ ---- -------- ---- + Running SQL Server (DB01) DB01 C:\Program Files\Microsoft SQL Server\MSSQL16.DB01\MSSQL + Running SQL Server (DB02) DB02 C:\Program Files\Microsoft SQL Server\MSSQL16.DB02\MSSQL + +PARAMETER: -CustomFieldParam "ReplaceMeWithAnyWysiwygCustomField" + Saves an html table to a Wysiwyg Custom Field with a list of SQL instances. +.EXAMPLE + -WysiwygCustomFieldName "ReplaceMeWithAnyWysiwygCustomField" + ## EXAMPLE OUTPUT WITH WysiwygCustomFieldName ## + Status Name Instance Path + ------ ---- -------- ---- + Running SQL Server (DB01) DB01 C:\Program Files\Microsoft SQL Server\MSSQL16.DB01\MSSQL + Running SQL Server (DB02) DB02 C:\Program Files\Microsoft SQL Server\MSSQL16.DB02\MSSQL +.OUTPUTS + None +.NOTES + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [String]$CustomFieldName, + [String]$WysiwygCustomFieldName +) + +begin { + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if ($env:multilineCustomFieldName -and $env:multilineCustomFieldName -notlike "null") { + $CustomFieldName = $env:multilineCustomFieldName + } + if ($env:WysiwygCustomFieldName -and $env:WysiwygCustomFieldName -notlike "null") { + $WysiwygCustomFieldName = $env:WysiwygCustomFieldName + } +} +process { + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + try { + $InstanceNames = $(Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\" -ErrorAction Stop).InstalledInstances + $SqlInstances = $InstanceNames | ForEach-Object { + $SqlPath = $(Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$_\Setup" -ErrorAction Stop).SQLPath + $SqlServices = Get-Service -Name "MSSQL`$$_" -ErrorAction Stop + $SqlService = $SqlServices | Where-Object { $_.Name -notlike $SqlServices.DependentServices.Name -and $_.Name -notlike "SQLTelemetry*" } + [PSCustomObject]@{ + Status = $SqlService.Status + Service = $SqlService.DisplayName + Instance = $_ + Path = $SqlPath + } + } + } + catch { + Write-Host "[Error] $($_.Message)" + Write-Host "[Info] Likely no MSSQL instance found." + exit 1 + } + + $SqlInstances | Out-String | Write-Host + + if ($CustomFieldName) { + Write-Host "" + Write-Host "Note: Custom field '$CustomFieldName' was specified but NinjaOne integration has been removed." + Write-Host "All output has been displayed above." + } + + if ($WysiwygCustomFieldName) { + Write-Host "" + Write-Host "Note: Custom field '$WysiwygCustomFieldName' was specified but NinjaOne integration has been removed." + Write-Host "All output has been displayed above." + } + exit 0 +} +end { + +} diff --git a/Powershell Scripts/Create Desktop Shortcut - EXE.ps1 b/Powershell Scripts/Create Desktop Shortcut - EXE.ps1 index 01f7f28..9d7f61c 100644 --- a/Powershell Scripts/Create Desktop Shortcut - EXE.ps1 +++ b/Powershell Scripts/Create Desktop Shortcut - EXE.ps1 @@ -1,561 +1,559 @@ # This script creates a desktop shortcut for an executable with specified options. It can create a shortcut for all users (including new ones) or for existing users only. - -<# -.SYNOPSIS - This script creates a desktop shortcut for an executable with specified options. It can create a shortcut for all users (including new ones) or for existing users only. -.DESCRIPTION - This script creates a desktop shortcut for an executable with specified options. - It can create a shortcut for all users (including new ones) or for existing users only. - - You can also provide a base64 string on line 79 enclosed in quotes and an icon directory, and the script will use that instead. -.EXAMPLE - This will create a shortcut that opens www.google.com in Firefox on JohnSmith's desktop. This is not limited to just browsers; you can specify any executable you would normally be able to via the "Create Shortcut" menu. - - -Name "ERP App" -EXEPath "C:\Program Files\Mozilla Firefox\firefox.exe" -StartIn "C:\Program Files\Mozilla Firefox" -IconPath "C:\ProgramData\ERPapp\customicon.ico" -Arguments "https://www.google.com" -User "JohnSmith" - - Creating Shortcut at C:\Users\JohnSmith\Desktop\ERP App.lnk - -.PARAMETER NAME - The name of the shortcut, e.g., "Login Portal". - -.PARAMETER ExePath - The target field in the shortcut, excluding arguments. - -.PARAMETER Arguments - The arguments for the executable inside the shortcut. - -.PARAMETER StartIn - Some executables require that they be opened in a specific directory. - -.PARAMETER Icon - The path to an image file to use for the shortcut. You could also place the base64 string on line 79 and specify an IconDirectory with the below parameter. - -.PARAMETER IconDirectory - Path to store the .ico file to use for the shortcut. - -.PARAMETER IconURL - A link to an image file you would like to use for the shortcut. You could also place the base64 string on line 79 and specify an IconDirectory using '-IconDirectory'. - -.PARAMETER AllExistingUsers - Creates the shortcut for all existing users but not for new users, e.g., C:\Users\*\Desktop\shortcut.lnk. - -.PARAMETER AllUsers - Creates the shortcut in C:\Users\Public\Desktop. - -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 7, Windows Server 2008 - Release Notes: Split the script into three separate scripts, added script variable support, and improved icon support. -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$Name, - [Parameter()] - [String]$ExePath, - [Parameter()] - [String]$Arguments, - [Parameter()] - [String]$StartIn, - [Parameter()] - [String]$Icon, - [Parameter()] - [String]$IconDirectory, - [Parameter()] - [String]$IconUrl, - [Parameter()] - [Switch]$AllExistingUsers, - [Parameter()] - [String]$ExcludeUsers, - [Parameter()] - [Switch]$AllUsers -) - -begin { - Add-Type -AssemblyName System.Drawing - - # If the line below is replaced with $IconBase64 = 'YourBase64EncodedImageInQuotes', the script will decode it and use it for the desktop shortcut. Be sure to provide an Icon Storage Directory. - $IconBase64 = $null - - # Replace existing parameters with Form Variables if used. - if ($env:shortcutName -and $env:shortcutName -notlike "null") { $Name = $env:shortcutName } - if ($env:createTheShortcutFor -and $env:createTheShortcutFor -notlike "null") { - if ($env:createTheShortcutFor -eq "All Users") { $AllUsers = $True } - if ($env:createTheShortcutFor -eq "All Existing Users") { $AllExistingUsers = $True } - } - if ($env:exePath -and $env:exePath -notlike "null") { $ExePath = $env:exePath } - if ($env:exeArguments -and $env:exeArguments -notlike "null") { $Arguments = $env:exeArguments } - if ($env:exeShouldStartIn -and $env:exeShouldStartIn -notlike "null") { $StartIn = $env:exeShouldStartIn } - if ($env:linkToIconFile -and $env:linkToIconFile -notlike "null") { $IconUrl = $env:linkToIconFile } - if ($env:iconStorageDirectory -and $env:iconStorageDirectory -notlike "null") { $IconDirectory = $env:iconStorageDirectory } - - # Ensure a user is specified for shortcut creation. - if (-not $AllUsers -and -not $AllExistingUsers -and -not $User) { - Write-Host "[Error] You must specify which desktop to create the shortcut on!" - exit 1 - } - - $invalidFileNames = '[<>:"/\\|?*\x00-\x1F]|\.$|\s$' - if ($Name -match $invalidFileNames) { - Write-Host '[Error] The name you specified contains one of the following invalid characters or ends with a period. <>:"/\|?*' - exit 1 - } - - $ExitCode = 0 - - # Icons are secondary. If no information is given, continue without them, but notify the technician. - if (($Icon -or $IconUrl) -and -not $IconDirectory) { - Write-Warning "An icon was provided, but no storage location was specified. Use the Icon Storage Directory parameter to specify a directory to store it. (You may want this directory to be accessible by all users.)" - Write-Warning "Ignoring supplied icon info." - $ExitCode = 1 - $Icon = $null - $IconUrl = $null - } - - if ($Icon) { - $FileName = Split-Path $Icon -Leaf - - # Check for valid icon formats. Only support .png, .jpg, .jpeg, .ico, and .gif. - if ($FileName -notmatch '\.bmp$' -and $FileName -notmatch '\.png$' -and $FileName -notmatch '\.jpg$' -and $FileName -notmatch '\.jpeg$' -and $FileName -notmatch '.ico$' -and $FileName -notmatch '.gif$') { - Write-Warning "Your icon is in an invalid format. Only .png, .jpg, .jpeg, .ico, and .gif formats are supported. Switching to the default icon. You can re-run the script to replace the icon." - $Icon = $null - } - - if (-not (Test-Path $Icon -ErrorAction SilentlyContinue)) { - Write-Warning "It looks like your icon is missing. Skipping for now; re-run the script with a valid path to add the icon." - $Icon = $null - } - } - - # Create the directory for the icon if it doesn't exist. - if ($IconDirectory -and -not (Test-Path $IconDirectory -ErrorAction SilentlyContinue)) { - New-Item -ItemType Directory -Path $IconDirectory | Out-Null - } - - # For PowerShell 2.0 and 3.0 compatibility we're going to need to create a Get-FileHash function - if ($PSVersionTable.PSVersion.Major -lt 4) { - function Get-FileHash { - param ( - [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] - [string[]]$Path, - [Parameter(Mandatory = $false)] - [ValidateSet("SHA1", "SHA256", "SHA384", "SHA512", "MD5")] - [string]$Algorithm = "SHA256" - ) - $Path | ForEach-Object { - # Only hash files that exist - $CurrentPath = $_ - if ($(Test-Path -Path $CurrentPath -ErrorAction SilentlyContinue)) { - - $HashAlgorithm = [System.Security.Cryptography.HashAlgorithm]::Create($Algorithm) - $Hash = [System.BitConverter]::ToString($hashAlgorithm.ComputeHash([System.IO.File]::ReadAllBytes($CurrentPath))) - @{ - Algorithm = $Algorithm - Path = $Path - Hash = $Hash.Replace('-', '') - } - } - } - } - } - - # Convert a Base64 string to a file. - function ConvertFrom-Base64 { - param( - $Base64, - $Path - ) - $bytes = [Convert]::FromBase64String($Base64) - - [IO.File]::WriteAllBytes($Path, $bytes) - } - - # Utility function for downloading files. - function Invoke-Download { - param( - [Parameter()] - [String]$URL, - [Parameter()] - [String]$BaseName, - [Parameter()] - [int]$Attempts = 3, - [Parameter()] - [Switch]$SkipSleep - ) - - # In case 'https://' is omitted from the URL. - if ($URL -notmatch "^http(s)?://") { - Write-Warning "http(s):// is required to download the file. Adding https:// to your input...." - $URL = "https://$URL" - Write-Warning "New Url $URL." - } - - Write-Host "Downloading using $URL" - - $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType') - if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12 - } - elseif ( $SupportedTLSversions -contains 'Tls12' ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 - } - else { - # Not everything requires TLS 1.2, but we'll try anyways. - Write-Warning "TLS 1.2 and or TLS 1.3 isn't supported on this system. This download may fail!" - if ($PSVersionTable.PSVersion.Major -lt 3) { - Write-Warning "PowerShell 2 / .NET 2.0 doesn't support TLS 1.2." - } - } - - $i = 1 - While ($i -le $Attempts) { - # Some cloud services have rate-limiting - if (-not ($SkipSleep)) { - $SleepTime = Get-Random -Minimum 3 -Maximum 15 - Write-Host "Waiting for $SleepTime seconds." - Start-Sleep -Seconds $SleepTime - } - if ($i -ne 1) { Write-Host "" } - Write-Host "Download Attempt $i" - - try { - # Invoke-WebRequest is preferred because it supports links that redirect, e.g., https://t.ly - if ($PSVersionTable.PSVersion.Major -lt 4) { - # Figures out the type of file - $WebClient = New-Object System.Net.WebClient - $Response = $WebClient.OpenRead($Url) - $MimeType = $WebClient.ResponseHeaders["Content-Type"] - $DesiredExtension = switch -regex ($MimeType) { - "image/jpeg|image/jpg" { "jpg" } - "image/png" { "png" } - "image/gif" { "gif" } - "image/bmp|image/x-windows-bmp|image/x-bmp" { "bmp" } - "image/x-icon|image/vnd.microsoft.icon|application/ico" { "ico" } - default { - throw "[Error] The URL you provided does not provide a supported image type. Image Types Supported: jpg, jpeg, ico, bmp, png and gif. Image Type detected: $MimeType" - } - } - # Downloads the file preserving the extension - $Path = "$BaseName.$DesiredExtension" - $WebClient.DownloadFile($URL, $Path) - } - else { - # Standard options - $WebRequestArgs = @{ - Uri = $URL - MaximumRedirection = 10 - UseBasicParsing = $true - Method = "GET" - } - - # Figures out the type of file - $Response = Invoke-WebRequest @WebRequestArgs - $MimeType = $Response.Headers.'Content-Type' - $DesiredExtension = switch -regex ($MimeType) { - "image/jpeg|image/jpg" { "jpg" } - "image/png" { "png" } - "image/gif" { "gif" } - "image/bmp|image/x-windows-bmp|image/x-bmp" { "bmp" } - "image/x-icon|image/vnd.microsoft.icon|application/ico" { "ico" } - default { - throw "[Error] The URL you provided does not provide a supported image type. Image Types Supported: jpg, jpeg, ico, bmp, png and gif. Image Type detected: $MimeType" - } - } - # Define the path for saving the file - $Path = "$BaseName.$DesiredExtension" - - # Save the content to the file - $Response.Content | Set-Content -Path $Path -Encoding Byte - } - - $File = Test-Path -Path $Path -ErrorAction SilentlyContinue - } - catch { - Write-Warning "An error has occurred while downloading!" - Write-Warning $_.Exception.Message - $_ - - if ($Path -and (Test-Path -Path $Path -ErrorAction SilentlyContinue)) { - Remove-Item $Path -Force -Confirm:$false -ErrorAction SilentlyContinue - } - - $File = $False - } - - if ($File) { - $i = $Attempts - } - else { - Write-Warning "File failed to download. Check the link/URL and ensure it is correct, please note Ninja may have stripped out the following characters '&|;$><`!' from the link/URL." - Write-Host "" - } - - $i++ - } - - if ($Path -and -not (Test-Path $Path)) { - Write-Warning "Failed to download file!" - } - else { - return $Path - } - } - - # Convert an image to an icon file. This method creates a png and then forms an ico file by appending the png's binary. - function ConvertFrom-Image { - param( - $ImagePath, - $Path - ) - - # Grab an instance of the image and blank bitmap - try { - $image = [Drawing.Image]::FromFile($ImagePath) - } - catch [System.OutOfMemoryException] { - Write-Host "[Error] Loading Image file is either an unsupported file, or to large to process." - return - } - catch { - Write-Host "[Error] $($_.Message)" - return - } - - # Resize the image to 255px by 255px while maintaining quality. - # If you want transparency, you'll need an Alpha channel in the pixel format. - $bitmap = New-Object System.Drawing.Bitmap (255, 255, [system.drawing.imaging.PixelFormat]::Format32bppArgb) - $bitmap.SetResolution(255, 255) - - # Create a graphics object which will be used to resize the image to 255px by 255px - $graphics = [System.Drawing.Graphics]::FromImage($bitmap) - - # Set some quality settings for the resize operation - $graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality - $graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic - $graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality - - # Draw the image onto the bitmap - $graphics.DrawImage($Image, 0, 0, 255, 255) - - # Temporarily save the image as a png - $RandomNumber = Get-Random -Maximum 1000000 - $bitmap.Save("$env:TEMP\image-$RandomNumber.png", [System.Drawing.Imaging.ImageFormat]::Png) - $png = "$env:TEMP\image-$RandomNumber.png" - - # Build the ico file using the png binary. - if ($PSVersionTable.PSVersion.Major -gt 5) { - $pngBytes = Get-Content -Path $png -AsByteStream - } - elseif ($PSVersionTable.PSVersion.Major -gt 2) { - $pngBytes = Get-Content -Path $png -Encoding Byte -Raw - } - else { - $pngBytes = [System.IO.File]::ReadAllBytes($png) - } - $icoHeader = [byte[]] @(0, 0, 1, 0, 1, 0) - $imageDataSize = $pngBytes.Length - $icoDirectory = [byte[]] @( - 255, 255, # icon size - 0, 0, # color count - 0, 0, # reserved - 0, 0, # hotspot x, hotspot y - ($imageDataSize -band 0xFF), - ([Math]::Floor($imageDataSize / [Math]::Pow(2, 8)) -band 0xFF), - ([Math]::Floor($imageDataSize / [Math]::Pow(2, 16)) -band 0xFF), - ([Math]::Floor($imageDataSize / [Math]::Pow(2, 24)) -band 0xFF), - 22, 0, 0, 0 # offset to image data - ) - $iconData = $icoHeader + $icoDirectory + $pngBytes - - # Save the completed icon file and clean up any temporary files. - if (Test-Path $Path -ErrorAction SilentlyContinue) { Remove-Item $Path -Force } - [System.IO.File]::WriteAllBytes($Path, $iconData) - - if (Test-Path $png -ErrorAction SilentlyContinue) { Remove-Item $png -Force } - $bitmap.Dispose() - $image.Dispose() - $graphics.Dispose() - [System.GC]::Collect() - - # Refresh the icon cache depending on the OS version. - if ([System.Environment]::OSVersion.Version.Major -ge 10) { - Invoke-Command { ie4uinit.exe -show } - } - else { - Invoke-Command { ie4uinit.exe -ClearIconCache } - } - } - - # Verify if the script is being run with elevated privileges. - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - if (-not (Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Retrieve all registry paths for actual users (excluding system or network service accounts). - function Get-UserHives { - param ( - [Parameter()] - [ValidateSet('AzureAD', 'DomainAndLocal', 'All')] - [String]$Type = "All", - [Parameter()] - [String[]]$ExcludedUsers, - [Parameter()] - [switch]$IncludeDefault - ) - - # Different SID patterns for user account types: AzureAD, Domain, or Local. - $Patterns = switch ($Type) { - "AzureAD" { "S-1-12-1-(\d+-?){4}$" } - "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" } - "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" } - } - - # Extract user profiles that match the SID patterns. - $UserProfiles = Foreach ($Pattern in $Patterns) { - Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | - Where-Object { $_.PSChildName -match $Pattern } | - Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, - @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, - @{Name = "UserName"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } }, - @{Name = "Path"; Expression = { $_.ProfileImagePath } } - } - - # Handle situations where information from the .Default user is required. - switch ($IncludeDefault) { - $True { - $DefaultProfile = "" | Select-Object UserName, SID, UserHive, Path - $DefaultProfile.UserName = "Default" - $DefaultProfile.SID = "DefaultProfile" - $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT" - $DefaultProfile.Path = "C:\Users\Default" - - $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.UserName } - } - } - - $UserProfiles | Where-Object { $ExcludedUsers -notcontains $_.UserName } - } - - # The actual shortcut creation - function New-Shortcut { - [CmdletBinding()] - param( - [Parameter()] - [String]$Arguments, - [Parameter()] - [String]$IconPath, - [Parameter(ValueFromPipeline = $True)] - [String]$Path, - [Parameter()] - [String]$Target, - [Parameter()] - [String]$WorkingDir - ) - process { - Write-Host "Creating Shortcut at $Path" - $ShellObject = New-Object -ComObject ("WScript.Shell") - $Shortcut = $ShellObject.CreateShortcut($Path) - $Shortcut.TargetPath = $Target - if ($WorkingDir) { $Shortcut.WorkingDirectory = $WorkingDir } - if ($Arguments) { $ShortCut.Arguments = $Arguments } - if ($IconPath) { $Shortcut.IconLocation = $IconPath } - $Shortcut.Save() - - if (-not (Test-Path $Path -ErrorAction SilentlyContinue)) { - Write-Host "[Error] Unable to create Shortcut at $Path" - exit 1 - } - } - } -} -process { - $ShortcutPath = New-Object System.Collections.Generic.List[String] - - # Creating the filename's for the path - if ($Url) { $File = "$Name.url"; $Target = $Url } - if ($ExePath) { $File = "$Name.lnk"; $Target = $ExePath } - if ($RDPTarget) { $File = "$Name.rdp" } - - # Grabing the excluded users - if ($ExcludeUsers) { $ExcludedUsers = ($ExcludeUsers -split ",").trim() } - - # Building the path's and adding it to the ShortcutPath list - if ($AllUsers) { $ShortcutPath.Add("$env:Public\Desktop\$File") } - - if ($AllExistingUsers) { - $UserProfiles = Get-UserHives -ExcludedUsers $ExcludedUsers - # Loop through each user profile - $UserProfiles | ForEach-Object { $ShortcutPath.Add("$($_.Path)\Desktop\$File") } - } - - if ($User) { - $UserProfile = Get-UserHives | Where-Object { $_.Username -like $User } - $ShortcutPath.Add("$($UserProfile.Path)\Desktop\$File") - } - - $ShortcutArguments = @{ - Target = $Target - WorkingDir = $StartIn - Arguments = $Arguments - } - - # If we're given a url we'll want to download it - if ($IconUrl) { - $DownloadArguments = @{ - URL = $IconUrl - BaseName = "$IconDirectory\$Name" - } - if ($SkipSleep) { $DownloadArguments["SkipSleep"] = $True } - - $Icon = Invoke-Download @DownloadArguments - if ($Icon -and -not (Test-Path $Icon -ErrorAction SilentlyContinue)) { - $ExitCode = 1 - $Icon = $Null - $IconUrl = $Null - } - } - - # This will convert the base64 into an image and save it to the temp folder - if ($IconBase64 -and $IconDirectory -and -not $Icon -and -not $IconUrl) { - Write-Verbose "Converting Icon base64 to original image and saving to $IconDirectory..." - ConvertFrom-Base64 -Base64 $IconBase64 -Path "$IconDirectory\$Name.Png" - $Icon = "$IconDirectory\$Name.Png" - } - - if ($Icon -and (Get-Item -Path $Icon).Extension -notlike ".ico") { - $FileHash = "$((Get-FileHash -Path $Icon -Algorithm MD5).Hash)" - Write-Verbose "Converting image to icon and saving to $IconDirectory\$FileHash.ico ..." - ConvertFrom-Image -ImagePath $Icon -Path "$IconDirectory\$FileHash.ico" - Remove-Item -Path $Icon -Force - $Icon = "$IconDirectory\$FileHash.ico" - } - elseif ($Icon -and (Test-Path $Icon -ErrorAction SilentlyContinue)) { - $FileHash = "$((Get-FileHash -Path $Icon -Algorithm MD5).Hash)" - Move-Item -Path $Icon -Destination "$IconDirectory\$FileHash.ico" - $Icon = "$IconDirectory\$FileHash.ico" - } - - if ($Icon -and (Test-Path $Icon -ErrorAction SilentlyContinue)) { - $ShortcutArguments["IconPath"] = $Icon - } - elseif ($Icon) { - $ExitCode = 1 - } - - $ShortcutPath | New-Shortcut @ShortcutArguments - - exit $ExitCode -}end { - - - -} + +<# +.SYNOPSIS + This script creates a desktop shortcut for an executable with specified options. It can create a shortcut for all users (including new ones) or for existing users only. +.DESCRIPTION + This script creates a desktop shortcut for an executable with specified options. + It can create a shortcut for all users (including new ones) or for existing users only. + + You can also provide a base64 string on line 79 enclosed in quotes and an icon directory, and the script will use that instead. +.EXAMPLE + This will create a shortcut that opens www.google.com in Firefox on JohnSmith's desktop. This is not limited to just browsers; you can specify any executable you would normally be able to via the "Create Shortcut" menu. + + -Name "ERP App" -EXEPath "C:\Program Files\Mozilla Firefox\firefox.exe" -StartIn "C:\Program Files\Mozilla Firefox" -IconPath "C:\ProgramData\ERPapp\customicon.ico" -Arguments "https://www.google.com" -User "JohnSmith" + + Creating Shortcut at C:\Users\JohnSmith\Desktop\ERP App.lnk + +.PARAMETER NAME + The name of the shortcut, e.g., "Login Portal". + +.PARAMETER ExePath + The target field in the shortcut, excluding arguments. + +.PARAMETER Arguments + The arguments for the executable inside the shortcut. + +.PARAMETER StartIn + Some executables require that they be opened in a specific directory. + +.PARAMETER Icon + The path to an image file to use for the shortcut. You could also place the base64 string on line 79 and specify an IconDirectory with the below parameter. + +.PARAMETER IconDirectory + Path to store the .ico file to use for the shortcut. + +.PARAMETER IconURL + A link to an image file you would like to use for the shortcut. You could also place the base64 string on line 79 and specify an IconDirectory using '-IconDirectory'. + +.PARAMETER AllExistingUsers + Creates the shortcut for all existing users but not for new users, e.g., C:\Users\*\Desktop\shortcut.lnk. + +.PARAMETER AllUsers + Creates the shortcut in C:\Users\Public\Desktop. + +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 7, Windows Server 2008 + Release Notes: Split the script into three separate scripts, added script variable support, and improved icon support. +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$Name, + [Parameter()] + [String]$ExePath, + [Parameter()] + [String]$Arguments, + [Parameter()] + [String]$StartIn, + [Parameter()] + [String]$Icon, + [Parameter()] + [String]$IconDirectory, + [Parameter()] + [String]$IconUrl, + [Parameter()] + [Switch]$AllExistingUsers, + [Parameter()] + [String]$ExcludeUsers, + [Parameter()] + [Switch]$AllUsers +) + +begin { + Add-Type -AssemblyName System.Drawing + + # If the line below is replaced with $IconBase64 = 'YourBase64EncodedImageInQuotes', the script will decode it and use it for the desktop shortcut. Be sure to provide an Icon Storage Directory. + $IconBase64 = $null + + # Replace existing parameters with Form Variables if used. + if ($env:shortcutName -and $env:shortcutName -notlike "null") { $Name = $env:shortcutName } + if ($env:createTheShortcutFor -and $env:createTheShortcutFor -notlike "null") { + if ($env:createTheShortcutFor -eq "All Users") { $AllUsers = $True } + if ($env:createTheShortcutFor -eq "All Existing Users") { $AllExistingUsers = $True } + } + if ($env:exePath -and $env:exePath -notlike "null") { $ExePath = $env:exePath } + if ($env:exeArguments -and $env:exeArguments -notlike "null") { $Arguments = $env:exeArguments } + if ($env:exeShouldStartIn -and $env:exeShouldStartIn -notlike "null") { $StartIn = $env:exeShouldStartIn } + if ($env:linkToIconFile -and $env:linkToIconFile -notlike "null") { $IconUrl = $env:linkToIconFile } + if ($env:iconStorageDirectory -and $env:iconStorageDirectory -notlike "null") { $IconDirectory = $env:iconStorageDirectory } + + # Ensure a user is specified for shortcut creation. + if (-not $AllUsers -and -not $AllExistingUsers -and -not $User) { + Write-Host "[Error] You must specify which desktop to create the shortcut on!" + exit 1 + } + + $invalidFileNames = '[<>:"/\\|?*\x00-\x1F]|\.$|\s$' + if ($Name -match $invalidFileNames) { + Write-Host '[Error] The name you specified contains one of the following invalid characters or ends with a period. <>:"/\|?*' + exit 1 + } + + $ExitCode = 0 + + # Icons are secondary. If no information is given, continue without them, but notify the technician. + if (($Icon -or $IconUrl) -and -not $IconDirectory) { + Write-Warning "An icon was provided, but no storage location was specified. Use the Icon Storage Directory parameter to specify a directory to store it. (You may want this directory to be accessible by all users.)" + Write-Warning "Ignoring supplied icon info." + $ExitCode = 1 + $Icon = $null + $IconUrl = $null + } + + if ($Icon) { + $FileName = Split-Path $Icon -Leaf + + # Check for valid icon formats. Only support .png, .jpg, .jpeg, .ico, and .gif. + if ($FileName -notmatch '\.bmp$' -and $FileName -notmatch '\.png$' -and $FileName -notmatch '\.jpg$' -and $FileName -notmatch '\.jpeg$' -and $FileName -notmatch '.ico$' -and $FileName -notmatch '.gif$') { + Write-Warning "Your icon is in an invalid format. Only .png, .jpg, .jpeg, .ico, and .gif formats are supported. Switching to the default icon. You can re-run the script to replace the icon." + $Icon = $null + } + + if (-not (Test-Path $Icon -ErrorAction SilentlyContinue)) { + Write-Warning "It looks like your icon is missing. Skipping for now; re-run the script with a valid path to add the icon." + $Icon = $null + } + } + + # Create the directory for the icon if it doesn't exist. + if ($IconDirectory -and -not (Test-Path $IconDirectory -ErrorAction SilentlyContinue)) { + New-Item -ItemType Directory -Path $IconDirectory | Out-Null + } + + # For PowerShell 2.0 and 3.0 compatibility we're going to need to create a Get-FileHash function + if ($PSVersionTable.PSVersion.Major -lt 4) { + function Get-FileHash { + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [string[]]$Path, + [Parameter(Mandatory = $false)] + [ValidateSet("SHA1", "SHA256", "SHA384", "SHA512", "MD5")] + [string]$Algorithm = "SHA256" + ) + $Path | ForEach-Object { + # Only hash files that exist + $CurrentPath = $_ + if ($(Test-Path -Path $CurrentPath -ErrorAction SilentlyContinue)) { + + $HashAlgorithm = [System.Security.Cryptography.HashAlgorithm]::Create($Algorithm) + $Hash = [System.BitConverter]::ToString($hashAlgorithm.ComputeHash([System.IO.File]::ReadAllBytes($CurrentPath))) + @{ + Algorithm = $Algorithm + Path = $Path + Hash = $Hash.Replace('-', '') + } + } + } + } + } + + # Convert a Base64 string to a file. + function ConvertFrom-Base64 { + param( + $Base64, + $Path + ) + $bytes = [Convert]::FromBase64String($Base64) + + [IO.File]::WriteAllBytes($Path, $bytes) + } + + # Utility function for downloading files. + function Invoke-Download { + param( + [Parameter()] + [String]$URL, + [Parameter()] + [String]$BaseName, + [Parameter()] + [int]$Attempts = 3, + [Parameter()] + [Switch]$SkipSleep + ) + + # In case 'https://' is omitted from the URL. + if ($URL -notmatch "^http(s)?://") { + Write-Warning "http(s):// is required to download the file. Adding https:// to your input...." + $URL = "https://$URL" + Write-Warning "New Url $URL." + } + + Write-Host "Downloading using $URL" + + $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType') + if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12 + } + elseif ( $SupportedTLSversions -contains 'Tls12' ) { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 + } + else { + # Not everything requires TLS 1.2, but we'll try anyways. + Write-Warning "TLS 1.2 and or TLS 1.3 isn't supported on this system. This download may fail!" + if ($PSVersionTable.PSVersion.Major -lt 3) { + Write-Warning "PowerShell 2 / .NET 2.0 doesn't support TLS 1.2." + } + } + + $i = 1 + While ($i -le $Attempts) { + # Some cloud services have rate-limiting + if (-not ($SkipSleep)) { + $SleepTime = Get-Random -Minimum 3 -Maximum 15 + Write-Host "Waiting for $SleepTime seconds." + Start-Sleep -Seconds $SleepTime + } + if ($i -ne 1) { Write-Host "" } + Write-Host "Download Attempt $i" + + try { + # Invoke-WebRequest is preferred because it supports links that redirect, e.g., https://t.ly + if ($PSVersionTable.PSVersion.Major -lt 4) { + # Figures out the type of file + $WebClient = New-Object System.Net.WebClient + $Response = $WebClient.OpenRead($Url) + $MimeType = $WebClient.ResponseHeaders["Content-Type"] + $DesiredExtension = switch -regex ($MimeType) { + "image/jpeg|image/jpg" { "jpg" } + "image/png" { "png" } + "image/gif" { "gif" } + "image/bmp|image/x-windows-bmp|image/x-bmp" { "bmp" } + "image/x-icon|image/vnd.microsoft.icon|application/ico" { "ico" } + default { + throw "[Error] The URL you provided does not provide a supported image type. Image Types Supported: jpg, jpeg, ico, bmp, png and gif. Image Type detected: $MimeType" + } + } + # Downloads the file preserving the extension + $Path = "$BaseName.$DesiredExtension" + $WebClient.DownloadFile($URL, $Path) + } + else { + # Standard options + $WebRequestArgs = @{ + Uri = $URL + MaximumRedirection = 10 + UseBasicParsing = $true + Method = "GET" + } + + # Figures out the type of file + $Response = Invoke-WebRequest @WebRequestArgs + $MimeType = $Response.Headers.'Content-Type' + $DesiredExtension = switch -regex ($MimeType) { + "image/jpeg|image/jpg" { "jpg" } + "image/png" { "png" } + "image/gif" { "gif" } + "image/bmp|image/x-windows-bmp|image/x-bmp" { "bmp" } + "image/x-icon|image/vnd.microsoft.icon|application/ico" { "ico" } + default { + throw "[Error] The URL you provided does not provide a supported image type. Image Types Supported: jpg, jpeg, ico, bmp, png and gif. Image Type detected: $MimeType" + } + } + # Define the path for saving the file + $Path = "$BaseName.$DesiredExtension" + + # Save the content to the file + $Response.Content | Set-Content -Path $Path -Encoding Byte + } + + $File = Test-Path -Path $Path -ErrorAction SilentlyContinue + } + catch { + Write-Warning "An error has occurred while downloading!" + Write-Warning $_.Exception.Message + $_ + + if ($Path -and (Test-Path -Path $Path -ErrorAction SilentlyContinue)) { + Remove-Item $Path -Force -Confirm:$false -ErrorAction SilentlyContinue + } + + $File = $False + } + + if ($File) { + $i = $Attempts + } + else { + Write-Warning "File failed to download. Check the link/URL and ensure it is correct, please note Ninja may have stripped out the following characters '&|;$><`!' from the link/URL." + Write-Host "" + } + + $i++ + } + + if ($Path -and -not (Test-Path $Path)) { + Write-Warning "Failed to download file!" + } + else { + return $Path + } + } + + # Convert an image to an icon file. This method creates a png and then forms an ico file by appending the png's binary. + function ConvertFrom-Image { + param( + $ImagePath, + $Path + ) + + # Grab an instance of the image and blank bitmap + try { + $image = [Drawing.Image]::FromFile($ImagePath) + } + catch [System.OutOfMemoryException] { + Write-Host "[Error] Loading Image file is either an unsupported file, or to large to process." + return + } + catch { + Write-Host "[Error] $($_.Message)" + return + } + + # Resize the image to 255px by 255px while maintaining quality. + # If you want transparency, you'll need an Alpha channel in the pixel format. + $bitmap = New-Object System.Drawing.Bitmap (255, 255, [system.drawing.imaging.PixelFormat]::Format32bppArgb) + $bitmap.SetResolution(255, 255) + + # Create a graphics object which will be used to resize the image to 255px by 255px + $graphics = [System.Drawing.Graphics]::FromImage($bitmap) + + # Set some quality settings for the resize operation + $graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality + $graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic + $graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality + + # Draw the image onto the bitmap + $graphics.DrawImage($Image, 0, 0, 255, 255) + + # Temporarily save the image as a png + $RandomNumber = Get-Random -Maximum 1000000 + $bitmap.Save("$env:TEMP\image-$RandomNumber.png", [System.Drawing.Imaging.ImageFormat]::Png) + $png = "$env:TEMP\image-$RandomNumber.png" + + # Build the ico file using the png binary. + if ($PSVersionTable.PSVersion.Major -gt 5) { + $pngBytes = Get-Content -Path $png -AsByteStream + } + elseif ($PSVersionTable.PSVersion.Major -gt 2) { + $pngBytes = Get-Content -Path $png -Encoding Byte -Raw + } + else { + $pngBytes = [System.IO.File]::ReadAllBytes($png) + } + $icoHeader = [byte[]] @(0, 0, 1, 0, 1, 0) + $imageDataSize = $pngBytes.Length + $icoDirectory = [byte[]] @( + 255, 255, # icon size + 0, 0, # color count + 0, 0, # reserved + 0, 0, # hotspot x, hotspot y + ($imageDataSize -band 0xFF), + ([Math]::Floor($imageDataSize / [Math]::Pow(2, 8)) -band 0xFF), + ([Math]::Floor($imageDataSize / [Math]::Pow(2, 16)) -band 0xFF), + ([Math]::Floor($imageDataSize / [Math]::Pow(2, 24)) -band 0xFF), + 22, 0, 0, 0 # offset to image data + ) + $iconData = $icoHeader + $icoDirectory + $pngBytes + + # Save the completed icon file and clean up any temporary files. + if (Test-Path $Path -ErrorAction SilentlyContinue) { Remove-Item $Path -Force } + [System.IO.File]::WriteAllBytes($Path, $iconData) + + if (Test-Path $png -ErrorAction SilentlyContinue) { Remove-Item $png -Force } + $bitmap.Dispose() + $image.Dispose() + $graphics.Dispose() + [System.GC]::Collect() + + # Refresh the icon cache depending on the OS version. + if ([System.Environment]::OSVersion.Version.Major -ge 10) { + Invoke-Command { ie4uinit.exe -show } + } + else { + Invoke-Command { ie4uinit.exe -ClearIconCache } + } + } + + # Verify if the script is being run with elevated privileges. + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if (-not (Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Retrieve all registry paths for actual users (excluding system or network service accounts). + function Get-UserHives { + param ( + [Parameter()] + [ValidateSet('AzureAD', 'DomainAndLocal', 'All')] + [String]$Type = "All", + [Parameter()] + [String[]]$ExcludedUsers, + [Parameter()] + [switch]$IncludeDefault + ) + + # Different SID patterns for user account types: AzureAD, Domain, or Local. + $Patterns = switch ($Type) { + "AzureAD" { "S-1-12-1-(\d+-?){4}$" } + "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" } + "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" } + } + + # Extract user profiles that match the SID patterns. + $UserProfiles = Foreach ($Pattern in $Patterns) { + Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | + Where-Object { $_.PSChildName -match $Pattern } | + Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, + @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, + @{Name = "UserName"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } }, + @{Name = "Path"; Expression = { $_.ProfileImagePath } } + } + + # Handle situations where information from the .Default user is required. + switch ($IncludeDefault) { + $True { + $DefaultProfile = "" | Select-Object UserName, SID, UserHive, Path + $DefaultProfile.UserName = "Default" + $DefaultProfile.SID = "DefaultProfile" + $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT" + $DefaultProfile.Path = "C:\Users\Default" + + $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.UserName } + } + } + + $UserProfiles | Where-Object { $ExcludedUsers -notcontains $_.UserName } + } + + # The actual shortcut creation + function New-Shortcut { + [CmdletBinding()] + param( + [Parameter()] + [String]$Arguments, + [Parameter()] + [String]$IconPath, + [Parameter(ValueFromPipeline = $True)] + [String]$Path, + [Parameter()] + [String]$Target, + [Parameter()] + [String]$WorkingDir + ) + process { + Write-Host "Creating Shortcut at $Path" + $ShellObject = New-Object -ComObject ("WScript.Shell") + $Shortcut = $ShellObject.CreateShortcut($Path) + $Shortcut.TargetPath = $Target + if ($WorkingDir) { $Shortcut.WorkingDirectory = $WorkingDir } + if ($Arguments) { $ShortCut.Arguments = $Arguments } + if ($IconPath) { $Shortcut.IconLocation = $IconPath } + $Shortcut.Save() + + if (-not (Test-Path $Path -ErrorAction SilentlyContinue)) { + Write-Host "[Error] Unable to create Shortcut at $Path" + exit 1 + } + } + } +} +process { + $ShortcutPath = New-Object System.Collections.Generic.List[String] + + # Creating the filename's for the path + if ($Url) { $File = "$Name.url"; $Target = $Url } + if ($ExePath) { $File = "$Name.lnk"; $Target = $ExePath } + if ($RDPTarget) { $File = "$Name.rdp" } + + # Grabing the excluded users + if ($ExcludeUsers) { $ExcludedUsers = ($ExcludeUsers -split ",").trim() } + + # Building the path's and adding it to the ShortcutPath list + if ($AllUsers) { $ShortcutPath.Add("$env:Public\Desktop\$File") } + + if ($AllExistingUsers) { + $UserProfiles = Get-UserHives -ExcludedUsers $ExcludedUsers + # Loop through each user profile + $UserProfiles | ForEach-Object { $ShortcutPath.Add("$($_.Path)\Desktop\$File") } + } + + if ($User) { + $UserProfile = Get-UserHives | Where-Object { $_.Username -like $User } + $ShortcutPath.Add("$($UserProfile.Path)\Desktop\$File") + } + + $ShortcutArguments = @{ + Target = $Target + WorkingDir = $StartIn + Arguments = $Arguments + } + + # If we're given a url we'll want to download it + if ($IconUrl) { + $DownloadArguments = @{ + URL = $IconUrl + BaseName = "$IconDirectory\$Name" + } + if ($SkipSleep) { $DownloadArguments["SkipSleep"] = $True } + + $Icon = Invoke-Download @DownloadArguments + if ($Icon -and -not (Test-Path $Icon -ErrorAction SilentlyContinue)) { + $ExitCode = 1 + $Icon = $Null + $IconUrl = $Null + } + } + + # This will convert the base64 into an image and save it to the temp folder + if ($IconBase64 -and $IconDirectory -and -not $Icon -and -not $IconUrl) { + Write-Verbose "Converting Icon base64 to original image and saving to $IconDirectory..." + ConvertFrom-Base64 -Base64 $IconBase64 -Path "$IconDirectory\$Name.Png" + $Icon = "$IconDirectory\$Name.Png" + } + + if ($Icon -and (Get-Item -Path $Icon).Extension -notlike ".ico") { + $FileHash = "$((Get-FileHash -Path $Icon -Algorithm MD5).Hash)" + Write-Verbose "Converting image to icon and saving to $IconDirectory\$FileHash.ico ..." + ConvertFrom-Image -ImagePath $Icon -Path "$IconDirectory\$FileHash.ico" + Remove-Item -Path $Icon -Force + $Icon = "$IconDirectory\$FileHash.ico" + } + elseif ($Icon -and (Test-Path $Icon -ErrorAction SilentlyContinue)) { + $FileHash = "$((Get-FileHash -Path $Icon -Algorithm MD5).Hash)" + Move-Item -Path $Icon -Destination "$IconDirectory\$FileHash.ico" + $Icon = "$IconDirectory\$FileHash.ico" + } + + if ($Icon -and (Test-Path $Icon -ErrorAction SilentlyContinue)) { + $ShortcutArguments["IconPath"] = $Icon + } + elseif ($Icon) { + $ExitCode = 1 + } + + $ShortcutPath | New-Shortcut @ShortcutArguments + + exit $ExitCode +}end { + +} diff --git a/Powershell Scripts/Create Desktop Shortcut - RDP.ps1 b/Powershell Scripts/Create Desktop Shortcut - RDP.ps1 index a76c2c3..61387b8 100644 --- a/Powershell Scripts/Create Desktop Shortcut - RDP.ps1 +++ b/Powershell Scripts/Create Desktop Shortcut - RDP.ps1 @@ -1,291 +1,289 @@ # This script will create an rdp desktop shortcut with your specified options. It can create a shortcut for all users (including new ones) or existing ones only. - -<# -.SYNOPSIS - This script will create an rdp desktop shortcut with your specified options. It can create a shortcut for all users (including new ones) or existing ones only. -.DESCRIPTION - This script will create an rdp desktop shortcut with your specified options. - It can create a shortcut for all users (including new ones) or existing ones only. -.EXAMPLE - To Create a windowed RDP Shortcut simply specify the size, the name of the shortcut and which users the shortcut is for. You can also specify "MultiMon" for multi-monitor support. Or a gateway to use. - - PS C:\> ./Create-DesktopShortcut.ps1 -Name "Test" -RDPTarget "SRV19-TEST" -RDPUser "TEST\jsmith" -Width "1920" -Height "1080" -AllExistingUsers -ExcludeUsers "ChrisWashington,JohnLocke" - - Creating Shortcut at C:\Users\JohnSmith\Desktop\Test.rdp - -.PARAMETER NAME - Name of the shortcut ex. "Login Portal". - -.PARAMETER RDPtarget - IP Address or DNS Name and port to the RDS Host ex. "TEST-RDSH:28665". - -.PARAMETER RDPuser - Username to autofill in username field. - -.PARAMETER AlwaysPrompt - Always Prompt for credentials. - -.PARAMETER Gateway - IP Address or DNS Name and port of the RD Gateway ex. "TESTrdp.example.com:4433". - -.PARAMETER SeperateGateWayCreds - If the RDS Gateway uses different creds than the Session Host use this parameter. - -.PARAMETER FullScreen - RDP Shortcut should open window in 'FullScreen' mode. - -.PARAMETER MultiMon - RDP Shortcut should open window with Multi-Monitor Support enabled. - -.PARAMETER Width - Width of RDP Window should open ex. "1920". - -.PARAMETER Height - Height of RDP Window shortcut should open ex. "1080". - -.PARAMETER AllExistingUsers - Create the Shortcut for all existing users but not new users ex. C:\Users\*\Desktop\shortcut.lnk. - -.PARAMETER ExcludeUsers - Comma seperated list of users to exclude from shortcut placement. - -.PARAMETER AllUsers - Create the Shortcut in C:\Users\Public\Desktop. - -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 7, Windows Server 2008 - Release Notes: Renamed script, Split script into three, added Script Variable support, fixed bugs in RDP Shortcut -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$Name, - [Parameter()] - [String]$RDPtarget, - [Parameter()] - [String]$RDPuser, - [Parameter()] - [Switch]$AlwaysPrompt = [System.Convert]::ToBoolean($env:alwaysPromptForRdpCredentials), - [Parameter()] - [String]$Gateway, - [Parameter()] - [Switch]$SeparateGateWayCreds = [System.Convert]::ToBoolean($env:separateRdpGatewayCredentials), - [Parameter()] - [Switch]$FullScreen, - [Parameter()] - [Switch]$MultiMon, - [Parameter()] - [Int]$Width, - [Parameter()] - [Int]$Height, - [Parameter()] - [Switch]$AllExistingUsers, - [Parameter()] - [Switch]$AllUsers -) - -begin { - - # Replace existing params with form variables if they're used. - if ($env:shortcutName -and $env:shortcutName -notlike "null") { $Name = $env:shortcutName } - if ($env:createTheShortcutFor -and $env:createTheShortcutFor -notlike "null") { - if ($env:createTheShortcutFor -eq "All Users") { $AllUsers = $True } - if ($env:createTheShortcutFor -eq "All Existing Users") { $AllExistingUsers = $True } - } - if ($env:rdpServerAddress -and $env:rdpServerAddress -notlike "null") { $RDPtarget = $env:rdpServerAddress } - if ($env:rdpUsername -and $env:rdpUsername -notlike "null") { $RDPuser = $env:rdpUsername } - if ($env:rdpGatewayServerAddress -and $env:rdpGatewayServerAddress -notlike "null") { $Gateway = $env:rdpGatewayServerAddress } - if ($env:rdpWindowSize -and $env:rdpWindowSize -notlike "null") { - if ($env:rdpWindowSize -eq "Fullscreen Multiple Monitor Mode") { $MultiMon = $True } - if ($env:rdpWindowSize -eq "Fullscreen") { $FullScreen = $True } - } - if ($env:customRdpWindowWidth -and $env:customRdpWindowWidth -notlike "null") { $Width = $env:customRdpWindowWidth } - if ($env:customRdpWindowHeight -and $env:customRdpWindowHeight -notlike "null") { $Height = $env:customRdpWindowHeight } - - # Output warnings for conflicting options. - if (($Width -and -not $Height ) -or ($Height -and -not $Width)) { - Write-Warning "You forgot to include both the width and height. RDP Window will be in fullscreen mode." - } - - if (($Width -or $Height) -and ($FullScreen -or $MultiMon)) { - if ($MultiMon) { - Write-Warning "Conflicting Display Option selected. Using Fullscreen Multi-monitor." - } - else { - Write-Warning "Conflicting Display Option selected. Using Fullscreen." - } - } - - # Double-check that a user is specified for shortcut creation. - if (-not $AllUsers -and -not $AllExistingUsers -and -not $User) { - Write-Error "You must specify which desktop to create the shortcut on!" - exit 1 - } - - # Double-check that a shortcut name was provided. - if (-not $Name -or -not $RDPtarget) { - Write-Error "You must specify a name and target for the shortcut!" - exit 1 - } - - # Creating a shortcut at C:\Users\Public\Desktop requires admin rights. - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - if (!(Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Retrieve all registry paths for actual users (excluding system or network service accounts). - function Get-UserHives { - param ( - [Parameter()] - [ValidateSet('AzureAD', 'DomainAndLocal', 'All')] - [String]$Type = "All", - [Parameter()] - [String[]]$ExcludedUsers, - [Parameter()] - [switch]$IncludeDefault - ) - - # User account SIDs follow a particular pattern depending on whether they're Azure AD, Domain, or local "workgroup" accounts. - $Patterns = switch ($Type) { - "AzureAD" { "S-1-12-1-(\d+-?){4}$" } - "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" } - "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" } - } - - # We'll need the NTuser.dat file to load each users registry hive. So we grab it if their account sid matches the above pattern. - $UserProfiles = Foreach ($Pattern in $Patterns) { - Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | - Where-Object { $_.PSChildName -match $Pattern } | - Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, - @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, - @{Name = "UserName"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } }, - @{Name = "Path"; Expression = { $_.ProfileImagePath } } - } - - # In some cases, it's necessary to retrieve the .Default user's information. - switch ($IncludeDefault) { - $True { - $DefaultProfile = "" | Select-Object UserName, SID, UserHive, Path - $DefaultProfile.UserName = "Default" - $DefaultProfile.SID = "DefaultProfile" - $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT" - $DefaultProfile.Path = "C:\Users\Default" - - $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.UserName } - } - } - - $UserProfiles | Where-Object { $ExcludedUsers -notcontains $_.UserName } - } -} -process { - $ShortcutPath = New-Object System.Collections.Generic.List[String] - - # Create the filenames for the path. - if ($RDPTarget) { $File = "$Name.rdp" } - - # Build the paths and add them to the ShortcutPath list. - if ($AllUsers) { $ShortcutPath.Add("$env:Public\Desktop\$File") } - - if ($AllExistingUsers) { - $UserProfiles = Get-UserHives - # Loop through each user profile - $UserProfiles | ForEach-Object { $ShortcutPath.Add("$($_.Path)\Desktop\$File") } - } - - if ($User) { - $UserProfile = Get-UserHives | Where-Object { $_.Username -like $User } - $ShortcutPath.Add("$($UserProfile.Path)\Desktop\$File") - } - - $RDPFile = New-Object System.Collections.Generic.List[String] - - # Base template of an .RDP file. Additional options will be appended based on user selection. - $Template = @" -session bpp:i:32 -compression:i:1 -keyboardhook:i:2 -audiocapturemode:i:0 -videoplaybackmode:i:1 -connection type:i:7 -networkautodetect:i:1 -bandwidthautodetect:i:1 -displayconnectionbar:i:1 -enableworkspacereconnect:i:0 -disable wallpaper:i:0 -allow font smoothing:i:0 -allow desktop composition:i:0 -disable full window drag:i:1 -disable menu anims:i:1 -disable themes:i:0 -disable cursor setting:i:0 -bitmapcachepersistenable:i:1 -audiomode:i:0 -redirectprinters:i:1 -redirectcomports:i:0 -redirectsmartcards:i:1 -redirectwebauthn:i:1 -redirectclipboard:i:1 -redirectposdevices:i:0 -autoreconnection enabled:i:1 -authentication level:i:2 -negotiate security layer:i:1 -remoteapplicationmode:i:0 -alternate shell:s: -shell working directory:s: -gatewaycredentialssource:i:4 -gatewaybrokeringtype:i:0 -use redirection server name:i:0 -rdgiskdcproxy:i:0 -kdcproxyname:s: -enablerdsaadauth:i:0 -"@ - $RDPFile.Add($Template) - - # This will generate the actual .rdp file - $ShortcutPath | ForEach-Object { - $RDPFile.Add("full address:s:$RDPTarget") - $RDPFile.Add("gatewayhostname:s:$Gateway") - - if ($Width) { $RDPFile.Add("desktopwidth:i:$Width") } - if ($Height) { $RDPFile.Add("desktopheight:i:$Height") } - if ($MultiMon) { $RDPFile.Add("use multimon:i:1") }else { $RDPFile.Add("use multimon:i:0") } - if ($FullScreen -or $MultiMon -or !$Height -or !$Width) { $RDPFile.Add("screen mode id:i:2") }else { $RDPFile.Add("screen mode id:i:1") } - if ($AlwaysPrompt) { $RDPFile.Add("prompt for credentials:i:1") }else { $RDPFile.Add("prompt for credentials:i:0") } - if ($Gateway) { $RDPFile.Add("gatewayusagemethod:i:2") }else { $RDPFile.Add("gatewayusagemethod:i:4") } - if ($SeparateGateWayCreds) { - $RDPFile.Add("promptcredentialonce:i:0") - $RDPFile.Add("gatewayprofileusagemethod:i:1") - } - else { - $RDPFile.Add("promptcredentialonce:i:1") - if ($Gateway) { $RDPFile.Add("gatewayprofileusagemethod:i:0") } - } - - if ($RDPUser) { $RDPFile.Add("username:s:$RDPUser") } - - Write-Host "Creating Shortcut at $_" - $RDPFile | Out-File $_ - - if (!(Test-Path $_ -ErrorAction SilentlyContinue)) { - Write-Error "Unable to create Shortcut at $_" - exit 1 - } - } - - exit 0 -}end { - - - -} + +<# +.SYNOPSIS + This script will create an rdp desktop shortcut with your specified options. It can create a shortcut for all users (including new ones) or existing ones only. +.DESCRIPTION + This script will create an rdp desktop shortcut with your specified options. + It can create a shortcut for all users (including new ones) or existing ones only. +.EXAMPLE + To Create a windowed RDP Shortcut simply specify the size, the name of the shortcut and which users the shortcut is for. You can also specify "MultiMon" for multi-monitor support. Or a gateway to use. + + PS C:\> ./Create-DesktopShortcut.ps1 -Name "Test" -RDPTarget "SRV19-TEST" -RDPUser "TEST\jsmith" -Width "1920" -Height "1080" -AllExistingUsers -ExcludeUsers "ChrisWashington,JohnLocke" + + Creating Shortcut at C:\Users\JohnSmith\Desktop\Test.rdp + +.PARAMETER NAME + Name of the shortcut ex. "Login Portal". + +.PARAMETER RDPtarget + IP Address or DNS Name and port to the RDS Host ex. "TEST-RDSH:28665". + +.PARAMETER RDPuser + Username to autofill in username field. + +.PARAMETER AlwaysPrompt + Always Prompt for credentials. + +.PARAMETER Gateway + IP Address or DNS Name and port of the RD Gateway ex. "TESTrdp.example.com:4433". + +.PARAMETER SeperateGateWayCreds + If the RDS Gateway uses different creds than the Session Host use this parameter. + +.PARAMETER FullScreen + RDP Shortcut should open window in 'FullScreen' mode. + +.PARAMETER MultiMon + RDP Shortcut should open window with Multi-Monitor Support enabled. + +.PARAMETER Width + Width of RDP Window should open ex. "1920". + +.PARAMETER Height + Height of RDP Window shortcut should open ex. "1080". + +.PARAMETER AllExistingUsers + Create the Shortcut for all existing users but not new users ex. C:\Users\*\Desktop\shortcut.lnk. + +.PARAMETER ExcludeUsers + Comma seperated list of users to exclude from shortcut placement. + +.PARAMETER AllUsers + Create the Shortcut in C:\Users\Public\Desktop. + +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 7, Windows Server 2008 + Release Notes: Renamed script, Split script into three, added Script Variable support, fixed bugs in RDP Shortcut +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$Name, + [Parameter()] + [String]$RDPtarget, + [Parameter()] + [String]$RDPuser, + [Parameter()] + [Switch]$AlwaysPrompt = [System.Convert]::ToBoolean($env:alwaysPromptForRdpCredentials), + [Parameter()] + [String]$Gateway, + [Parameter()] + [Switch]$SeparateGateWayCreds = [System.Convert]::ToBoolean($env:separateRdpGatewayCredentials), + [Parameter()] + [Switch]$FullScreen, + [Parameter()] + [Switch]$MultiMon, + [Parameter()] + [Int]$Width, + [Parameter()] + [Int]$Height, + [Parameter()] + [Switch]$AllExistingUsers, + [Parameter()] + [Switch]$AllUsers +) + +begin { + + # Replace existing params with form variables if they're used. + if ($env:shortcutName -and $env:shortcutName -notlike "null") { $Name = $env:shortcutName } + if ($env:createTheShortcutFor -and $env:createTheShortcutFor -notlike "null") { + if ($env:createTheShortcutFor -eq "All Users") { $AllUsers = $True } + if ($env:createTheShortcutFor -eq "All Existing Users") { $AllExistingUsers = $True } + } + if ($env:rdpServerAddress -and $env:rdpServerAddress -notlike "null") { $RDPtarget = $env:rdpServerAddress } + if ($env:rdpUsername -and $env:rdpUsername -notlike "null") { $RDPuser = $env:rdpUsername } + if ($env:rdpGatewayServerAddress -and $env:rdpGatewayServerAddress -notlike "null") { $Gateway = $env:rdpGatewayServerAddress } + if ($env:rdpWindowSize -and $env:rdpWindowSize -notlike "null") { + if ($env:rdpWindowSize -eq "Fullscreen Multiple Monitor Mode") { $MultiMon = $True } + if ($env:rdpWindowSize -eq "Fullscreen") { $FullScreen = $True } + } + if ($env:customRdpWindowWidth -and $env:customRdpWindowWidth -notlike "null") { $Width = $env:customRdpWindowWidth } + if ($env:customRdpWindowHeight -and $env:customRdpWindowHeight -notlike "null") { $Height = $env:customRdpWindowHeight } + + # Output warnings for conflicting options. + if (($Width -and -not $Height ) -or ($Height -and -not $Width)) { + Write-Warning "You forgot to include both the width and height. RDP Window will be in fullscreen mode." + } + + if (($Width -or $Height) -and ($FullScreen -or $MultiMon)) { + if ($MultiMon) { + Write-Warning "Conflicting Display Option selected. Using Fullscreen Multi-monitor." + } + else { + Write-Warning "Conflicting Display Option selected. Using Fullscreen." + } + } + + # Double-check that a user is specified for shortcut creation. + if (-not $AllUsers -and -not $AllExistingUsers -and -not $User) { + Write-Error "You must specify which desktop to create the shortcut on!" + exit 1 + } + + # Double-check that a shortcut name was provided. + if (-not $Name -or -not $RDPtarget) { + Write-Error "You must specify a name and target for the shortcut!" + exit 1 + } + + # Creating a shortcut at C:\Users\Public\Desktop requires admin rights. + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if (!(Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Retrieve all registry paths for actual users (excluding system or network service accounts). + function Get-UserHives { + param ( + [Parameter()] + [ValidateSet('AzureAD', 'DomainAndLocal', 'All')] + [String]$Type = "All", + [Parameter()] + [String[]]$ExcludedUsers, + [Parameter()] + [switch]$IncludeDefault + ) + + # User account SIDs follow a particular pattern depending on whether they're Azure AD, Domain, or local "workgroup" accounts. + $Patterns = switch ($Type) { + "AzureAD" { "S-1-12-1-(\d+-?){4}$" } + "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" } + "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" } + } + + # We'll need the NTuser.dat file to load each users registry hive. So we grab it if their account sid matches the above pattern. + $UserProfiles = Foreach ($Pattern in $Patterns) { + Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | + Where-Object { $_.PSChildName -match $Pattern } | + Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, + @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, + @{Name = "UserName"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } }, + @{Name = "Path"; Expression = { $_.ProfileImagePath } } + } + + # In some cases, it's necessary to retrieve the .Default user's information. + switch ($IncludeDefault) { + $True { + $DefaultProfile = "" | Select-Object UserName, SID, UserHive, Path + $DefaultProfile.UserName = "Default" + $DefaultProfile.SID = "DefaultProfile" + $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT" + $DefaultProfile.Path = "C:\Users\Default" + + $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.UserName } + } + } + + $UserProfiles | Where-Object { $ExcludedUsers -notcontains $_.UserName } + } +} +process { + $ShortcutPath = New-Object System.Collections.Generic.List[String] + + # Create the filenames for the path. + if ($RDPTarget) { $File = "$Name.rdp" } + + # Build the paths and add them to the ShortcutPath list. + if ($AllUsers) { $ShortcutPath.Add("$env:Public\Desktop\$File") } + + if ($AllExistingUsers) { + $UserProfiles = Get-UserHives + # Loop through each user profile + $UserProfiles | ForEach-Object { $ShortcutPath.Add("$($_.Path)\Desktop\$File") } + } + + if ($User) { + $UserProfile = Get-UserHives | Where-Object { $_.Username -like $User } + $ShortcutPath.Add("$($UserProfile.Path)\Desktop\$File") + } + + $RDPFile = New-Object System.Collections.Generic.List[String] + + # Base template of an .RDP file. Additional options will be appended based on user selection. + $Template = @" +session bpp:i:32 +compression:i:1 +keyboardhook:i:2 +audiocapturemode:i:0 +videoplaybackmode:i:1 +connection type:i:7 +networkautodetect:i:1 +bandwidthautodetect:i:1 +displayconnectionbar:i:1 +enableworkspacereconnect:i:0 +disable wallpaper:i:0 +allow font smoothing:i:0 +allow desktop composition:i:0 +disable full window drag:i:1 +disable menu anims:i:1 +disable themes:i:0 +disable cursor setting:i:0 +bitmapcachepersistenable:i:1 +audiomode:i:0 +redirectprinters:i:1 +redirectcomports:i:0 +redirectsmartcards:i:1 +redirectwebauthn:i:1 +redirectclipboard:i:1 +redirectposdevices:i:0 +autoreconnection enabled:i:1 +authentication level:i:2 +negotiate security layer:i:1 +remoteapplicationmode:i:0 +alternate shell:s: +shell working directory:s: +gatewaycredentialssource:i:4 +gatewaybrokeringtype:i:0 +use redirection server name:i:0 +rdgiskdcproxy:i:0 +kdcproxyname:s: +enablerdsaadauth:i:0 +"@ + $RDPFile.Add($Template) + + # This will generate the actual .rdp file + $ShortcutPath | ForEach-Object { + $RDPFile.Add("full address:s:$RDPTarget") + $RDPFile.Add("gatewayhostname:s:$Gateway") + + if ($Width) { $RDPFile.Add("desktopwidth:i:$Width") } + if ($Height) { $RDPFile.Add("desktopheight:i:$Height") } + if ($MultiMon) { $RDPFile.Add("use multimon:i:1") }else { $RDPFile.Add("use multimon:i:0") } + if ($FullScreen -or $MultiMon -or !$Height -or !$Width) { $RDPFile.Add("screen mode id:i:2") }else { $RDPFile.Add("screen mode id:i:1") } + if ($AlwaysPrompt) { $RDPFile.Add("prompt for credentials:i:1") }else { $RDPFile.Add("prompt for credentials:i:0") } + if ($Gateway) { $RDPFile.Add("gatewayusagemethod:i:2") }else { $RDPFile.Add("gatewayusagemethod:i:4") } + if ($SeparateGateWayCreds) { + $RDPFile.Add("promptcredentialonce:i:0") + $RDPFile.Add("gatewayprofileusagemethod:i:1") + } + else { + $RDPFile.Add("promptcredentialonce:i:1") + if ($Gateway) { $RDPFile.Add("gatewayprofileusagemethod:i:0") } + } + + if ($RDPUser) { $RDPFile.Add("username:s:$RDPUser") } + + Write-Host "Creating Shortcut at $_" + $RDPFile | Out-File $_ + + if (!(Test-Path $_ -ErrorAction SilentlyContinue)) { + Write-Error "Unable to create Shortcut at $_" + exit 1 + } + } + + exit 0 +}end { + +} diff --git a/Powershell Scripts/Create Desktop Shortcut - URL.ps1 b/Powershell Scripts/Create Desktop Shortcut - URL.ps1 index 4f29d3a..a873c13 100644 --- a/Powershell Scripts/Create Desktop Shortcut - URL.ps1 +++ b/Powershell Scripts/Create Desktop Shortcut - URL.ps1 @@ -1,181 +1,179 @@ # This script creates a URL desktop shortcut with your specified options. It can create a shortcut for all users (including new ones) or for existing ones only. - -<# -.SYNOPSIS - This script creates a URL desktop shortcut with your specified options. It can create a shortcut for all users (including new ones) or for existing ones only. -.DESCRIPTION - This script creates a URL desktop shortcut with your specified options. - It can create a shortcut for all users (including new ones) or for existing ones only. -.EXAMPLE - To create a URL shortcut that opens in the default browser: - - -Name "Test" -URL "https://www.google.com" -AllUsers - - Creating Shortcut at C:\Users\JohnSmith\Desktop\Test.url - -.PARAMETER NAME - The name of the shortcut, e.g., "Login Portal". - -.PARAMETER URL - The website URL to open, e.g., "https://www.google.com". - -.PARAMETER AllExistingUsers - Creates the shortcut for all existing users but not for new users, e.g., C:\Users\*\Desktop\shortcut.url. - -.PARAMETER AllUsers - Creates the shortcut in C:\Users\Public\Desktop. - -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 7, Windows Server 2008 - Release Notes: Split the script into three separate scripts and added Script Variable support. -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$Name, - [Parameter()] - [String]$Url, - [Parameter()] - [Switch]$AllExistingUsers, - [Parameter()] - [Switch]$AllUsers -) -begin { - # If Form Variables are used, replace the existing params with them. - if ($env:shortcutName -and $env:shortcutName -notlike "null") { $Name = $env:shortcutName } - if ($env:createTheShortcutFor -and $env:createTheShortcutFor -notlike "null") { - if ($env:createTheShortcutFor -eq "All Users") { $AllUsers = $True } - if ($env:createTheShortcutFor -eq "All Existing Users") { $AllExistingUsers = $True } - } - if ($env:linkForUrlShortcut -and $env:linkForUrlShortcut -notlike "null") { $Url = $env:linkForUrlShortcut } - - # Double-check that a user was specified for shortcut creation. - if (!$AllUsers -and !$AllExistingUsers) { - Write-Error "You must specify which desktop to create the shortcut on!" - exit 1 - } - - # Double-check that a shortcut name was given. - if (-not $Name) { - Write-Error "You must specify a name for the shortcut!" - exit 1 - } - - # Creating a shortcut at C:\Users\Public\Desktop requires admin rights. - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - if (!(Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - # This will get all the registry paths for all actual users (not system or network service accounts, but actual users). - function Get-UserHives { - param ( - [Parameter()] - [ValidateSet('AzureAD', 'DomainAndLocal', 'All')] - [String]$Type = "All", - [Parameter()] - [String[]]$ExcludedUsers, - [Parameter()] - [switch]$IncludeDefault - ) - - # User account SIDs follow a particular pattern depending on whether they're Azure AD, a Domain account, or a local "workgroup" account. - $Patterns = switch ($Type) { - "AzureAD" { "S-1-12-1-(\d+-?){4}$" } - "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" } - "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" } - } - - # We'll need the NTuser.dat file to load each user's registry hive. So, we grab it if their account SID matches the above pattern. - $UserProfiles = Foreach ($Pattern in $Patterns) { - Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | - Where-Object { $_.PSChildName -match $Pattern } | - Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, - @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, - @{Name = "UserName"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } }, - @{Name = "Path"; Expression = { $_.ProfileImagePath } } - } - - # There are some situations where grabbing the .Default user's info is needed. - switch ($IncludeDefault) { - $True { - $DefaultProfile = "" | Select-Object UserName, SID, UserHive, Path - $DefaultProfile.UserName = "Default" - $DefaultProfile.SID = "DefaultProfile" - $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT" - $DefaultProfile.Path = "C:\Users\Default" - - $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.UserName } - } - } - - $UserProfiles | Where-Object { $ExcludedUsers -notcontains $_.UserName } - } - - # The actual shortcut creation - function New-Shortcut { - [CmdletBinding()] - param( - [Parameter()] - [String]$Arguments, - [Parameter()] - [String]$IconPath, - [Parameter(ValueFromPipeline = $True)] - [String]$Path, - [Parameter()] - [String]$Target, - [Parameter()] - [String]$WorkingDir - ) - process { - Write-Host "Creating Shortcut at $Path" - $ShellObject = New-Object -ComObject ("WScript.Shell") - $Shortcut = $ShellObject.CreateShortcut($Path) - $Shortcut.TargetPath = $Target - if ($WorkingDir) { $Shortcut.WorkingDirectory = $WorkingDir } - if ($Arguments) { $ShortCut.Arguments = $Arguments } - if ($IconPath) { $Shortcut.IconLocation = $IconPath } - $Shortcut.Save() - - if (!(Test-Path $Path -ErrorAction SilentlyContinue)) { - Write-Error "Unable to create Shortcut at $Path" - exit 1 - } - } - } -} -process { - $ShortcutPath = New-Object System.Collections.Generic.List[String] - - # Creating the filenames for the path - if ($Url) { - $File = "$Name.url" - $Target = $Url - } - - # Building the path's and adding it to the ShortcutPath list - if ($AllUsers) { $ShortcutPath.Add("$env:Public\Desktop\$File") } - - if ($AllExistingUsers) { - $UserProfiles = Get-UserHives - # Loop through each user profile - $UserProfiles | ForEach-Object { $ShortcutPath.Add("$($_.Path)\Desktop\$File") } - } - - $ShortcutPath | ForEach-Object { New-Shortcut -Target $Target -Path $_ } - - exit 0 -}end { - - - -} + +<# +.SYNOPSIS + This script creates a URL desktop shortcut with your specified options. It can create a shortcut for all users (including new ones) or for existing ones only. +.DESCRIPTION + This script creates a URL desktop shortcut with your specified options. + It can create a shortcut for all users (including new ones) or for existing ones only. +.EXAMPLE + To create a URL shortcut that opens in the default browser: + + -Name "Test" -URL "https://www.google.com" -AllUsers + + Creating Shortcut at C:\Users\JohnSmith\Desktop\Test.url + +.PARAMETER NAME + The name of the shortcut, e.g., "Login Portal". + +.PARAMETER URL + The website URL to open, e.g., "https://www.google.com". + +.PARAMETER AllExistingUsers + Creates the shortcut for all existing users but not for new users, e.g., C:\Users\*\Desktop\shortcut.url. + +.PARAMETER AllUsers + Creates the shortcut in C:\Users\Public\Desktop. + +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 7, Windows Server 2008 + Release Notes: Split the script into three separate scripts and added Script Variable support. +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$Name, + [Parameter()] + [String]$Url, + [Parameter()] + [Switch]$AllExistingUsers, + [Parameter()] + [Switch]$AllUsers +) +begin { + # If Form Variables are used, replace the existing params with them. + if ($env:shortcutName -and $env:shortcutName -notlike "null") { $Name = $env:shortcutName } + if ($env:createTheShortcutFor -and $env:createTheShortcutFor -notlike "null") { + if ($env:createTheShortcutFor -eq "All Users") { $AllUsers = $True } + if ($env:createTheShortcutFor -eq "All Existing Users") { $AllExistingUsers = $True } + } + if ($env:linkForUrlShortcut -and $env:linkForUrlShortcut -notlike "null") { $Url = $env:linkForUrlShortcut } + + # Double-check that a user was specified for shortcut creation. + if (!$AllUsers -and !$AllExistingUsers) { + Write-Error "You must specify which desktop to create the shortcut on!" + exit 1 + } + + # Double-check that a shortcut name was given. + if (-not $Name) { + Write-Error "You must specify a name for the shortcut!" + exit 1 + } + + # Creating a shortcut at C:\Users\Public\Desktop requires admin rights. + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if (!(Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + # This will get all the registry paths for all actual users (not system or network service accounts, but actual users). + function Get-UserHives { + param ( + [Parameter()] + [ValidateSet('AzureAD', 'DomainAndLocal', 'All')] + [String]$Type = "All", + [Parameter()] + [String[]]$ExcludedUsers, + [Parameter()] + [switch]$IncludeDefault + ) + + # User account SIDs follow a particular pattern depending on whether they're Azure AD, a Domain account, or a local "workgroup" account. + $Patterns = switch ($Type) { + "AzureAD" { "S-1-12-1-(\d+-?){4}$" } + "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" } + "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" } + } + + # We'll need the NTuser.dat file to load each user's registry hive. So, we grab it if their account SID matches the above pattern. + $UserProfiles = Foreach ($Pattern in $Patterns) { + Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | + Where-Object { $_.PSChildName -match $Pattern } | + Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, + @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, + @{Name = "UserName"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } }, + @{Name = "Path"; Expression = { $_.ProfileImagePath } } + } + + # There are some situations where grabbing the .Default user's info is needed. + switch ($IncludeDefault) { + $True { + $DefaultProfile = "" | Select-Object UserName, SID, UserHive, Path + $DefaultProfile.UserName = "Default" + $DefaultProfile.SID = "DefaultProfile" + $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT" + $DefaultProfile.Path = "C:\Users\Default" + + $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.UserName } + } + } + + $UserProfiles | Where-Object { $ExcludedUsers -notcontains $_.UserName } + } + + # The actual shortcut creation + function New-Shortcut { + [CmdletBinding()] + param( + [Parameter()] + [String]$Arguments, + [Parameter()] + [String]$IconPath, + [Parameter(ValueFromPipeline = $True)] + [String]$Path, + [Parameter()] + [String]$Target, + [Parameter()] + [String]$WorkingDir + ) + process { + Write-Host "Creating Shortcut at $Path" + $ShellObject = New-Object -ComObject ("WScript.Shell") + $Shortcut = $ShellObject.CreateShortcut($Path) + $Shortcut.TargetPath = $Target + if ($WorkingDir) { $Shortcut.WorkingDirectory = $WorkingDir } + if ($Arguments) { $ShortCut.Arguments = $Arguments } + if ($IconPath) { $Shortcut.IconLocation = $IconPath } + $Shortcut.Save() + + if (!(Test-Path $Path -ErrorAction SilentlyContinue)) { + Write-Error "Unable to create Shortcut at $Path" + exit 1 + } + } + } +} +process { + $ShortcutPath = New-Object System.Collections.Generic.List[String] + + # Creating the filenames for the path + if ($Url) { + $File = "$Name.url" + $Target = $Url + } + + # Building the path's and adding it to the ShortcutPath list + if ($AllUsers) { $ShortcutPath.Add("$env:Public\Desktop\$File") } + + if ($AllExistingUsers) { + $UserProfiles = Get-UserHives + # Loop through each user profile + $UserProfiles | ForEach-Object { $ShortcutPath.Add("$($_.Path)\Desktop\$File") } + } + + $ShortcutPath | ForEach-Object { New-Shortcut -Target $Target -Path $_ } + + exit 0 +}end { + +} diff --git a/Powershell Scripts/Create New Local User.ps1 b/Powershell Scripts/Create New Local User.ps1 index 5d9517c..f2a0252 100644 --- a/Powershell Scripts/Create New Local User.ps1 +++ b/Powershell Scripts/Create New Local User.ps1 @@ -1,494 +1,406 @@ # Create a local user account with options to enable and disable at specific dates, and add to local admin group. Saves randomly generated password to a custom field. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Create a local user account with options to enable and disable at specific dates, and add to local admin group. Saves randomly generated password to a custom field. -.DESCRIPTION - Create a local user account with options to enable and disable at specific dates, and add to local admin group. Saves randomly generated password to a custom field. - -.EXAMPLE - -UserNameToAdd "JohnTSmith" -Name "John T Smith" - ## EXAMPLE OUTPUT ## - User JohnTSmith has been created successfully. - User JohnTSmith was added to the local Users group. - -PARAMETER: -UserNameToAdd "JohnTSmith" -Name "John T Smith" - Creates user with the name JohnTSmith and display name of John T Smith. - -PARAMETER: -UserNameToAdd "JohnTSmith" -Name "John T Smith" -DateAndTimeToEnable "Monday, January 1, 2020 1:00:00 PM" - Create user with the name JohnTSmith and display name of John T Smith. - The user will start out disabled. - A scheduled task will be created to enable the user after "Monday, January 1, 2020 1:00:00 PM". -.EXAMPLE - -UserNameToAdd "JohnTSmith" -Name "John T Smith" -DateAndTimeToEnable "Monday, January 1, 2020 1:00:00 PM" - ## EXAMPLE OUTPUT ## - User JohnTSmith has been created successfully. - User JohnTSmith was added to the local Users group. - Created Scheduled Task: Enable User JohnTSmith - User JohnTSmith will be able to login after Monday, January 1, 2020 1:00:00 PM. - -PARAMETER: -UserNameToAdd "JohnTSmith" -Name "John T Smith" -DisableAfterDays 10 - Create user with the name JohnTSmith and display name of John T Smith. - The user will be disabled after 10 days after the user's creation. -.EXAMPLE - -UserNameToAdd "JohnTSmith" -Name "John T Smith" -DisableAfterDays 10 - ## EXAMPLE OUTPUT ## - User JohnTSmith has been created successfully. - User JohnTSmith was added to the local Users group. - -PARAMETER: -UserNameToAdd "JohnTSmith" -Name "John T Smith" -AddToLocalAdminGroup - Create user with the name JohnTSmith and display name of John T Smith. - User will be added as a member of the local Administrators group. -.EXAMPLE - -UserNameToAdd "JohnTSmith" -Name "John T Smith" -AddToLocalAdminGroup - ## EXAMPLE OUTPUT ## - User JohnTSmith has been created successfully. - User JohnTSmith was added to the local Users group. - User JohnTSmith was added to the local Administrators group. -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Update Calculated Name, reduced nesting, added more validation of parameters, fixed bug with adding to local admin group, made changes to scheduled task, improved password generation. -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$Username, - [Parameter()] - [String]$DisplayName, - [Parameter()] - [Int]$PasswordLength = 20, - [Parameter()] - [DateTime]$EnableDate, - [Parameter()] - $DisableAfterDays, - [Parameter()] - [String]$CustomField, - [Parameter()] - [String]$PasswordExpireOption = "User Must Change Password", - [Parameter()] - [Switch]$AddToLocalAdminGroup = [System.Convert]::ToBoolean($env:addToLocalAdminGroup) -) - -begin { - # Retrieve script form variables and replace the parameters with them, handling 'null' values. - if ($env:usernameToAdd -and $env:usernameToAdd -notlike "null") { $Username = $env:usernameToAdd } - if ($env:displayName -and $env:displayName -notlike "null") { $DisplayName = $env:displayName } - if ($env:customFieldToStorePassword -and $env:customFieldToStorePassword -notlike "null") { $CustomField = $env:customFieldToStorePassword } - if ($env:passwordLength -and $env:passwordLength -notlike "null") { $PasswordLength = $env:passwordLength } - if ($env:dateAndTimeToEnable -and $env:dateAndTimeToEnable -notlike "null") { $EnableDate = $env:dateAndTimeToEnable } - if ($env:disableAfterDays -and $env:disableAfterDays -notlike "null") { [int]$DisableAfterDays = $env:disableAfterDays } - if ($env:passwordExpireOptions -and $env:passwordExpireOptions -notlike "null" ) { - if ($env:passwordExpireOptions -eq "Neither") { - $PasswordExpireOption = $null - } - else { - $PasswordExpireOption = $env:passwordExpireOptions - } - } - - # Validate input parameters for user creation, checking for absence, invalid characters, length, and options. - - if (!$Username) { - Write-Host -Object "[Error] Please enter in a username!" - exit 1 - } - - if (!$CustomField) { - Write-Host -Object "[Error] A Custom Field to store the password is required!" - exit 1 - } - - # Ensure username does not contain illegal characters. - if ($Username -match '\[|\]|:|;|\||=|\+|\*|\?|<|>|/|\\|,|"|@') { - Write-Host -Object ("[Error] $Username contains one of the following invalid characters." + ' " [ ] : ; | = + * ? < > / \ , @') - exit 1 - } - - # Ensure the username does not contain spaces. - if ($Username -match '\s') { - Write-Host -Object ("[Error] '$Username' contains a space.") - exit 1 - } - - # Ensure the username is not longer than 20 characters. - $UserNameCharacters = $Username | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($UserNameCharacters -gt 20) { - Write-Host -Object "[Error] '$Username' is too long. The username needs to be less than or equal to 20 characters." - exit 1 - } - - # Validate password length, must be 8 or more. - if (!$PasswordLength -or $PasswordLength -lt 8) { - Write-Host -Object "[Error] Password length must be greater than or equal to 8!" - exit 1 - } - - # Validate disable after days, cannot be negative. - if ($DisableAfterDays -and $DisableAfterDays -lt 0) { - Write-Host -Object "[Error] Disable After Days cannot be less than 0." - exit 1 - } - - # Validate password expiration options. - $ValidExpireOption = "User Must Change Password", "Password Never Expires" - if ($PasswordExpireOption -and $ValidExpireOption -notcontains $PasswordExpireOption) { - Write-Host -Object "[Error] Invalid password expire option given. Must be either 'User Must Change Password' or 'Password Never Expires'" - exit 1 - } - - # Default Password Policy - $PasswordPolicy = [PSCustomObject]@{ - MinimumLength = 0 - Complexity = 1 - } - - # Export the security policy - $Arguments = @( - "/export" - "/cfg" - "$env:TEMP\secconfig.cfg" - ) - $SecurityExport = Start-Process -FilePath "secedit.exe" -ArgumentList $Arguments -PassThru -Wait -WindowStyle Hidden - - # If export was successful parse through the security policy for the minimum password length required. - if ($SecurityExport.ExitCode -ne 0) { - Write-Host -Object "[Error] Failed to retrieve password complexity policy. Assuming Microsoft Default policy is in effect." - } - else { - $SecurityPolicy = Get-Content -Path "$env:TEMP\secconfig.cfg" - - $PasswordLengthField = $SecurityPolicy | Select-String "MinimumPasswordLength" - $PasswordPolicy.MinimumLength = ($PasswordLengthField -split "=").Trim()[1] - } - - # Remove the export if it exists - if (Test-Path -Path "$env:TEMP\secconfig.cfg" -ErrorAction SilentlyContinue) { - Remove-Item -Path "$env:TEMP\secconfig.cfg" - } - - # Error out if the password length does not meet the minimum requirements. - if ($PasswordLength -lt $PasswordPolicy.MinimumLength) { - Write-Host "[Error] The minimum password length of $($PasswordPolicy.MinimumLength) is greater than the password length you requested to generate ($PasswordLength)." - exit 1 - } - - # Check if script is running with elevated permissions. - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - # Function to retrieve a local group name via its sid. - function Get-LocalGroupName { - param( - [Parameter(Mandatory = $True)] - [String]$Sid - ) - - if ($PSVersionTable.PSVersion.Major -lt 5) { - (Get-WmiObject -Class Win32_Group -Filter "LocalAccount=True and SID='$Sid'").Name - } - else { - (Get-CimInstance -Class Win32_Group -Filter "LocalAccount=True and SID='$Sid'").Name - } - } - - # Function to retrieve local groups using net command. - function Get-NetLocalGroup { - param( - [Parameter()] - [String]$Group = "Users" - ) - Invoke-Command -ScriptBlock { net.exe localgroup "$Group" } | Where-Object { $_ -AND $_ -notmatch "command completed successfully" } | Select-Object -Skip 4 - } - - # Function to add to a local group using the net command. - function Add-NetLocalGroupMember { - param( - [Parameter(Mandatory = $True)] - [String]$User, - [Parameter(Mandatory = $True)] - [String]$Group - ) - - Invoke-Command -ScriptBlock { net.exe localgroup "$Group" "$Username" /add } | Where-Object { $_ -AND $_ -notmatch "command completed successfully" } - } - - # Generate a cryptographically secure password. - function New-SecurePassword { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $false)] - [int]$Length = 16, - [Parameter(Mandatory = $false)] - [switch]$IncludeSpecialCharacters - ) - # .NET class for generating cryptographically secure random numbers - $cryptoProvider = New-Object System.Security.Cryptography.RNGCryptoServiceProvider - $baseChars = "abcdefghjknpqrstuvwxyzABCDEFGHIJKMNPQRSTUVWXYZ0123456789" - $SpecialCharacters = '!@#$%&-' - $passwordChars = $baseChars + $(if ($IncludeSpecialCharacters) { $SpecialCharacters } else { '' }) - $password = for ($i = 0; $i -lt $Length; $i++) { - $byte = [byte[]]::new(1) - $cryptoProvider.GetBytes($byte) - $charIndex = $byte[0] % $passwordChars.Length - $passwordChars[$charIndex] - } - - return $password -join '' - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 10000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") - } - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to be set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received it will have an exception property, the function will exit with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the Date-Time to be in Unix Epoch time so we'll convert it here. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - - $ExitCode = 0 -} -process { - # Check if the script is running with elevated (Administrator) privileges - if (-not (Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # The Users and Administrators group can have a different name depending on the language set. - $UsersGroup = Get-LocalGroupName -Sid "S-1-5-32-545" - $AdministratorsGroup = Get-LocalGroupName -Sid "S-1-5-32-544" - - # Check if the user already exists in the local group - if ((Get-NetLocalGroup -Group $UsersGroup) -contains $Username) { - Write-Host "[Error] User $Username already exists!" - exit 1 - } - - # Generate a password according to the complexity policy - $i = 0 - do { - $Password = New-SecurePassword -Length $PasswordLength -IncludeSpecialCharacters - $i++ - }while ($i -lt 1000 -and !($Password -match '[@!#$%&\-]+' -and $Password -match '[A-Z]+' -and $Password -match '[a-z]+' -and $Password -match '[0-9]+')) - - if ($i -eq 1000) { - Write-Host "[Error] Unable to generate a secure password after 1000 tries." - exit 1 - } - - try { - # Attempt to set the custom field with the generated password - Write-Host "Attempting to set password in Custom Field '$CustomField'." - Set-NinjaProperty -Name $CustomField -Value $Password - # Confirmation of successful custom field update - Write-Host "Successfully set password in Custom Field '$CustomField'!" - } - catch { - # Error handling for custom field update failure - Write-Host "[Error] $($_.Exception.Message)" - exit 1 - } - - # Prepare parameters for creating a new local user account - $UserSplat = @{ - Name = $Username - Password = (ConvertTo-SecureString -String $Password -AsPlainText -Force) - Description = "User account created on $(Get-Date)" - } - - # If a display name is provided, add it to the user account parameters - if ($DisplayName) { - $UserSplat["FullName"] = $DisplayName - } - - # If a future enable date is provided, create the user in a disabled state; else warn if date is in the past - if ($EnableDate -and $EnableDate -gt (Get-Date)) { - $UserSplat['Disabled'] = $true - $ScheduleEnable = $True - } - elseif ($EnableDate) { - Write-Warning -Message "This script is set to enable the account after a date in the past!" - Write-Warning -Message "Date to enable: $EnableDate" - } - - # If the user is to be disabled immediately, set the disabled flag - if ($DisableAfterDays -eq 0) { - $UserSplat['Disabled'] = $true - } - - # If the password is set to never expire, add this to the user account parameters - if ($PasswordExpireOption -eq "Password Never Expires") { - $UserSplat['PasswordNeverExpires'] = $True - } - - # If an account expiration period is provided, calculate the expiration date based on the enable date - if ($DisableAfterDays -and $DisableAfterDays -gt 0) { - if (-not $EnableDate) { $EnableDate = Get-Date } - $UserSplat['AccountExpires'] = $(Get-Date $EnableDate).AddDays($DisableAfterDays) - } - - # Attempt to create the new local user account with the specified parameters - try { - New-LocalUser @UserSplat -ErrorAction Stop - Add-NetLocalGroupMember -User $Username -Group $UsersGroup - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - exit 1 - } - - # If specified, force the user to change their password at the next logon - if ($PasswordExpireOption -eq "User Must Change Password") { - Invoke-Command -ScriptBlock { net.exe user "$Username" /logonpasswordchg:yes } | Where-Object { $_ -AND $_ -notmatch "command completed successfully" } - } - - # If the user needs to be added to the local Administrators group, attempt to add them - if ($AddToLocalAdminGroup) { - Add-NetLocalGroupMember -User $Username -Group $AdministratorsGroup - # Verify that the user was added to the Administrators group - $LocalAdmins = Get-NetLocalGroup -Group $AdministratorsGroup - $LocalAdmins | ForEach-Object { - if ($_ -match [regex]::Escape($Username)) { - $IsLocalAdmin = $True - } - } - # If the user wasn't added to the Administrators group, print an error - if (-not $IsLocalAdmin) { - Write-Host -Object "[Error] Failed to add $Username to the '$AdministratorsGroup' group." - $ExitCode = 1 - } - } - - # Check if enabling the user is scheduled. - if ($ScheduleEnable) { - # Set up properties for the scheduled task (splatting method for cleaner code). - $TaskSplat = @{ - Description = "Ninja Automation Enable User $Username" - Action = New-ScheduledTaskAction -Execute "net.exe" -Argument "user `"$Username`" /active:yes" - Trigger = New-ScheduledTaskTrigger -Once -At $EnableDate - Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount - Settings = New-ScheduledTaskSettingsSet -DontStopIfGoingOnBatteries -AllowStartIfOnBatteries -WakeToRun -StartWhenAvailable - } - - # Attempt to create and register the scheduled task - try { - # Create and register the scheduled task to enable the user - New-ScheduledTask @TaskSplat | Register-ScheduledTask -User "System" -TaskName "Enable User $Username $(Get-Date -Date $EnableDate -Format yyyyMMdd)" | Out-Null - - # Verify task creation - if ($(Get-ScheduledTask -TaskName "Enable User $Username $(Get-Date -Date $EnableDate -Format yyyyMMdd)" )) { - Write-Host "Created Scheduled Task: Enable User $Username" - } - else { - # Task creation verification failed - Write-Host "[Error] Failed to find scheduled task with the name 'Enable User $Username'" - $ExitCode = 1 - } - } - catch { - # Error handling for task registration failure - Write-Host "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - - # Inform the user of the enable date - Write-Host "User $Username will be able to login after $EnableDate." - } - elseif ($DisableAfterDays -ne 0) { - # No enable date was specified, user can login immediately - Write-Host "No Enable Date is Set, $Username is able to login now." - } - - if ($DisableAfterDays -eq 0) { - Write-Host "Account $Username was successfully created, account is currently disabled as requested!" - } - - # Exit the script with the final status code - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Create a local user account with options to enable and disable at specific dates, and add to local admin group. Saves randomly generated password to a custom field. +.DESCRIPTION + Create a local user account with options to enable and disable at specific dates, and add to local admin group. Saves randomly generated password to a custom field. + +.EXAMPLE + -UserNameToAdd "JohnTSmith" -Name "John T Smith" + ## EXAMPLE OUTPUT ## + User JohnTSmith has been created successfully. + User JohnTSmith was added to the local Users group. + +PARAMETER: -UserNameToAdd "JohnTSmith" -Name "John T Smith" + Creates user with the name JohnTSmith and display name of John T Smith. + +PARAMETER: -UserNameToAdd "JohnTSmith" -Name "John T Smith" -DateAndTimeToEnable "Monday, January 1, 2020 1:00:00 PM" + Create user with the name JohnTSmith and display name of John T Smith. + The user will start out disabled. + A scheduled task will be created to enable the user after "Monday, January 1, 2020 1:00:00 PM". +.EXAMPLE + -UserNameToAdd "JohnTSmith" -Name "John T Smith" -DateAndTimeToEnable "Monday, January 1, 2020 1:00:00 PM" + ## EXAMPLE OUTPUT ## + User JohnTSmith has been created successfully. + User JohnTSmith was added to the local Users group. + Created Scheduled Task: Enable User JohnTSmith + User JohnTSmith will be able to login after Monday, January 1, 2020 1:00:00 PM. + +PARAMETER: -UserNameToAdd "JohnTSmith" -Name "John T Smith" -DisableAfterDays 10 + Create user with the name JohnTSmith and display name of John T Smith. + The user will be disabled after 10 days after the user's creation. +.EXAMPLE + -UserNameToAdd "JohnTSmith" -Name "John T Smith" -DisableAfterDays 10 + ## EXAMPLE OUTPUT ## + User JohnTSmith has been created successfully. + User JohnTSmith was added to the local Users group. + +PARAMETER: -UserNameToAdd "JohnTSmith" -Name "John T Smith" -AddToLocalAdminGroup + Create user with the name JohnTSmith and display name of John T Smith. + User will be added as a member of the local Administrators group. +.EXAMPLE + -UserNameToAdd "JohnTSmith" -Name "John T Smith" -AddToLocalAdminGroup + ## EXAMPLE OUTPUT ## + User JohnTSmith has been created successfully. + User JohnTSmith was added to the local Users group. + User JohnTSmith was added to the local Administrators group. +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Update Calculated Name, reduced nesting, added more validation of parameters, fixed bug with adding to local admin group, made changes to scheduled task, improved password generation. +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$Username, + [Parameter()] + [String]$DisplayName, + [Parameter()] + [Int]$PasswordLength = 20, + [Parameter()] + [DateTime]$EnableDate, + [Parameter()] + $DisableAfterDays, + [Parameter()] + [String]$CustomField, + [Parameter()] + [String]$PasswordExpireOption = "User Must Change Password", + [Parameter()] + [Switch]$AddToLocalAdminGroup = [System.Convert]::ToBoolean($env:addToLocalAdminGroup) +) + +begin { + # Retrieve script form variables and replace the parameters with them, handling 'null' values. + if ($env:usernameToAdd -and $env:usernameToAdd -notlike "null") { $Username = $env:usernameToAdd } + if ($env:displayName -and $env:displayName -notlike "null") { $DisplayName = $env:displayName } + if ($env:customFieldToStorePassword -and $env:customFieldToStorePassword -notlike "null") { $CustomField = $env:customFieldToStorePassword } + if ($env:passwordLength -and $env:passwordLength -notlike "null") { $PasswordLength = $env:passwordLength } + if ($env:dateAndTimeToEnable -and $env:dateAndTimeToEnable -notlike "null") { $EnableDate = $env:dateAndTimeToEnable } + if ($env:disableAfterDays -and $env:disableAfterDays -notlike "null") { [int]$DisableAfterDays = $env:disableAfterDays } + if ($env:passwordExpireOptions -and $env:passwordExpireOptions -notlike "null" ) { + if ($env:passwordExpireOptions -eq "Neither") { + $PasswordExpireOption = $null + } + else { + $PasswordExpireOption = $env:passwordExpireOptions + } + } + + # Validate input parameters for user creation, checking for absence, invalid characters, length, and options. + + if (!$Username) { + Write-Host -Object "[Error] Please enter in a username!" + exit 1 + } + + if (!$CustomField) { + Write-Host -Object "[Error] A Custom Field to store the password is required!" + exit 1 + } + + # Ensure username does not contain illegal characters. + if ($Username -match '\[|\]|:|;|\||=|\+|\*|\?|<|>|/|\\|,|"|@') { + Write-Host -Object ("[Error] $Username contains one of the following invalid characters." + ' " [ ] : ; | = + * ? < > / \ , @') + exit 1 + } + + # Ensure the username does not contain spaces. + if ($Username -match '\s') { + Write-Host -Object ("[Error] '$Username' contains a space.") + exit 1 + } + + # Ensure the username is not longer than 20 characters. + $UserNameCharacters = $Username | Measure-Object -Character | Select-Object -ExpandProperty Characters + if ($UserNameCharacters -gt 20) { + Write-Host -Object "[Error] '$Username' is too long. The username needs to be less than or equal to 20 characters." + exit 1 + } + + # Validate password length, must be 8 or more. + if (!$PasswordLength -or $PasswordLength -lt 8) { + Write-Host -Object "[Error] Password length must be greater than or equal to 8!" + exit 1 + } + + # Validate disable after days, cannot be negative. + if ($DisableAfterDays -and $DisableAfterDays -lt 0) { + Write-Host -Object "[Error] Disable After Days cannot be less than 0." + exit 1 + } + + # Validate password expiration options. + $ValidExpireOption = "User Must Change Password", "Password Never Expires" + if ($PasswordExpireOption -and $ValidExpireOption -notcontains $PasswordExpireOption) { + Write-Host -Object "[Error] Invalid password expire option given. Must be either 'User Must Change Password' or 'Password Never Expires'" + exit 1 + } + + # Default Password Policy + $PasswordPolicy = [PSCustomObject]@{ + MinimumLength = 0 + Complexity = 1 + } + + # Export the security policy + $Arguments = @( + "/export" + "/cfg" + "$env:TEMP\secconfig.cfg" + ) + $SecurityExport = Start-Process -FilePath "secedit.exe" -ArgumentList $Arguments -PassThru -Wait -WindowStyle Hidden + + # If export was successful parse through the security policy for the minimum password length required. + if ($SecurityExport.ExitCode -ne 0) { + Write-Host -Object "[Error] Failed to retrieve password complexity policy. Assuming Microsoft Default policy is in effect." + } + else { + $SecurityPolicy = Get-Content -Path "$env:TEMP\secconfig.cfg" + + $PasswordLengthField = $SecurityPolicy | Select-String "MinimumPasswordLength" + $PasswordPolicy.MinimumLength = ($PasswordLengthField -split "=").Trim()[1] + } + + # Remove the export if it exists + if (Test-Path -Path "$env:TEMP\secconfig.cfg" -ErrorAction SilentlyContinue) { + Remove-Item -Path "$env:TEMP\secconfig.cfg" + } + + # Error out if the password length does not meet the minimum requirements. + if ($PasswordLength -lt $PasswordPolicy.MinimumLength) { + Write-Host "[Error] The minimum password length of $($PasswordPolicy.MinimumLength) is greater than the password length you requested to generate ($PasswordLength)." + exit 1 + } + + # Check if script is running with elevated permissions. + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + # Function to retrieve a local group name via its sid. + function Get-LocalGroupName { + param( + [Parameter(Mandatory = $True)] + [String]$Sid + ) + + if ($PSVersionTable.PSVersion.Major -lt 5) { + (Get-WmiObject -Class Win32_Group -Filter "LocalAccount=True and SID='$Sid'").Name + } + else { + (Get-CimInstance -Class Win32_Group -Filter "LocalAccount=True and SID='$Sid'").Name + } + } + + # Function to retrieve local groups using net command. + function Get-NetLocalGroup { + param( + [Parameter()] + [String]$Group = "Users" + ) + Invoke-Command -ScriptBlock { net.exe localgroup "$Group" } | Where-Object { $_ -AND $_ -notmatch "command completed successfully" } | Select-Object -Skip 4 + } + + # Function to add to a local group using the net command. + function Add-NetLocalGroupMember { + param( + [Parameter(Mandatory = $True)] + [String]$User, + [Parameter(Mandatory = $True)] + [String]$Group + ) + + Invoke-Command -ScriptBlock { net.exe localgroup "$Group" "$Username" /add } | Where-Object { $_ -AND $_ -notmatch "command completed successfully" } + } + + # Generate a cryptographically secure password. + function New-SecurePassword { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [int]$Length = 16, + [Parameter(Mandatory = $false)] + [switch]$IncludeSpecialCharacters + ) + # .NET class for generating cryptographically secure random numbers + $cryptoProvider = New-Object System.Security.Cryptography.RNGCryptoServiceProvider + $baseChars = "abcdefghjknpqrstuvwxyzABCDEFGHIJKMNPQRSTUVWXYZ0123456789" + $SpecialCharacters = '!@#$%&-' + $passwordChars = $baseChars + $(if ($IncludeSpecialCharacters) { $SpecialCharacters } else { '' }) + $password = for ($i = 0; $i -lt $Length; $i++) { + $byte = [byte[]]::new(1) + $cryptoProvider.GetBytes($byte) + $charIndex = $byte[0] % $passwordChars.Length + $passwordChars[$charIndex] + } + + return $password -join '' + } + + $ExitCode = 0 +} +process { + # Check if the script is running with elevated (Administrator) privileges + if (-not (Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # The Users and Administrators group can have a different name depending on the language set. + $UsersGroup = Get-LocalGroupName -Sid "S-1-5-32-545" + $AdministratorsGroup = Get-LocalGroupName -Sid "S-1-5-32-544" + + # Check if the user already exists in the local group + if ((Get-NetLocalGroup -Group $UsersGroup) -contains $Username) { + Write-Host "[Error] User $Username already exists!" + exit 1 + } + + # Generate a password according to the complexity policy + $i = 0 + do { + $Password = New-SecurePassword -Length $PasswordLength -IncludeSpecialCharacters + $i++ + }while ($i -lt 1000 -and !($Password -match '[@!#$%&\-]+' -and $Password -match '[A-Z]+' -and $Password -match '[a-z]+' -and $Password -match '[0-9]+')) + + if ($i -eq 1000) { + Write-Host "[Error] Unable to generate a secure password after 1000 tries." + exit 1 + } + + try { + # Attempt to set the custom field with the generated password + Write-Host "Attempting to set password in Custom Field '$CustomField'." + # Confirmation of successful custom field update + Write-Host "Successfully set password in Custom Field '$CustomField'!" + } + catch { + # Error handling for custom field update failure + Write-Host "[Error] $($_.Exception.Message)" + exit 1 + } + + # Prepare parameters for creating a new local user account + $UserSplat = @{ + Name = $Username + Password = (ConvertTo-SecureString -String $Password -AsPlainText -Force) + Description = "User account created on $(Get-Date)" + } + + # If a display name is provided, add it to the user account parameters + if ($DisplayName) { + $UserSplat["FullName"] = $DisplayName + } + + # If a future enable date is provided, create the user in a disabled state; else warn if date is in the past + if ($EnableDate -and $EnableDate -gt (Get-Date)) { + $UserSplat['Disabled'] = $true + $ScheduleEnable = $True + } + elseif ($EnableDate) { + Write-Warning -Message "This script is set to enable the account after a date in the past!" + Write-Warning -Message "Date to enable: $EnableDate" + } + + # If the user is to be disabled immediately, set the disabled flag + if ($DisableAfterDays -eq 0) { + $UserSplat['Disabled'] = $true + } + + # If the password is set to never expire, add this to the user account parameters + if ($PasswordExpireOption -eq "Password Never Expires") { + $UserSplat['PasswordNeverExpires'] = $True + } + + # If an account expiration period is provided, calculate the expiration date based on the enable date + if ($DisableAfterDays -and $DisableAfterDays -gt 0) { + if (-not $EnableDate) { $EnableDate = Get-Date } + $UserSplat['AccountExpires'] = $(Get-Date $EnableDate).AddDays($DisableAfterDays) + } + + # Attempt to create the new local user account with the specified parameters + try { + New-LocalUser @UserSplat -ErrorAction Stop + Add-NetLocalGroupMember -User $Username -Group $UsersGroup + } + catch { + Write-Host "[Error] $($_.Exception.Message)" + exit 1 + } + + # If specified, force the user to change their password at the next logon + if ($PasswordExpireOption -eq "User Must Change Password") { + Invoke-Command -ScriptBlock { net.exe user "$Username" /logonpasswordchg:yes } | Where-Object { $_ -AND $_ -notmatch "command completed successfully" } + } + + # If the user needs to be added to the local Administrators group, attempt to add them + if ($AddToLocalAdminGroup) { + Add-NetLocalGroupMember -User $Username -Group $AdministratorsGroup + # Verify that the user was added to the Administrators group + $LocalAdmins = Get-NetLocalGroup -Group $AdministratorsGroup + $LocalAdmins | ForEach-Object { + if ($_ -match [regex]::Escape($Username)) { + $IsLocalAdmin = $True + } + } + # If the user wasn't added to the Administrators group, print an error + if (-not $IsLocalAdmin) { + Write-Host -Object "[Error] Failed to add $Username to the '$AdministratorsGroup' group." + $ExitCode = 1 + } + } + + # Check if enabling the user is scheduled. + if ($ScheduleEnable) { + # Set up properties for the scheduled task (splatting method for cleaner code). + $TaskSplat = @{ + Description = "Ninja Automation Enable User $Username" + Action = New-ScheduledTaskAction -Execute "net.exe" -Argument "user `"$Username`" /active:yes" + Trigger = New-ScheduledTaskTrigger -Once -At $EnableDate + Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount + Settings = New-ScheduledTaskSettingsSet -DontStopIfGoingOnBatteries -AllowStartIfOnBatteries -WakeToRun -StartWhenAvailable + } + + # Attempt to create and register the scheduled task + try { + # Create and register the scheduled task to enable the user + New-ScheduledTask @TaskSplat | Register-ScheduledTask -User "System" -TaskName "Enable User $Username $(Get-Date -Date $EnableDate -Format yyyyMMdd)" | Out-Null + + # Verify task creation + if ($(Get-ScheduledTask -TaskName "Enable User $Username $(Get-Date -Date $EnableDate -Format yyyyMMdd)" )) { + Write-Host "Created Scheduled Task: Enable User $Username" + } + else { + # Task creation verification failed + Write-Host "[Error] Failed to find scheduled task with the name 'Enable User $Username'" + $ExitCode = 1 + } + } + catch { + # Error handling for task registration failure + Write-Host "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + + # Inform the user of the enable date + Write-Host "User $Username will be able to login after $EnableDate." + } + elseif ($DisableAfterDays -ne 0) { + # No enable date was specified, user can login immediately + Write-Host "No Enable Date is Set, $Username is able to login now." + } + + if ($DisableAfterDays -eq 0) { + Write-Host "Account $Username was successfully created, account is currently disabled as requested!" + } + + # Exit the script with the final status code + exit $ExitCode +} +end { + +} diff --git a/Powershell Scripts/Create Windows VPN.ps1 b/Powershell Scripts/Create Windows VPN.ps1 index c29e9f7..84f1349 100644 --- a/Powershell Scripts/Create Windows VPN.ps1 +++ b/Powershell Scripts/Create Windows VPN.ps1 @@ -1,870 +1,860 @@ # Creates a VPN using the built-in Windows VPN client, with configuration options from Ninja Documentation or Device Custom Fields. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Creates a VPN using the built-in Windows VPN client, with configuration options from Ninja Documentation or Device Custom Fields. -.DESCRIPTION - This script will create a VPN using the built-in Windows VPN client. It will not create PPTP or "No Encryption" VPNs. - It can pull all the necessary VPN information from a Ninja Document or custom field you specify. - You can override the Documentation fields with parameters or only use parameters if you would like. - - For more information on Ninja Documentation: https://ninjarmm.zendesk.com/hc/en-us/articles/360061218431-Documentation - -OPTIONAL: Custom Fields - All Fields must be readable by scripts - # Name # - # Field Type and what values to use for dropdowns. # - vpnName - text field - vpnHost - text field - tunnelType - dropdown field. Acceptable values are L2TP, SSTP, IKEV2, Automatic - authMethod - multi-select field. Acceptable values are PAP, Chap, MSChapv2, MachineCertificate - encryptionLevel - dropdown field. Acceptable values are Optional, Required, Maximum - dnsSuffix - text field - rememberCreds - Checkbox - useWinCredentials - Checkbox - assumeUdpEncapsulation - Checkbox - splitTunneling - Checkbox - createVpnShortcut - Checkbox - shortcutUrl - URL or Text - shortcutIconDirectory - Text - -PARAMETER: -DocumentName "ReplaceWithNameOfyourNinjaDocument" - Replace the value in quotes with the document you'd like to retireve the vpn parameters from (if any). - -PARAMETER: -Name "Contoso VPN" - The name of the VPN you would like to create - -PARAMETER: -Server "replace.me" - The endpoint/server the VPN will try to establish a connection with. - -PARAMETER: -PreSharedKeyField "ReplaceMeWithNameOfSecureCustomField" - Name of a secure custom field containing your pre-shared key. - -PARAMETER: -TunnelType "L2TP" - The Type of VPN ex. L2TP, SSTP, IKEV2, Automatic - -PARAMETER: -AuthMethod "PAP,CHAP" - The authentication methods supported by the VPN separated by commas. - -PARAMETER: -EncryptionLevel "Required" - The Encryption level used by the VPN. - -PARAMETER: -DNSSuffix "contoso.local" - The DNS Suffix used by the connection. - -PARAMETER: -RememberCreds - Whether or not the VPN should remember the previously used credentials for future connections. - -PARAMETER: -UseWinlogonCredential - Whether or not the VPN should use the Windows logon credentials for authentication. - -PARAMETER: -AssumeUDPEncapsulation - Sets the AssumeUDPEncapsulation registry key which is required by many VPNs -.LINK - https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/configure-l2tp-ipsec-server-behind-nat-t-device - -PARAMETER: -SplitTunneling - Enables split tunneling. - -PARAMETER: -CreateShortcut - Creates a desktop shortcut for accessing the VPN. - -PARAMETER: -URL - A URL to an image you'd like to download and use for the shortcut icon. Icon will be stored at -IconDirectory. - -PARAMETER: -IconDirectory "C:\ReplaceMe" - The directory where the shortcut's icon file will be stored. - -PRESET PARAMETER: -SkipSleep - By default the script sleeps for a random interval between 3 and 60 seconds prior to downloading an icon (if the script is given a url). This parameter skips the sleep. - -PARAMETER: -Overwrite - Overwrites an existing VPN with the same name, if present. -.OUTPUTS - None -.NOTES - Minimum Supported OS: Windows 10 - Release Notes: Update calculated name -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$DocumentName, - [Parameter()] - [String]$Name, - [Parameter()] - [String]$NameField = "vpnName", - [Parameter()] - [String]$Server, - [Parameter()] - [String]$ServerField = "vpnHost", - [Parameter()] - [String]$PreSharedKeyField, - [Parameter()] - [String]$TunnelType = "Automatic", - [Parameter()] - [String]$TunnelTypeField = "tunnelType", - [Parameter()] - [String[]]$AuthMethod, - [Parameter()] - [String]$AuthMethodField = "authMethod", - [Parameter()] - [String]$EncryptionLevel, - [Parameter()] - [String]$EncryptionLevelField = "encryptionLevel", - [Parameter()] - [String]$DNSSuffix, - [Parameter()] - [String]$DNSSuffixField = "dnsSuffix", - [Parameter()] - [Switch]$RememberCreds = [System.Convert]::ToBoolean($env:rememberUserCredentials), - [Parameter()] - [String]$RememberCredsField = "rememberCreds", - [Parameter()] - [Switch]$UseWinlogonCredential = [System.Convert]::ToBoolean($env:useWindowsCredentials), - [Parameter()] - [String]$UseWinlogonCredsField = "useWinCredentials", - [Parameter()] - [Switch]$AssumeUDPencapsulation = [System.Convert]::ToBoolean($env:assumeUdpEncapsulation), - [Parameter()] - [String]$UDPField = "assumeUdpEncapsulation", - [Parameter()] - [Switch]$SplitTunneling = [System.Convert]::ToBoolean($env:splitTunneling), - [Parameter()] - [String]$SplitTunnelingField = "splitTunneling", - [Parameter()] - [Switch]$CreateShortcut = [System.Convert]::ToBoolean($env:createVpnDesktopShortcut), - [Parameter()] - [String]$ShortcutField = "createVpnShortcut", - [Parameter()] - [String]$Url, - [Parameter()] - [String]$UrlField = "shortcutUrl", - [Parameter()] - [String]$IconDirectory, - [Parameter()] - [String]$IconDirectoryField = "shortcutIconDirectory", - [Parameter()] - [Switch]$Overwrite = [System.Convert]::ToBoolean($env:overwrite), - [Parameter()] - [Switch]$SkipSleep -) -begin { - Add-Type -AssemblyName System.Drawing - - if ([System.Convert]::ToBoolean($env:verboseOutput)) { - $VerbosePreference = 'Continue' - } - - # You can replace the line below with $IconBase64 = 'ReplaceThisWithYourBase64EncodedImageEncasedInQuotes', and the script will decode the image and use it for the VPN shortcut. - $IconBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAElBMVEUAAAD///////////////////8+Uq06AAAABXRSTlMAAECAv9KsvScAAAEsSURBVEjH7ZUxDsIwDEWtFA7ADbqwM8COhLpXtLn/VbDzXduFRpEYEEOzRP15+ontL5VOjUU78DuAiK45P3hLOT+Jzjn3rhXglnlNhDNQpglwyGXdYQED0wQY8DHDAgamMdDx3qdhsVgMoF0YOBYpmYUaQBsZuBVJjmABA9UmBgY5gKtobiDazIB+JC0kGIhWgPJBWkgwEG0N2GUwMMCugIUb4Ap75IxCggEeaWVOUrcaHEKZ1qiRhV4NhtAoa/WF0lMNutjqOCx7QRxWGLeXsBq3ByaUEANj8Vr1IEbOV+iBrjcAJdQB7cG9CqCJy1A/ga4YHKVl20Bo+jbgY6sAPvgK4NGpAuv9G6BxRfORzTKbjQrprA/L01kZt6dzMzAhndtASKcD+//iX4AX3T+7h6Wwmo8AAAAASUVORK5CYII=' - function Test-IsSystem { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - return $id.Name -like "NT AUTHORITY*" -or $id.IsSystem - } - - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - function ConvertFrom-Base64 { - param( - $Base64, - $Path - ) - $bytes = [Convert]::FromBase64String($Base64) - - [IO.File]::WriteAllBytes($Path, $bytes) - } - - # There are many ways to create icon files. The method below creates a PNG and then creates an ICO file in binary form by creating the header and adding the PNG's binary at the bottom. - # Once this has been done, we can simply write all the bytes to our new file. - function ConvertFrom-Image { - param( - $ImagePath, - $Path - ) - - # Grab an instance of the image and a blank bitmap - $image = [Drawing.Image]::FromFile($ImagePath) - - # If you want transparency, you'll need an Alpha channel in the pixel format - $bitmap = New-Object System.Drawing.Bitmap (255, 255, [system.drawing.imaging.PixelFormat]::Format32bppArgb) - $bitmap.SetResolution(255, 255) - - # Create a graphics object which will be used to resize the image to 255px by 255px - $graphics = [System.Drawing.Graphics]::FromImage($bitmap) - - # Set some quality settings for the resize operation - $graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality - $graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic - $graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality - - # Draw the image onto the bitmap - $graphics.DrawImage($Image, 0, 0, 255, 255) - - # Temporarily save the image as a PNG - $RandomNumber = Get-Random -Maximum 1000000 - $bitmap.Save("$env:TEMP\image-$RandomNumber.png", [System.Drawing.Imaging.ImageFormat]::Png) - $png = "$env:TEMP\image-$RandomNumber.png" - - # Begin building the ICO file in binary using the PNG file (ICO files are comprised of PNG(s)) - if ($PSVersionTable.PSVersion.Major -gt 5) { - $pngBytes = Get-Content -Path $png -AsByteStream - } - else { - $pngBytes = Get-Content -Path $png -Encoding Byte -Raw - } - $icoHeader = [byte[]] @(0, 0, 1, 0, 1, 0) - $imageDataSize = $pngBytes.Length - $icoDirectory = [byte[]] @( - 255, 255, # icon size - 0, 0, # color count - 0, 0, # reserved - 0, 0, # hotspot x, hotspot y - ($imageDataSize -band 0xFF), - (($imageDataSize -shr 8) -band 0xFF), - (($imageDataSize -shr 16) -band 0xFF), - (($imageDataSize -shr 24) -band 0xFF), - 22, 0, 0, 0 # offset to image data - ) - $iconData = $icoHeader + $icoDirectory + $pngBytes - - # Once complete, save the icon file - if (Test-Path $Path -ErrorAction SilentlyContinue) { Remove-Item $Path -Force } - [System.IO.File]::WriteAllBytes($Path, $iconData) - - # Close out of everything and remove the temporary file - if (Test-Path $png -ErrorAction SilentlyContinue) { Remove-Item $png -Force } - $bitmap.Dispose() - $image.Dispose() - $graphics.Dispose() - [System.GC]::Collect() - - # Refresh the icon cache - if ([System.Environment]::OSVersion.Version.Major -ge 10) { - Invoke-Command { ie4uinit.exe -show } - } - else { - Invoke-Command { ie4uinit.exe -ClearIconCache } - } - } - - # Utility function for downloading files. - function Invoke-Download { - param( - [Parameter()] - [String]$URL, - [Parameter()] - [String]$Path, - [Parameter()] - [int]$Attempts = 3, - [Parameter()] - [Switch]$SkipSleep - ) - Write-Host "URL given, downloading the file..." - - $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType') - if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12 - } - elseif ( $SupportedTLSversions -contains 'Tls12' ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 - } - else { - # Not everything requires TLS 1.2, but we'll try anyways. - Write-Warning "TLS 1.2 and or TLS 1.3 isn't supported on this system. This download may fail!" - if ($PSVersionTable.PSVersion.Major -lt 3) { - Write-Warning "PowerShell 2 / .NET 2.0 doesn't support TLS 1.2." - } - } - - $i = 1 - While ($i -le $Attempts) { - # Some cloud services have rate limiting. - if (-not ($SkipSleep)) { - $SleepTime = Get-Random -Minimum 3 -Maximum 15 - Write-Host "Waiting for $SleepTime seconds." - Start-Sleep -Seconds $SleepTime - } - if ($i -ne 1) { Write-Host "" } - Write-Host "Download Attempt $i" - - try { - # Invoke-WebRequest is preferred because it supports links that redirect, e.g., https://t.ly. - if ($PSVersionTable.PSVersion.Major -lt 4) { - # Downloads the file - $WebClient = New-Object System.Net.WebClient - $WebClient.DownloadFile($URL, $Path) - } - else { - # Standard options for Invoke-WebRequest. - $WebRequestArgs = @{ - Uri = $URL - OutFile = $Path - MaximumRedirection = 10 - UseBasicParsing = $true - } - - # Downloads the file - Invoke-WebRequest @WebRequestArgs - } - - $File = Test-Path -Path $Path -ErrorAction SilentlyContinue - } - catch { - Write-Warning "An error has occurred while downloading!" - Write-Warning $_.Exception.Message - - if (Test-Path -Path $Path -ErrorAction SilentlyContinue) { - Remove-Item $Path -Force -Confirm:$false -ErrorAction SilentlyContinue - } - - $File = $False - } - - if ($File) { - $i = $Attempts - } - else { - Write-Warning "File failed to download." - Write-Host "" - } - - $i++ - } - - if (-not (Test-Path $Path)) { - Write-Warning "Failed to download file!" - } - else { - return $Path - } - } - - # Used for creating desktop shortcuts. - function New-Shortcut { - [CmdletBinding()] - param( - [Parameter()] - [String]$Arguments, - [Parameter()] - [String]$IconPath, - [Parameter(ValueFromPipeline)] - [String]$Path, - [Parameter()] - [String]$Target, - [Parameter()] - [String]$WorkingDir - ) - process { - Write-Host "Creating shortcut at $Path" - $ShellObject = New-Object -ComObject ("WScript.Shell") - $Shortcut = $ShellObject.CreateShortcut($Path) - $Shortcut.TargetPath = $Target - if ($WorkingDir) { $Shortcut.WorkingDirectory = $WorkingDir } - if ($Arguments) { $ShortCut.Arguments = $Arguments } - if ($IconPath) { $Shortcut.IconLocation = $IconPath } - $Shortcut.Save() - - if (-not(Test-Path $Path -ErrorAction Ignore)) { - Write-Host "[Error] Unable to create shortcut at $Path" - exit 1 - } - } - } - - function Set-HKProperty { - param ( - $Path, - $Name, - $Value, - [ValidateSet('DWord', 'QWord', 'String', 'ExpandedString', 'Binary', 'MultiString', 'Unknown')] - $PropertyType = 'DWord' - ) - if (-not $(Test-Path -Path $Path)) { - # Check if the path does not exist and create the path. - New-Item -Path $Path -Force | Out-Null - } - if ((Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore)) { - # Update the property and print out what it was changed from and what it was changed to. - $CurrentValue = (Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore).$Name - try { - Set-ItemProperty -Path $Path -Name $Name -Value $Value -Force -Confirm:$false -ErrorAction Stop | Out-Null - } - catch { - Write-Host "[Error] Unable to Set registry key for $Name; please see below error!" - Write-Host "$($_.Exception.Message)" - exit 1 - } - Write-Host "$Path\$Name changed from $CurrentValue to $($(Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore).$Name)" - } - else { - # Create the property with a value. - try { - New-ItemProperty -Path $Path -Name $Name -Value $Value -PropertyType $PropertyType -Force -Confirm:$false -ErrorAction Stop | Out-Null - } - catch { - Write-Host "[Error] Unable to Set registry key for $Name please see below error!" - Write-Host "$($_.Exception.Message)" - exit 1 - } - Write-Host "Set $Path\$Name to $($(Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore).$Name)" - } - } - - # This function is to make it easier to parse Ninja Custom Fields. - function Get-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter()] - [String]$DocumentName - ) - - # If we're requested to get the field value from a Ninja document, we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # These two types require more information to parse. - $NeedsOptions = "DropDown", "MultiSelect" - - # Grabbing document values requires a slightly different command. - if ($DocumentName) { - # Secure fields are only readable when they're a device custom field - if ($Type -Like "Secure") { throw "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyValue = Ninja-Property-Docs-Get -AttributeName $Name @DocumentationParams 2>&1 - - # Certain fields require more information to parse. - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - # We'll redirect error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1 - - # Certain fields require more information to parse. - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If we received some sort of error it should have an exception property and we'll exit the function outputing nothing. - if ($NinjaPropertyValue.Exception) { - Write-Verbose $NinjaPropertyValue.ToString() - return $null - } - if ($NinjaPropertyOptions.Exception) { - Write-Verbose $NinjaPropertyOptions.ToString() - return $null - } - - # This switch will compare the type given with the quoted string. If it matches, it'll parse it further; otherwise, the default option will be selected. - switch ($Type) { - "Checkbox" { - # Checkboxes come in as a string representing an integer. We'll need to cast that string into an integer and then convert it to a more traditional boolean. - [System.Convert]::ToBoolean([int]$NinjaPropertyValue) - } - "Dropdown" { - # Drop-down custom fields come in as a comma-separated list of GUIDs; we'll compare these with all the options and return just the option values selected instead of a GUID. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name - } - "MultiSelect" { - # Multi-Select custom fields come in as a comma-separated list of GUIDs; we'll compare these with all the options and return just the option values selected instead of a GUID. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = ($NinjaPropertyValue -split ',') | ForEach-Object { $_.Trim() } - - foreach ($Item in $Selection) { - $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name - } - } - default { - # If no type was given or not one that matches the above types just output what we retrieved. - $NinjaPropertyValue - } - } - } - - function Get-NinjaProperty-WithError { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter()] - [String]$DocumentName - ) - - # If we're requested to get the field value from a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # These two types require more information to parse. - $NeedsOptions = "DropDown", "MultiSelect" - - # Grabbing document values requires a slightly different command. - if ($DocumentName) { - # Secure fields are only readable when they're a device custom field - if ($Type -Like "Secure") { throw [System.ArgumentOutOfRangeException]::New("$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality") } - - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - Write-Host "Retrieving value from Ninja Document..." - $NinjaPropertyValue = Ninja-Property-Docs-Get -AttributeName $Name @DocumentationParams 2>&1 - - # Certain fields require more information to parse. - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - # We'll redirect error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1 - - # Certain fields require more information to parse. - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If we received some sort of error it should have an exception property and we'll exit the function with that error information. - if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - if (-not $NinjaPropertyValue) { - throw [System.NullReferenceException]::New("The Custom Field '$Name' is empty!") - } - - # This switch will compare the type given with the quoted string. If it matches, it'll parse it further; otherwise, the default option will be selected. - switch ($Type) { - "Checkbox" { - # Checkbox's come in as a string representing an integer. We'll need to cast that string into an integer and then convert it to a more traditional boolean. - [System.Convert]::ToBoolean([int]$NinjaPropertyValue) - } - "Decimal" { - # In ninja decimals are strings that represent a decimal this will cast it into a double data type. - [double]$NinjaPropertyValue - } - "Dropdown" { - # Drop-Down custom fields come in as a comma-separated list of GUIDs; we'll compare these with all the options and return just the option values selected instead of a GUID. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name - } - "Integer" { - # Cast's the Ninja provided string into an integer. - [int]$NinjaPropertyValue - } - "MultiSelect" { - # Multi-Select custom fields come in as a comma-separated list of GUID's we'll compare these with all the options and return just the option values selected instead of a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = ($NinjaPropertyValue -split ',').trim() - - foreach ($Item in $Selection) { - $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name - } - } - default { - # If no type was given or not one that matches the above types just output what we retrieved. - $NinjaPropertyValue - } - } - } - - # Pulling the document name from the script form. - if ($env:ninjaDocumentName -and $env:ninjaDocumentName -notlike "null") { $DocumentName = $env:ninjaDocumentName } - - if ($DocumentName) { - Write-Host "Retrieving values from Ninja Document '$DocumentName'..." - $DocumentationParams = @{ - DocumentName = $DocumentName - } - } - - if (Test-IsElevated) { - Write-Host "Checking custom fields for VPN configuration settings ..." - - if ($env:presharedKeyCustomField -and $env:presharedKeyCustomField -notlike "null") { $PreSharedKeyField = $env:presharedKeyCustomField } - - # Grabbing Documentation or Custom Field Values if the parameter wasn't given or is the default value. - if (-not $AssumeUDPencapsulation) { $AssumeUDPencapsulation = Get-NinjaProperty -Name $UDPField -Type "Checkbox" @DocumentationParams } - if (-not $Name) { $Name = Get-NinjaProperty -Name $NameField @DocumentationParams } - if (-not $Server) { $Server = Get-NinjaProperty -Name $ServerField @DocumentationParams } - if ($PreSharedKeyField) { - try { - $PreSharedKey = Get-NinjaProperty-WithError -Name $PreSharedKeyField @DocumentationParams - } - catch { - Write-Host -Object "[Error] Unable to retrieve the PreShared key from '$PreSharedKeyField'." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - if (-not $TunnelType -and $TunnelType -eq "Automatic") { - $Tunnel = Get-NinjaProperty -Name $TunnelTypeField -Type "Dropdown" @DocumentationParams - if ($Tunnel) { $TunnelType = $Tunnel } - } - if (-not $AuthMethod) { $AuthMethod = Get-NinjaProperty -Name $AuthMethodField -Type "Multiselect" @DocumentationParams } - if (-not $EncryptionLevel) { $EncryptionLevel = Get-NinjaProperty -Name $EncryptionLevelField -Type "Dropdown" @DocumentationParams } - if (-not $DNSSuffix) { $DNSSuffix = Get-NinjaProperty -Name $DNSSuffixField @DocumentationParams } - if (-not $RememberCreds) { $RememberCreds = Get-NinjaProperty -Name $RememberCredsField -Type "Checkbox" @DocumentationParams } - if (-not $UseWinlogonCredential) { $UseWinLogonCredential = Get-NinjaProperty -Name $UseWinlogonCredsField -Type "Checkbox" @DocumentationParams } - if (-not $SplitTunneling) { $SplitTunneling = Get-NinjaProperty -Name $SplitTunnelingField -Type "Checkbox" @DocumentationParams } - if (-not $CreateShortcut) { $CreateShortcut = Get-NinjaProperty -Name $ShortcutField -Type "Checkbox" @DocumentationParams } - if (-not $IconDirectory) { $IconDirectory = Get-NinjaProperty -Name $IconDirectoryField @DocumentationParams } - if (-not $Url) { $Url = Get-NinjaProperty -Name $UrlField @DocumentationParams } - } - else { - Write-Warning "Reading Custom Fields requires local admin privileges." - } - - # If someone specifies something in the script form, we'll want to overwrite what the documentation says. Makes the script more useful for troubleshooting. - if ($env:vpnName -and $env:vpnName -notlike "null") { $Name = $env:vpnName } - if ($env:vpnServerAddress -and $env:vpnServerAddress -notlike "null") { $Server = $env:vpnServerAddress } - if ($env:vpnTunnelType -and $env:vpnTunnelType -notlike "null") { $TunnelType = $env:vpnTunnelType } - if ($env:authenticationMethod -and $env:authenticationMethod -notlike "null") { $AuthMethod = $env:authenticationMethod } - if ($env:encryptionLevel -and $env:encryptionLevel -notlike "null") { $EncryptionLevel = $env:encryptionLevel } - if ($env:dnsSuffix -and $env:dnsSuffix -notlike "null") { $DNSSuffix = $env:dnsSuffix } - if ($env:iconPath -and $env:iconPath -notlike "null") { $Icon = $env:iconPath } - if ($env:iconUrl -and $env:iconUrl -notlike "null") { $Url = $env:iconUrl } - if ($env:iconDirectory -and $env:iconDirectory -notlike "null") { $IconDirectory = $env:iconDirectory } - - # If an authentication method was given, we'll want to parse that and validate it. - if ($AuthMethod) { - $AuthSelections = ($AuthMethod.Split(',')).Trim() - - $AuthSelections | ForEach-Object { - switch ($_) { - "PAP" { Write-Verbose "PAP Selected" } - "Chap" { Write-Verbose "Chap Selected" } - "MSChapv2" { Write-Verbose "Eap Selected" } - "MachineCertificate" { Write-Verbose "MachineCertificate" } - default { - Write-Host "[Error] $_ is invalid! The Valid auth types are 'PAP','Chap','MSChapv2' or 'MachineCertificate'." - exit 1 - } - } - } - } - - # Validating encryption level. Not supporting "No Encryption". - if ($EncryptionLevel) { - switch ($EncryptionLevel) { - "Optional" { Write-Verbose "Optional Selected" } - "Required" { Write-Verbose "Required Selected" } - "Maximum" { Write-Verbose "Maximum Selected" } - default { - Write-Host "[Error] $EncryptionLevel is invalid! The valid encryption levels are 'Optional', 'Required', or 'Maximum'." - } - } - } - - # This will set the tunnel type. - switch ($TunnelType) { - "L2TP" { Write-Verbose "L2TP Selected" } - "SSTP" { Write-Verbose "SSTP Selected" } - "IKEV2" { Write-Verbose "IKEV2 Selected" } - "Automatic" { Write-Verbose "Automatic Selected" } - default { - Write-Host "[Error] $TunnelType is invalid! The valid tunnel types are 'L2TP', 'SSTP', 'IKEv2', or 'Automatic'." - exit 1 - } - } - - # Error out if the only two mandatory parameters don't exist. - if (-not $Server -or -not $Name) { - Write-Host "[Error] Name and Server are required! If using documentation fields or custom fields, double-check that your field exists and is readable by Automations." - exit 1 - } - - # Icons are minor, so if we're not given the information, we'll continue on without it and just let the technician know. They can always re-run it. - if (($Url -or $IconDirectory) -and -not $CreateShortcut) { - Write-Warning "An icon was given, but Create Shortcut was never used? Ignoring icon info..." - $Url = $null - $Icon = $null - $IconDirectory = $null - } - - # Icons are minor, so if we're not given the information, we'll continue on without it and just let the technician know. They can always re-run it. - if ($Url -and -not $IconDirectory) { - Write-Warning "An icon was given, but a place to store it wasn't? Use Icon Directory box to specify a directory to store it. (You may want this directory to be accessible by all users)" - Write-Warning "Ignoring supplied icon info." - $Icon = $null - $Url = $null - } - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # Checking for existing VPNs - $UserVPNs = Get-VpnConnection -Name $Name -EA Ignore - $GlobalVPNs = Get-VpnConnection -Name $Name -AllUserConnection -EA Ignore - - # If the overwrite parameter wasn't specified, we'll error out as we won't be able to create the VPN. - if (($UserVPNs -or $GlobalVPNs) -and -not $Overwrite) { - Write-Host "[Error] $Name already exists! Use -Overwrite to replace it." - if ($GlobalVPNs -and -not (Test-IsElevated)) { Write-Host "[Error] $($GlobalVPNs.Name) requires elevation to replace/overwrite." } - exit 1 - } - - # Removing previous VPNs if overwrite was specified. - if ($UserVPNs -and $Overwrite) { - $UserVPNs | Remove-VpnConnection -Force - } - - if ($GlobalVPNs -and $Overwrite) { - if (Test-IsElevated) { - $GlobalVPNs | Remove-VpnConnection -AllUserConnection -Force - } - else { - Write-Host "[Error] $($GlobalVPNs.Name) requires elevation to replace/overwrite." - exit 1 - } - } - - # Setting the AssumeUDPEncapsulationContextOnSendRule - if ($AssumeUDPencapsulation -and (Test-IsElevated)) { - Write-Warning "AssumeUDPEncapsulation requires a reboot to take effect. This script does NOT reboot the machine." - Set-HKProperty -Name "AssumeUDPEncapsulationContextOnSendRule" -Path "HKLM:\SYSTEM\CurrentControlSet\Services\PolicyAgent" -Value "2" - } - - # Building the Add-VPNConnection command based on what information was entered - $ArgumentList = @{} - if ($SplitTunneling) { $ArgumentList["SplitTunneling"] = $True } - if ($Server) { $ArgumentList["ServerAddress"] = $Server } - if ($Name) { $ArgumentList["Name"] = $Name } - if ($PreSharedKey) { $ArgumentList["L2TPPsk"] = $PreSharedKey } - if ($TunnelType) { $ArgumentList["TunnelType"] = $TunnelType } - if ($AuthMethod) { $ArgumentList["AuthenticationMethod"] = $AuthSelections } - if ($EncryptionLevel) { $ArgumentList["EncryptionLevel"] = $EncryptionLevel } - if ($DNSSuffix) { $ArgumentList["DnsSuffix"] = $DNSSuffix } - if ($RememberCreds) { $ArgumentList["RememberCredential"] = $True } - if ($UseWinlogonCredential) { $ArgumentList["UseWinLogonCredential"] = $True } - - # If the script wasn't run as system, only the current user should be able to access the VPN. - if (Test-IsSystem) { $ArgumentList["AllUserConnection"] = $True } - $ArgumentList["Force"] = $True - $ArgumentList["Passthru"] = $True - - try { - Add-VpnConnection @ArgumentList -ErrorAction Stop | Format-Table -Property Name, ServerAddress, TunnelType, EncryptionLevel, AllUserConnection, SplitTunneling | Out-String | Write-Host - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - - switch (Test-IsSystem) { - $True { - $ShortcutPath = "$env:Public\Desktop\$Name.lnk" - $RasFile = Join-Path $env:PROGRAMDATA "Microsoft\Network\Connections\Pbk\rasphone.pbk" - } - default { - $ShortcutPath = "$([Environment]::GetFolderPath("Desktop"))\$Name.lnk" - $RasFile = Join-Path $env:APPDATA "Microsoft\Network\Connections\Pbk\rasphone.pbk" - } - } - - if (-not (Test-Path $RasFile -EA Ignore)) { - Write-Host "[Error] Failed to create vpn!" - exit 1 - } - - # All Windows VPNs get a phonebook entry; we're going to check that the previous Add-VPNConnection was successful by looping through each line in the phonebook. - $Phonebook = Get-Content -Path $RasFile - - for ($lineNumber = 0; $lineNumber -lt $Phonebook.Length; $lineNumber++) { - if ($Phonebook[$lineNumber] -eq "[$Name]") { - $Entry = $lineNumber - break - } - } - - # If the VPN wasn't found, we're going to error out. - if ($Entry -notmatch "\d") { - Write-Host "[Error] Failed to create vpn!" - exit 1 - } - - if ($CreateShortcut) { - if ($IconDirectory -and -not (Test-Path $IconDirectory -ErrorAction SilentlyContinue)) { - New-Item -ItemType Directory -Path $IconDirectory | Out-Null - } - - # If we're given a URL, we'll want to download it. - if ($Url) { - $DownloadArguments = @{ - URL = $Url - Path = "$IconDirectory\vpnscript-$Name.png" - } - if ($SkipSleep) { $DownloadArguments["SkipSleep"] = $True } - - Invoke-Download @DownloadArguments - - if (Test-Path "$IconDirectory\vpnscript-$Name.png" -ErrorAction SilentlyContinue) { - $Icon = "$IconDirectory\vpnscript-$Name.png" - } - else { - $Icon = $Null - } - } - - # This will convert the base64 into an image and save it to the temp folder. - if ($IconBase64 -and $IconDirectory -and -not $Icon -and -not $Url) { - Write-Verbose "Converting Icon base64 to original image and saving to $IconDirectory..." - - ConvertFrom-Base64 -Base64 $IconBase64 -Path "$IconDirectory\vpnscript-$Name.Png" - - if (Test-Path "$IconDirectory\vpnscript-$Name.Png" -ErrorAction SilentlyContinue) { - $Icon = "$IconDirectory\vpnscript-$Name.Png" - } - else { - $Icon = $Null - } - } - - if ($Icon) { - Write-Verbose "Converting image to icon and saving to $IconDirectory\vpnscript-$((Get-FileHash -Path $Icon -Algorithm MD5).Hash).ico ..." - - $NewName = "vpnscript-$((Get-FileHash -Path $Icon -Algorithm MD5).Hash).ico" - ConvertFrom-Image -ImagePath $Icon -Path "$IconDirectory\$NewName" - - if (Test-Path "$IconDirectory\$NewName" -ErrorAction SilentlyContinue) { - $Icon = "$IconDirectory\$NewName" - } - else { - $Icon = $Null - } - } - } - - if ($CreateShortcut) { - $ShortcutArgs = @{ - Path = $ShortcutPath - Target = "rasphone.exe" - Arguments = "-d `"$Name`"" - WorkingDir = "$env:SystemRoot\System32" - } - if ($Icon) { $ShortcutArgs["IconPath"] = $Icon } - - New-Shortcut @ShortcutArgs - } - - exit $ExitCode -} -end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + Creates a VPN using the built-in Windows VPN client, with configuration options from Ninja Documentation or Device Custom Fields. +.DESCRIPTION + This script will create a VPN using the built-in Windows VPN client. It will not create PPTP or "No Encryption" VPNs. + It can pull all the necessary VPN information from a Ninja Document or custom field you specify. + You can override the Documentation fields with parameters or only use parameters if you would like. + + For more information on Ninja Documentation: https://ninjarmm.zendesk.com/hc/en-us/articles/360061218431-Documentation + +OPTIONAL: Custom Fields - All Fields must be readable by scripts + # Name # - # Field Type and what values to use for dropdowns. # + vpnName - text field + vpnHost - text field + tunnelType - dropdown field. Acceptable values are L2TP, SSTP, IKEV2, Automatic + authMethod - multi-select field. Acceptable values are PAP, Chap, MSChapv2, MachineCertificate + encryptionLevel - dropdown field. Acceptable values are Optional, Required, Maximum + dnsSuffix - text field + rememberCreds - Checkbox + useWinCredentials - Checkbox + assumeUdpEncapsulation - Checkbox + splitTunneling - Checkbox + createVpnShortcut - Checkbox + shortcutUrl - URL or Text + shortcutIconDirectory - Text + +PARAMETER: -DocumentName "ReplaceWithNameOfyourNinjaDocument" + Replace the value in quotes with the document you'd like to retireve the vpn parameters from (if any). + +PARAMETER: -Name "Contoso VPN" + The name of the VPN you would like to create + +PARAMETER: -Server "replace.me" + The endpoint/server the VPN will try to establish a connection with. + +PARAMETER: -PreSharedKeyField "ReplaceMeWithNameOfSecureCustomField" + Name of a secure custom field containing your pre-shared key. + +PARAMETER: -TunnelType "L2TP" + The Type of VPN ex. L2TP, SSTP, IKEV2, Automatic + +PARAMETER: -AuthMethod "PAP,CHAP" + The authentication methods supported by the VPN separated by commas. + +PARAMETER: -EncryptionLevel "Required" + The Encryption level used by the VPN. + +PARAMETER: -DNSSuffix "contoso.local" + The DNS Suffix used by the connection. + +PARAMETER: -RememberCreds + Whether or not the VPN should remember the previously used credentials for future connections. + +PARAMETER: -UseWinlogonCredential + Whether or not the VPN should use the Windows logon credentials for authentication. + +PARAMETER: -AssumeUDPEncapsulation + Sets the AssumeUDPEncapsulation registry key which is required by many VPNs +.LINK + https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/configure-l2tp-ipsec-server-behind-nat-t-device + +PARAMETER: -SplitTunneling + Enables split tunneling. + +PARAMETER: -CreateShortcut + Creates a desktop shortcut for accessing the VPN. + +PARAMETER: -URL + A URL to an image you'd like to download and use for the shortcut icon. Icon will be stored at -IconDirectory. + +PARAMETER: -IconDirectory "C:\ReplaceMe" + The directory where the shortcut's icon file will be stored. + +PRESET PARAMETER: -SkipSleep + By default the script sleeps for a random interval between 3 and 60 seconds prior to downloading an icon (if the script is given a url). This parameter skips the sleep. + +PARAMETER: -Overwrite + Overwrites an existing VPN with the same name, if present. +.OUTPUTS + None +.NOTES + Minimum Supported OS: Windows 10 + Release Notes: Update calculated name +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$DocumentName, + [Parameter()] + [String]$Name, + [Parameter()] + [String]$NameField = "vpnName", + [Parameter()] + [String]$Server, + [Parameter()] + [String]$ServerField = "vpnHost", + [Parameter()] + [String]$PreSharedKeyField, + [Parameter()] + [String]$TunnelType = "Automatic", + [Parameter()] + [String]$TunnelTypeField = "tunnelType", + [Parameter()] + [String[]]$AuthMethod, + [Parameter()] + [String]$AuthMethodField = "authMethod", + [Parameter()] + [String]$EncryptionLevel, + [Parameter()] + [String]$EncryptionLevelField = "encryptionLevel", + [Parameter()] + [String]$DNSSuffix, + [Parameter()] + [String]$DNSSuffixField = "dnsSuffix", + [Parameter()] + [Switch]$RememberCreds = [System.Convert]::ToBoolean($env:rememberUserCredentials), + [Parameter()] + [String]$RememberCredsField = "rememberCreds", + [Parameter()] + [Switch]$UseWinlogonCredential = [System.Convert]::ToBoolean($env:useWindowsCredentials), + [Parameter()] + [String]$UseWinlogonCredsField = "useWinCredentials", + [Parameter()] + [Switch]$AssumeUDPencapsulation = [System.Convert]::ToBoolean($env:assumeUdpEncapsulation), + [Parameter()] + [String]$UDPField = "assumeUdpEncapsulation", + [Parameter()] + [Switch]$SplitTunneling = [System.Convert]::ToBoolean($env:splitTunneling), + [Parameter()] + [String]$SplitTunnelingField = "splitTunneling", + [Parameter()] + [Switch]$CreateShortcut = [System.Convert]::ToBoolean($env:createVpnDesktopShortcut), + [Parameter()] + [String]$ShortcutField = "createVpnShortcut", + [Parameter()] + [String]$Url, + [Parameter()] + [String]$UrlField = "shortcutUrl", + [Parameter()] + [String]$IconDirectory, + [Parameter()] + [String]$IconDirectoryField = "shortcutIconDirectory", + [Parameter()] + [Switch]$Overwrite = [System.Convert]::ToBoolean($env:overwrite), + [Parameter()] + [Switch]$SkipSleep +) +begin { + Add-Type -AssemblyName System.Drawing + + if ([System.Convert]::ToBoolean($env:verboseOutput)) { + $VerbosePreference = 'Continue' + } + + # You can replace the line below with $IconBase64 = 'ReplaceThisWithYourBase64EncodedImageEncasedInQuotes', and the script will decode the image and use it for the VPN shortcut. + $IconBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAElBMVEUAAAD///////////////////8+Uq06AAAABXRSTlMAAECAv9KsvScAAAEsSURBVEjH7ZUxDsIwDEWtFA7ADbqwM8COhLpXtLn/VbDzXduFRpEYEEOzRP15+ontL5VOjUU78DuAiK45P3hLOT+Jzjn3rhXglnlNhDNQpglwyGXdYQED0wQY8DHDAgamMdDx3qdhsVgMoF0YOBYpmYUaQBsZuBVJjmABA9UmBgY5gKtobiDazIB+JC0kGIhWgPJBWkgwEG0N2GUwMMCugIUb4Ap75IxCggEeaWVOUrcaHEKZ1qiRhV4NhtAoa/WF0lMNutjqOCx7QRxWGLeXsBq3ByaUEANj8Vr1IEbOV+iBrjcAJdQB7cG9CqCJy1A/ga4YHKVl20Bo+jbgY6sAPvgK4NGpAuv9G6BxRfORzTKbjQrprA/L01kZt6dzMzAhndtASKcD+//iX4AX3T+7h6Wwmo8AAAAASUVORK5CYII=' + function Test-IsSystem { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + return $id.Name -like "NT AUTHORITY*" -or $id.IsSystem + } + + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + function ConvertFrom-Base64 { + param( + $Base64, + $Path + ) + $bytes = [Convert]::FromBase64String($Base64) + + [IO.File]::WriteAllBytes($Path, $bytes) + } + + # There are many ways to create icon files. The method below creates a PNG and then creates an ICO file in binary form by creating the header and adding the PNG's binary at the bottom. + # Once this has been done, we can simply write all the bytes to our new file. + function ConvertFrom-Image { + param( + $ImagePath, + $Path + ) + + # Grab an instance of the image and a blank bitmap + $image = [Drawing.Image]::FromFile($ImagePath) + + # If you want transparency, you'll need an Alpha channel in the pixel format + $bitmap = New-Object System.Drawing.Bitmap (255, 255, [system.drawing.imaging.PixelFormat]::Format32bppArgb) + $bitmap.SetResolution(255, 255) + + # Create a graphics object which will be used to resize the image to 255px by 255px + $graphics = [System.Drawing.Graphics]::FromImage($bitmap) + + # Set some quality settings for the resize operation + $graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality + $graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic + $graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality + + # Draw the image onto the bitmap + $graphics.DrawImage($Image, 0, 0, 255, 255) + + # Temporarily save the image as a PNG + $RandomNumber = Get-Random -Maximum 1000000 + $bitmap.Save("$env:TEMP\image-$RandomNumber.png", [System.Drawing.Imaging.ImageFormat]::Png) + $png = "$env:TEMP\image-$RandomNumber.png" + + # Begin building the ICO file in binary using the PNG file (ICO files are comprised of PNG(s)) + if ($PSVersionTable.PSVersion.Major -gt 5) { + $pngBytes = Get-Content -Path $png -AsByteStream + } + else { + $pngBytes = Get-Content -Path $png -Encoding Byte -Raw + } + $icoHeader = [byte[]] @(0, 0, 1, 0, 1, 0) + $imageDataSize = $pngBytes.Length + $icoDirectory = [byte[]] @( + 255, 255, # icon size + 0, 0, # color count + 0, 0, # reserved + 0, 0, # hotspot x, hotspot y + ($imageDataSize -band 0xFF), + (($imageDataSize -shr 8) -band 0xFF), + (($imageDataSize -shr 16) -band 0xFF), + (($imageDataSize -shr 24) -band 0xFF), + 22, 0, 0, 0 # offset to image data + ) + $iconData = $icoHeader + $icoDirectory + $pngBytes + + # Once complete, save the icon file + if (Test-Path $Path -ErrorAction SilentlyContinue) { Remove-Item $Path -Force } + [System.IO.File]::WriteAllBytes($Path, $iconData) + + # Close out of everything and remove the temporary file + if (Test-Path $png -ErrorAction SilentlyContinue) { Remove-Item $png -Force } + $bitmap.Dispose() + $image.Dispose() + $graphics.Dispose() + [System.GC]::Collect() + + # Refresh the icon cache + if ([System.Environment]::OSVersion.Version.Major -ge 10) { + Invoke-Command { ie4uinit.exe -show } + } + else { + Invoke-Command { ie4uinit.exe -ClearIconCache } + } + } + + # Utility function for downloading files. + function Invoke-Download { + param( + [Parameter()] + [String]$URL, + [Parameter()] + [String]$Path, + [Parameter()] + [int]$Attempts = 3, + [Parameter()] + [Switch]$SkipSleep + ) + Write-Host "URL given, downloading the file..." + + $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType') + if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12 + } + elseif ( $SupportedTLSversions -contains 'Tls12' ) { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 + } + else { + # Not everything requires TLS 1.2, but we'll try anyways. + Write-Warning "TLS 1.2 and or TLS 1.3 isn't supported on this system. This download may fail!" + if ($PSVersionTable.PSVersion.Major -lt 3) { + Write-Warning "PowerShell 2 / .NET 2.0 doesn't support TLS 1.2." + } + } + + $i = 1 + While ($i -le $Attempts) { + # Some cloud services have rate limiting. + if (-not ($SkipSleep)) { + $SleepTime = Get-Random -Minimum 3 -Maximum 15 + Write-Host "Waiting for $SleepTime seconds." + Start-Sleep -Seconds $SleepTime + } + if ($i -ne 1) { Write-Host "" } + Write-Host "Download Attempt $i" + + try { + # Invoke-WebRequest is preferred because it supports links that redirect, e.g., https://t.ly. + if ($PSVersionTable.PSVersion.Major -lt 4) { + # Downloads the file + $WebClient = New-Object System.Net.WebClient + $WebClient.DownloadFile($URL, $Path) + } + else { + # Standard options for Invoke-WebRequest. + $WebRequestArgs = @{ + Uri = $URL + OutFile = $Path + MaximumRedirection = 10 + UseBasicParsing = $true + } + + # Downloads the file + Invoke-WebRequest @WebRequestArgs + } + + $File = Test-Path -Path $Path -ErrorAction SilentlyContinue + } + catch { + Write-Warning "An error has occurred while downloading!" + Write-Warning $_.Exception.Message + + if (Test-Path -Path $Path -ErrorAction SilentlyContinue) { + Remove-Item $Path -Force -Confirm:$false -ErrorAction SilentlyContinue + } + + $File = $False + } + + if ($File) { + $i = $Attempts + } + else { + Write-Warning "File failed to download." + Write-Host "" + } + + $i++ + } + + if (-not (Test-Path $Path)) { + Write-Warning "Failed to download file!" + } + else { + return $Path + } + } + + # Used for creating desktop shortcuts. + function New-Shortcut { + [CmdletBinding()] + param( + [Parameter()] + [String]$Arguments, + [Parameter()] + [String]$IconPath, + [Parameter(ValueFromPipeline)] + [String]$Path, + [Parameter()] + [String]$Target, + [Parameter()] + [String]$WorkingDir + ) + process { + Write-Host "Creating shortcut at $Path" + $ShellObject = New-Object -ComObject ("WScript.Shell") + $Shortcut = $ShellObject.CreateShortcut($Path) + $Shortcut.TargetPath = $Target + if ($WorkingDir) { $Shortcut.WorkingDirectory = $WorkingDir } + if ($Arguments) { $ShortCut.Arguments = $Arguments } + if ($IconPath) { $Shortcut.IconLocation = $IconPath } + $Shortcut.Save() + + if (-not(Test-Path $Path -ErrorAction Ignore)) { + Write-Host "[Error] Unable to create shortcut at $Path" + exit 1 + } + } + } + + function Set-HKProperty { + param ( + $Path, + $Name, + $Value, + [ValidateSet('DWord', 'QWord', 'String', 'ExpandedString', 'Binary', 'MultiString', 'Unknown')] + $PropertyType = 'DWord' + ) + if (-not $(Test-Path -Path $Path)) { + # Check if the path does not exist and create the path. + New-Item -Path $Path -Force | Out-Null + } + if ((Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore)) { + # Update the property and print out what it was changed from and what it was changed to. + $CurrentValue = (Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore).$Name + try { + Set-ItemProperty -Path $Path -Name $Name -Value $Value -Force -Confirm:$false -ErrorAction Stop | Out-Null + } + catch { + Write-Host "[Error] Unable to Set registry key for $Name; please see below error!" + Write-Host "$($_.Exception.Message)" + exit 1 + } + Write-Host "$Path\$Name changed from $CurrentValue to $($(Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore).$Name)" + } + else { + # Create the property with a value. + try { + New-ItemProperty -Path $Path -Name $Name -Value $Value -PropertyType $PropertyType -Force -Confirm:$false -ErrorAction Stop | Out-Null + } + catch { + Write-Host "[Error] Unable to Set registry key for $Name please see below error!" + Write-Host "$($_.Exception.Message)" + exit 1 + } + Write-Host "Set $Path\$Name to $($(Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore).$Name)" + } + } + + # This function is to make it easier to parse Ninja Custom Fields. + function Get-NinjaProperty { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [String]$Name, + [Parameter()] + [String]$Type, + [Parameter()] + [String]$DocumentName + ) + + # If we're requested to get the field value from a Ninja document, we'll specify it here. + $DocumentationParams = @{} + if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } + + # These two types require more information to parse. + $NeedsOptions = "DropDown", "MultiSelect" + + # Grabbing document values requires a slightly different command. + if ($DocumentName) { + # Secure fields are only readable when they're a device custom field + if ($Type -Like "Secure") { throw "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } + + # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. + + # Certain fields require more information to parse. + if ($NeedsOptions -contains $Type) { + } + } + else { + # We'll redirect error output to the success stream to make it easier to error out if nothing was found or something else went wrong. + + # Certain fields require more information to parse. + if ($NeedsOptions -contains $Type) { + } + } + + # If we received some sort of error it should have an exception property and we'll exit the function outputing nothing. + if ($NinjaPropertyValue.Exception) { + Write-Verbose $NinjaPropertyValue.ToString() + return $null + } + if ($NinjaPropertyOptions.Exception) { + Write-Verbose $NinjaPropertyOptions.ToString() + return $null + } + + # This switch will compare the type given with the quoted string. If it matches, it'll parse it further; otherwise, the default option will be selected. + switch ($Type) { + "Checkbox" { + # Checkboxes come in as a string representing an integer. We'll need to cast that string into an integer and then convert it to a more traditional boolean. + [System.Convert]::ToBoolean([int]$NinjaPropertyValue) + } + "Dropdown" { + # Drop-down custom fields come in as a comma-separated list of GUIDs; we'll compare these with all the options and return just the option values selected instead of a GUID. + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name + } + "MultiSelect" { + # Multi-Select custom fields come in as a comma-separated list of GUIDs; we'll compare these with all the options and return just the option values selected instead of a GUID. + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Selection = ($NinjaPropertyValue -split ',') | ForEach-Object { $_.Trim() } + + foreach ($Item in $Selection) { + $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name + } + } + default { + # If no type was given or not one that matches the above types just output what we retrieved. + $NinjaPropertyValue + } + } + } + + function Get-NinjaProperty-WithError { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [String]$Name, + [Parameter()] + [String]$Type, + [Parameter()] + [String]$DocumentName + ) + + # If we're requested to get the field value from a Ninja document we'll specify it here. + $DocumentationParams = @{} + if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } + + # These two types require more information to parse. + $NeedsOptions = "DropDown", "MultiSelect" + + # Grabbing document values requires a slightly different command. + if ($DocumentName) { + # Secure fields are only readable when they're a device custom field + if ($Type -Like "Secure") { throw [System.ArgumentOutOfRangeException]::New("$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality") } + + # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. + Write-Host "Retrieving value from Ninja Document..." + + # Certain fields require more information to parse. + if ($NeedsOptions -contains $Type) { + } + } + else { + # We'll redirect error output to the success stream to make it easier to error out if nothing was found or something else went wrong. + + # Certain fields require more information to parse. + if ($NeedsOptions -contains $Type) { + } + } + + # If we received some sort of error it should have an exception property and we'll exit the function with that error information. + if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } + if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } + + if (-not $NinjaPropertyValue) { + throw [System.NullReferenceException]::New("The Custom Field '$Name' is empty!") + } + + # This switch will compare the type given with the quoted string. If it matches, it'll parse it further; otherwise, the default option will be selected. + switch ($Type) { + "Checkbox" { + # Checkbox's come in as a string representing an integer. We'll need to cast that string into an integer and then convert it to a more traditional boolean. + [System.Convert]::ToBoolean([int]$NinjaPropertyValue) + } + "Decimal" { + # In ninja decimals are strings that represent a decimal this will cast it into a double data type. + [double]$NinjaPropertyValue + } + "Dropdown" { + # Drop-Down custom fields come in as a comma-separated list of GUIDs; we'll compare these with all the options and return just the option values selected instead of a GUID. + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name + } + "Integer" { + # Cast's the Ninja provided string into an integer. + [int]$NinjaPropertyValue + } + "MultiSelect" { + # Multi-Select custom fields come in as a comma-separated list of GUID's we'll compare these with all the options and return just the option values selected instead of a guid. + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Selection = ($NinjaPropertyValue -split ',').trim() + + foreach ($Item in $Selection) { + $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name + } + } + default { + # If no type was given or not one that matches the above types just output what we retrieved. + $NinjaPropertyValue + } + } + } + + # Pulling the document name from the script form. + if ($env:ninjaDocumentName -and $env:ninjaDocumentName -notlike "null") { $DocumentName = $env:ninjaDocumentName } + + if ($DocumentName) { + Write-Host "Retrieving values from Ninja Document '$DocumentName'..." + $DocumentationParams = @{ + DocumentName = $DocumentName + } + } + + if (Test-IsElevated) { + Write-Host "Checking custom fields for VPN configuration settings ..." + + if ($env:presharedKeyCustomField -and $env:presharedKeyCustomField -notlike "null") { $PreSharedKeyField = $env:presharedKeyCustomField } + + # Grabbing Documentation or Custom Field Values if the parameter wasn't given or is the default value. + if (-not $AssumeUDPencapsulation) { $AssumeUDPencapsulation = Get-NinjaProperty -Name $UDPField -Type "Checkbox" @DocumentationParams } + if (-not $Name) { $Name = Get-NinjaProperty -Name $NameField @DocumentationParams } + if (-not $Server) { $Server = Get-NinjaProperty -Name $ServerField @DocumentationParams } + if ($PreSharedKeyField) { + try { + $PreSharedKey = Get-NinjaProperty-WithError -Name $PreSharedKeyField @DocumentationParams + } + catch { + Write-Host -Object "[Error] Unable to retrieve the PreShared key from '$PreSharedKeyField'." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + } + if (-not $TunnelType -and $TunnelType -eq "Automatic") { + $Tunnel = Get-NinjaProperty -Name $TunnelTypeField -Type "Dropdown" @DocumentationParams + if ($Tunnel) { $TunnelType = $Tunnel } + } + if (-not $AuthMethod) { $AuthMethod = Get-NinjaProperty -Name $AuthMethodField -Type "Multiselect" @DocumentationParams } + if (-not $EncryptionLevel) { $EncryptionLevel = Get-NinjaProperty -Name $EncryptionLevelField -Type "Dropdown" @DocumentationParams } + if (-not $DNSSuffix) { $DNSSuffix = Get-NinjaProperty -Name $DNSSuffixField @DocumentationParams } + if (-not $RememberCreds) { $RememberCreds = Get-NinjaProperty -Name $RememberCredsField -Type "Checkbox" @DocumentationParams } + if (-not $UseWinlogonCredential) { $UseWinLogonCredential = Get-NinjaProperty -Name $UseWinlogonCredsField -Type "Checkbox" @DocumentationParams } + if (-not $SplitTunneling) { $SplitTunneling = Get-NinjaProperty -Name $SplitTunnelingField -Type "Checkbox" @DocumentationParams } + if (-not $CreateShortcut) { $CreateShortcut = Get-NinjaProperty -Name $ShortcutField -Type "Checkbox" @DocumentationParams } + if (-not $IconDirectory) { $IconDirectory = Get-NinjaProperty -Name $IconDirectoryField @DocumentationParams } + if (-not $Url) { $Url = Get-NinjaProperty -Name $UrlField @DocumentationParams } + } + else { + Write-Warning "Reading Custom Fields requires local admin privileges." + } + + # If someone specifies something in the script form, we'll want to overwrite what the documentation says. Makes the script more useful for troubleshooting. + if ($env:vpnName -and $env:vpnName -notlike "null") { $Name = $env:vpnName } + if ($env:vpnServerAddress -and $env:vpnServerAddress -notlike "null") { $Server = $env:vpnServerAddress } + if ($env:vpnTunnelType -and $env:vpnTunnelType -notlike "null") { $TunnelType = $env:vpnTunnelType } + if ($env:authenticationMethod -and $env:authenticationMethod -notlike "null") { $AuthMethod = $env:authenticationMethod } + if ($env:encryptionLevel -and $env:encryptionLevel -notlike "null") { $EncryptionLevel = $env:encryptionLevel } + if ($env:dnsSuffix -and $env:dnsSuffix -notlike "null") { $DNSSuffix = $env:dnsSuffix } + if ($env:iconPath -and $env:iconPath -notlike "null") { $Icon = $env:iconPath } + if ($env:iconUrl -and $env:iconUrl -notlike "null") { $Url = $env:iconUrl } + if ($env:iconDirectory -and $env:iconDirectory -notlike "null") { $IconDirectory = $env:iconDirectory } + + # If an authentication method was given, we'll want to parse that and validate it. + if ($AuthMethod) { + $AuthSelections = ($AuthMethod.Split(',')).Trim() + + $AuthSelections | ForEach-Object { + switch ($_) { + "PAP" { Write-Verbose "PAP Selected" } + "Chap" { Write-Verbose "Chap Selected" } + "MSChapv2" { Write-Verbose "Eap Selected" } + "MachineCertificate" { Write-Verbose "MachineCertificate" } + default { + Write-Host "[Error] $_ is invalid! The Valid auth types are 'PAP','Chap','MSChapv2' or 'MachineCertificate'." + exit 1 + } + } + } + } + + # Validating encryption level. Not supporting "No Encryption". + if ($EncryptionLevel) { + switch ($EncryptionLevel) { + "Optional" { Write-Verbose "Optional Selected" } + "Required" { Write-Verbose "Required Selected" } + "Maximum" { Write-Verbose "Maximum Selected" } + default { + Write-Host "[Error] $EncryptionLevel is invalid! The valid encryption levels are 'Optional', 'Required', or 'Maximum'." + } + } + } + + # This will set the tunnel type. + switch ($TunnelType) { + "L2TP" { Write-Verbose "L2TP Selected" } + "SSTP" { Write-Verbose "SSTP Selected" } + "IKEV2" { Write-Verbose "IKEV2 Selected" } + "Automatic" { Write-Verbose "Automatic Selected" } + default { + Write-Host "[Error] $TunnelType is invalid! The valid tunnel types are 'L2TP', 'SSTP', 'IKEv2', or 'Automatic'." + exit 1 + } + } + + # Error out if the only two mandatory parameters don't exist. + if (-not $Server -or -not $Name) { + Write-Host "[Error] Name and Server are required! If using documentation fields or custom fields, double-check that your field exists and is readable by Automations." + exit 1 + } + + # Icons are minor, so if we're not given the information, we'll continue on without it and just let the technician know. They can always re-run it. + if (($Url -or $IconDirectory) -and -not $CreateShortcut) { + Write-Warning "An icon was given, but Create Shortcut was never used? Ignoring icon info..." + $Url = $null + $Icon = $null + $IconDirectory = $null + } + + # Icons are minor, so if we're not given the information, we'll continue on without it and just let the technician know. They can always re-run it. + if ($Url -and -not $IconDirectory) { + Write-Warning "An icon was given, but a place to store it wasn't? Use Icon Directory box to specify a directory to store it. (You may want this directory to be accessible by all users)" + Write-Warning "Ignoring supplied icon info." + $Icon = $null + $Url = $null + } + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # Checking for existing VPNs + $UserVPNs = Get-VpnConnection -Name $Name -EA Ignore + $GlobalVPNs = Get-VpnConnection -Name $Name -AllUserConnection -EA Ignore + + # If the overwrite parameter wasn't specified, we'll error out as we won't be able to create the VPN. + if (($UserVPNs -or $GlobalVPNs) -and -not $Overwrite) { + Write-Host "[Error] $Name already exists! Use -Overwrite to replace it." + if ($GlobalVPNs -and -not (Test-IsElevated)) { Write-Host "[Error] $($GlobalVPNs.Name) requires elevation to replace/overwrite." } + exit 1 + } + + # Removing previous VPNs if overwrite was specified. + if ($UserVPNs -and $Overwrite) { + $UserVPNs | Remove-VpnConnection -Force + } + + if ($GlobalVPNs -and $Overwrite) { + if (Test-IsElevated) { + $GlobalVPNs | Remove-VpnConnection -AllUserConnection -Force + } + else { + Write-Host "[Error] $($GlobalVPNs.Name) requires elevation to replace/overwrite." + exit 1 + } + } + + # Setting the AssumeUDPEncapsulationContextOnSendRule + if ($AssumeUDPencapsulation -and (Test-IsElevated)) { + Write-Warning "AssumeUDPEncapsulation requires a reboot to take effect. This script does NOT reboot the machine." + Set-HKProperty -Name "AssumeUDPEncapsulationContextOnSendRule" -Path "HKLM:\SYSTEM\CurrentControlSet\Services\PolicyAgent" -Value "2" + } + + # Building the Add-VPNConnection command based on what information was entered + $ArgumentList = @{} + if ($SplitTunneling) { $ArgumentList["SplitTunneling"] = $True } + if ($Server) { $ArgumentList["ServerAddress"] = $Server } + if ($Name) { $ArgumentList["Name"] = $Name } + if ($PreSharedKey) { $ArgumentList["L2TPPsk"] = $PreSharedKey } + if ($TunnelType) { $ArgumentList["TunnelType"] = $TunnelType } + if ($AuthMethod) { $ArgumentList["AuthenticationMethod"] = $AuthSelections } + if ($EncryptionLevel) { $ArgumentList["EncryptionLevel"] = $EncryptionLevel } + if ($DNSSuffix) { $ArgumentList["DnsSuffix"] = $DNSSuffix } + if ($RememberCreds) { $ArgumentList["RememberCredential"] = $True } + if ($UseWinlogonCredential) { $ArgumentList["UseWinLogonCredential"] = $True } + + # If the script wasn't run as system, only the current user should be able to access the VPN. + if (Test-IsSystem) { $ArgumentList["AllUserConnection"] = $True } + $ArgumentList["Force"] = $True + $ArgumentList["Passthru"] = $True + + try { + Add-VpnConnection @ArgumentList -ErrorAction Stop | Format-Table -Property Name, ServerAddress, TunnelType, EncryptionLevel, AllUserConnection, SplitTunneling | Out-String | Write-Host + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + + switch (Test-IsSystem) { + $True { + $ShortcutPath = "$env:Public\Desktop\$Name.lnk" + $RasFile = Join-Path $env:PROGRAMDATA "Microsoft\Network\Connections\Pbk\rasphone.pbk" + } + default { + $ShortcutPath = "$([Environment]::GetFolderPath("Desktop"))\$Name.lnk" + $RasFile = Join-Path $env:APPDATA "Microsoft\Network\Connections\Pbk\rasphone.pbk" + } + } + + if (-not (Test-Path $RasFile -EA Ignore)) { + Write-Host "[Error] Failed to create vpn!" + exit 1 + } + + # All Windows VPNs get a phonebook entry; we're going to check that the previous Add-VPNConnection was successful by looping through each line in the phonebook. + $Phonebook = Get-Content -Path $RasFile + + for ($lineNumber = 0; $lineNumber -lt $Phonebook.Length; $lineNumber++) { + if ($Phonebook[$lineNumber] -eq "[$Name]") { + $Entry = $lineNumber + break + } + } + + # If the VPN wasn't found, we're going to error out. + if ($Entry -notmatch "\d") { + Write-Host "[Error] Failed to create vpn!" + exit 1 + } + + if ($CreateShortcut) { + if ($IconDirectory -and -not (Test-Path $IconDirectory -ErrorAction SilentlyContinue)) { + New-Item -ItemType Directory -Path $IconDirectory | Out-Null + } + + # If we're given a URL, we'll want to download it. + if ($Url) { + $DownloadArguments = @{ + URL = $Url + Path = "$IconDirectory\vpnscript-$Name.png" + } + if ($SkipSleep) { $DownloadArguments["SkipSleep"] = $True } + + Invoke-Download @DownloadArguments + + if (Test-Path "$IconDirectory\vpnscript-$Name.png" -ErrorAction SilentlyContinue) { + $Icon = "$IconDirectory\vpnscript-$Name.png" + } + else { + $Icon = $Null + } + } + + # This will convert the base64 into an image and save it to the temp folder. + if ($IconBase64 -and $IconDirectory -and -not $Icon -and -not $Url) { + Write-Verbose "Converting Icon base64 to original image and saving to $IconDirectory..." + + ConvertFrom-Base64 -Base64 $IconBase64 -Path "$IconDirectory\vpnscript-$Name.Png" + + if (Test-Path "$IconDirectory\vpnscript-$Name.Png" -ErrorAction SilentlyContinue) { + $Icon = "$IconDirectory\vpnscript-$Name.Png" + } + else { + $Icon = $Null + } + } + + if ($Icon) { + Write-Verbose "Converting image to icon and saving to $IconDirectory\vpnscript-$((Get-FileHash -Path $Icon -Algorithm MD5).Hash).ico ..." + + $NewName = "vpnscript-$((Get-FileHash -Path $Icon -Algorithm MD5).Hash).ico" + ConvertFrom-Image -ImagePath $Icon -Path "$IconDirectory\$NewName" + + if (Test-Path "$IconDirectory\$NewName" -ErrorAction SilentlyContinue) { + $Icon = "$IconDirectory\$NewName" + } + else { + $Icon = $Null + } + } + } + + if ($CreateShortcut) { + $ShortcutArgs = @{ + Path = $ShortcutPath + Target = "rasphone.exe" + Arguments = "-d `"$Name`"" + WorkingDir = "$env:SystemRoot\System32" + } + if ($Icon) { $ShortcutArgs["IconPath"] = $Icon } + + New-Shortcut @ShortcutArgs + } + + exit $ExitCode +} +end { + +} + diff --git a/Powershell Scripts/Credential Guard Status.ps1 b/Powershell Scripts/Credential Guard Status.ps1 index e7855af..72952be 100644 --- a/Powershell Scripts/Credential Guard Status.ps1 +++ b/Powershell Scripts/Credential Guard Status.ps1 @@ -1,326 +1,205 @@ # Reports on whether Credential Guard is configured and running on a given device. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Reports on whether Credential Guard is configured and running on a given device. -.DESCRIPTION - Reports on whether Credential Guard is configured and running on a given device. - -.EXAMPLE - (No Parameters) - - CredentialGuardConfiguration CredentialGuardRunning - ---------------------------- ---------------------- - Enabled without UEFI lock Running - -.EXAMPLE - -TextCustomFieldName "Test" - - [Info] Attempting to set Ninja custom field Text... - [Info] Successfully set Ninja custom field Text to value 'Enabled without UEFI lock | Running'. - - CredentialGuardConfiguration CredentialGuardRunning - ---------------------------- ---------------------- - Enabled without UEFI lock Running - -.PARAMETER -TextCustomFieldName - Name of the text custom field where Credential Guard information will be stored. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows 11, Windows Server 2016+ - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [string]$TextCustomFieldName -) - -begin { - if ($env:TextCustomFieldName -and $TextCustomFieldName -ne 'null'){ - $TextCustomFieldName = $env:TextCustomFieldName - } - - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName, - [Parameter()] - [Switch]$Piped - ) - # Remove the non-breaking space character - if ($Type -eq "WYSIWYG") { - $Value = $Value -replace ' ', ' ' - } - - # Measure the number of characters in the provided value - $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # Throw an error if the value exceeds the character limit of 200,000 characters - if ($Piped -and $Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - if (!$Piped -and $Characters -ge 45000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") - } - - # Initialize a hashtable for additional documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define a list of valid field types - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - - # Warn the user if the provided type is not valid - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # Define types that require options to be retrieved - $NeedsOptions = "Dropdown" - - # If the property is being set in a document or field and the type needs options, retrieve them - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an error if there was an issue retrieving the property options - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Process the property value based on its type - switch ($Type) { - "Checkbox" { - # Convert the value to a boolean for Checkbox type - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Convert the value to a Unix timestamp for Date or Date Time type - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Convert the dropdown value to its corresponding GUID - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - # Throw an error if the value is not present in the dropdown options - if (!($Selection)) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # For other types, use the value as is - $NinjaValue = $Value - } - } - - # Set the property value in the document if a document name is provided - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - try { - # Otherwise, set the standard property value - if ($Piped) { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - } - catch { - Write-Host -Object "[Error] Failed to set custom field." - throw $_.Exception.Message - } - } - - # Throw an error if setting the property failed - if ($CustomField.Exception) { - throw $CustomField - } - } - - function Test-IsCredentialGuardRunning { - if ($PSVersionTable.PSVersion.Major -lt 3) { - $CGRunning = (Get-WmiObject -Class Win32_DeviceGuard -Namespace root\Microsoft\Windows\DeviceGuard -ErrorAction SilentlyContinue).SecurityServicesRunning - } - else { - $CGRunning = (Get-CimInstance -ClassName Win32_DeviceGuard -Namespace root\Microsoft\Windows\DeviceGuard -ErrorAction SilentlyContinue).SecurityServicesRunning - } - - # if 1 is present, Credential Guard is running per https://learn.microsoft.com/en-us/windows/security/hardware-security/enable-virtualization-based-protection-of-code-integrity?tabs=security - if ($CGRunning -contains 1){ - return $true - } - else{ - return $false - } - } -} -process { - if (-not (Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - $ExitCode = 0 - - # check if running on supported OS - $OS = try{ - if ($PSVersionTable.PSVersion.Major -lt 3) { - Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop - } - else { - Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop - } - } - catch{ - Write-Host "[Error] Error retrieving operating system information." - Write-Host "$($_.Exception.Message)" - exit 1 - } - - # assume supported OS, below checks will be used to negate it if needed - $supportedOS = $true - - if ($OS.Caption -match "Windows (10|11)" -and $OS.Caption -notmatch "Enterprise|Education"){ - # if this registry value is not null on Windows 10/11 Pro, then this may have been a downgrade from Enterprise/Education, and the OS is supported in that case - # see the note here: https://learn.microsoft.com/en-us/windows/security/identity-protection/credential-guard/ - $regKeyValue = (Get-ItemProperty "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0\" -ErrorAction SilentlyContinue).IsolatedCredentialsRootSecret - - if ([string]::IsNullOrWhiteSpace($regKeyValue)){ - $supportedOS = $false - } - } - elseif ($OS.Caption -notmatch "Windows.+(Enterprise|Education|Server (2016|2019|[2-9]0[2-9][0-9]))"){ - # otherwise, if device is not Enterprise/Education/Server 2016+, the OS is not supported - $supportedOS = $false - } - - # error if not running on supported OS - if (-not $supportedOS){ - Write-Host "[Error] Credential Guard is not supported on this OS." - Write-Host "Script supports:" - Write-Host " - Windows 10 and 11, Enterprise or Education edition" - Write-Host " - Windows Server 2016 and above" - Write-Host "See more info on prerequisites here: https://learn.microsoft.com/en-us/windows/security/identity-protection/credential-guard/" - - # write to custom field if specified - if ($TextCustomFieldName){ - $value = "Incompatible with System" - # attempt custom field write - try { - Write-Host "`n[Info] Attempting to set Ninja custom field $TextCustomFieldName..." - Set-NinjaProperty -Name $TextCustomFieldName -Type "Text" -Value $value -ErrorAction Stop - Write-Host "[Info] Successfully set Ninja custom field $TextCustomFieldName to value '$value'." - } - catch { - Write-Host "[Error] Error setting custom field $TextFieldCustomName to value '$value'." - Write-Host "$($_.Exception.Message)" - $ExitCode = 1 - } - } - exit $ExitCode - } - - # if OS is supported, continue with checks - # check if Credential Guard is running - try { - if (Test-IsCredentialGuardRunning){ - $CGRunningStatus = "Running" - } - else{ - $CGRunningStatus = "Not running" - } - } - catch { - Write-Host "[Error] Error getting Credential Guard running status." - Write-Host "$($_.Exception.Message)" - $CGRunningStatus = "Error" - $ExitCode = 1 - } - - # check if Credential Guard is configured - try { - $CGConfiguration = (Get-ItemProperty "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa" -ErrorAction Stop).LsaCfgFlags - - # if nothing present for custom regkey, check default regkey - if ($null -eq $CGConfiguration){ - $CGConfiguration = (Get-ItemProperty "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa" -ErrorAction Stop).LsaCfgFlagsDefault - } - } - catch{ - Write-Host "[Error] Error when testing if Credential Guard is enabled in the registry." - Write-Host "$($_.Exception.Message)" - $ExitCode = 1 - } - - # translate value into readable text for output - $CGConfigurationStatus = switch ($CGConfiguration){ - 0 { "Disabled" } - 1 { "Enabled with UEFI lock" } - 2 { "Enabled without UEFI lock" } - default { "Unable to Determine" } - } - - # write result to custom field if specified - if ($TextCustomFieldName){ - $value = "$CGConfigurationStatus | $CGRunningStatus" - # attempt custom field write - try { - Write-Host "`n[Info] Attempting to set Ninja custom field $TextCustomFieldName..." - Set-NinjaProperty -Name $TextCustomFieldName -Type "Text" -Value $value -ErrorAction Stop - Write-Host "[Info] Successfully set Ninja custom field $TextCustomFieldName to value '$value'." - } - catch { - Write-Host "[Error] Error setting custom field $TextFieldCustomName to value '$value'." - Write-Host "$($_.Exception.Message)" - $ExitCode = 1 - } - } - - # warn if CG is configured to be disabled but is still running - if ($CGConfigurationStatus -eq "Disabled" -and $CGRunningStatus -eq "Running"){ - Write-Host "`n[Warning] Credential Guard is disabled in the registry but currently running." - Write-Host "You may need to restart $env:computername, or Credential Guard is UEFI locked and needs to be reset." - Write-Host "See more information here: https://learn.microsoft.com/en-us/windows/security/identity-protection/credential-guard/configure?tabs=intune#disable-credential-guard-with-uefi-lock" - } - - [PSCustomObject]@{ - "CredentialGuardConfiguration" = $CGConfigurationStatus - "CredentialGuardRunning" = $CGRunningStatus - } | Format-Table -AutoSize | Out-String | Write-Host - - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Reports on whether Credential Guard is configured and running on a given device. +.DESCRIPTION + Reports on whether Credential Guard is configured and running on a given device. + +.EXAMPLE + (No Parameters) + + CredentialGuardConfiguration CredentialGuardRunning + ---------------------------- ---------------------- + Enabled without UEFI lock Running + +.EXAMPLE + -TextCustomFieldName "Test" + + [Info] Attempting to set Ninja custom field Text... + [Info] Successfully set Ninja custom field Text to value 'Enabled without UEFI lock | Running'. + + CredentialGuardConfiguration CredentialGuardRunning + ---------------------------- ---------------------- + Enabled without UEFI lock Running + +.PARAMETER -TextCustomFieldName + Name of the text custom field where Credential Guard information will be stored. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows 11, Windows Server 2016+ + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [string]$TextCustomFieldName +) + +begin { + if ($env:TextCustomFieldName -and $TextCustomFieldName -ne 'null'){ + $TextCustomFieldName = $env:TextCustomFieldName + } + + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + function Test-IsCredentialGuardRunning { + if ($PSVersionTable.PSVersion.Major -lt 3) { + $CGRunning = (Get-WmiObject -Class Win32_DeviceGuard -Namespace root\Microsoft\Windows\DeviceGuard -ErrorAction SilentlyContinue).SecurityServicesRunning + } + else { + $CGRunning = (Get-CimInstance -ClassName Win32_DeviceGuard -Namespace root\Microsoft\Windows\DeviceGuard -ErrorAction SilentlyContinue).SecurityServicesRunning + } + + # if 1 is present, Credential Guard is running per https://learn.microsoft.com/en-us/windows/security/hardware-security/enable-virtualization-based-protection-of-code-integrity?tabs=security + if ($CGRunning -contains 1){ + return $true + } + else{ + return $false + } + } +} +process { + if (-not (Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + $ExitCode = 0 + + # check if running on supported OS + $OS = try{ + if ($PSVersionTable.PSVersion.Major -lt 3) { + Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop + } + else { + Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop + } + } + catch{ + Write-Host "[Error] Error retrieving operating system information." + Write-Host "$($_.Exception.Message)" + exit 1 + } + + # assume supported OS, below checks will be used to negate it if needed + $supportedOS = $true + + if ($OS.Caption -match "Windows (10|11)" -and $OS.Caption -notmatch "Enterprise|Education"){ + # if this registry value is not null on Windows 10/11 Pro, then this may have been a downgrade from Enterprise/Education, and the OS is supported in that case + # see the note here: https://learn.microsoft.com/en-us/windows/security/identity-protection/credential-guard/ + $regKeyValue = (Get-ItemProperty "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0\" -ErrorAction SilentlyContinue).IsolatedCredentialsRootSecret + + if ([string]::IsNullOrWhiteSpace($regKeyValue)){ + $supportedOS = $false + } + } + elseif ($OS.Caption -notmatch "Windows.+(Enterprise|Education|Server (2016|2019|[2-9]0[2-9][0-9]))"){ + # otherwise, if device is not Enterprise/Education/Server 2016+, the OS is not supported + $supportedOS = $false + } + + # error if not running on supported OS + if (-not $supportedOS){ + Write-Host "[Error] Credential Guard is not supported on this OS." + Write-Host "Script supports:" + Write-Host " - Windows 10 and 11, Enterprise or Education edition" + Write-Host " - Windows Server 2016 and above" + Write-Host "See more info on prerequisites here: https://learn.microsoft.com/en-us/windows/security/identity-protection/credential-guard/" + + # write to custom field if specified + if ($TextCustomFieldName){ + $value = "Incompatible with System" + # attempt custom field write + try { + Write-Host "`n[Info] Attempting to set Ninja custom field $TextCustomFieldName..." + Write-Host "[Info] Successfully set Ninja custom field $TextCustomFieldName to value '$value'." + } + catch { + Write-Host "[Error] Error setting custom field $TextFieldCustomName to value '$value'." + Write-Host "$($_.Exception.Message)" + $ExitCode = 1 + } + } + exit $ExitCode + } + + # if OS is supported, continue with checks + # check if Credential Guard is running + try { + if (Test-IsCredentialGuardRunning){ + $CGRunningStatus = "Running" + } + else{ + $CGRunningStatus = "Not running" + } + } + catch { + Write-Host "[Error] Error getting Credential Guard running status." + Write-Host "$($_.Exception.Message)" + $CGRunningStatus = "Error" + $ExitCode = 1 + } + + # check if Credential Guard is configured + try { + $CGConfiguration = (Get-ItemProperty "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa" -ErrorAction Stop).LsaCfgFlags + + # if nothing present for custom regkey, check default regkey + if ($null -eq $CGConfiguration){ + $CGConfiguration = (Get-ItemProperty "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa" -ErrorAction Stop).LsaCfgFlagsDefault + } + } + catch{ + Write-Host "[Error] Error when testing if Credential Guard is enabled in the registry." + Write-Host "$($_.Exception.Message)" + $ExitCode = 1 + } + + # translate value into readable text for output + $CGConfigurationStatus = switch ($CGConfiguration){ + 0 { "Disabled" } + 1 { "Enabled with UEFI lock" } + 2 { "Enabled without UEFI lock" } + default { "Unable to Determine" } + } + + # write result to custom field if specified + if ($TextCustomFieldName){ + $value = "$CGConfigurationStatus | $CGRunningStatus" + # attempt custom field write + try { + Write-Host "`n[Info] Attempting to set Ninja custom field $TextCustomFieldName..." + Write-Host "[Info] Successfully set Ninja custom field $TextCustomFieldName to value '$value'." + } + catch { + Write-Host "[Error] Error setting custom field $TextFieldCustomName to value '$value'." + Write-Host "$($_.Exception.Message)" + $ExitCode = 1 + } + } + + # warn if CG is configured to be disabled but is still running + if ($CGConfigurationStatus -eq "Disabled" -and $CGRunningStatus -eq "Running"){ + Write-Host "`n[Warning] Credential Guard is disabled in the registry but currently running." + Write-Host "You may need to restart $env:computername, or Credential Guard is UEFI locked and needs to be reset." + Write-Host "See more information here: https://learn.microsoft.com/en-us/windows/security/identity-protection/credential-guard/configure?tabs=intune#disable-credential-guard-with-uefi-lock" + } + + [PSCustomObject]@{ + "CredentialGuardConfiguration" = $CGConfigurationStatus + "CredentialGuardRunning" = $CGRunningStatus + } | Format-Table -AutoSize | Out-String | Write-Host + + exit $ExitCode +} +end { + +} diff --git a/Powershell Scripts/Deploy Latest Windows 11 Feature Update.ps1 b/Powershell Scripts/Deploy Latest Windows 11 Feature Update.ps1 index c8aed59..709c0c0 100644 --- a/Powershell Scripts/Deploy Latest Windows 11 Feature Update.ps1 +++ b/Powershell Scripts/Deploy Latest Windows 11 Feature Update.ps1 @@ -1,1139 +1,1136 @@ # Update Windows 11 to the latest available Feature Update using the Windows 11 Install Assistant. This script does not wait for the installation assistant to complete. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Update Windows 11 to the latest available Feature Update using the Windows 11 Install Assistant. This script does not wait for the installation assistant to complete. -.DESCRIPTION - Update Windows 11 to the latest available Feature Update using the Windows 11 Install Assistant. This script does not wait for the installation assistant to complete. -.EXAMPLE - (No Parameters) - - Checking https://endoflife.date/windows for the latest Windows build information. - Parsing the response from https://endoflife.date/windows - Retrieving the systems current Windows 11 version. - The latest supported feature update is Windows 11 24H2 and it was released on 10/1/2024. - The system currently has the feature update Windows 11 23H2 installed. - - Verifying the feature update compatibility. - Successfully retrieved the compatibility results. - - Compatibility Test Result: Capable - The upgrade is not currently in progress. - - Downloading the Windows 11 Installation Assistant executable. - URL 'https://go.microsoft.com/fwlink/?linkid=2171764' was given. - Downloading the file... - Waiting for 7 seconds. - Download Attempt 1 - Download complete. - - Verifying the executable's signature. - The signature is valid and appears to be what was expected. - - Initiating Windows 11 feature upgrade. - [Warning] This may take a few hours to complete. You can view the logs at 'C:\Windows\Logs\Windows11InstallAssistant' and 'C:\Program Files (x86)\WindowsInstallationAssistant\Logs' if any failure occurs. - If no failure occurs, these files may be empty. - - ### Windows 11 Upgrade Process ### - PID : 2548 - Name : Windows10UpgraderApp - Description : Windows Installation Assistant - Path : C:\Program Files (x86)\WindowsInstallationAssistant\Windows10UpgraderApp.exe - -PARAMETER: -InstallAssistantDownloadURL "https://wwww.ReplaceMeWithTheURLToTheWindows11InstallationAssistant.com" - Defines the URL from which the Windows 11 Installation Assistant will be downloaded. - -PARAMETER: -DownloadDestination "C:\ReplaceMeWithALocationToDownloadTheFileTo.exe" - Provides the destination file path where Windows11InstallationAssistant.exe should be saved. - -PARAMETER: -UpdateLogLocation "C:\ReplaceMeWithAFolder" - Specifies the folder location where the installation assistant logs will be stored. - -PARAMETER: -EndOfLifeURL "https://endoflife.date/api/windows.json" - Defines the URL to the endoflife.date api. - -.NOTES - Minimum OS Architecture Supported: Windows 11 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$InstallAssistantDownloadURL = 'https://go.microsoft.com/fwlink/?linkid=2171764', - [Parameter()] - [String]$DownloadDestination = "$env:TEMP\Windows11InstallAssistant\Windows11InstallationAssistant.exe", - [Parameter()] - [String]$UpdateLogLocation = "$env:SYSTEMROOT\Logs\Windows11InstallAssistant", - [Parameter()] - [String]$EndOfLifeURL = "https://endoflife.date/api/windows.json" -) - -begin { - # Determine the method to retrieve the operating system information based on PowerShell version. - try { - $OS = if ($PSVersionTable.PSVersion.Major -lt 3) { - Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop - } - else { - Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop - } - } - catch { - # If the above retrieval fails, display an error message and exit. - Write-Host -Object "[Error] Unable to retrieve information about the current operating system." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Check if the system is running Windows 11. If not, display an error message and exit. - if ($OS.Caption -notmatch "Windows 11") { - Write-Host -Object "[Error] This device is not currently running Windows 11. It is currently running '$($OS.Caption)'." - exit 1 - } - - try { - # Retrieve the volume information for the system drive (C: or equivalent). - # Replace ":" in the drive letter environment variable to match the Get-Volume cmdlet parameter. - $osDrive = Get-Volume -DriveLetter ($env:SystemDrive -replace ":") -ErrorAction Stop - - # If there's no remaining size property, throw an error to be caught below. - if (!$osDrive.SizeRemaining) { - throw "Failed to retrieve the remaining size for drive '$env:SystemDrive'." - } - - } - catch { - # If the volume information retrieval fails, output an error message and exit. - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to get the size of the current os drive ($env:SystemDrive)." - exit 1 - } - - # Ensure there is at least 64GB of free space on the system drive before continuing. - if ($osDrive.SizeRemaining -lt 64GB) { - Write-Host -Object "[Error] The current free space for the system drive '$env:SystemDrive' is $([math]::Round(($osDrive.SizeRemaining / 1GB),2)). There is not enough free space. You must have at least 64GB of free space." - exit 1 - } - - function Get-HardwareReadiness() { - # Modified copy of https://aka.ms/HWReadinessScript minus the signature, as of 7/26/2023. - # Only modification was replacing Get-WmiObject with Get-CimInstance for PowerShell 7 compatibility - # Source Microsoft article: https://techcommunity.microsoft.com/t5/microsoft-endpoint-manager-blog/understanding-readiness-for-windows-11-with-microsoft-endpoint/ba-p/2770866 - - #============================================================================================================================= - # - # Script Name: HardwareReadiness.ps1 - # Description: Verifies the hardware compliance. Return code 0 for success. - # In case of failure, returns non zero error code along with error message. - - # This script is not supported under any Microsoft standard support program or service and is distributed under the MIT license - - # Copyright (C) 2021 Microsoft Corporation - - # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation - # files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, - # modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software - # is furnished to do so, subject to the following conditions: - - # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE - # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, - # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - #============================================================================================================================= - - [int]$MinOSDiskSizeGB = 64 - [int]$MinMemoryGB = 4 - [Uint32]$MinClockSpeedMHz = 1000 - [Uint32]$MinLogicalCores = 2 - [Uint16]$RequiredAddressWidth = 64 - - $PASS_STRING = "PASS" - $FAIL_STRING = "FAIL" - $FAILED_TO_RUN_STRING = "FAILED TO RUN" - $UNDETERMINED_CAPS_STRING = "UNDETERMINED" - $UNDETERMINED_STRING = "Undetermined" - $CAPABLE_STRING = "Capable" - $NOT_CAPABLE_STRING = "Not capable" - $CAPABLE_CAPS_STRING = "CAPABLE" - $NOT_CAPABLE_CAPS_STRING = "NOT CAPABLE" - $STORAGE_STRING = "Storage" - $OS_DISK_SIZE_STRING = "OSDiskSize" - $MEMORY_STRING = "Memory" - $SYSTEM_MEMORY_STRING = "System_Memory" - $GB_UNIT_STRING = "GB" - $TPM_STRING = "TPM" - $TPM_VERSION_STRING = "TPMVersion" - $PROCESSOR_STRING = "Processor" - $SECUREBOOT_STRING = "SecureBoot" - $I7_7820HQ_CPU_STRING = "i7-7820hq CPU" - - # 0=name of check, 1=attribute checked, 2=value, 3=PASS/FAIL/UNDETERMINED - $logFormat = '{0}: {1}={2}. {3}; ' - - # 0=name of check, 1=attribute checked, 2=value, 3=unit of the value, 4=PASS/FAIL/UNDETERMINED - $logFormatWithUnit = '{0}: {1}={2}{3}. {4}; ' - - # 0=name of check. - $logFormatReturnReason = '{0}, ' - - # 0=exception. - $logFormatException = '{0}; ' - - # 0=name of check, 1= attribute checked and its value, 2=PASS/FAIL/UNDETERMINED - $logFormatWithBlob = '{0}: {1}. {2}; ' - - # return returnCode is -1 when an exception is thrown. 1 if the value does not meet requirements. 0 if successful. -2 default, script didn't run. - $outObject = @{ returnCode = -2; returnResult = $FAILED_TO_RUN_STRING; returnReason = ""; logging = "" } - - # NOT CAPABLE(1) state takes precedence over UNDETERMINED(-1) state - function Private:UpdateReturnCode { - param( - [Parameter(Mandatory = $true)] - [ValidateRange(-2, 1)] - [int] $ReturnCode - ) - - Switch ($ReturnCode) { - - 0 { - if ($outObject.returnCode -eq -2) { - $outObject.returnCode = $ReturnCode - } - } - 1 { - $outObject.returnCode = $ReturnCode - } - -1 { - if ($outObject.returnCode -ne 1) { - $outObject.returnCode = $ReturnCode - } - } - } - } - - $Source = @" -using Microsoft.Win32; -using System; -using System.Runtime.InteropServices; - - public class CpuFamilyResult - { - public bool IsValid { get; set; } - public string Message { get; set; } - } - - public class CpuFamily - { - [StructLayout(LayoutKind.Sequential)] - public struct SYSTEM_INFO - { - public ushort ProcessorArchitecture; - ushort Reserved; - public uint PageSize; - public IntPtr MinimumApplicationAddress; - public IntPtr MaximumApplicationAddress; - public IntPtr ActiveProcessorMask; - public uint NumberOfProcessors; - public uint ProcessorType; - public uint AllocationGranularity; - public ushort ProcessorLevel; - public ushort ProcessorRevision; - } - - [DllImport("kernel32.dll")] - internal static extern void GetNativeSystemInfo(ref SYSTEM_INFO lpSystemInfo); - - public enum ProcessorFeature : uint - { - ARM_SUPPORTED_INSTRUCTIONS = 34 - } - - [DllImport("kernel32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - static extern bool IsProcessorFeaturePresent(ProcessorFeature processorFeature); - - private const ushort PROCESSOR_ARCHITECTURE_X86 = 0; - private const ushort PROCESSOR_ARCHITECTURE_ARM64 = 12; - private const ushort PROCESSOR_ARCHITECTURE_X64 = 9; - - private const string INTEL_MANUFACTURER = "GenuineIntel"; - private const string AMD_MANUFACTURER = "AuthenticAMD"; - private const string QUALCOMM_MANUFACTURER = "Qualcomm Technologies Inc"; - - public static CpuFamilyResult Validate(string manufacturer, ushort processorArchitecture) - { - CpuFamilyResult cpuFamilyResult = new CpuFamilyResult(); - - if (string.IsNullOrWhiteSpace(manufacturer)) - { - cpuFamilyResult.IsValid = false; - cpuFamilyResult.Message = "Manufacturer is null or empty"; - return cpuFamilyResult; - } - - string registryPath = "HKEY_LOCAL_MACHINE\\Hardware\\Description\\System\\CentralProcessor\\0"; - SYSTEM_INFO sysInfo = new SYSTEM_INFO(); - GetNativeSystemInfo(ref sysInfo); - - switch (processorArchitecture) - { - case PROCESSOR_ARCHITECTURE_ARM64: - - if (manufacturer.Equals(QUALCOMM_MANUFACTURER, StringComparison.OrdinalIgnoreCase)) - { - bool isArmv81Supported = IsProcessorFeaturePresent(ProcessorFeature.ARM_SUPPORTED_INSTRUCTIONS); - - if (!isArmv81Supported) - { - string registryName = "CP 4030"; - long registryValue = (long)Registry.GetValue(registryPath, registryName, -1); - long atomicResult = (registryValue >> 20) & 0xF; - - if (atomicResult >= 2) - { - isArmv81Supported = true; - } - } - - cpuFamilyResult.IsValid = isArmv81Supported; - cpuFamilyResult.Message = isArmv81Supported ? "" : "Processor does not implement ARM v8.1 atomic instruction"; - } - else - { - cpuFamilyResult.IsValid = false; - cpuFamilyResult.Message = "The processor isn't currently supported for Windows 11"; - } - - break; - - case PROCESSOR_ARCHITECTURE_X64: - case PROCESSOR_ARCHITECTURE_X86: - - int cpuFamily = sysInfo.ProcessorLevel; - int cpuModel = (sysInfo.ProcessorRevision >> 8) & 0xFF; - int cpuStepping = sysInfo.ProcessorRevision & 0xFF; - - if (manufacturer.Equals(INTEL_MANUFACTURER, StringComparison.OrdinalIgnoreCase)) - { - try - { - cpuFamilyResult.IsValid = true; - cpuFamilyResult.Message = ""; - - if (cpuFamily >= 6 && cpuModel <= 95 && !(cpuFamily == 6 && cpuModel == 85)) - { - cpuFamilyResult.IsValid = false; - cpuFamilyResult.Message = ""; - } - else if (cpuFamily == 6 && (cpuModel == 142 || cpuModel == 158) && cpuStepping == 9) - { - string registryName = "Platform Specific Field 1"; - int registryValue = (int)Registry.GetValue(registryPath, registryName, -1); - - if ((cpuModel == 142 && registryValue != 16) || (cpuModel == 158 && registryValue != 8)) - { - cpuFamilyResult.IsValid = false; - } - cpuFamilyResult.Message = "PlatformId " + registryValue; - } - } - catch (Exception ex) - { - cpuFamilyResult.IsValid = false; - cpuFamilyResult.Message = "Exception:" + ex.GetType().Name; - } - } - else if (manufacturer.Equals(AMD_MANUFACTURER, StringComparison.OrdinalIgnoreCase)) - { - cpuFamilyResult.IsValid = true; - cpuFamilyResult.Message = ""; - - if (cpuFamily < 23 || (cpuFamily == 23 && (cpuModel == 1 || cpuModel == 17))) - { - cpuFamilyResult.IsValid = false; - } - } - else - { - cpuFamilyResult.IsValid = false; - cpuFamilyResult.Message = "Unsupported Manufacturer: " + manufacturer + ", Architecture: " + processorArchitecture + ", CPUFamily: " + sysInfo.ProcessorLevel + ", ProcessorRevision: " + sysInfo.ProcessorRevision; - } - - break; - - default: - cpuFamilyResult.IsValid = false; - cpuFamilyResult.Message = "Unsupported CPU category. Manufacturer: " + manufacturer + ", Architecture: " + processorArchitecture + ", CPUFamily: " + sysInfo.ProcessorLevel + ", ProcessorRevision: " + sysInfo.ProcessorRevision; - break; - } - return cpuFamilyResult; - } - } -"@ - - # Storage - try { - $osDrive = Get-CimInstance -Class Win32_OperatingSystem | Select-Object -Property SystemDrive - $osDriveSize = Get-CimInstance -Class Win32_LogicalDisk -Filter "DeviceID='$($osDrive.SystemDrive)'" | Select-Object @{Name = "SizeGB"; Expression = { $_.Size / 1GB -as [int] } } - - if ($null -eq $osDriveSize) { - UpdateReturnCode -ReturnCode 1 - $outObject.returnReason += $logFormatReturnReason -f $STORAGE_STRING - $outObject.logging += $logFormatWithBlob -f $STORAGE_STRING, "Storage is null", $FAIL_STRING - - } - elseif ($osDriveSize.SizeGB -lt $MinOSDiskSizeGB) { - UpdateReturnCode -ReturnCode 1 - $outObject.returnReason += $logFormatReturnReason -f $STORAGE_STRING - $outObject.logging += $logFormatWithUnit -f $STORAGE_STRING, $OS_DISK_SIZE_STRING, ($osDriveSize.SizeGB), $GB_UNIT_STRING, $FAIL_STRING - } - else { - $outObject.logging += $logFormatWithUnit -f $STORAGE_STRING, $OS_DISK_SIZE_STRING, ($osDriveSize.SizeGB), $GB_UNIT_STRING, $PASS_STRING - UpdateReturnCode -ReturnCode 0 - } - } - catch { - UpdateReturnCode -ReturnCode -1 - $outObject.logging += $logFormat -f $STORAGE_STRING, $OS_DISK_SIZE_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING - $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" - } - - # Memory (bytes) - try { - $memory = Get-CimInstance Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum | Select-Object @{Name = "SizeGB"; Expression = { $_.Sum / 1GB -as [int] } } - - if ($null -eq $memory) { - UpdateReturnCode -ReturnCode 1 - $outObject.returnReason += $logFormatReturnReason -f $MEMORY_STRING - $outObject.logging += $logFormatWithBlob -f $MEMORY_STRING, "Memory is null", $FAIL_STRING - } - elseif ($memory.SizeGB -lt $MinMemoryGB) { - UpdateReturnCode -ReturnCode 1 - $outObject.returnReason += $logFormatReturnReason -f $MEMORY_STRING - $outObject.logging += $logFormatWithUnit -f $MEMORY_STRING, $SYSTEM_MEMORY_STRING, ($memory.SizeGB), $GB_UNIT_STRING, $FAIL_STRING - } - else { - $outObject.logging += $logFormatWithUnit -f $MEMORY_STRING, $SYSTEM_MEMORY_STRING, ($memory.SizeGB), $GB_UNIT_STRING, $PASS_STRING - UpdateReturnCode -ReturnCode 0 - } - } - catch { - UpdateReturnCode -ReturnCode -1 - $outObject.logging += $logFormat -f $MEMORY_STRING, $SYSTEM_MEMORY_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING - $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" - } - - # TPM - try { - $tpm = Get-Tpm - - if ($null -eq $tpm) { - UpdateReturnCode -ReturnCode 1 - $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING - $outObject.logging += $logFormatWithBlob -f $TPM_STRING, "TPM is null", $FAIL_STRING - } - elseif ($tpm.TpmPresent) { - $tpmVersion = Get-CimInstance -Class Win32_Tpm -Namespace root\CIMV2\Security\MicrosoftTpm | Select-Object -Property SpecVersion - - if ($null -eq $tpmVersion.SpecVersion) { - UpdateReturnCode -ReturnCode 1 - $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING - $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, "null", $FAIL_STRING - } - - $majorVersion = $tpmVersion.SpecVersion.Split(",")[0] -as [int] - if ($majorVersion -lt 2) { - UpdateReturnCode -ReturnCode 1 - $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING - $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, ($tpmVersion.SpecVersion), $FAIL_STRING - - } - else { - $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, ($tpmVersion.SpecVersion), $PASS_STRING - UpdateReturnCode -ReturnCode 0 - } - } - else { - if ($tpm.GetType().Name -eq "String") { - UpdateReturnCode -ReturnCode -1 - $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING - $outObject.logging += $logFormatException -f $tpm - } - else { - UpdateReturnCode -ReturnCode 1 - $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING - $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, ($tpm.TpmPresent), $FAIL_STRING - } - - } - } - catch { - UpdateReturnCode -ReturnCode -1 - $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING - $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" - } - - # CPU Details - $cpuDetails; - try { - $cpuDetails = @(Get-CimInstance -Class Win32_Processor)[0] - - if ($null -eq $cpuDetails) { - UpdateReturnCode -ReturnCode 1 - - $outObject.returnReason += $logFormatReturnReason -f $PROCESSOR_STRING - $outObject.logging += $logFormatWithBlob -f $PROCESSOR_STRING, "CpuDetails is null", $FAIL_STRING - } - else { - $processorCheckFailed = $false - - # AddressWidth - if ($null -eq $cpuDetails.AddressWidth -or $cpuDetails.AddressWidth -ne $RequiredAddressWidth) { - UpdateReturnCode -ReturnCode 1 - $processorCheckFailed = $true - } - - # ClockSpeed is in MHz - if ($null -eq $cpuDetails.MaxClockSpeed -or $cpuDetails.MaxClockSpeed -le $MinClockSpeedMHz) { - UpdateReturnCode -ReturnCode 1; - $processorCheckFailed = $true - } - - # Number of Logical Cores - if ($null -eq $cpuDetails.NumberOfLogicalProcessors -or $cpuDetails.NumberOfLogicalProcessors -lt $MinLogicalCores) { - UpdateReturnCode -ReturnCode 1 - $processorCheckFailed = $true - } - - # CPU Family - Add-Type -TypeDefinition $Source - $cpuFamilyResult = [CpuFamily]::Validate([String]$cpuDetails.Manufacturer, [uint16]$cpuDetails.Architecture) - - $cpuDetailsLog = "{AddressWidth=$($cpuDetails.AddressWidth); MaxClockSpeed=$($cpuDetails.MaxClockSpeed); NumberOfLogicalCores=$($cpuDetails.NumberOfLogicalProcessors); Manufacturer=$($cpuDetails.Manufacturer); Caption=$($cpuDetails.Caption); $($cpuFamilyResult.Message)}" - - if (!$cpuFamilyResult.IsValid) { - UpdateReturnCode -ReturnCode 1 - $processorCheckFailed = $true - - } - - if ($processorCheckFailed) { - $outObject.returnReason += $logFormatReturnReason -f $PROCESSOR_STRING - $outObject.logging += $logFormatWithBlob -f $PROCESSOR_STRING, ($cpuDetailsLog), $FAIL_STRING - } - else { - $outObject.logging += $logFormatWithBlob -f $PROCESSOR_STRING, ($cpuDetailsLog), $PASS_STRING - UpdateReturnCode -ReturnCode 0 - } - } - } - catch { - UpdateReturnCode -ReturnCode -1 - $outObject.logging += $logFormat -f $PROCESSOR_STRING, $PROCESSOR_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING - $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" - } - - # SecureBoot - try { - $isSecureBootEnabled = Confirm-SecureBootUEFI - $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $CAPABLE_STRING, $PASS_STRING - UpdateReturnCode -ReturnCode 0 - } - catch [System.PlatformNotSupportedException] { - # PlatformNotSupportedException "Cmdlet not supported on this platform." - SecureBoot is not supported or is non-UEFI computer. - UpdateReturnCode -ReturnCode 1 - $outObject.returnReason += $logFormatReturnReason -f $SECUREBOOT_STRING - $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $NOT_CAPABLE_STRING, $FAIL_STRING - } - catch [System.UnauthorizedAccessException] { - UpdateReturnCode -ReturnCode -1 - $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING - $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" - } - catch { - UpdateReturnCode -ReturnCode -1 - $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING - $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" - } - - # i7-7820hq CPU - try { - $supportedDevices = @('surface studio 2', 'precision 5520') - $systemInfo = @(Get-CimInstance -Class Win32_ComputerSystem)[0] - - if ($null -ne $cpuDetails) { - if ($cpuDetails.Name -match 'i7-7820hq cpu @ 2.90ghz') { - $modelOrSKUCheckLog = $systemInfo.Model.Trim() - if ($supportedDevices -contains $modelOrSKUCheckLog) { - $outObject.logging += $logFormatWithBlob -f $I7_7820HQ_CPU_STRING, $modelOrSKUCheckLog, $PASS_STRING - $outObject.returnCode = 0 - } - } - } - } - catch { - if ($outObject.returnCode -ne 0) { - UpdateReturnCode -ReturnCode -1 - $outObject.logging += $logFormatWithBlob -f $I7_7820HQ_CPU_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING - $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" - - } - } - - Switch ($outObject.returnCode) { - - 0 { $outObject.returnResult = $CAPABLE_CAPS_STRING } - 1 { $outObject.returnResult = $NOT_CAPABLE_CAPS_STRING } - -1 { $outObject.returnResult = $UNDETERMINED_CAPS_STRING } - -2 { $outObject.returnResult = $FAILED_TO_RUN_STRING } - } - - $outObject | ConvertTo-Json -Compress - } - - # Utility function for downloading files. - function Invoke-Download { - param( - [Parameter(Mandatory = $True)] - [String]$URL, - [Parameter(Mandatory = $True)] - [String]$Path, - [Parameter()] - [int]$Attempts = 3, - [Parameter()] - [Switch]$SkipSleep, - [Parameter()] - [Switch]$Overwrite - ) - - # Determine the supported TLS versions and set the appropriate security protocol - # Prefer Tls13 and Tls12 if both are available, otherwise just Tls12, or warn if unsupported. - $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType') - if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12 - } - elseif ( $SupportedTLSversions -contains 'Tls12' ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 - } - else { - # Warn the user if TLS 1.2 and 1.3 are not supported, which may cause the download to fail - Write-Host -Object "[Warning] TLS 1.2 and/or TLS 1.3 are not supported on this system. This download may fail!" - if ($PSVersionTable.PSVersion.Major -lt 3) { - Write-Host -Object "[Warning] PowerShell 2 / .NET 2.0 doesn't support TLS 1.2." - } - } - - # Trim whitespace from the URL and Path parameters. - if ($URL) { $URL = $URL.Trim() } - if ($Path) { $Path = $Path.Trim() } - - # Throw an error if no URL or Path was provided. - if (!$URL) { throw [System.ArgumentNullException]::New("You must provide a URL.") } - if (!$Path) { throw [System.ArgumentNullException]::New("You must provide a file path.") } - - # Display the URL being used for the download. - Write-Host -Object "URL '$URL' was given." - - # If the URL doesn't start with http or https, prepend https. - if ($URL -notmatch "^http") { - $URL = "https://$URL" - Write-Host -Object "[Warning] The URL given is missing http(s). The URL has been modified to the following '$URL'." - } - - # Validate that the URL does not contain invalid characters according to RFC3986. - if ($URL -match "[^A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]") { - throw [System.IO.InvalidDataException]::New("[Error] The url '$URL' contains an invalid character according to RFC3986.") - } - - # Check if the path contains invalid characters or reserved characters after the drive letter. - if ($Path -and ($Path -match '[/*?"<>|]' -or $Path.SubString(3) -match "[:]")) { - throw [System.IO.InvalidDataException]::New("[Error] The file path specified '$Path' contains one of the following invalid characters: '/*?`"<>|:'") - } - - # Check each folder in the path to ensure it isn't a reserved name (CON, PRN, AUX, etc.). - $Path -split '\\' | ForEach-Object { - $Folder = ($_).Trim() - if ($Folder -match '^CON$' -or $Folder -match '^PRN$' -or $Folder -match '^AUX$' -or $Folder -match '^NUL$' -or $Folder -match '^LPT\d$' -or $Folder -match '^COM\d+$') { - throw [System.IO.InvalidDataException]::New("[Error] An invalid folder name was given in '$Path'. The following folder names are reserved: CON, PRN, AUX, NUL, COM1-9, LPT1-9") - } - } - - # Temporarily disable progress reporting to speed up script performance - $PreviousProgressPreference = $ProgressPreference - $ProgressPreference = 'SilentlyContinue' - - # If no filename is included in the path (no extension), try to determine it from Content-Disposition. - if (($Path | Split-Path -Leaf) -notmatch "[.]") { - - Write-Host -Object "No filename provided in '$Path'. Checking the URL for a suitable filename." - - $ProposedFilename = Split-Path $URL -Leaf - - # Verify that the proposed filename doesn't contain invalid characters. - if ($ProposedFilename -and $ProposedFilename -notmatch "[^A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]" -and $ProposedFilename -match "[.]") { - $Filename = $ProposedFilename - } - - # If running on older PowerShell versions without Invoke-WebRequest require a filename. - if ($PSVersionTable.PSVersion.Major -lt 4) { - # Restore the original progress preference setting - $ProgressPreference = $PreviousProgressPreference - - throw [System.NotSupportedException]::New("You must provide a filename for systems not running PowerShell 4 or higher.") - } - - if (!$Filename) { - Write-Host -Object "No filename was discovered in the URL. Attempting to discover the filename via the Content-Disposition header." - $Request = 1 - - # Make multiple attempts (as defined by $Attempts) to retrieve the Content-Disposition header. - While ($Request -le $Attempts) { - # If SkipSleep is not set, wait for a random time between 3 and 15 seconds before each attempt - if (!($SkipSleep)) { - $SleepTime = Get-Random -Minimum 3 -Maximum 15 - Write-Host -Object "Waiting for $SleepTime seconds." - Start-Sleep -Seconds $SleepTime - } - - if ($Request -ne 1) { Write-Host "" } - Write-Host -Object "Attempt $Request" - - # Perform a HEAD request to get headers only. - # If the HEAD request fails, print a warning. - try { - $HeaderRequest = Invoke-WebRequest -Uri $URL -Method "HEAD" -MaximumRedirection 10 -UseBasicParsing -ErrorAction Stop - } - catch { - Write-Host -Object "[Warning] $($_.Exception.Message)" - Write-Host -Object "[Warning] The header request failed." - } - - # Check if the Content-Disposition header is present. - # If present, parse it to extract the filename. - if (!$HeaderRequest.Headers."Content-Disposition") { - Write-Host -Object "[Warning] The web server did not provide a Content-Disposition header." - } - else { - $Content = [System.Net.Mime.ContentDisposition]::new($HeaderRequest.Headers."Content-Disposition") - $Filename = $Content.FileName - } - - # If a filename was found, break out of the loop. - if ($Filename) { - $Request = $Attempts - } - - $Request++ - } - } - - # If a filename is still not found, throw an error. - if ($Filename) { - $Path = "$Path\$Filename" - } - else { - # Restore the original progress preference setting - $ProgressPreference = $PreviousProgressPreference - - throw [System.IO.FileNotFoundException]::New("Unable to find a suitable filename from the URL.") - } - } - - # If the file already exists at the specified path, restore the progress setting and throw an error. - if ((Test-Path -Path $Path -ErrorAction SilentlyContinue) -and !$Overwrite) { - # Restore the original progress preference setting - $ProgressPreference = $PreviousProgressPreference - - throw [System.IO.IOException]::New("A file already exists at the path '$Path'.") - } - - # Ensure that the destination folder exists, if not, try to create it. - $DestinationFolder = $Path | Split-Path - if (!(Test-Path -Path $DestinationFolder -ErrorAction SilentlyContinue)) { - try { - Write-Host -Object "Attempting to create the folder '$DestinationFolder' as it does not exist." - New-Item -Path $DestinationFolder -ItemType "directory" -ErrorAction Stop | Out-Null - Write-Host -Object "Successfully created the folder." - } - catch { - # Restore the original progress preference setting - $ProgressPreference = $PreviousProgressPreference - - throw $_ - } - } - - Write-Host -Object "Downloading the file..." - - # Initialize the download attempt counter. - $DownloadAttempt = 1 - While ($DownloadAttempt -le $Attempts) { - # If SkipSleep is not set, wait for a random time between 3 and 15 seconds before each attempt - if (!($SkipSleep)) { - $SleepTime = Get-Random -Minimum 3 -Maximum 15 - Write-Host -Object "Waiting for $SleepTime seconds." - Start-Sleep -Seconds $SleepTime - } - - # Provide a visual break between attempts - if ($DownloadAttempt -ne 1) { Write-Host "" } - Write-Host -Object "Download Attempt $DownloadAttempt" - - try { - if ($PSVersionTable.PSVersion.Major -lt 4) { - # For older versions of PowerShell, use WebClient to download the file - $WebClient = New-Object System.Net.WebClient - $WebClient.DownloadFile($URL, $Path) - } - else { - # For PowerShell 4.0 and above, use Invoke-WebRequest with specified arguments - $WebRequestArgs = @{ - Uri = $URL - OutFile = $Path - MaximumRedirection = 10 - UseBasicParsing = $true - } - - Invoke-WebRequest @WebRequestArgs - } - - # Verify if the file was successfully downloaded - $File = Test-Path -Path $Path -ErrorAction SilentlyContinue - } - catch { - # Handle any errors that occur during the download attempt - Write-Host -Object "[Warning] An error has occurred while downloading!" - Write-Host -Object "[Warning] $($_.Exception.Message)" - - # If the file partially downloaded, delete it to avoid corruption - if (Test-Path -Path $Path -ErrorAction SilentlyContinue) { - Remove-Item $Path -Force -Confirm:$false -ErrorAction SilentlyContinue - } - - $File = $False - } - - # If the file was successfully downloaded, exit the loop - if ($File) { - $DownloadAttempt = $Attempts - } - else { - # Warn the user if the download attempt failed - Write-Host -Object "[Warning] File failed to download.`n" - } - - # Increment the attempt counter - $DownloadAttempt++ - } - - # Restore the original progress preference setting - $ProgressPreference = $PreviousProgressPreference - - # Final check: if the file still doesn't exist, report an error and exit - if (!(Test-Path $Path)) { - throw [System.IO.FileNotFoundException]::New("[Error] Failed to download file. Please verify the URL of '$URL'.") - } - else { - # If the download succeeded, return the path to the downloaded file - return $Path - } - } - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # Check if the script is running with elevated privileges (administrator rights) - if (!(Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Inform the user about the URL being checked - Write-Host -Object "Checking https://endoflife.date/windows for the latest Windows build information." - try { - # Invoke a REST call to retrieve JSON data about Windows builds - $EndOfLifeResponse = Invoke-RestMethod -Method Get -Uri $EndOfLifeURL -ContentType "application/json" -MaximumRedirection 10 -UseBasicParsing -ErrorAction Stop - - # Filter the JSON response for Windows 11 builds (cycles that match '11-' and end in 'w') - $Windows11BuildJSON = $EndOfLifeResponse | Where-Object { $_.Cycle -match '^11-' -and $_.Cycle -match "w$" } - - # Throw an error if no Windows 11 builds are found - if (!$Windows11BuildJSON) { - throw "No Windows 11 builds found in the response." - } - } - catch { - # Catch any errors from the REST call or the filtering process - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to retrieve the latest Windows build information from $EndOfLifeURL." - exit 1 - } - - # Inform the user that the response will be parsed - Write-Host -Object "Parsing the response from https://endoflife.date/windows" - - # Create a list to store all relevant Windows 11 build information - $Windows11Builds = New-Object System.Collections.Generic.List[Object] - - $ErrorActionPreference = "Stop" - - # Iterate through each Windows 11 build JSON object - $Windows11BuildJSON | ForEach-Object { - try { - # Extract major, minor, and build numbers from the 'latest' version string - $Major = $_.latest -replace '^(\d+)\.(\d+)\.(\d+)$', '$1' - $Minor = $_.latest -replace '^(\d+)\.(\d+)\.(\d+)$', '$2' - $Build = $_.latest -replace '^(\d+)\.(\d+)\.(\d+)$', '$3' - - # Construct a custom PowerShell object for each build - $WindowBuild = [PSCustomObject]@{ - cycle = $_.cycle - releaseLabel = $_.releaseLabel - releaseDate = Get-Date $_.releaseDate - eol = Get-Date $_.eol - version = $_.latest - major = $Major - minor = $Minor - build = $Build - link = $_.link - lts = $_.lts - support = Get-Date $_.support - } - - # Add the newly created object to the Windows 11 builds collection - $Windows11Builds.Add($WindowBuild) - } - catch { - # Capture any errors in parsing the date or version properties - Write-Host -Object "[Warning] $($_.Exception.Message)" - Write-Host -Object "[Warning] Failed to parse the date for the build $($_.releaseLabel)." - return - } - } - - $ErrorActionPreference = "Continue" - - # If no Windows 11 builds were successfully parsed, show an error and exit - if ($Windows11Builds.Count -lt 1) { - Write-Host -Object "[Error] Failed to parse any of the Windows 11 builds." - exit 1 - } - - # Retrieve the current Windows 11 version from the system's registry - Write-Host -Object "Retrieving the system's current Windows 11 version." - try { - $CurrentVersion = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -ErrorAction Stop | Select-Object -ExpandProperty DisplayVersion -ErrorAction SilentlyContinue - }catch{ - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to retrieve the latest feature update installed." - exit 1 - } - - # Sort the Windows 11 build objects by release date in descending order - $Windows11Builds = $Windows11Builds | Sort-Object releaseDate -Descending - - # Retrieve the first (newest) build from the sorted list - $LatestBuild = $Windows11Builds | Select-Object -First 1 - - # Display information about the latest supported Windows 11 feature update - Write-Host -Object "The latest supported feature update is Windows $(($LatestBuild.releaseLabel -replace '\(W\)').Trim()) and it was released on $($LatestBuild.releaseDate.ToShortDateString())." - Write-Host -Object "The system currently has the feature update Windows 11 $CurrentVersion installed." - - # Compare the running system's build number to the newest build's number - if ([System.Environment]::OSVersion.Version.Build -ge $LatestBuild.build) { - Write-Host -Object "[Error] The system is running the same or a newer feature update than the latest supported version." - exit 1 - } - - try { - # Temporarily set the ErrorActionPreference to "Stop" so that any errors are caught as exceptions. - $ErrorActionPreference = "Stop" - - Write-Host -Object "`nVerifying the feature update compatibility." - - # Retrieve hardware readiness data, convert the JSON results into a PowerShell object. - $Result = Get-HardwareReadiness | Select-Object -Unique | ConvertFrom-Json - Write-Host -Object "Successfully retrieved the compatibility results.`n" - - # Reset the ErrorActionPreference to default ("Continue") so that non-terminating errors don't stop the script. - $ErrorActionPreference = "Continue" - } - catch { - # If any error occurs while fetching hardware readiness, display error messages and exit. - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to retrieve the compatibility results." - exit 1 - } - - # Based on the returnCode property in the JSON result, evaluate the device's compatibility. - switch ($Result.returnCode) { - 0 { - $ResultString = "Capable" - } - 1 { - $ResultString = "[Alert] Not capable" - $Incompatible = $True - } - -2 { - $ResultString = "[Error] Failed to run" - $Incompatible = $True - } - default { - $ResultString = "[Error] Undetermined" - $Incompatible = $True - } - } - - # If there's a more detailed reason for incompatibility, append it to the result string. - if ($Result.returnReason) { - $ResultString = "$ResultString - $($Result.returnReason)" - - # This removes any trailing commas or spaces at the end if they exist. - $ResultString = $ResultString -replace ",\s*$" - } - - Write-Host -Object "Compatibility Test Result: $ResultString" - - # If the system is flagged as incompatible, display an error and exit. - if ($Incompatible) { - Write-Host -Object "[Error] This device is either incompatible with the feature update or its compatibility could not be determined." - exit 1 - } - - # Check if the Windows 11 Upgrade process is already running. - $Windows11UpgradeApp = Get-Process -Name "Windows10UpgraderApp" -ErrorAction SilentlyContinue - - if (!$Windows11UpgradeApp) { - # No upgrade process is detected. - Write-Host -Object "The upgrade is not currently in progress." - } - else { - # If found, display an error with information on the process and exit. - Write-Host -Object "[Error] The Windows 11 upgrade is already in progress via the process below." - Write-Host -Object "`n### Windows 11 Upgrade Process ###" - ($Windows11UpgradeApp | Select-Object @{ Name = 'PID'; Expression = { $_.Id } }, Name, Description, Path | - Format-List PID, Name, Description, Path | Out-String).Trim() | Write-Host - exit 1 - } - - Write-Host -Object "`nDownloading the Windows 11 Installation Assistant executable." - try { - $WindowsInstallAssistant = Invoke-Download -Path $DownloadDestination -URL $InstallAssistantDownloadURL -Overwrite -ErrorAction Stop - Write-Host -Object "Download complete." - } - catch { - # If the download fails, display an error message and exit. - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Unable to download the Windows 11 Installation Assistant at '$InstallAssistantDownloadURL'." - exit 1 - } - - Write-Host -Object "`nVerifying the executable's signature." - try { - # Check the digital signature of the downloaded executable to ensure authenticity. - $InstallationAssistantSignature = Get-AuthenticodeSignature $WindowsInstallAssistant -ErrorAction Stop - } - catch { - # If signature retrieval fails, display an error and exit. - Write-Host -Object "$($_.Exception.Message)" - Write-Host -Object "[Error] Failed to read the executable signature for the file '$WindowsInstallAssistant'." - exit 1 - } - - # If the signature isn't valid, assume the file is corrupted or tampered with, and exit. - if ($InstallationAssistantSignature.Status -ne "Valid") { - Write-Host -Object "[Error] An invalid signature status of '$($InstallationAssistantSignature.Status)' was provided. Perhaps the downloaded file '$WindowsInstallAssistant' was corrupted in transit?" - exit 1 - } - - - # Check the signer's certificate subject to confirm it's Microsoft Corporation. - if ($InstallationAssistantSignature.SignerCertificate.Subject -ne "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US") { - Write-Host -Object "[Error] An invalid signature subject of '$($InstallationAssistantSignature.SignerCertificate.Subject)' was provided. 'CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US' was expected." - exit 1 - } - - Write-Host -Object "The signature is valid and appears to be what was expected." - - # Ensure the log folder exists, and if not, create it. - if (!(Test-Path -Path $UpdateLogLocation -ErrorAction SilentlyContinue)) { - Write-Host -Object "`nThe log folder '$UpdateLogLocation' does not currently exist. Attempting to create the folder." - try { - New-Item -Path $UpdateLogLocation -ItemType Directory -Force | Out-Null - Write-Host -Object "Successfully created the log folder." - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to create log folder '$UpdateLogLocation'" - exit 1 - } - } - - # Define the arguments to be passed to the Installation Assistant. - $InstallAssistantArguments = @( - "/QuietInstall" - "/SkipEULA" - "/NoRestartUI" - "/Auto Upgrade" - "/CopyLogs `"$UpdateLogLocation`"" - ) - - # Set up the process invocation parameters, including where to log standard output and errors. - $InstallAssistantProcessArguments = @{ - FilePath = $WindowsInstallAssistant - ArgumentList = $InstallAssistantArguments - RedirectStandardOutput = "$UpdateLogLocation\$(New-Guid).stdout.log" - RedirectStandardError = "$UpdateLogLocation\$(New-Guid).stderr.log" - NoNewWindow = $True - } - - Write-Host -Object "`nInitiating Windows 11 feature upgrade." - Write-Host -Object "[Warning] This may take a few hours to complete. You can view the logs at '$UpdateLogLocation' and '${env:ProgramFiles(x86)}\WindowsInstallationAssistant\Logs' if any failure occurs." - Write-Host -Object "If no failure occurs, these files may be empty." - - try { - # Start the Windows 11 upgrade process silently in the background. - Start-Process @InstallAssistantProcessArguments - } - catch { - # If the process fails to start, display an error message and exit. - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to start the Windows 11 upgrade process using the file '$WindowsInstallAssistant'." - exit 1 - } - - Start-Sleep -Seconds 30 - # Check if the Windows 10 Upgrade process is running. - $Windows11UpgradeApp = Get-Process -Name "Windows10UpgraderApp" -ErrorAction SilentlyContinue - - if (!$Windows11UpgradeApp) { - # No upgrade process is detected. - Write-Host -Object "[Error] Failed to detect the upgrade process." - Write-Host -Object "[Error] Failed to start the Windows 11 upgrade process using the file '$WindowsInstallAssistant'." - exit 1 - } - else { - Write-Host -Object "`n### Windows 11 Upgrade Process ###" - ($Windows11UpgradeApp | Select-Object @{ Name = 'PID'; Expression = { $_.Id } }, Name, Description, Path | - Format-List PID, Name, Description, Path | Out-String).Trim() | Write-Host - } - - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Update Windows 11 to the latest available Feature Update using the Windows 11 Install Assistant. This script does not wait for the installation assistant to complete. +.DESCRIPTION + Update Windows 11 to the latest available Feature Update using the Windows 11 Install Assistant. This script does not wait for the installation assistant to complete. +.EXAMPLE + (No Parameters) + + Checking https://endoflife.date/windows for the latest Windows build information. + Parsing the response from https://endoflife.date/windows + Retrieving the systems current Windows 11 version. + The latest supported feature update is Windows 11 24H2 and it was released on 10/1/2024. + The system currently has the feature update Windows 11 23H2 installed. + + Verifying the feature update compatibility. + Successfully retrieved the compatibility results. + + Compatibility Test Result: Capable + The upgrade is not currently in progress. + + Downloading the Windows 11 Installation Assistant executable. + URL 'https://go.microsoft.com/fwlink/?linkid=2171764' was given. + Downloading the file... + Waiting for 7 seconds. + Download Attempt 1 + Download complete. + + Verifying the executable's signature. + The signature is valid and appears to be what was expected. + + Initiating Windows 11 feature upgrade. + [Warning] This may take a few hours to complete. You can view the logs at 'C:\Windows\Logs\Windows11InstallAssistant' and 'C:\Program Files (x86)\WindowsInstallationAssistant\Logs' if any failure occurs. + If no failure occurs, these files may be empty. + + ### Windows 11 Upgrade Process ### + PID : 2548 + Name : Windows10UpgraderApp + Description : Windows Installation Assistant + Path : C:\Program Files (x86)\WindowsInstallationAssistant\Windows10UpgraderApp.exe + +PARAMETER: -InstallAssistantDownloadURL "https://wwww.ReplaceMeWithTheURLToTheWindows11InstallationAssistant.com" + Defines the URL from which the Windows 11 Installation Assistant will be downloaded. + +PARAMETER: -DownloadDestination "C:\ReplaceMeWithALocationToDownloadTheFileTo.exe" + Provides the destination file path where Windows11InstallationAssistant.exe should be saved. + +PARAMETER: -UpdateLogLocation "C:\ReplaceMeWithAFolder" + Specifies the folder location where the installation assistant logs will be stored. + +PARAMETER: -EndOfLifeURL "https://endoflife.date/api/windows.json" + Defines the URL to the endoflife.date api. + +.NOTES + Minimum OS Architecture Supported: Windows 11 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$InstallAssistantDownloadURL = 'https://go.microsoft.com/fwlink/?linkid=2171764', + [Parameter()] + [String]$DownloadDestination = "$env:TEMP\Windows11InstallAssistant\Windows11InstallationAssistant.exe", + [Parameter()] + [String]$UpdateLogLocation = "$env:SYSTEMROOT\Logs\Windows11InstallAssistant", + [Parameter()] + [String]$EndOfLifeURL = "https://endoflife.date/api/windows.json" +) + +begin { + # Determine the method to retrieve the operating system information based on PowerShell version. + try { + $OS = if ($PSVersionTable.PSVersion.Major -lt 3) { + Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop + } + else { + Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop + } + } + catch { + # If the above retrieval fails, display an error message and exit. + Write-Host -Object "[Error] Unable to retrieve information about the current operating system." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Check if the system is running Windows 11. If not, display an error message and exit. + if ($OS.Caption -notmatch "Windows 11") { + Write-Host -Object "[Error] This device is not currently running Windows 11. It is currently running '$($OS.Caption)'." + exit 1 + } + + try { + # Retrieve the volume information for the system drive (C: or equivalent). + # Replace ":" in the drive letter environment variable to match the Get-Volume cmdlet parameter. + $osDrive = Get-Volume -DriveLetter ($env:SystemDrive -replace ":") -ErrorAction Stop + + # If there's no remaining size property, throw an error to be caught below. + if (!$osDrive.SizeRemaining) { + throw "Failed to retrieve the remaining size for drive '$env:SystemDrive'." + } + + } + catch { + # If the volume information retrieval fails, output an error message and exit. + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to get the size of the current os drive ($env:SystemDrive)." + exit 1 + } + + # Ensure there is at least 64GB of free space on the system drive before continuing. + if ($osDrive.SizeRemaining -lt 64GB) { + Write-Host -Object "[Error] The current free space for the system drive '$env:SystemDrive' is $([math]::Round(($osDrive.SizeRemaining / 1GB),2)). There is not enough free space. You must have at least 64GB of free space." + exit 1 + } + + function Get-HardwareReadiness() { + # Modified copy of https://aka.ms/HWReadinessScript minus the signature, as of 7/26/2023. + # Only modification was replacing Get-WmiObject with Get-CimInstance for PowerShell 7 compatibility + # Source Microsoft article: https://techcommunity.microsoft.com/t5/microsoft-endpoint-manager-blog/understanding-readiness-for-windows-11-with-microsoft-endpoint/ba-p/2770866 + + #============================================================================================================================= + # + # Script Name: HardwareReadiness.ps1 + # Description: Verifies the hardware compliance. Return code 0 for success. + # In case of failure, returns non zero error code along with error message. + + # This script is not supported under any Microsoft standard support program or service and is distributed under the MIT license + + # Copyright (C) 2021 Microsoft Corporation + + # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation + # files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, + # modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software + # is furnished to do so, subject to the following conditions: + + # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + #============================================================================================================================= + + [int]$MinOSDiskSizeGB = 64 + [int]$MinMemoryGB = 4 + [Uint32]$MinClockSpeedMHz = 1000 + [Uint32]$MinLogicalCores = 2 + [Uint16]$RequiredAddressWidth = 64 + + $PASS_STRING = "PASS" + $FAIL_STRING = "FAIL" + $FAILED_TO_RUN_STRING = "FAILED TO RUN" + $UNDETERMINED_CAPS_STRING = "UNDETERMINED" + $UNDETERMINED_STRING = "Undetermined" + $CAPABLE_STRING = "Capable" + $NOT_CAPABLE_STRING = "Not capable" + $CAPABLE_CAPS_STRING = "CAPABLE" + $NOT_CAPABLE_CAPS_STRING = "NOT CAPABLE" + $STORAGE_STRING = "Storage" + $OS_DISK_SIZE_STRING = "OSDiskSize" + $MEMORY_STRING = "Memory" + $SYSTEM_MEMORY_STRING = "System_Memory" + $GB_UNIT_STRING = "GB" + $TPM_STRING = "TPM" + $TPM_VERSION_STRING = "TPMVersion" + $PROCESSOR_STRING = "Processor" + $SECUREBOOT_STRING = "SecureBoot" + $I7_7820HQ_CPU_STRING = "i7-7820hq CPU" + + # 0=name of check, 1=attribute checked, 2=value, 3=PASS/FAIL/UNDETERMINED + $logFormat = '{0}: {1}={2}. {3}; ' + + # 0=name of check, 1=attribute checked, 2=value, 3=unit of the value, 4=PASS/FAIL/UNDETERMINED + $logFormatWithUnit = '{0}: {1}={2}{3}. {4}; ' + + # 0=name of check. + $logFormatReturnReason = '{0}, ' + + # 0=exception. + $logFormatException = '{0}; ' + + # 0=name of check, 1= attribute checked and its value, 2=PASS/FAIL/UNDETERMINED + $logFormatWithBlob = '{0}: {1}. {2}; ' + + # return returnCode is -1 when an exception is thrown. 1 if the value does not meet requirements. 0 if successful. -2 default, script didn't run. + $outObject = @{ returnCode = -2; returnResult = $FAILED_TO_RUN_STRING; returnReason = ""; logging = "" } + + # NOT CAPABLE(1) state takes precedence over UNDETERMINED(-1) state + function Private:UpdateReturnCode { + param( + [Parameter(Mandatory = $true)] + [ValidateRange(-2, 1)] + [int] $ReturnCode + ) + + Switch ($ReturnCode) { + + 0 { + if ($outObject.returnCode -eq -2) { + $outObject.returnCode = $ReturnCode + } + } + 1 { + $outObject.returnCode = $ReturnCode + } + -1 { + if ($outObject.returnCode -ne 1) { + $outObject.returnCode = $ReturnCode + } + } + } + } + + $Source = @" +using Microsoft.Win32; +using System; +using System.Runtime.InteropServices; + + public class CpuFamilyResult + { + public bool IsValid { get; set; } + public string Message { get; set; } + } + + public class CpuFamily + { + [StructLayout(LayoutKind.Sequential)] + public struct SYSTEM_INFO + { + public ushort ProcessorArchitecture; + ushort Reserved; + public uint PageSize; + public IntPtr MinimumApplicationAddress; + public IntPtr MaximumApplicationAddress; + public IntPtr ActiveProcessorMask; + public uint NumberOfProcessors; + public uint ProcessorType; + public uint AllocationGranularity; + public ushort ProcessorLevel; + public ushort ProcessorRevision; + } + + [DllImport("kernel32.dll")] + internal static extern void GetNativeSystemInfo(ref SYSTEM_INFO lpSystemInfo); + + public enum ProcessorFeature : uint + { + ARM_SUPPORTED_INSTRUCTIONS = 34 + } + + [DllImport("kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool IsProcessorFeaturePresent(ProcessorFeature processorFeature); + + private const ushort PROCESSOR_ARCHITECTURE_X86 = 0; + private const ushort PROCESSOR_ARCHITECTURE_ARM64 = 12; + private const ushort PROCESSOR_ARCHITECTURE_X64 = 9; + + private const string INTEL_MANUFACTURER = "GenuineIntel"; + private const string AMD_MANUFACTURER = "AuthenticAMD"; + private const string QUALCOMM_MANUFACTURER = "Qualcomm Technologies Inc"; + + public static CpuFamilyResult Validate(string manufacturer, ushort processorArchitecture) + { + CpuFamilyResult cpuFamilyResult = new CpuFamilyResult(); + + if (string.IsNullOrWhiteSpace(manufacturer)) + { + cpuFamilyResult.IsValid = false; + cpuFamilyResult.Message = "Manufacturer is null or empty"; + return cpuFamilyResult; + } + + string registryPath = "HKEY_LOCAL_MACHINE\\Hardware\\Description\\System\\CentralProcessor\\0"; + SYSTEM_INFO sysInfo = new SYSTEM_INFO(); + GetNativeSystemInfo(ref sysInfo); + + switch (processorArchitecture) + { + case PROCESSOR_ARCHITECTURE_ARM64: + + if (manufacturer.Equals(QUALCOMM_MANUFACTURER, StringComparison.OrdinalIgnoreCase)) + { + bool isArmv81Supported = IsProcessorFeaturePresent(ProcessorFeature.ARM_SUPPORTED_INSTRUCTIONS); + + if (!isArmv81Supported) + { + string registryName = "CP 4030"; + long registryValue = (long)Registry.GetValue(registryPath, registryName, -1); + long atomicResult = (registryValue >> 20) & 0xF; + + if (atomicResult >= 2) + { + isArmv81Supported = true; + } + } + + cpuFamilyResult.IsValid = isArmv81Supported; + cpuFamilyResult.Message = isArmv81Supported ? "" : "Processor does not implement ARM v8.1 atomic instruction"; + } + else + { + cpuFamilyResult.IsValid = false; + cpuFamilyResult.Message = "The processor isn't currently supported for Windows 11"; + } + + break; + + case PROCESSOR_ARCHITECTURE_X64: + case PROCESSOR_ARCHITECTURE_X86: + + int cpuFamily = sysInfo.ProcessorLevel; + int cpuModel = (sysInfo.ProcessorRevision >> 8) & 0xFF; + int cpuStepping = sysInfo.ProcessorRevision & 0xFF; + + if (manufacturer.Equals(INTEL_MANUFACTURER, StringComparison.OrdinalIgnoreCase)) + { + try + { + cpuFamilyResult.IsValid = true; + cpuFamilyResult.Message = ""; + + if (cpuFamily >= 6 && cpuModel <= 95 && !(cpuFamily == 6 && cpuModel == 85)) + { + cpuFamilyResult.IsValid = false; + cpuFamilyResult.Message = ""; + } + else if (cpuFamily == 6 && (cpuModel == 142 || cpuModel == 158) && cpuStepping == 9) + { + string registryName = "Platform Specific Field 1"; + int registryValue = (int)Registry.GetValue(registryPath, registryName, -1); + + if ((cpuModel == 142 && registryValue != 16) || (cpuModel == 158 && registryValue != 8)) + { + cpuFamilyResult.IsValid = false; + } + cpuFamilyResult.Message = "PlatformId " + registryValue; + } + } + catch (Exception ex) + { + cpuFamilyResult.IsValid = false; + cpuFamilyResult.Message = "Exception:" + ex.GetType().Name; + } + } + else if (manufacturer.Equals(AMD_MANUFACTURER, StringComparison.OrdinalIgnoreCase)) + { + cpuFamilyResult.IsValid = true; + cpuFamilyResult.Message = ""; + + if (cpuFamily < 23 || (cpuFamily == 23 && (cpuModel == 1 || cpuModel == 17))) + { + cpuFamilyResult.IsValid = false; + } + } + else + { + cpuFamilyResult.IsValid = false; + cpuFamilyResult.Message = "Unsupported Manufacturer: " + manufacturer + ", Architecture: " + processorArchitecture + ", CPUFamily: " + sysInfo.ProcessorLevel + ", ProcessorRevision: " + sysInfo.ProcessorRevision; + } + + break; + + default: + cpuFamilyResult.IsValid = false; + cpuFamilyResult.Message = "Unsupported CPU category. Manufacturer: " + manufacturer + ", Architecture: " + processorArchitecture + ", CPUFamily: " + sysInfo.ProcessorLevel + ", ProcessorRevision: " + sysInfo.ProcessorRevision; + break; + } + return cpuFamilyResult; + } + } +"@ + + # Storage + try { + $osDrive = Get-CimInstance -Class Win32_OperatingSystem | Select-Object -Property SystemDrive + $osDriveSize = Get-CimInstance -Class Win32_LogicalDisk -Filter "DeviceID='$($osDrive.SystemDrive)'" | Select-Object @{Name = "SizeGB"; Expression = { $_.Size / 1GB -as [int] } } + + if ($null -eq $osDriveSize) { + UpdateReturnCode -ReturnCode 1 + $outObject.returnReason += $logFormatReturnReason -f $STORAGE_STRING + $outObject.logging += $logFormatWithBlob -f $STORAGE_STRING, "Storage is null", $FAIL_STRING + + } + elseif ($osDriveSize.SizeGB -lt $MinOSDiskSizeGB) { + UpdateReturnCode -ReturnCode 1 + $outObject.returnReason += $logFormatReturnReason -f $STORAGE_STRING + $outObject.logging += $logFormatWithUnit -f $STORAGE_STRING, $OS_DISK_SIZE_STRING, ($osDriveSize.SizeGB), $GB_UNIT_STRING, $FAIL_STRING + } + else { + $outObject.logging += $logFormatWithUnit -f $STORAGE_STRING, $OS_DISK_SIZE_STRING, ($osDriveSize.SizeGB), $GB_UNIT_STRING, $PASS_STRING + UpdateReturnCode -ReturnCode 0 + } + } + catch { + UpdateReturnCode -ReturnCode -1 + $outObject.logging += $logFormat -f $STORAGE_STRING, $OS_DISK_SIZE_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING + $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" + } + + # Memory (bytes) + try { + $memory = Get-CimInstance Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum | Select-Object @{Name = "SizeGB"; Expression = { $_.Sum / 1GB -as [int] } } + + if ($null -eq $memory) { + UpdateReturnCode -ReturnCode 1 + $outObject.returnReason += $logFormatReturnReason -f $MEMORY_STRING + $outObject.logging += $logFormatWithBlob -f $MEMORY_STRING, "Memory is null", $FAIL_STRING + } + elseif ($memory.SizeGB -lt $MinMemoryGB) { + UpdateReturnCode -ReturnCode 1 + $outObject.returnReason += $logFormatReturnReason -f $MEMORY_STRING + $outObject.logging += $logFormatWithUnit -f $MEMORY_STRING, $SYSTEM_MEMORY_STRING, ($memory.SizeGB), $GB_UNIT_STRING, $FAIL_STRING + } + else { + $outObject.logging += $logFormatWithUnit -f $MEMORY_STRING, $SYSTEM_MEMORY_STRING, ($memory.SizeGB), $GB_UNIT_STRING, $PASS_STRING + UpdateReturnCode -ReturnCode 0 + } + } + catch { + UpdateReturnCode -ReturnCode -1 + $outObject.logging += $logFormat -f $MEMORY_STRING, $SYSTEM_MEMORY_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING + $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" + } + + # TPM + try { + $tpm = Get-Tpm + + if ($null -eq $tpm) { + UpdateReturnCode -ReturnCode 1 + $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING + $outObject.logging += $logFormatWithBlob -f $TPM_STRING, "TPM is null", $FAIL_STRING + } + elseif ($tpm.TpmPresent) { + $tpmVersion = Get-CimInstance -Class Win32_Tpm -Namespace root\CIMV2\Security\MicrosoftTpm | Select-Object -Property SpecVersion + + if ($null -eq $tpmVersion.SpecVersion) { + UpdateReturnCode -ReturnCode 1 + $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING + $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, "null", $FAIL_STRING + } + + $majorVersion = $tpmVersion.SpecVersion.Split(",")[0] -as [int] + if ($majorVersion -lt 2) { + UpdateReturnCode -ReturnCode 1 + $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING + $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, ($tpmVersion.SpecVersion), $FAIL_STRING + + } + else { + $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, ($tpmVersion.SpecVersion), $PASS_STRING + UpdateReturnCode -ReturnCode 0 + } + } + else { + if ($tpm.GetType().Name -eq "String") { + UpdateReturnCode -ReturnCode -1 + $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING + $outObject.logging += $logFormatException -f $tpm + } + else { + UpdateReturnCode -ReturnCode 1 + $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING + $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, ($tpm.TpmPresent), $FAIL_STRING + } + + } + } + catch { + UpdateReturnCode -ReturnCode -1 + $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING + $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" + } + + # CPU Details + $cpuDetails; + try { + $cpuDetails = @(Get-CimInstance -Class Win32_Processor)[0] + + if ($null -eq $cpuDetails) { + UpdateReturnCode -ReturnCode 1 + + $outObject.returnReason += $logFormatReturnReason -f $PROCESSOR_STRING + $outObject.logging += $logFormatWithBlob -f $PROCESSOR_STRING, "CpuDetails is null", $FAIL_STRING + } + else { + $processorCheckFailed = $false + + # AddressWidth + if ($null -eq $cpuDetails.AddressWidth -or $cpuDetails.AddressWidth -ne $RequiredAddressWidth) { + UpdateReturnCode -ReturnCode 1 + $processorCheckFailed = $true + } + + # ClockSpeed is in MHz + if ($null -eq $cpuDetails.MaxClockSpeed -or $cpuDetails.MaxClockSpeed -le $MinClockSpeedMHz) { + UpdateReturnCode -ReturnCode 1; + $processorCheckFailed = $true + } + + # Number of Logical Cores + if ($null -eq $cpuDetails.NumberOfLogicalProcessors -or $cpuDetails.NumberOfLogicalProcessors -lt $MinLogicalCores) { + UpdateReturnCode -ReturnCode 1 + $processorCheckFailed = $true + } + + # CPU Family + Add-Type -TypeDefinition $Source + $cpuFamilyResult = [CpuFamily]::Validate([String]$cpuDetails.Manufacturer, [uint16]$cpuDetails.Architecture) + + $cpuDetailsLog = "{AddressWidth=$($cpuDetails.AddressWidth); MaxClockSpeed=$($cpuDetails.MaxClockSpeed); NumberOfLogicalCores=$($cpuDetails.NumberOfLogicalProcessors); Manufacturer=$($cpuDetails.Manufacturer); Caption=$($cpuDetails.Caption); $($cpuFamilyResult.Message)}" + + if (!$cpuFamilyResult.IsValid) { + UpdateReturnCode -ReturnCode 1 + $processorCheckFailed = $true + + } + + if ($processorCheckFailed) { + $outObject.returnReason += $logFormatReturnReason -f $PROCESSOR_STRING + $outObject.logging += $logFormatWithBlob -f $PROCESSOR_STRING, ($cpuDetailsLog), $FAIL_STRING + } + else { + $outObject.logging += $logFormatWithBlob -f $PROCESSOR_STRING, ($cpuDetailsLog), $PASS_STRING + UpdateReturnCode -ReturnCode 0 + } + } + } + catch { + UpdateReturnCode -ReturnCode -1 + $outObject.logging += $logFormat -f $PROCESSOR_STRING, $PROCESSOR_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING + $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" + } + + # SecureBoot + try { + $isSecureBootEnabled = Confirm-SecureBootUEFI + $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $CAPABLE_STRING, $PASS_STRING + UpdateReturnCode -ReturnCode 0 + } + catch [System.PlatformNotSupportedException] { + # PlatformNotSupportedException "Cmdlet not supported on this platform." - SecureBoot is not supported or is non-UEFI computer. + UpdateReturnCode -ReturnCode 1 + $outObject.returnReason += $logFormatReturnReason -f $SECUREBOOT_STRING + $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $NOT_CAPABLE_STRING, $FAIL_STRING + } + catch [System.UnauthorizedAccessException] { + UpdateReturnCode -ReturnCode -1 + $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING + $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" + } + catch { + UpdateReturnCode -ReturnCode -1 + $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING + $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" + } + + # i7-7820hq CPU + try { + $supportedDevices = @('surface studio 2', 'precision 5520') + $systemInfo = @(Get-CimInstance -Class Win32_ComputerSystem)[0] + + if ($null -ne $cpuDetails) { + if ($cpuDetails.Name -match 'i7-7820hq cpu @ 2.90ghz') { + $modelOrSKUCheckLog = $systemInfo.Model.Trim() + if ($supportedDevices -contains $modelOrSKUCheckLog) { + $outObject.logging += $logFormatWithBlob -f $I7_7820HQ_CPU_STRING, $modelOrSKUCheckLog, $PASS_STRING + $outObject.returnCode = 0 + } + } + } + } + catch { + if ($outObject.returnCode -ne 0) { + UpdateReturnCode -ReturnCode -1 + $outObject.logging += $logFormatWithBlob -f $I7_7820HQ_CPU_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING + $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)" + + } + } + + Switch ($outObject.returnCode) { + + 0 { $outObject.returnResult = $CAPABLE_CAPS_STRING } + 1 { $outObject.returnResult = $NOT_CAPABLE_CAPS_STRING } + -1 { $outObject.returnResult = $UNDETERMINED_CAPS_STRING } + -2 { $outObject.returnResult = $FAILED_TO_RUN_STRING } + } + + $outObject | ConvertTo-Json -Compress + } + + # Utility function for downloading files. + function Invoke-Download { + param( + [Parameter(Mandatory = $True)] + [String]$URL, + [Parameter(Mandatory = $True)] + [String]$Path, + [Parameter()] + [int]$Attempts = 3, + [Parameter()] + [Switch]$SkipSleep, + [Parameter()] + [Switch]$Overwrite + ) + + # Determine the supported TLS versions and set the appropriate security protocol + # Prefer Tls13 and Tls12 if both are available, otherwise just Tls12, or warn if unsupported. + $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType') + if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12 + } + elseif ( $SupportedTLSversions -contains 'Tls12' ) { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 + } + else { + # Warn the user if TLS 1.2 and 1.3 are not supported, which may cause the download to fail + Write-Host -Object "[Warning] TLS 1.2 and/or TLS 1.3 are not supported on this system. This download may fail!" + if ($PSVersionTable.PSVersion.Major -lt 3) { + Write-Host -Object "[Warning] PowerShell 2 / .NET 2.0 doesn't support TLS 1.2." + } + } + + # Trim whitespace from the URL and Path parameters. + if ($URL) { $URL = $URL.Trim() } + if ($Path) { $Path = $Path.Trim() } + + # Throw an error if no URL or Path was provided. + if (!$URL) { throw [System.ArgumentNullException]::New("You must provide a URL.") } + if (!$Path) { throw [System.ArgumentNullException]::New("You must provide a file path.") } + + # Display the URL being used for the download. + Write-Host -Object "URL '$URL' was given." + + # If the URL doesn't start with http or https, prepend https. + if ($URL -notmatch "^http") { + $URL = "https://$URL" + Write-Host -Object "[Warning] The URL given is missing http(s). The URL has been modified to the following '$URL'." + } + + # Validate that the URL does not contain invalid characters according to RFC3986. + if ($URL -match "[^A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]") { + throw [System.IO.InvalidDataException]::New("[Error] The url '$URL' contains an invalid character according to RFC3986.") + } + + # Check if the path contains invalid characters or reserved characters after the drive letter. + if ($Path -and ($Path -match '[/*?"<>|]' -or $Path.SubString(3) -match "[:]")) { + throw [System.IO.InvalidDataException]::New("[Error] The file path specified '$Path' contains one of the following invalid characters: '/*?`"<>|:'") + } + + # Check each folder in the path to ensure it isn't a reserved name (CON, PRN, AUX, etc.). + $Path -split '\\' | ForEach-Object { + $Folder = ($_).Trim() + if ($Folder -match '^CON$' -or $Folder -match '^PRN$' -or $Folder -match '^AUX$' -or $Folder -match '^NUL$' -or $Folder -match '^LPT\d$' -or $Folder -match '^COM\d+$') { + throw [System.IO.InvalidDataException]::New("[Error] An invalid folder name was given in '$Path'. The following folder names are reserved: CON, PRN, AUX, NUL, COM1-9, LPT1-9") + } + } + + # Temporarily disable progress reporting to speed up script performance + $PreviousProgressPreference = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + + # If no filename is included in the path (no extension), try to determine it from Content-Disposition. + if (($Path | Split-Path -Leaf) -notmatch "[.]") { + + Write-Host -Object "No filename provided in '$Path'. Checking the URL for a suitable filename." + + $ProposedFilename = Split-Path $URL -Leaf + + # Verify that the proposed filename doesn't contain invalid characters. + if ($ProposedFilename -and $ProposedFilename -notmatch "[^A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]" -and $ProposedFilename -match "[.]") { + $Filename = $ProposedFilename + } + + # If running on older PowerShell versions without Invoke-WebRequest require a filename. + if ($PSVersionTable.PSVersion.Major -lt 4) { + # Restore the original progress preference setting + $ProgressPreference = $PreviousProgressPreference + + throw [System.NotSupportedException]::New("You must provide a filename for systems not running PowerShell 4 or higher.") + } + + if (!$Filename) { + Write-Host -Object "No filename was discovered in the URL. Attempting to discover the filename via the Content-Disposition header." + $Request = 1 + + # Make multiple attempts (as defined by $Attempts) to retrieve the Content-Disposition header. + While ($Request -le $Attempts) { + # If SkipSleep is not set, wait for a random time between 3 and 15 seconds before each attempt + if (!($SkipSleep)) { + $SleepTime = Get-Random -Minimum 3 -Maximum 15 + Write-Host -Object "Waiting for $SleepTime seconds." + Start-Sleep -Seconds $SleepTime + } + + if ($Request -ne 1) { Write-Host "" } + Write-Host -Object "Attempt $Request" + + # Perform a HEAD request to get headers only. + # If the HEAD request fails, print a warning. + try { + $HeaderRequest = Invoke-WebRequest -Uri $URL -Method "HEAD" -MaximumRedirection 10 -UseBasicParsing -ErrorAction Stop + } + catch { + Write-Host -Object "[Warning] $($_.Exception.Message)" + Write-Host -Object "[Warning] The header request failed." + } + + # Check if the Content-Disposition header is present. + # If present, parse it to extract the filename. + if (!$HeaderRequest.Headers."Content-Disposition") { + Write-Host -Object "[Warning] The web server did not provide a Content-Disposition header." + } + else { + $Content = [System.Net.Mime.ContentDisposition]::new($HeaderRequest.Headers."Content-Disposition") + $Filename = $Content.FileName + } + + # If a filename was found, break out of the loop. + if ($Filename) { + $Request = $Attempts + } + + $Request++ + } + } + + # If a filename is still not found, throw an error. + if ($Filename) { + $Path = "$Path\$Filename" + } + else { + # Restore the original progress preference setting + $ProgressPreference = $PreviousProgressPreference + + throw [System.IO.FileNotFoundException]::New("Unable to find a suitable filename from the URL.") + } + } + + # If the file already exists at the specified path, restore the progress setting and throw an error. + if ((Test-Path -Path $Path -ErrorAction SilentlyContinue) -and !$Overwrite) { + # Restore the original progress preference setting + $ProgressPreference = $PreviousProgressPreference + + throw [System.IO.IOException]::New("A file already exists at the path '$Path'.") + } + + # Ensure that the destination folder exists, if not, try to create it. + $DestinationFolder = $Path | Split-Path + if (!(Test-Path -Path $DestinationFolder -ErrorAction SilentlyContinue)) { + try { + Write-Host -Object "Attempting to create the folder '$DestinationFolder' as it does not exist." + New-Item -Path $DestinationFolder -ItemType "directory" -ErrorAction Stop | Out-Null + Write-Host -Object "Successfully created the folder." + } + catch { + # Restore the original progress preference setting + $ProgressPreference = $PreviousProgressPreference + + throw $_ + } + } + + Write-Host -Object "Downloading the file..." + + # Initialize the download attempt counter. + $DownloadAttempt = 1 + While ($DownloadAttempt -le $Attempts) { + # If SkipSleep is not set, wait for a random time between 3 and 15 seconds before each attempt + if (!($SkipSleep)) { + $SleepTime = Get-Random -Minimum 3 -Maximum 15 + Write-Host -Object "Waiting for $SleepTime seconds." + Start-Sleep -Seconds $SleepTime + } + + # Provide a visual break between attempts + if ($DownloadAttempt -ne 1) { Write-Host "" } + Write-Host -Object "Download Attempt $DownloadAttempt" + + try { + if ($PSVersionTable.PSVersion.Major -lt 4) { + # For older versions of PowerShell, use WebClient to download the file + $WebClient = New-Object System.Net.WebClient + $WebClient.DownloadFile($URL, $Path) + } + else { + # For PowerShell 4.0 and above, use Invoke-WebRequest with specified arguments + $WebRequestArgs = @{ + Uri = $URL + OutFile = $Path + MaximumRedirection = 10 + UseBasicParsing = $true + } + + Invoke-WebRequest @WebRequestArgs + } + + # Verify if the file was successfully downloaded + $File = Test-Path -Path $Path -ErrorAction SilentlyContinue + } + catch { + # Handle any errors that occur during the download attempt + Write-Host -Object "[Warning] An error has occurred while downloading!" + Write-Host -Object "[Warning] $($_.Exception.Message)" + + # If the file partially downloaded, delete it to avoid corruption + if (Test-Path -Path $Path -ErrorAction SilentlyContinue) { + Remove-Item $Path -Force -Confirm:$false -ErrorAction SilentlyContinue + } + + $File = $False + } + + # If the file was successfully downloaded, exit the loop + if ($File) { + $DownloadAttempt = $Attempts + } + else { + # Warn the user if the download attempt failed + Write-Host -Object "[Warning] File failed to download.`n" + } + + # Increment the attempt counter + $DownloadAttempt++ + } + + # Restore the original progress preference setting + $ProgressPreference = $PreviousProgressPreference + + # Final check: if the file still doesn't exist, report an error and exit + if (!(Test-Path $Path)) { + throw [System.IO.FileNotFoundException]::New("[Error] Failed to download file. Please verify the URL of '$URL'.") + } + else { + # If the download succeeded, return the path to the downloaded file + return $Path + } + } + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # Check if the script is running with elevated privileges (administrator rights) + if (!(Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Inform the user about the URL being checked + Write-Host -Object "Checking https://endoflife.date/windows for the latest Windows build information." + try { + # Invoke a REST call to retrieve JSON data about Windows builds + $EndOfLifeResponse = Invoke-RestMethod -Method Get -Uri $EndOfLifeURL -ContentType "application/json" -MaximumRedirection 10 -UseBasicParsing -ErrorAction Stop + + # Filter the JSON response for Windows 11 builds (cycles that match '11-' and end in 'w') + $Windows11BuildJSON = $EndOfLifeResponse | Where-Object { $_.Cycle -match '^11-' -and $_.Cycle -match "w$" } + + # Throw an error if no Windows 11 builds are found + if (!$Windows11BuildJSON) { + throw "No Windows 11 builds found in the response." + } + } + catch { + # Catch any errors from the REST call or the filtering process + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to retrieve the latest Windows build information from $EndOfLifeURL." + exit 1 + } + + # Inform the user that the response will be parsed + Write-Host -Object "Parsing the response from https://endoflife.date/windows" + + # Create a list to store all relevant Windows 11 build information + $Windows11Builds = New-Object System.Collections.Generic.List[Object] + + $ErrorActionPreference = "Stop" + + # Iterate through each Windows 11 build JSON object + $Windows11BuildJSON | ForEach-Object { + try { + # Extract major, minor, and build numbers from the 'latest' version string + $Major = $_.latest -replace '^(\d+)\.(\d+)\.(\d+)$', '$1' + $Minor = $_.latest -replace '^(\d+)\.(\d+)\.(\d+)$', '$2' + $Build = $_.latest -replace '^(\d+)\.(\d+)\.(\d+)$', '$3' + + # Construct a custom PowerShell object for each build + $WindowBuild = [PSCustomObject]@{ + cycle = $_.cycle + releaseLabel = $_.releaseLabel + releaseDate = Get-Date $_.releaseDate + eol = Get-Date $_.eol + version = $_.latest + major = $Major + minor = $Minor + build = $Build + link = $_.link + lts = $_.lts + support = Get-Date $_.support + } + + # Add the newly created object to the Windows 11 builds collection + $Windows11Builds.Add($WindowBuild) + } + catch { + # Capture any errors in parsing the date or version properties + Write-Host -Object "[Warning] $($_.Exception.Message)" + Write-Host -Object "[Warning] Failed to parse the date for the build $($_.releaseLabel)." + return + } + } + + $ErrorActionPreference = "Continue" + + # If no Windows 11 builds were successfully parsed, show an error and exit + if ($Windows11Builds.Count -lt 1) { + Write-Host -Object "[Error] Failed to parse any of the Windows 11 builds." + exit 1 + } + + # Retrieve the current Windows 11 version from the system's registry + Write-Host -Object "Retrieving the system's current Windows 11 version." + try { + $CurrentVersion = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -ErrorAction Stop | Select-Object -ExpandProperty DisplayVersion -ErrorAction SilentlyContinue + }catch{ + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to retrieve the latest feature update installed." + exit 1 + } + + # Sort the Windows 11 build objects by release date in descending order + $Windows11Builds = $Windows11Builds | Sort-Object releaseDate -Descending + + # Retrieve the first (newest) build from the sorted list + $LatestBuild = $Windows11Builds | Select-Object -First 1 + + # Display information about the latest supported Windows 11 feature update + Write-Host -Object "The latest supported feature update is Windows $(($LatestBuild.releaseLabel -replace '\(W\)').Trim()) and it was released on $($LatestBuild.releaseDate.ToShortDateString())." + Write-Host -Object "The system currently has the feature update Windows 11 $CurrentVersion installed." + + # Compare the running system's build number to the newest build's number + if ([System.Environment]::OSVersion.Version.Build -ge $LatestBuild.build) { + Write-Host -Object "[Error] The system is running the same or a newer feature update than the latest supported version." + exit 1 + } + + try { + # Temporarily set the ErrorActionPreference to "Stop" so that any errors are caught as exceptions. + $ErrorActionPreference = "Stop" + + Write-Host -Object "`nVerifying the feature update compatibility." + + # Retrieve hardware readiness data, convert the JSON results into a PowerShell object. + $Result = Get-HardwareReadiness | Select-Object -Unique | ConvertFrom-Json + Write-Host -Object "Successfully retrieved the compatibility results.`n" + + # Reset the ErrorActionPreference to default ("Continue") so that non-terminating errors don't stop the script. + $ErrorActionPreference = "Continue" + } + catch { + # If any error occurs while fetching hardware readiness, display error messages and exit. + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to retrieve the compatibility results." + exit 1 + } + + # Based on the returnCode property in the JSON result, evaluate the device's compatibility. + switch ($Result.returnCode) { + 0 { + $ResultString = "Capable" + } + 1 { + $ResultString = "[Alert] Not capable" + $Incompatible = $True + } + -2 { + $ResultString = "[Error] Failed to run" + $Incompatible = $True + } + default { + $ResultString = "[Error] Undetermined" + $Incompatible = $True + } + } + + # If there's a more detailed reason for incompatibility, append it to the result string. + if ($Result.returnReason) { + $ResultString = "$ResultString - $($Result.returnReason)" + + # This removes any trailing commas or spaces at the end if they exist. + $ResultString = $ResultString -replace ",\s*$" + } + + Write-Host -Object "Compatibility Test Result: $ResultString" + + # If the system is flagged as incompatible, display an error and exit. + if ($Incompatible) { + Write-Host -Object "[Error] This device is either incompatible with the feature update or its compatibility could not be determined." + exit 1 + } + + # Check if the Windows 11 Upgrade process is already running. + $Windows11UpgradeApp = Get-Process -Name "Windows10UpgraderApp" -ErrorAction SilentlyContinue + + if (!$Windows11UpgradeApp) { + # No upgrade process is detected. + Write-Host -Object "The upgrade is not currently in progress." + } + else { + # If found, display an error with information on the process and exit. + Write-Host -Object "[Error] The Windows 11 upgrade is already in progress via the process below." + Write-Host -Object "`n### Windows 11 Upgrade Process ###" + ($Windows11UpgradeApp | Select-Object @{ Name = 'PID'; Expression = { $_.Id } }, Name, Description, Path | + Format-List PID, Name, Description, Path | Out-String).Trim() | Write-Host + exit 1 + } + + Write-Host -Object "`nDownloading the Windows 11 Installation Assistant executable." + try { + $WindowsInstallAssistant = Invoke-Download -Path $DownloadDestination -URL $InstallAssistantDownloadURL -Overwrite -ErrorAction Stop + Write-Host -Object "Download complete." + } + catch { + # If the download fails, display an error message and exit. + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Unable to download the Windows 11 Installation Assistant at '$InstallAssistantDownloadURL'." + exit 1 + } + + Write-Host -Object "`nVerifying the executable's signature." + try { + # Check the digital signature of the downloaded executable to ensure authenticity. + $InstallationAssistantSignature = Get-AuthenticodeSignature $WindowsInstallAssistant -ErrorAction Stop + } + catch { + # If signature retrieval fails, display an error and exit. + Write-Host -Object "$($_.Exception.Message)" + Write-Host -Object "[Error] Failed to read the executable signature for the file '$WindowsInstallAssistant'." + exit 1 + } + + # If the signature isn't valid, assume the file is corrupted or tampered with, and exit. + if ($InstallationAssistantSignature.Status -ne "Valid") { + Write-Host -Object "[Error] An invalid signature status of '$($InstallationAssistantSignature.Status)' was provided. Perhaps the downloaded file '$WindowsInstallAssistant' was corrupted in transit?" + exit 1 + } + + # Check the signer's certificate subject to confirm it's Microsoft Corporation. + if ($InstallationAssistantSignature.SignerCertificate.Subject -ne "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US") { + Write-Host -Object "[Error] An invalid signature subject of '$($InstallationAssistantSignature.SignerCertificate.Subject)' was provided. 'CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US' was expected." + exit 1 + } + + Write-Host -Object "The signature is valid and appears to be what was expected." + + # Ensure the log folder exists, and if not, create it. + if (!(Test-Path -Path $UpdateLogLocation -ErrorAction SilentlyContinue)) { + Write-Host -Object "`nThe log folder '$UpdateLogLocation' does not currently exist. Attempting to create the folder." + try { + New-Item -Path $UpdateLogLocation -ItemType Directory -Force | Out-Null + Write-Host -Object "Successfully created the log folder." + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to create log folder '$UpdateLogLocation'" + exit 1 + } + } + + # Define the arguments to be passed to the Installation Assistant. + $InstallAssistantArguments = @( + "/QuietInstall" + "/SkipEULA" + "/NoRestartUI" + "/Auto Upgrade" + "/CopyLogs `"$UpdateLogLocation`"" + ) + + # Set up the process invocation parameters, including where to log standard output and errors. + $InstallAssistantProcessArguments = @{ + FilePath = $WindowsInstallAssistant + ArgumentList = $InstallAssistantArguments + RedirectStandardOutput = "$UpdateLogLocation\$(New-Guid).stdout.log" + RedirectStandardError = "$UpdateLogLocation\$(New-Guid).stderr.log" + NoNewWindow = $True + } + + Write-Host -Object "`nInitiating Windows 11 feature upgrade." + Write-Host -Object "[Warning] This may take a few hours to complete. You can view the logs at '$UpdateLogLocation' and '${env:ProgramFiles(x86)}\WindowsInstallationAssistant\Logs' if any failure occurs." + Write-Host -Object "If no failure occurs, these files may be empty." + + try { + # Start the Windows 11 upgrade process silently in the background. + Start-Process @InstallAssistantProcessArguments + } + catch { + # If the process fails to start, display an error message and exit. + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to start the Windows 11 upgrade process using the file '$WindowsInstallAssistant'." + exit 1 + } + + Start-Sleep -Seconds 30 + # Check if the Windows 10 Upgrade process is running. + $Windows11UpgradeApp = Get-Process -Name "Windows10UpgraderApp" -ErrorAction SilentlyContinue + + if (!$Windows11UpgradeApp) { + # No upgrade process is detected. + Write-Host -Object "[Error] Failed to detect the upgrade process." + Write-Host -Object "[Error] Failed to start the Windows 11 upgrade process using the file '$WindowsInstallAssistant'." + exit 1 + } + else { + Write-Host -Object "`n### Windows 11 Upgrade Process ###" + ($Windows11UpgradeApp | Select-Object @{ Name = 'PID'; Expression = { $_.Id } }, Name, Description, Path | + Format-List PID, Name, Description, Path | Out-String).Trim() | Write-Host + } + + exit $ExitCode +} +end { + +} diff --git a/Powershell Scripts/Deploy Wi-Fi Profile.ps1 b/Powershell Scripts/Deploy Wi-Fi Profile.ps1 index fbe4599..6095fae 100644 --- a/Powershell Scripts/Deploy Wi-Fi Profile.ps1 +++ b/Powershell Scripts/Deploy Wi-Fi Profile.ps1 @@ -1,557 +1,550 @@ # Deploy a wifi profile to all users on a given device. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Deploy a wifi profile to all users on a given device. -.DESCRIPTION - Deploy a wifi profile to all users on a given device. -.EXAMPLE - -SSID "cookiemonster" -PreSharedKeyCustomField "WifiPass" - - Retrieving preshared key from secure custom field 'WifiPassword'. - Successfully retrieved preshared key. - Creating XML for Wi-Fi profile 'cookiemonster'. - Saving XML to C:\Windows\Temp\wi-fi.251d970a-299d-48a2-a256-341542983464.xml - Importing Wi-Fi profile 'cookiemonster' from XML. - ExitCode: 0 - Profile 'cookiemonster' is added on interface Wi-Fi. - Removing xml. - -PARAMETER: -SSID "ReplaceMeWithYourWi-FiName" - Specify the Wi-Fi SSID/name. - -PARAMETER: -AuthType "WPA3SAE" - Select either WPA2 authentication or WPA3.. - -PARAMETER: -PreSharedKeyCustomField "ReplaceMeWithASecureCustomField" - Specify the name of a secure custom field that contains the preshared key. - -PARAMETER: -Overwrite - If the profile already exists overwrite it. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$SSID, - [Parameter()] - [String]$AuthType = "WPA2PSK", - [Parameter()] - [String]$PreSharedKeyCustomField, - [Parameter()] - [Switch]$Overwrite = [System.Convert]::ToBoolean($env:overwrite) -) - -begin { - # If script form variables are used replace the command line parameters. - if ($env:ssid -and $env:ssid -notlike "null") { $SSID = $env:ssid } - if ($env:authenticationType -and $env:authenticationType -notlike "null") { $AuthType = $env:authenticationType } - if ($env:nameOfASecureCustomFieldContainingPresharedKey -and $env:nameOfASecureCustomFieldContainingPresharedKey -notlike "null") { $PreSharedKeyCustomField = $env:nameOfASecureCustomFieldContainingPresharedKey } - - # If no Wi-Fi interfaces exist or the wireless service is not running, display an error message indicating that they are required. - try { - $WifiAdapters = Get-NetAdapter -ErrorAction Stop | Where-Object { $_.PhysicalMediaType -match '802\.11' } - if (!$WifiAdapters) { - Write-Host -Object "[Error] No Wi-Fi network interfaces exist on the system." - exit 1 - } - - $WlanService = Get-Service -Name 'wlansvc' -ErrorAction Stop | Where-Object { $_.Status -eq 'Running' } - if (!$WlanService) { - Write-Host -Object "[Error] The service 'wlansvc' is not running. The service 'wlansvc' is required to add the Wi-Fi network." - exit 1 - } - } - catch { - Write-Host -Object "[Error] Unable to verify if a Wi-Fi network interface exists and that the 'wlansvc' service is running." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # If $SSID is provided, trim any leading or trailing whitespace from the SSID - if ($SSID) { - $SSID = $SSID.Trim() - } - - # If $SSID is not provided or is empty after trimming, display an error message indicating the SSID is required - if (!$SSID) { - Write-Host -Object "[Error] The Wi-Fi SSID/name is required to add Wi-Fi profile to the device." - exit 1 - } - - # If $AuthType is provided, trim any leading or trailing whitespace from the authentication type - if ($AuthType) { - $AuthType = $AuthType.Trim() - } - - # If $AuthType is not provided or is empty after trimming, display an error message indicating the authentication type is required - if (!$AuthType) { - Write-Host -Object "[Error] No authentication type given. The authentication type is required." - exit 1 - } - - # If $PreSharedKeyCustomField is provided, trim any leading or trailing whitespace from the preshared key custom field - if ($PreSharedKeyCustomField) { - $PreSharedKeyCustomField = $PreSharedKeyCustomField.Trim() - } - - # If $PreSharedKeyCustomField is not provided or is empty after trimming, display an error message indicating the preshared key custom field is required - if (!$PreSharedKeyCustomField) { - Write-Host -Object "[Error] You must provide the name of a secure custom field that contains the preshared key." - exit 1 - } - - # Measure the length of the SSID and store it in $SSIDCharcterLength - $SSIDCharacterLength = $SSID | Measure-Object -Character | Select-Object -ExpandProperty Characters - # If the SSID length is greater than 32 characters, display an error message indicating the SSID length constraint - if ($SSIDCharacterLength -gt 32) { - Write-Host -Object "[Error] The SSID '$SSID' is greater than 32 characters. SSIDs must be less than or equal to 32 characters." - exit 1 - } - - # Define valid authentication types - $ValidAuthTypes = "WPA2PSK", "WPA3SAE" - # If the provided authentication type is not valid, display an error message indicating the valid authentication types - if ($ValidAuthTypes -notcontains $AuthType) { - Write-Host -Object "[Error] The authentication type '$AuthType' is invalid. The only valid authentication types are 'WPA2PSK' and 'WPA3SAE'." - exit 1 - } - - - function Get-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter()] - [String]$DocumentName - ) - - # Initialize a hashtable for documentation parameters - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define types that need options - $NeedsOptions = "DropDown", "MultiSelect" - - if ($DocumentName) { - # Check for invalid type 'Secure' - if ($Type -Like "Secure") { throw [System.ArgumentOutOfRangeException]::New("$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality") } - - # Retrieve the property value from Ninja Document - Write-Host "Retrieving value from Ninja Document..." - $NinjaPropertyValue = Ninja-Property-Docs-Get -AttributeName $Name @DocumentationParams 2>&1 - - # Retrieve property options if needed - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - # Retrieve the property value directly - $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1 - - # Retrieve property options if needed - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw exceptions if errors occur during retrieval - if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Throw an exception if the property value is empty - if (-not $NinjaPropertyValue) { - throw [System.NullReferenceException]::New("The Custom Field '$Name' is empty!") - } - - # Process the property value based on its type - switch ($Type) { - "Attachment" { - $NinjaPropertyValue | ConvertFrom-Json - } - "Checkbox" { - [System.Convert]::ToBoolean([int]$NinjaPropertyValue) - } - "Date or Date Time" { - $UnixTimeStamp = $NinjaPropertyValue - $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp) - $TimeZone = [TimeZoneInfo]::Local - [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) - } - "Decimal" { - [double]$NinjaPropertyValue - } - "Device Dropdown" { - $NinjaPropertyValue | ConvertFrom-Json - } - "Device MultiSelect" { - $NinjaPropertyValue | ConvertFrom-Json - } - "Dropdown" { - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name - } - "Integer" { - [int]$NinjaPropertyValue - } - "MultiSelect" { - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = ($NinjaPropertyValue -split ',').trim() - - foreach ($Item in $Selection) { - $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name - } - } - "Organization Dropdown" { - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization Location Dropdown" { - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization Location MultiSelect" { - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization MultiSelect" { - $NinjaPropertyValue | ConvertFrom-Json - } - "Time" { - $Seconds = $NinjaPropertyValue - $UTC = ([timespan]::fromseconds($Seconds)).ToString("hh\:mm\:ss") - $TimeZone = [TimeZoneInfo]::Local - $ConvertedTime = [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) - - Get-Date $ConvertedTime -DisplayHint Time - } - default { - $NinjaPropertyValue - } - } - } - - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # If the script is not running with elevated privileges, display an error and exit - if (!(Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - try { - Write-Host -Object "Retrieving preshared key from secure custom field '$PreSharedKeyCustomField'." - - # Attempt to get the custom field value - $PreSharedKey = Get-NinjaProperty -Name $PreSharedKeyCustomField -ErrorAction Stop - - Write-Host -Object "Successfully retrieved preshared key." - } - catch { - # If an error occurs, display the error message and exit - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # If $PreSharedKey is not retrieved or is empty, display an error and exit - if (!$PreSharedKey) { - Write-Host -Object "Failed to retrieve preshared key." - exit 1 - } - else { - # Measure the length of the preshared key and store it in $PreSharedKeyCharacters - $PreSharedKeyCharacters = $PreSharedKey | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # If the preshared key length is less than 8 or greater than 63 characters, display an error and exit - if ($PreSharedKeyCharacters -lt 8 -or $PreSharedKeyCharacters -gt 63) { - Write-Host -Object "[Error] The preshared key needs to be at least 8 characters and less than 64 characters." - exit 1 - } - } - - # Define the paths for standard error and output logs - $StandardErrorPath = "$env:TEMP\wi-fi.prof.$(New-Guid).err.log" - $StandardOutputPath = "$env:TEMP\wi-fi.prof.$(New-Guid).out.log" - - # Define the arguments for the netsh command to show existing Wi-Fi profiles - $ExistingProfilesArguments = @( - "wlan" - "show" - "profiles" - ) - - # Define the arguments for starting the netsh process - $ExistingProfilesProcessArguments = @{ - Wait = $True - PassThru = $True - NoNewWindow = $True - ArgumentList = $ExistingProfilesArguments - RedirectStandardError = $StandardErrorPath - RedirectStandardOutput = $StandardOutputPath - FilePath = "$env:SystemRoot\System32\netsh.exe" - } - - # Attempt to start the netsh process to show existing Wi-Fi profiles - try { - Write-Host -Object "Checking for existing Wi-Fi profiles" - $ExistingProfilesProcess = Start-Process @ExistingProfilesProcessArguments -ErrorAction Stop - } - catch { - # If an error occurs while starting netsh, display an error message and exit - Write-Host -Object "[Error] Unable to check for existing Wi-Fi profiles." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Display the exit code of the netsh process - Write-Host -Object "ExitCode: $($ExistingProfilesProcess.ExitCode)" - - # If the exit code indicates failure, display an error message - if ($ExistingProfilesProcess.ExitCode -ne 0) { - Write-Host -Object "[Error] Exit code does not indicate success. Failed to check for existing Wi-Fi profiles." - $ExitCode = 1 - } - - # If the standard error log file exists, read its content - if (Test-Path -Path $StandardErrorPath -ErrorAction SilentlyContinue) { - $ExistingProfilesErrors = Get-Content -Path $StandardErrorPath -ErrorAction SilentlyContinue - Remove-Item -Path $StandardErrorPath -Force -ErrorAction SilentlyContinue - } - - # If there are any errors in the standard error log, display them - if ($ExistingProfilesErrors) { - Write-Host -Object "[Error] An error has occurred when executing netsh." - - $ExistingProfilesErrors | ForEach-Object { - Write-Host -Object "[Error] $_" - } - - $ExitCode = 1 - } - - # If the standard output log file exists, read and display its content - if (Test-Path -Path $StandardOutputPath -ErrorAction SilentlyContinue) { - $ExistingProfilesOutput = Get-Content -Path $StandardOutputPath -ErrorAction SilentlyContinue - Remove-Item -Path $StandardOutputPath -Force -ErrorAction SilentlyContinue - } - - if($ExistingProfilesOutput){ - # Prepare a CSV list to store the profile data - $CSVData = New-Object System.Collections.Generic.List[string] - $CSVData.Add("ProfileType,ProfileName") - - # Process the output to format it as CSV - $ExistingProfilesOutput | Where-Object { $_ -match ':' -and $_ -notmatch 'Profiles on interface' } | ForEach-Object { - $CSVData.Add( - ($_ -replace "\s+:\s+",",").Trim() - ) - } - - # Convert the CSV data to objects - $ExistingProfiles = $CSVData | ConvertFrom-CSV - - # Check if the specified SSID is already present - $ProfileToOverwrite = $ExistingProfiles | Where-Object { $_.ProfileName -like $SSID } - - # If the profile is found and overwrite is requested, indicate that it will be overwritten - if($ProfileToOverwrite -and $Overwrite){ - Write-Host -Object "Wi-Fi network profile '$SSID' was detected. Overwriting as requested." - } - - # If the profile is found and overwrite is not requested, display an error and list existing profiles - if($ProfileToOverwrite -and !$Overwrite){ - $ExistingProfiles | Format-Table | Out-String | Write-Host - Write-Host -Object "[Error] Wi-Fi network profile '$SSID' is already deployed to this machine. Please select the 'Overwrite' checkbox to overwrite it." - exit 1 - } - } - - Write-Host -Object "Creating XML for Wi-Fi profile '$SSID'." - - # Define the XML template for the Wi-Fi profile - [XML]$ProfileXML = @" - - - - - - - - - - ESS - auto - - - - - AES - false - - - passPhrase - false - - - - - - false - - - -"@ - # Create a namespace manager and add namespaces - $namespaceManager = New-Object System.Xml.XmlNamespaceManager($ProfileXML.NameTable) - $namespaceManager.AddNamespace("ns", "http://www.microsoft.com/networking/WLAN/profile/v1") - $namespaceManager.AddNamespace("m", "http://www.microsoft.com/networking/WLAN/profile/v3") - - # Set the WLAN name in the XML profile - $WLANNameXML = $ProfileXML.SelectSingleNode("/ns:WLANProfile/ns:name", $namespaceManager) - $WLANNameXML.InnerText = $SSID - - # Convert SSID to hexadecimal and set it in the XML profile - $SSIDhex = [System.Text.Encoding]::UTF8.GetBytes($SSID) | ForEach-Object { - [System.String]::Format("{0:X2}", $_) - } - $SSIDHexXML = $ProfileXML.SelectSingleNode("/ns:WLANProfile/ns:SSIDConfig/ns:SSID/ns:hex", $namespaceManager) - $SSIDHexXml.InnerText = $($SSIDhex -join '') - - # Set the SSID name in the XML profile - $SSIDNameXML = $ProfileXML.SelectSingleNode("/ns:WLANProfile/ns:SSIDConfig/ns:SSID/ns:name", $namespaceManager) - $SSIDNameXML.InnerText = $SSID - - # Set the authentication type in the XML profile - $AuthenticationXML = $ProfileXML.SelectSingleNode("/ns:WLANProfile/ns:MSM/ns:security/ns:authEncryption/ns:authentication", $namespaceManager) - $AuthenticationXML.InnerText = $AuthType - - # Set the preshared key in the XML profile - $keyMaterialXML = $ProfileXML.SelectSingleNode("/ns:WLANProfile/ns:MSM/ns:security/ns:sharedKey/ns:keyMaterial", $namespaceManager) - $keyMaterialXML.InnerText = $PreSharedKey - - try { - # Generate a random 32-bit unsigned integer for the randomization seed - $Random = [System.Security.Cryptography.RandomNumberGenerator]::Create() - $SeedBytes = New-Object byte[] 4 - $Random.GetBytes($seedBytes) - $randomizationSeed = [BitConverter]::ToUInt32($seedBytes, 0) - } - catch { - # If an error occurs while creating the randomization seed, display an error message and exit - Write-Host -Object "[Error] Failed to create randomization seed." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Set the randomization seed in the XML profile - $randomizationXML = $ProfileXML.SelectSingleNode("/ns:WLANProfile/m:MacRandomization/m:randomizationSeed", $namespaceManager) - $randomizationXML.InnerText = $randomizationSeed - - # Define the path to save the XML profile - $ProfilePath = "$env:TEMP\wi-fi.$(New-Guid).xml" - Write-Host -Object "Saving XML to $ProfilePath" - $ProfileXML.Save($ProfilePath) - - # Define the arguments for the netsh command - $NetshArguments = @( - "wlan" - "add" - "profile" - "filename=`"$ProfilePath`"" - "user=all" - ) - - # Define the paths for standard error and output logs - $StandardErrorPath = "$env:TEMP\wi-fi.$(New-Guid).err.log" - $StandardOutputPath = "$env:TEMP\wi-fi.$(New-Guid).out.log" - - # Define the arguments for starting the netsh process - $NetShProcessArguments = @{ - Wait = $True - PassThru = $True - NoNewWindow = $True - ArgumentList = $NetshArguments - RedirectStandardError = $StandardErrorPath - RedirectStandardOutput = $StandardOutputPath - FilePath = "$env:SystemRoot\System32\netsh.exe" - } - - # Attempt to start the netsh process to add the Wi-Fi profile - try { - Write-Host -Object "Importing Wi-Fi profile '$SSID' from XML." - $NetshProcess = Start-Process @NetShProcessArguments -ErrorAction Stop - } - catch { - # If an error occurs while starting netsh, display an error message and exit - Write-Host -Object "[Error] Failed to start netsh." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Display the exit code of the netsh process - Write-Host -Object "ExitCode: $($NetshProcess.ExitCode)" - - # If the exit code indicates failure, display an error message - if ($NetshProcess.ExitCode -ne 0) { - Write-Host -Object "[Error] Exit code does not indicate success. Failed to add Wi-Fi profile." - $ExitCode = 1 - } - - # If the standard error log file exists, read its content - if (Test-Path -Path $StandardErrorPath -ErrorAction SilentlyContinue) { - $NetshErrors = Get-Content -Path $StandardErrorPath -ErrorAction SilentlyContinue - Remove-Item -Path $StandardErrorPath -Force -ErrorAction SilentlyContinue - } - - # If there are any errors in the standard error log, display them - if ($NetshErrors) { - Write-Host -Object "[Error] An error has occurred when executing netsh." - - $NetshErrors | ForEach-Object { - Write-Host -Object "[Error] $_" - } - - $ExitCode = 1 - } - - # If the standard output log file exists, read and display its content - if (Test-Path -Path $StandardOutputPath -ErrorAction SilentlyContinue) { - $NetshOutput = Get-Content -Path $StandardOutputPath -ErrorAction SilentlyContinue - Write-Host -Object $NetshOutput - Remove-Item -Path $StandardOutputPath -Force -ErrorAction SilentlyContinue - } - - # Attempt to remove the XML profile and log files - try { - Write-Host -Object "Removing xml." - Remove-Item -Path $ProfilePath -Force - } - catch { - # If an error occurs while removing files, display an error message and set the exit code to 1 - Write-Host -Object "[Error] Failed to remove XML or log files." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Deploy a wifi profile to all users on a given device. +.DESCRIPTION + Deploy a wifi profile to all users on a given device. +.EXAMPLE + -SSID "cookiemonster" -PreSharedKeyCustomField "WifiPass" + + Retrieving preshared key from secure custom field 'WifiPassword'. + Successfully retrieved preshared key. + Creating XML for Wi-Fi profile 'cookiemonster'. + Saving XML to C:\Windows\Temp\wi-fi.251d970a-299d-48a2-a256-341542983464.xml + Importing Wi-Fi profile 'cookiemonster' from XML. + ExitCode: 0 + Profile 'cookiemonster' is added on interface Wi-Fi. + Removing xml. + +PARAMETER: -SSID "ReplaceMeWithYourWi-FiName" + Specify the Wi-Fi SSID/name. + +PARAMETER: -AuthType "WPA3SAE" + Select either WPA2 authentication or WPA3.. + +PARAMETER: -PreSharedKeyCustomField "ReplaceMeWithASecureCustomField" + Specify the name of a secure custom field that contains the preshared key. + +PARAMETER: -Overwrite + If the profile already exists overwrite it. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$SSID, + [Parameter()] + [String]$AuthType = "WPA2PSK", + [Parameter()] + [String]$PreSharedKeyCustomField, + [Parameter()] + [Switch]$Overwrite = [System.Convert]::ToBoolean($env:overwrite) +) + +begin { + # If script form variables are used replace the command line parameters. + if ($env:ssid -and $env:ssid -notlike "null") { $SSID = $env:ssid } + if ($env:authenticationType -and $env:authenticationType -notlike "null") { $AuthType = $env:authenticationType } + if ($env:nameOfASecureCustomFieldContainingPresharedKey -and $env:nameOfASecureCustomFieldContainingPresharedKey -notlike "null") { $PreSharedKeyCustomField = $env:nameOfASecureCustomFieldContainingPresharedKey } + + # If no Wi-Fi interfaces exist or the wireless service is not running, display an error message indicating that they are required. + try { + $WifiAdapters = Get-NetAdapter -ErrorAction Stop | Where-Object { $_.PhysicalMediaType -match '802\.11' } + if (!$WifiAdapters) { + Write-Host -Object "[Error] No Wi-Fi network interfaces exist on the system." + exit 1 + } + + $WlanService = Get-Service -Name 'wlansvc' -ErrorAction Stop | Where-Object { $_.Status -eq 'Running' } + if (!$WlanService) { + Write-Host -Object "[Error] The service 'wlansvc' is not running. The service 'wlansvc' is required to add the Wi-Fi network." + exit 1 + } + } + catch { + Write-Host -Object "[Error] Unable to verify if a Wi-Fi network interface exists and that the 'wlansvc' service is running." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # If $SSID is provided, trim any leading or trailing whitespace from the SSID + if ($SSID) { + $SSID = $SSID.Trim() + } + + # If $SSID is not provided or is empty after trimming, display an error message indicating the SSID is required + if (!$SSID) { + Write-Host -Object "[Error] The Wi-Fi SSID/name is required to add Wi-Fi profile to the device." + exit 1 + } + + # If $AuthType is provided, trim any leading or trailing whitespace from the authentication type + if ($AuthType) { + $AuthType = $AuthType.Trim() + } + + # If $AuthType is not provided or is empty after trimming, display an error message indicating the authentication type is required + if (!$AuthType) { + Write-Host -Object "[Error] No authentication type given. The authentication type is required." + exit 1 + } + + # If $PreSharedKeyCustomField is provided, trim any leading or trailing whitespace from the preshared key custom field + if ($PreSharedKeyCustomField) { + $PreSharedKeyCustomField = $PreSharedKeyCustomField.Trim() + } + + # If $PreSharedKeyCustomField is not provided or is empty after trimming, display an error message indicating the preshared key custom field is required + if (!$PreSharedKeyCustomField) { + Write-Host -Object "[Error] You must provide the name of a secure custom field that contains the preshared key." + exit 1 + } + + # Measure the length of the SSID and store it in $SSIDCharcterLength + $SSIDCharacterLength = $SSID | Measure-Object -Character | Select-Object -ExpandProperty Characters + # If the SSID length is greater than 32 characters, display an error message indicating the SSID length constraint + if ($SSIDCharacterLength -gt 32) { + Write-Host -Object "[Error] The SSID '$SSID' is greater than 32 characters. SSIDs must be less than or equal to 32 characters." + exit 1 + } + + # Define valid authentication types + $ValidAuthTypes = "WPA2PSK", "WPA3SAE" + # If the provided authentication type is not valid, display an error message indicating the valid authentication types + if ($ValidAuthTypes -notcontains $AuthType) { + Write-Host -Object "[Error] The authentication type '$AuthType' is invalid. The only valid authentication types are 'WPA2PSK' and 'WPA3SAE'." + exit 1 + } + + function Get-NinjaProperty { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [String]$Name, + [Parameter()] + [String]$Type, + [Parameter()] + [String]$DocumentName + ) + + # Initialize a hashtable for documentation parameters + $DocumentationParams = @{} + if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } + + # Define types that need options + $NeedsOptions = "DropDown", "MultiSelect" + + if ($DocumentName) { + # Check for invalid type 'Secure' + if ($Type -Like "Secure") { throw [System.ArgumentOutOfRangeException]::New("$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality") } + + # Retrieve the property value from Ninja Document + Write-Host "Retrieving value from Ninja Document..." + + # Retrieve property options if needed + if ($NeedsOptions -contains $Type) { + } + } + else { + # Retrieve the property value directly + + # Retrieve property options if needed + if ($NeedsOptions -contains $Type) { + } + } + + # Throw exceptions if errors occur during retrieval + if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } + if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } + + # Throw an exception if the property value is empty + if (-not $NinjaPropertyValue) { + throw [System.NullReferenceException]::New("The Custom Field '$Name' is empty!") + } + + # Process the property value based on its type + switch ($Type) { + "Attachment" { + $NinjaPropertyValue | ConvertFrom-Json + } + "Checkbox" { + [System.Convert]::ToBoolean([int]$NinjaPropertyValue) + } + "Date or Date Time" { + $UnixTimeStamp = $NinjaPropertyValue + $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp) + $TimeZone = [TimeZoneInfo]::Local + [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) + } + "Decimal" { + [double]$NinjaPropertyValue + } + "Device Dropdown" { + $NinjaPropertyValue | ConvertFrom-Json + } + "Device MultiSelect" { + $NinjaPropertyValue | ConvertFrom-Json + } + "Dropdown" { + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name + } + "Integer" { + [int]$NinjaPropertyValue + } + "MultiSelect" { + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Selection = ($NinjaPropertyValue -split ',').trim() + + foreach ($Item in $Selection) { + $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name + } + } + "Organization Dropdown" { + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization Location Dropdown" { + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization Location MultiSelect" { + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization MultiSelect" { + $NinjaPropertyValue | ConvertFrom-Json + } + "Time" { + $Seconds = $NinjaPropertyValue + $UTC = ([timespan]::fromseconds($Seconds)).ToString("hh\:mm\:ss") + $TimeZone = [TimeZoneInfo]::Local + $ConvertedTime = [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) + + Get-Date $ConvertedTime -DisplayHint Time + } + default { + $NinjaPropertyValue + } + } + } + + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # If the script is not running with elevated privileges, display an error and exit + if (!(Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + try { + Write-Host -Object "Retrieving preshared key from secure custom field '$PreSharedKeyCustomField'." + + # Attempt to get the custom field value + $PreSharedKey = Get-NinjaProperty -Name $PreSharedKeyCustomField -ErrorAction Stop + + Write-Host -Object "Successfully retrieved preshared key." + } + catch { + # If an error occurs, display the error message and exit + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # If $PreSharedKey is not retrieved or is empty, display an error and exit + if (!$PreSharedKey) { + Write-Host -Object "Failed to retrieve preshared key." + exit 1 + } + else { + # Measure the length of the preshared key and store it in $PreSharedKeyCharacters + $PreSharedKeyCharacters = $PreSharedKey | Measure-Object -Character | Select-Object -ExpandProperty Characters + + # If the preshared key length is less than 8 or greater than 63 characters, display an error and exit + if ($PreSharedKeyCharacters -lt 8 -or $PreSharedKeyCharacters -gt 63) { + Write-Host -Object "[Error] The preshared key needs to be at least 8 characters and less than 64 characters." + exit 1 + } + } + + # Define the paths for standard error and output logs + $StandardErrorPath = "$env:TEMP\wi-fi.prof.$(New-Guid).err.log" + $StandardOutputPath = "$env:TEMP\wi-fi.prof.$(New-Guid).out.log" + + # Define the arguments for the netsh command to show existing Wi-Fi profiles + $ExistingProfilesArguments = @( + "wlan" + "show" + "profiles" + ) + + # Define the arguments for starting the netsh process + $ExistingProfilesProcessArguments = @{ + Wait = $True + PassThru = $True + NoNewWindow = $True + ArgumentList = $ExistingProfilesArguments + RedirectStandardError = $StandardErrorPath + RedirectStandardOutput = $StandardOutputPath + FilePath = "$env:SystemRoot\System32\netsh.exe" + } + + # Attempt to start the netsh process to show existing Wi-Fi profiles + try { + Write-Host -Object "Checking for existing Wi-Fi profiles" + $ExistingProfilesProcess = Start-Process @ExistingProfilesProcessArguments -ErrorAction Stop + } + catch { + # If an error occurs while starting netsh, display an error message and exit + Write-Host -Object "[Error] Unable to check for existing Wi-Fi profiles." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Display the exit code of the netsh process + Write-Host -Object "ExitCode: $($ExistingProfilesProcess.ExitCode)" + + # If the exit code indicates failure, display an error message + if ($ExistingProfilesProcess.ExitCode -ne 0) { + Write-Host -Object "[Error] Exit code does not indicate success. Failed to check for existing Wi-Fi profiles." + $ExitCode = 1 + } + + # If the standard error log file exists, read its content + if (Test-Path -Path $StandardErrorPath -ErrorAction SilentlyContinue) { + $ExistingProfilesErrors = Get-Content -Path $StandardErrorPath -ErrorAction SilentlyContinue + Remove-Item -Path $StandardErrorPath -Force -ErrorAction SilentlyContinue + } + + # If there are any errors in the standard error log, display them + if ($ExistingProfilesErrors) { + Write-Host -Object "[Error] An error has occurred when executing netsh." + + $ExistingProfilesErrors | ForEach-Object { + Write-Host -Object "[Error] $_" + } + + $ExitCode = 1 + } + + # If the standard output log file exists, read and display its content + if (Test-Path -Path $StandardOutputPath -ErrorAction SilentlyContinue) { + $ExistingProfilesOutput = Get-Content -Path $StandardOutputPath -ErrorAction SilentlyContinue + Remove-Item -Path $StandardOutputPath -Force -ErrorAction SilentlyContinue + } + + if($ExistingProfilesOutput){ + # Prepare a CSV list to store the profile data + $CSVData = New-Object System.Collections.Generic.List[string] + $CSVData.Add("ProfileType,ProfileName") + + # Process the output to format it as CSV + $ExistingProfilesOutput | Where-Object { $_ -match ':' -and $_ -notmatch 'Profiles on interface' } | ForEach-Object { + $CSVData.Add( + ($_ -replace "\s+:\s+",",").Trim() + ) + } + + # Convert the CSV data to objects + $ExistingProfiles = $CSVData | ConvertFrom-CSV + + # Check if the specified SSID is already present + $ProfileToOverwrite = $ExistingProfiles | Where-Object { $_.ProfileName -like $SSID } + + # If the profile is found and overwrite is requested, indicate that it will be overwritten + if($ProfileToOverwrite -and $Overwrite){ + Write-Host -Object "Wi-Fi network profile '$SSID' was detected. Overwriting as requested." + } + + # If the profile is found and overwrite is not requested, display an error and list existing profiles + if($ProfileToOverwrite -and !$Overwrite){ + $ExistingProfiles | Format-Table | Out-String | Write-Host + Write-Host -Object "[Error] Wi-Fi network profile '$SSID' is already deployed to this machine. Please select the 'Overwrite' checkbox to overwrite it." + exit 1 + } + } + + Write-Host -Object "Creating XML for Wi-Fi profile '$SSID'." + + # Define the XML template for the Wi-Fi profile + [XML]$ProfileXML = @" + + + + + + + + + + ESS + auto + + + + + AES + false + + + passPhrase + false + + + + + + false + + + +"@ + # Create a namespace manager and add namespaces + $namespaceManager = New-Object System.Xml.XmlNamespaceManager($ProfileXML.NameTable) + $namespaceManager.AddNamespace("ns", "http://www.microsoft.com/networking/WLAN/profile/v1") + $namespaceManager.AddNamespace("m", "http://www.microsoft.com/networking/WLAN/profile/v3") + + # Set the WLAN name in the XML profile + $WLANNameXML = $ProfileXML.SelectSingleNode("/ns:WLANProfile/ns:name", $namespaceManager) + $WLANNameXML.InnerText = $SSID + + # Convert SSID to hexadecimal and set it in the XML profile + $SSIDhex = [System.Text.Encoding]::UTF8.GetBytes($SSID) | ForEach-Object { + [System.String]::Format("{0:X2}", $_) + } + $SSIDHexXML = $ProfileXML.SelectSingleNode("/ns:WLANProfile/ns:SSIDConfig/ns:SSID/ns:hex", $namespaceManager) + $SSIDHexXml.InnerText = $($SSIDhex -join '') + + # Set the SSID name in the XML profile + $SSIDNameXML = $ProfileXML.SelectSingleNode("/ns:WLANProfile/ns:SSIDConfig/ns:SSID/ns:name", $namespaceManager) + $SSIDNameXML.InnerText = $SSID + + # Set the authentication type in the XML profile + $AuthenticationXML = $ProfileXML.SelectSingleNode("/ns:WLANProfile/ns:MSM/ns:security/ns:authEncryption/ns:authentication", $namespaceManager) + $AuthenticationXML.InnerText = $AuthType + + # Set the preshared key in the XML profile + $keyMaterialXML = $ProfileXML.SelectSingleNode("/ns:WLANProfile/ns:MSM/ns:security/ns:sharedKey/ns:keyMaterial", $namespaceManager) + $keyMaterialXML.InnerText = $PreSharedKey + + try { + # Generate a random 32-bit unsigned integer for the randomization seed + $Random = [System.Security.Cryptography.RandomNumberGenerator]::Create() + $SeedBytes = New-Object byte[] 4 + $Random.GetBytes($seedBytes) + $randomizationSeed = [BitConverter]::ToUInt32($seedBytes, 0) + } + catch { + # If an error occurs while creating the randomization seed, display an error message and exit + Write-Host -Object "[Error] Failed to create randomization seed." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Set the randomization seed in the XML profile + $randomizationXML = $ProfileXML.SelectSingleNode("/ns:WLANProfile/m:MacRandomization/m:randomizationSeed", $namespaceManager) + $randomizationXML.InnerText = $randomizationSeed + + # Define the path to save the XML profile + $ProfilePath = "$env:TEMP\wi-fi.$(New-Guid).xml" + Write-Host -Object "Saving XML to $ProfilePath" + $ProfileXML.Save($ProfilePath) + + # Define the arguments for the netsh command + $NetshArguments = @( + "wlan" + "add" + "profile" + "filename=`"$ProfilePath`"" + "user=all" + ) + + # Define the paths for standard error and output logs + $StandardErrorPath = "$env:TEMP\wi-fi.$(New-Guid).err.log" + $StandardOutputPath = "$env:TEMP\wi-fi.$(New-Guid).out.log" + + # Define the arguments for starting the netsh process + $NetShProcessArguments = @{ + Wait = $True + PassThru = $True + NoNewWindow = $True + ArgumentList = $NetshArguments + RedirectStandardError = $StandardErrorPath + RedirectStandardOutput = $StandardOutputPath + FilePath = "$env:SystemRoot\System32\netsh.exe" + } + + # Attempt to start the netsh process to add the Wi-Fi profile + try { + Write-Host -Object "Importing Wi-Fi profile '$SSID' from XML." + $NetshProcess = Start-Process @NetShProcessArguments -ErrorAction Stop + } + catch { + # If an error occurs while starting netsh, display an error message and exit + Write-Host -Object "[Error] Failed to start netsh." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Display the exit code of the netsh process + Write-Host -Object "ExitCode: $($NetshProcess.ExitCode)" + + # If the exit code indicates failure, display an error message + if ($NetshProcess.ExitCode -ne 0) { + Write-Host -Object "[Error] Exit code does not indicate success. Failed to add Wi-Fi profile." + $ExitCode = 1 + } + + # If the standard error log file exists, read its content + if (Test-Path -Path $StandardErrorPath -ErrorAction SilentlyContinue) { + $NetshErrors = Get-Content -Path $StandardErrorPath -ErrorAction SilentlyContinue + Remove-Item -Path $StandardErrorPath -Force -ErrorAction SilentlyContinue + } + + # If there are any errors in the standard error log, display them + if ($NetshErrors) { + Write-Host -Object "[Error] An error has occurred when executing netsh." + + $NetshErrors | ForEach-Object { + Write-Host -Object "[Error] $_" + } + + $ExitCode = 1 + } + + # If the standard output log file exists, read and display its content + if (Test-Path -Path $StandardOutputPath -ErrorAction SilentlyContinue) { + $NetshOutput = Get-Content -Path $StandardOutputPath -ErrorAction SilentlyContinue + Write-Host -Object $NetshOutput + Remove-Item -Path $StandardOutputPath -Force -ErrorAction SilentlyContinue + } + + # Attempt to remove the XML profile and log files + try { + Write-Host -Object "Removing xml." + Remove-Item -Path $ProfilePath -Force + } + catch { + # If an error occurs while removing files, display an error message and set the exit code to 1 + Write-Host -Object "[Error] Failed to remove XML or log files." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + + exit $ExitCode +} +end { + +} diff --git a/Powershell Scripts/Detect Installed Antivirus.ps1 b/Powershell Scripts/Detect Installed Antivirus.ps1 index 5c5aa00..9ce3540 100644 --- a/Powershell Scripts/Detect Installed Antivirus.ps1 +++ b/Powershell Scripts/Detect Installed Antivirus.ps1 @@ -1,945 +1,752 @@ # Detect the antivirus software currently installed and set the relevant custom fields accordingly. This script is a best effort and should be treated as such; we recommend verifying any results. Supports 19 antivirus solutions on Windows Server. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Detect the antivirus software currently installed and set the relevant custom fields accordingly. This script is a best effort and should be treated as such; we recommend verifying any results. Supports 19 antivirus solutions on Windows Server. -.DESCRIPTION - Detect the antivirus software currently installed and set the relevant custom fields accordingly. This script is a best effort and should be treated as such; we recommend verifying any results. Supports 19 antivirus solutions on Windows Server. -.EXAMPLE - Attempting to set the custom field 'WYSIWYG'. - Successfully set the custom field 'WYSIWYG'! - - Attempting to set the custom field 'antivirusName'. - Successfully set the custom field 'antivirusName'! - - Attempting to set the custom field 'definitionDateAndStatus'. - Successfully set the custom field 'definitionDateAndStatus'! - - Antivirus Name Status Definition Status Definition Date - -------------- ------ ----------------- --------------- - MalwareBytes Running Up-To-Date 11/13/2024 - Windows Defender Running Up-To-Date 11/13/2024 - -Supported Antivirus Detections: Avast Antivirus, AVG Antivirus Business Edition, Bitdefender Endpoint Security Antimalware, CrowdStrike, Cylance, -Elastic Defend, ESET Security, F-Secure, Huntress, Kaspersky Endpoint Security for Windows, Kaspersky Small Office Security, MalwareBytes, Sentinel -Agent, Sophos Intercept X, Trend Micro Maximum Security, Trend Micro Security Agent, VIPRE Business Agent, Webroot SecureAnywhere, and Windows Defender. - -PARAMETER: -DaysUntilConsideredOutdated "7" - Specify the number of days until the definitions are considered 'out-of-date'. - -PARAMETER: -WYSIWYGCustomField "ReplaceMeWithNameOfWYSIWYGCustomField" - Name of the WYSIWYG custom field to export all results to. - -PARAMETER: -NameCustomField "ReplaceMeWithNameOfTextCustomField" - Name of the text custom field to export the names of the detected antiviruses. - -PARAMETER: -StatusCustomField "ReplaceMeWithNameOfTextCustomField" - Name of the text custom field to export the current antivirus status. - -PARAMETER: -DefinitionDateAndStatusCustomField "ReplaceMeWithNameOfTextCustomField" - Name of the text custom field to export the antivirus definition date and indicate if they are 'Up-To-Date'. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Enhanced detection of installed antivirus software, simplified script options, and streamlined custom field settings. Removed the detection of Carbon Black. -#> - -[CmdletBinding()] -param ( - [Parameter()] - $DaysUntilConsideredOutdated = "7", - [Parameter()] - [String]$WYSIWYGCustomField, - [Parameter()] - [String]$NameCustomField, - [Parameter()] - [String]$StatusCustomField, - [Parameter()] - [String]$DefinitionDateAndStatusCustomField -) - -begin { - # If script form variables are used, replace the command line parameters with their value. - if ($env:daysConsideredOutdated -and $env:daysConsideredOutdated -notlike "null") { $DaysUntilConsideredOutdated = $env:daysConsideredOutdated } - if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $WYSIWYGCustomField = $env:wysiwygCustomFieldName } - if ($env:antivirusNameCustomField -and $env:antivirusNameCustomField -notlike "null") { $NameCustomField = $env:antivirusNameCustomField } - if ($env:statusCustomFieldName -and $env:statusCustomFieldName -notlike "null") { $StatusCustomField = $env:statusCustomFieldName } - if ($env:definitionDateAndStatusCustomField -and $env:definitionDateAndStatusCustomField -notlike "null") { $DefinitionDateAndStatusCustomField = $env:definitionDateAndStatusCustomField } - - # Trim leading and trailing whitespace from each specified variable, if it is set - if($DaysUntilConsideredOutdated) { $DaysUntilConsideredOutdated = $DaysUntilConsideredOutdated.Trim() } - if($WYSIWYGCustomField){ $WYSIWYGCustomField = $WYSIWYGCustomField.Trim() } - if($NameCustomField){ $NameCustomField = $NameCustomField.Trim() } - if($StatusCustomField){ $StatusCustomField = $StatusCustomField.Trim() } - if($DefinitionDateAndStatusCustomField){ $DefinitionDateAndStatusCustomField = $DefinitionDateAndStatusCustomField.Trim() } - - # Check if the $DaysUntilConsideredOutdated variable is not set or null - # Display an error message and exit if it is not set - if (!$DaysUntilConsideredOutdated) { - Write-Host -Object "[Error] Please specify a valid definition age limit that is a positive whole number greater than 0." - exit 1 - } - - # Validate that $DaysUntilConsideredOutdated contains only numeric characters - if ($DaysUntilConsideredOutdated -match "[^0-9]") { - Write-Host -Object "[Error] An invalid definition age limit of '$DaysUntilConsideredOutdated' was specified. Please specify a positive whole number greater than 0." - exit 1 - } - - # Attempt to convert $DaysUntilConsideredOutdated to a long integer - try { - $ErrorActionPreference = "Stop" - $DaysUntilConsideredOutdated = [long]$DaysUntilConsideredOutdated - $ErrorActionPreference = "Continue" - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] An invalid definition age limit of '$DaysUntilConsideredOutdated' was specified. Unable to convert '$DaysUntilConsideredOutdated' to an integer." - exit 1 - } - - # Check if the value of $DaysUntilConsideredOutdated is less than 1 - if ([long]$DaysUntilConsideredOutdated -lt 1) { - Write-Host -Object "[Error] An invalid definition age limit of '$DaysUntilConsideredOutdated' was specified. Please specify a positive whole number greater than 0." - exit 1 - } - - # Define a function to check if the current system is a server - function Test-IsServer { - # Determine the method to retrieve the operating system information based on PowerShell version - try { - $OS = if ($PSVersionTable.PSVersion.Major -lt 3) { - Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop - } - else { - Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop - } - } - catch { - Write-Host -Object "[Error] Unable to validate whether or not this device is a server." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Check if the ProductType is "2", which indicates that the system is a domain controller or is a server - if ($OS.ProductType -eq "2" -or $OS.ProductType -eq "3") { - return $true - } - } - - # Define a function to check if the script is running with elevated privileges - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - # Define a function to find the installation key of a specified program - function Find-InstallKey { - [CmdletBinding()] - param ( - [Parameter(ValueFromPipeline = $True)] - [String]$DisplayName, - [Parameter()] - [Switch]$UninstallString, - [Parameter()] - [String]$UserBaseKey - ) - process { - # Initialize a list to store found installation keys - $InstallList = New-Object System.Collections.Generic.List[Object] - - # If no custom user base key is provided, search in the standard HKLM paths - if (!$UserBaseKey) { - $ErrorActionPreference = "Stop" - # Search in the 32-bit uninstall registry key and add results to the list - try { - $Result = Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } - if ($Result) { $InstallList.Add($Result) } - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to retrieve registry keys at 'HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'." - exit 1 - } - - # Search in the 64-bit uninstall registry key and add results to the list - try { - $Result = Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } - if ($Result) { $InstallList.Add($Result) } - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to retrieve registry keys at 'HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'." - exit 1 - } - - $ErrorActionPreference = "Continue" - } - else { - $ErrorActionPreference = "Stop" - # If a custom user base key is provided, search in the corresponding Wow6432Node path and add results to the list - try { - $Result = Get-ChildItem -Path "$UserBaseKey\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } - if ($Result) { $InstallList.Add($Result) } - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to retrieve registry keys at '$UserBaseKey\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'." - exit 1 - } - - try { - # Search in the custom user base key for the standard uninstall path and add results to the list - $Result = Get-ChildItem -Path "$UserBaseKey\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } - if ($Result) { $InstallList.Add($Result) } - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to retrieve registry keys at '$UserBaseKey\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'." - exit 1 - } - - $ErrorActionPreference = "Continue" - } - - # If the UninstallString switch is set, return only the UninstallString property of the found keys - if ($UninstallString) { - $InstallList | Select-Object -ExpandProperty UninstallString -ErrorAction SilentlyContinue - } - else { - $InstallList - } - } - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName, - [Parameter()] - [Switch]$Piped - ) - # Remove the non-breaking space character - if ($Type -eq "WYSIWYG") { - $Value = $Value -replace ' ', ' ' - } - - # Measure the number of characters in the provided value - $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # Throw an error if the value exceeds the character limit of 200,000 characters - if ($Piped -and $Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - if (!$Piped -and $Characters -ge 45000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") - } - - # Initialize a hashtable for additional documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define a list of valid field types - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - - # Warn the user if the provided type is not valid - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # Define types that require options to be retrieved - $NeedsOptions = "Dropdown" - - # If the property is being set in a document or field and the type needs options, retrieve them - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an error if there was an issue retrieving the property options - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Process the property value based on its type - switch ($Type) { - "Checkbox" { - # Convert the value to a boolean for Checkbox type - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Convert the value to a Unix timestamp for Date or Date Time type - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Convert the dropdown value to its corresponding GUID - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - # Throw an error if the value is not present in the dropdown options - if (!($Selection)) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # For other types, use the value as is - $NinjaValue = $Value - } - } - - # Set the property value in the document if a document name is provided - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - try { - # Otherwise, set the standard property value - if ($Piped) { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - } - catch { - Write-Host -Object "[Error] Failed to set custom field." - throw $_.Exception.Message - } - } - - # Throw an error if setting the property failed - if ($CustomField.Exception) { - throw $CustomField - } - } - - # Define an array of custom objects representing different antivirus software to check - $AVsToCheck = @( - [PSCustomObject]@{ Name = "Avast Antivirus" ; ControlPanelName = "Avast Free Antivirus" ; InstallPath = "$env:ProgramFiles\Avast Software\Avast" ; RelevantProcesses = "AvastSvc", "aswEngSrv" ; RelevantServices = 'avast! Antivirus' } - [PSCustomObject]@{ Name = "AVG Antivirus Business Edition" ; ControlPanelName = "AVG Business Security" ; InstallPath = "$env:ProgramFiles\AVG\Antivirus" ; RelevantProcesses = "AVGSvc", "avgToolsSvc", "bcc", "bccavsvc" ; RelevantServices = "AVG Antivirus", "avgBcc", "AVG Business Console Client Antivirus Service" } - [PSCustomObject]@{ Name = "Bitdefender Endpoint Security Antimalware" ; ControlPanelName = "Bitdefender Endpoint Security Tools", "Bitdefender Agent" ; InstallPath = "$env:ProgramFiles\Bitdefender\Endpoint Security" ; RelevantProcesses = "EPSecurityService", "EPProtectedService" ; RelevantServices = "EPSecurityService", "EPProtectedService" } - [PSCustomObject]@{ Name = "CrowdStrike" ; ControlPanelName = "CrowdStrike Windows Sensor" ; InstallPath = "$env:ProgramFiles\CrowdStrike" ; RelevantProcesses = "CSFalconService" ; RelevantServices = "CSFalconService" } - [PSCustomObject]@{ Name = "Cylance"; ControlPanelName = "Cylance OPTICS", "Cylance Smart Antivirus", "Cylance PROTECT"; InstallPath = "$env:ProgramFiles\Cylance" } - [PSCustomObject]@{ Name = "Elastic Defend" ; ControlPanelName = "Elastic Agent" ; RelevantProcesses = "elastic-agent", "elastic-endpoint" ; RelevantServices = "Elastic Agent", "ElasticEndpoint" } - [PSCustomObject]@{ Name = "ESET Security" ; ControlPanelName = "ESET Security", "ESET Server Security" ; InstallPath = "$env:ProgramFiles\ESET\ESET Security" ; RelevantProcesses = "ekrn" ; RelevantServices = "ekrn", "efwd" } - [PSCustomObject]@{ Name = "F-Secure" ; ControlPanelName = "F-Secure" ; InstallPath = "$env:ProgramFiles\F-Secure" ; RelevantProcesses = "fshoster64" ; RelevantServices = "fshoster" } - [PSCustomObject]@{ Name = "Huntress" ; ControlPanelName = "Huntress Agent" ; InstallPath = "$env:ProgramFiles\Huntress" ; RelevantProcesses = "HuntressAgent", "HuntressRio" ; RelevantServices = "HuntressAgent", "HuntressRio" } - [PSCustomObject]@{ Name = "Kaspersky Endpoint Security for Windows" ; ControlPanelName = "Kaspersky Endpoint Security" ; InstallPath = "${env:ProgramFiles(x86)}\Kaspersky Lab\KES*" ; RelevantProcesses = "avp" ; RelevantServices = "AVP.KES*" } - [PSCustomObject]@{ Name = "Kaspersky Small Office Security" ; ControlPanelName = "Kaspersky Small Office Security" ; InstallPath = "${env:ProgramFiles(x86)}\Kaspersky Lab\Kaspersky Small Office Security*" ; RelevantProcesses = "avp" ; RelevantServices = "AVP*.*" } - [PSCustomObject]@{ Name = "MalwareBytes" ; InstallPath = "$env:ProgramFiles\Malwarebytes\Anti-Malware" ; ControlPanelName = "Malwarebytes" ; RelevantProcesses = "Malwarebytes" ; RelevantServices = "MBAMService" } - [PSCustomObject]@{ Name = "Sentinel Agent" ; ControlPanelName = "Sentinel Agent" ; InstallPath = "$env:ProgramFiles\SentinelOne" ; RelevantProcesses = "SentinelServiceHost", "SentinelStaticEngine", "SentinelStaticEngineScanner" ; RelevantServices = "SentinelAgent", "SentinelHelperService", "SentinelStaticEngine" } - [PSCustomObject]@{ Name = "Sophos Intercept X" ; ControlPanelName = "Sophos Endpoint Agent", "Sophos Endpoint Defense" ; InstallPath = "$env:ProgramFiles\Sophos\Sophos Endpoint Agent", "$env:ProgramFiles\Sophos\Endpoint Defense" ; RelevantProcesses = "SophosFileScanner", "SophosFS" ; RelevantServices = "Sophos Endpoint Defense Service", "Sophos System Protection Service" } - [PSCustomObject]@{ Name = "Trend Micro Maximum Security" ; ControlPanelName = "Trend Micro Maximum Security" ; InstallPath = "$env:ProgramFiles\Trend Micro" ; RelevantProcesses = "coreServiceShell" ; RelevantServices = "Amsp" } - [PSCustomObject]@{ Name = "Trend Micro Security Agent" ; ControlPanelName = "Trend Micro Worry-Free Business Security Agent" ; InstallPath = "${env:ProgramFiles(x86)}\Trend Micro\Security Agent" ; RelevantProcesses = "NTRTScan", "TmListen" ; RelevantServices = "ntrtscan", "TmCCSF" } - [PSCustomObject]@{ Name = "VIPRE Business Agent" ; ControlPanelName = "VIPRE Business Agent" ; InstallPath = "$env:ProgramFiles\VIPRE Business Agent" ; RelevantProcesses = "SBAMSvc" ; RelevantServices = "SBAMSvc" } - [PSCustomObject]@{ Name = "Webroot SecureAnywhere"; DisplayName = "Webroot SecureAnywhere" ; InstallPath = "$env:ProgramFiles\Webroot" ; RelevantProcesses = "WRSA" ; RelevantServices = "WRCoreService", "WRSkyClient", "WRSVC" } - [PSCustomObject]@{ Name = "Windows Defender" ; RelevantProcesses = "MsMpEng" ; RelevantServices = "WinDefend" } - ) - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # Check if the script is running with elevated privileges - if (!(Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Create a new list to store detected antivirus information - $DetectedAVs = New-Object System.Collections.Generic.List[object] - - # Check if the current system is not a server - if (!(Test-IsServer)) { - # Get antivirus products from the Security Center namespace based on PowerShell version - if ($PSVersionTable.PSVersion.Major -lt 3) { - $AVsInSecurityCenter = Get-WmiObject -Namespace root/SecurityCenter2 -Class AntivirusProduct - } - else { - $AVsInSecurityCenter = Get-CimInstance -Namespace root/SecurityCenter2 -Class AntivirusProduct - } - - # Iterate over each antivirus product found - $AVsInSecurityCenter | ForEach-Object { - $AVsToSkip = "Kaspersky Small Office Security", "AVG Antivirus" - if ($AVsToSkip -contains $_.displayName -or $AVsToCheck.Name -contains $_.displayName) { - return - } - - # Define enumerations with flag attributes for product states, signature statuses, product owners, and product flags - [Flags()] enum ProductState { - Off = 0x0000 - On = 0x1000 - Snoozed = 0x2000 - Expired = 0x3000 - CustomState = 0x4000 - } - - [Flags()] enum SignatureStatus { - UpToDate = 0x00 - OutOfDate = 0x10 - } - - [Flags()] enum ProductOwner { - NonMs = 0x000 - Windows = 0x100 - } - - [Flags()] enum ProductFlags { - SignatureStatus = 0x00F0 - ProductOwner = 0x0F00 - ProductState = 0xF000 - } - - # Get the current product state - [UInt32]$CurrentState = $_.ProductState - - try { - # Decode the product state and signature status by masking the relevant bits and converting - $ProductState = [ProductState]($CurrentState -band [ProductFlags]::ProductState) - $SignatureStatus = [SignatureStatus]($CurrentState -band [ProductFlags]::SignatureStatus) - } - catch { - Write-Host "[Error] Translating the product state for '$($_.DisplayName)' with a product state of '$CurrentState'." - } - - # Determine the running status based on the product state - $RunningStatus = switch ($ProductState) { - "On" { "Running" } - default { "Not Running" } - } - - # Determine if the definitions are up to date based on the hexadecimal value - $UpToDateWMI = switch ($SignatureStatus) { - "UpToDate" { $True } - default { $False } - } - - # Get the date of the last definition update - $DefinitionsDate = Get-Date $_.timestamp -ErrorAction SilentlyContinue - - # Determine if the antivirus definitions are up to date based on the date and WMI status - $UpToDate = if ($DefinitionsDate -and $DefinitionsDate -gt (Get-Date).AddDays(-$DaysUntilConsideredOutdated) -and ($UpToDateWMI -like "True")) { - "Up To Date" - } - else { - "Outdated" - } - - # Add the antivirus information to the detected list as a custom object - if ($Installed) { - $DetectedAVs.Add( - [PSCustomObject]@{ - "Antivirus Name" = $_.DisplayName - Status = $RunningStatus - "Definition Status" = $UpToDate - "Definition Date" = if ($DefinitionsDate) { "$($DefinitionsDate.ToShortDateString())" } - } - ) - } - } - } - - # Iterate through each antivirus in the $AVsToCheck array - foreach ($AntiVirus in $AVsToCheck) { - $InstallationKey = $null - $InstallPathExists = $null - $RunningServices = $null - $RunningProcesses = $null - $DefinitionStatus = $null - $DefinitionDate = $null - $installed = $Null - - # Find installation key based on ControlPanelName if available - if ($AntiVirus.ControlPanelName) { - $InstallationKey = $AntiVirus.ControlPanelName | ForEach-Object { Find-InstallKey -DisplayName $_ } - } - - # Check if the install path exists for the antivirus - if ($AntiVirus.InstallPath) { - $InstallPathExists = $AntiVirus.InstallPath | ForEach-Object { - if (Test-Path -Path $_ -ErrorAction SilentlyContinue) { - $InstallPathInfo = (Get-Item -Path $_ -ErrorAction SilentlyContinue) - - if (!$InstallPathInfo.PSIsContainer) { - $True - } - else { - if ((Get-ChildItem -Path $_ | Measure-Object).Count -gt 0) { - $True - } - } - } - } - } - - # Check if relevant services are running - if ($AntiVirus.RelevantServices) { - $RunningServices = $AntiVirus.RelevantServices | ForEach-Object { - if (!(Get-Service -Name $_ -ErrorAction SilentlyContinue | Where-Object { $_.Status -eq "Running" })) { - "Not Running" - } - } - - if (!$RunningServices) { - $RunningServices = "Running" - } - } - - # Check if relevant processes are running - if ($AntiVirus.RelevantProcesses) { - $RunningProcesses = $AntiVirus.RelevantProcesses | ForEach-Object { - if (!(Get-Process -Name $_ -ErrorAction SilentlyContinue)) { - "Not Running" - } - } - - if (!$RunningProcesses) { - $RunningProcesses = "Running" - } - } - - # Check specific antivirus products for their definition status and date - switch ($AntiVirus.Name) { - "Avast Antivirus" { - if (Test-Path -Path "$env:ProgramFiles\Avast Software\Avast\defs\aswdefs.ini" -ErrorAction SilentlyContinue) { - $DefinitionFile = Get-Content -Path "$env:ProgramFiles\Avast Software\Avast\defs\aswdefs.ini" -ErrorAction SilentlyContinue - $DefinitionFileDate = $DefinitionFile -replace '[^0-9]' | Where-Object { $_ } - - if ($DefinitionFileDate) { - $DefinitionDate = [datetime]::parseexact($DefinitionFileDate, 'yyMMddHH', $null) - $DefinitionDate = [System.TimeZoneInfo]::ConvertTimeFromUtc($DefinitionDate, [System.TimeZoneInfo]::Local) - } - } - } - "AVG Antivirus Business Edition" { - if (Test-Path -Path "$env:ProgramFiles\AVG\Antivirus\defs\aswdefs.ini" -ErrorAction SilentlyContinue) { - $DefinitionFile = Get-Content -Path "$env:ProgramFiles\AVG\Antivirus\defs\aswdefs.ini" -ErrorAction SilentlyContinue - $DefinitionFileDate = $DefinitionFile -replace '[^0-9]' | Where-Object { $_ } - - if ($DefinitionFileDate) { - $DefinitionDate = [datetime]::parseexact($DefinitionFileDate, 'yyMMddHH', $null) - $DefinitionDate = [System.TimeZoneInfo]::ConvertTimeFromUtc($DefinitionDate, [System.TimeZoneInfo]::Local) - } - } - } - "Bitdefender Endpoint Security Antimalware" { - if (Test-Path -Path "$env:ProgramFiles\Bitdefender\Bitdefender Endpoint Security\update_statistics.xml" -ErrorAction SilentlyContinue) { - $UpdateStatistics = New-Object -TypeName XML - $UpdateStatistics.Load("$env:ProgramFiles\Bitdefender\Bitdefender Endpoint Security\update_statistics.xml") - } - - if ($UpdateStatistics) { - [datetime]$UnixStart = '1970-01-01 00:00:00' - $BitDefenderUpdateDate = $UnixStart.AddSeconds($UpdateStatistics.UpdateStatistics.Antivirus.Update.succtime) - - if ($BitDefenderUpdateDate) { - $DefinitionDate = Get-Date ($BitDefenderUpdateDate.ToLocalTime()) - } - } - } - "Crowdstrike" { - $DefinitionStatus = "Not Applicable" - } - "Elastic Defend" { - $DefinitionStatus = "Not Applicable" - } - "ESET Security" { - if (Test-Path -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\ESET\ESET Security\CurrentVersion\Info" -ErrorAction SilentlyContinue) { - $ScannerVersion = Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\ESET\ESET Security\CurrentVersion\Info" -Name "ScannerVersion" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty ScannerVersion -ErrorAction SilentlyContinue - } - - if ($ScannerVersion) { - $ScannerDateString = $ScannerVersion -replace '^.*\(' -replace '\)' - if ($ScannerDateString) { - $DefinitionDate = [datetime]::parseexact($ScannerDateString, 'yyyyMMdd', $null) - } - } - } - "F-Secure" { - if (Test-Path -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\F-Secure\Ultralight\updates" -ErrorAction SilentlyContinue) { - $FSecureEngines = Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\F-Secure\Ultralight\updates" -ErrorAction SilentlyContinue - - $LatestFSecureEngines = New-Object System.Collections.Generic.List[String] - $FSecureEngines | ForEach-Object { - $FSecureEngineVersions = Get-ChildItem -Path "Registry::$($_.Name)" -ErrorAction SilentlyContinue - - $LatestEngine = $FsecureEngineVersions | Sort-Object Name -Descending | Select-Object -First 1 - $LatestEngine = ($LatestEngine | Select-Object -ExpandProperty Name) | ForEach-Object { $_ -replace "^.*\\" } - - if ($LatestEngine) { - $LatestFSecureEngines.Add($LatestEngine) - } - } - - $LatestFSecureEngine = $LatestFSecureEngines | Sort-Object -Descending | Select-Object -First 1 - - if ($LatestFSecureEngine) { - [datetime]$UnixStart = '1970-01-01 00:00:00' - $FSecureUpdateDate = $UnixStart.AddSeconds($LatestFSecureEngine) - $DefinitionDate = Get-Date ($FSecureUpdateDate.ToLocalTime()) - } - } - } - "Huntress" { - $DefinitionStatus = "Not Applicable" - } - "Kaspersky Endpoint Security for Windows" { - $KESConsole = Get-ChildItem "${env:ProgramFiles(x86)}\Kaspersky Lab\KES.*\kescli.exe" -File -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName -First 1 -ErrorAction SilentlyContinue - - if ($KESConsole) { - $KESprocess = Start-Process -FilePath $KESConsole -ArgumentList "--opswat", "GetDefinitionState" -RedirectStandardOutput "$env:TEMP\KESdata.txt" -NoNewWindow -Wait -PassThru - - if ($KESprocess.ExitCode -ne 0) { - Write-Host "Exit Code: $($KESprocess.ExitCode)" - Write-Host "[Error] Exit Code does not indicate success!" - } - - $KESdata = Get-Content -Path "$env:TEMP\KESdata.txt" - } - - if ($KESdata -match '\d+/\d+/\d{4}') { - $DefinitionDate = [datetime]::parseexact($KESdata, 'M/d/yyyy H:m:s', $null) - $DefinitionDate = $DefinitionDate.ToLocalTime() - - Remove-Item -Path "$env:TEMP\KESdata.txt" - } - else { - if (!$InstallPathExists -or !$InstallationKey -and ($RunningProcesses -or $RunningServices)) { - $RunningProcesses = $null - $RunningServices = $null - } - } - } - "Kaspersky Small Office Security" { - $AVPConsole = Get-ChildItem "${env:ProgramFiles(x86)}\Kaspersky Lab\Kaspersky Small Office Security *\avp.com" -File -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName -First 1 -ErrorAction SilentlyContinue - - if ($AVPConsole) { - $KSOprocess = Start-Process -FilePath $AVPConsole -ArgumentList "STATISTICS", "Updater" -RedirectStandardOutput "$env:TEMP\KSOdata.txt" -NoNewWindow -Wait -PassThru - - if ($KSOprocess.ExitCode -ne 0) { - Write-Host "Exit Code: $($KSOprocess.ExitCode)" - Write-Host "[Error] Exit Code does not indicate success!" - } - - $KSOdata = Get-Content -Path "$env:TEMP\KSOdata.txt" - } - - if ($KSOdata) { - $KsoTimeFinish = $KSOdata | Where-Object { $_ -match "Time Finish" } - if ($KsoTimeFinish) { - $KsoTimeFinish = ($KsoTimeFinish -replace "Time Finish:").Trim() - - $UpdateSucceeded = $KSOdata | Where-Object { $_ -match "Update succeeded" } - - if ($UpdateSucceeded) { - $DefinitionDate = [datetime]::parseexact($KsoTimeFinish, 'yyyy-MM-dd HH:mm:ss', $null) - } - } - - Remove-Item -Path "$env:TEMP\KSOdata.txt" - } - else { - if (!$InstallPathExists -or !$InstallationKey -and ($RunningProcesses -or $RunningServices)) { - $RunningProcesses = $null - $RunningServices = $null - } - } - } - "MalwareBytes" { - if (Test-Path -Path "$env:ProgramData\Malwarebytes\MBAMService\config\UpdateControllerConfig.json" -ErrorAction SilentlyContinue) { - $UpdateConfigFile = Get-Content -Path "$env:ProgramData\Malwarebytes\MBAMService\config\UpdateControllerConfig.json" | Select-Object -Skip 1 | ConvertFrom-Json - } - - if ($UpdateConfigFile) { - [datetime]$UnixStart = '1970-01-01 00:00:00' - $MalwarebytesUpdateDate = $UnixStart.AddSeconds($UpdateConfigFile.db_pub_date) - - if ($MalwarebytesUpdateDate) { - $DefinitionDate = Get-Date ($MalwarebytesUpdateDate.ToLocalTime()) - } - } - } - "Sentinel Agent" { - $DefinitionStatus = "Not Applicable" - } - "Sophos Intercept X" { - if (Test-Path -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Sophos\EndpointDefense\Acknowledged" -ErrorAction SilentlyContinue) { - $SophosVirusData = Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Sophos\EndpointDefense\Acknowledged" -Name "VirusDataVersion" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty VirusDataVersion -ErrorAction SilentlyContinue - } - - if ($SophosVirusData) { - $DefinitionDate = [datetime]::parseexact($SophosVirusData, 'yyyyMMddHH', $null) - } - } - "Trend Micro Maximum Security" { - if (Test-Path -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\TrendMicro\AMSP" -ErrorAction SilentlyContinue) { - $MaximumSecurityInstallTime = Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\TrendMicro\AMSP" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty InstallTime -ErrorAction SilentlyContinue - - } - - if ($MaximumSecurityInstallTime) { - [datetime]$UnixStart = '1970-01-01 00:00:00' - $MaximumSecurityUnixDate = $UnixStart.AddSeconds($MaximumSecurityInstallTime) - - if ($MaximumSecurityUnixDate) { - $DefinitionDate = Get-Date ($MaximumSecurityUnixDate.ToLocalTime()) - } - } - } - "Trend Micro Security Agent" { - if (Test-Path -Path "${env:ProgramFiles(x86)}\Trend Micro\Security Agent\ofcscan.ini" -ErrorAction SilentlyContinue) { - $ofcscanFile = Get-Content -Path "${env:ProgramFiles(x86)}\Trend Micro\Security Agent\ofcscan.ini" - - } - - if ($ofcscanFile) { - $ofcscanFile | ForEach-Object { - if ($_ -match '^\[(.+)\]') { - $Section = $_ - } - - if ($Section -match 'INI_PROGRAM_VERSION_SECTION' -and $_ -match '^Pattern_Last_Update') { - $ofcscanDateString = ($_ -replace 'Pattern_Last_Update=' -replace '').Trim() - $ofcscanDateString = $ofcscanDateString -replace "[^0-9]" - } - } - - if ($ofcscanDateString) { - $DefinitionDate = [datetime]::parseexact($ofcscanDateString, 'yyyyMMddHHmmss', $null) - } - } - } - "VIPRE Business Agent" { - if (Test-Path -Path "$env:ProgramFiles\VIPRE Business Agent\Definitions\DefVer.txt") { - $DefVerFile = Get-Content -Path "$env:ProgramFiles\VIPRE Business Agent\Definitions\DefVer.txt" - } - - if ($DefVerFile) { - $VipreDateString = ($DefVerFile -replace '.*,').Trim() - - if ($VipreDateString) { - $DefinitionDate = Get-Date $VipreDateString - } - } - } - "Webroot SecureAnywhere" { - $DefinitionStatus = "Not Applicable" - } - "Windows Defender" { - if ((Get-Command Get-MpComputerStatus -ErrorAction SilentlyContinue).Count -ne 0) { - try { - Get-MpComputerStatus -ErrorAction Stop | Out-Null - $Installed = $True - } - catch { - $Installed = $False - } - } - - if ($Installed) { - if (!((Get-MpComputerStatus -ErrorAction SilentlyContinue).RealTimeProtectionEnabled)) { - $RunningValue = "Not Running" - } - else { - $RunningValue = "Running" - } - - if ((Get-MpComputerStatus -ErrorAction SilentlyContinue).AntivirusSignatureLastUpdated) { - $DefinitionDate = (Get-MpComputerStatus).AntivirusSignatureLastUpdated - } - else { - $DefinitionStatus = "Outdated" - } - } - } - } - - # Determine if the antivirus is installed based on various check - if (!$Installed -and ($InstallPathExists -or $InstallationKey -or $RunningProcesses -eq "Running" -or $RunningServices -eq "Running")) { - - $RequireInstallationKey = "Trend Micro Maximum Security", "Trend Micro Security Agent", "Trend Micro Apex One Antivirus" - if ($RequireInstallationKey -contains $AntiVirus.Name -and !$InstallationKey) { - $Installed = $False - } - else { - $Installed = $True - } - } - elseif (!$Installed) { - $Installed = $False - } - - # Determine the running status based on process and service status - if ($RunningProcesses -eq "Running" -and $RunningServices -eq "Running") { - $RunningValue = "Running" - } - elseif (!$RunningProcesses -or !$RunningServices) { - $RunningValue = "Unable to Determine" - } - else { - $RunningValue = "Not Running" - } - - # Determine the definition status based on the date - if ($DefinitionDate) { - if ((Get-Date).AddDays(-$DaysUntilConsideredOutdated) -ge $DefinitionDate) { - $DefinitionStatus = "Outdated" - } - elseif ((Get-Date).AddDays(-$DaysUntilConsideredOutdated) -lt $DefinitionDate) { - $DefinitionStatus = "Up To Date" - } - else { - $DefinitionStatus = "Unable to Determine" - } - } - elseif (!$DefinitionStatus) { - $DefinitionStatus = "Unable to Determine" - } - - # Add the antivirus information to the list - if ($Installed) { - $DetectedAVs.Add( - [PSCustomObject]@{ - "Antivirus Name" = $AntiVirus.Name - Status = $RunningValue - "Definition Status" = $DefinitionStatus - "Definition Date" = if ($DefinitionDate) { "$($DefinitionDate.ToShortDateString())" } - } - ) - } - } - - # Check if no antivirus products were detected - if ($DetectedAVs.Count -eq 0) { - Write-Host "[Alert] No antivirus was detected." - } - - if ($WYSIWYGCustomField) { - try { - Write-Host "`nAttempting to set Custom Field '$WYSIWYGCustomField'." - - if ($DetectedAVs.Count -eq 0) { - $AntivirusTable = "

[Alert] No antivirus was detected.

" - } - else { - $AntivirusTable = $DetectedAVs | ConvertTo-Html -Fragment - $AntivirusTable = $AntivirusTable -replace "", "" -replace "", "" - $AntivirusTable = $AntivirusTable -replace "", "
" - } - - # Set the custom field with the combined antivirus names - Set-NinjaProperty -Name $WYSIWYGCustomField -Value $AntivirusTable - Write-Host "Successfully set Custom Field '$WYSIWYGCustomField'!" - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - - if ($NameCustomField) { - # Attempt to set the custom field for the antivirus names - try { - Write-Host "`nAttempting to set Custom Field '$NameCustomField'." - - # Combine all antivirus names into a single string, separated by commas - $NameString = $DetectedAVs."Antivirus Name" -join ", " - - if ($DetectedAVs.Count -eq 0) { - $NameString = "[Alert] No antivirus was detected." - } - - # Set the custom field with the combined antivirus names - Set-NinjaProperty -Name $NameCustomField -Value $NameString - Write-Host "Successfully set Custom Field '$NameCustomField'!" - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - - if ($StatusCustomField) { - # Attempt to set the custom field for the antivirus running status - try { - Write-Host "`nAttempting to set Custom Field '$StatusCustomField'." - - # Combine all antivirus running statuses into a single string, separated by commas - $StatusString = $DetectedAVs.Status -join ", " - - if ($DetectedAVs.Count -eq 0) { - $StatusString = "[Alert] No antivirus was detected." - } - - # Set the custom field with the combined running statuses - Set-NinjaProperty -Name $StatusCustomField -Value $StatusString - Write-Host "Successfully set Custom Field '$StatusCustomField'!" - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - - if ($DefinitionDateAndStatusCustomField) { - # Attempt to set the custom field for the antivirus definition status - try { - Write-Host "`nAttempting to set Custom Field '$DefinitionDateAndStatusCustomField'." - - # Combine all antivirus running statuses into a single string, separated by commas - $DefinitionStrings = $DetectedAVs | ForEach-Object { - if ($_."Definition Date") { - "$($_."Definition Date") | $($_."Definition Status")" - } - else { - $_."Definition Status" - } - } - - $CustomFieldValue = $DefinitionStrings -join ", " - - if ($DetectedAVs.Count -eq 0) { - $CustomFieldValue = "[Alert] No antivirus was detected." - } - - # Set the custom field with the combined running statuses - Set-NinjaProperty -Name $DefinitionDateAndStatusCustomField -Value $CustomFieldValue - Write-Host "Successfully set Custom Field '$DefinitionDateAndStatusCustomField'!" - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - - if ($DetectedAVs.Count -gt 0) { - Write-Host -Object "" - ($DetectedAVs | Sort-Object "Antivirus Name" | Format-Table -AutoSize | Out-String).Trim() | Write-Host - } - - exit $ExitCode -}end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Detect the antivirus software currently installed and set the relevant custom fields accordingly. This script is a best effort and should be treated as such; we recommend verifying any results. Supports 19 antivirus solutions on Windows Server. +.DESCRIPTION + Detect the antivirus software currently installed and set the relevant custom fields accordingly. This script is a best effort and should be treated as such; we recommend verifying any results. Supports 19 antivirus solutions on Windows Server. +.EXAMPLE + Attempting to set the custom field 'WYSIWYG'. + Successfully set the custom field 'WYSIWYG'! + + Attempting to set the custom field 'antivirusName'. + Successfully set the custom field 'antivirusName'! + + Attempting to set the custom field 'definitionDateAndStatus'. + Successfully set the custom field 'definitionDateAndStatus'! + + Antivirus Name Status Definition Status Definition Date + -------------- ------ ----------------- --------------- + MalwareBytes Running Up-To-Date 11/13/2024 + Windows Defender Running Up-To-Date 11/13/2024 + +Supported Antivirus Detections: Avast Antivirus, AVG Antivirus Business Edition, Bitdefender Endpoint Security Antimalware, CrowdStrike, Cylance, +Elastic Defend, ESET Security, F-Secure, Huntress, Kaspersky Endpoint Security for Windows, Kaspersky Small Office Security, MalwareBytes, Sentinel +Agent, Sophos Intercept X, Trend Micro Maximum Security, Trend Micro Security Agent, VIPRE Business Agent, Webroot SecureAnywhere, and Windows Defender. + +PARAMETER: -DaysUntilConsideredOutdated "7" + Specify the number of days until the definitions are considered 'out-of-date'. + +PARAMETER: -WYSIWYGCustomField "ReplaceMeWithNameOfWYSIWYGCustomField" + Name of the WYSIWYG custom field to export all results to. + +PARAMETER: -NameCustomField "ReplaceMeWithNameOfTextCustomField" + Name of the text custom field to export the names of the detected antiviruses. + +PARAMETER: -StatusCustomField "ReplaceMeWithNameOfTextCustomField" + Name of the text custom field to export the current antivirus status. + +PARAMETER: -DefinitionDateAndStatusCustomField "ReplaceMeWithNameOfTextCustomField" + Name of the text custom field to export the antivirus definition date and indicate if they are 'Up-To-Date'. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Enhanced detection of installed antivirus software, simplified script options, and streamlined custom field settings. Removed the detection of Carbon Black. +#> + +[CmdletBinding()] +param ( + [Parameter()] + $DaysUntilConsideredOutdated = "7", + [Parameter()] + [String]$WYSIWYGCustomField, + [Parameter()] + [String]$NameCustomField, + [Parameter()] + [String]$StatusCustomField, + [Parameter()] + [String]$DefinitionDateAndStatusCustomField +) + +begin { + # If script form variables are used, replace the command line parameters with their value. + if ($env:daysConsideredOutdated -and $env:daysConsideredOutdated -notlike "null") { $DaysUntilConsideredOutdated = $env:daysConsideredOutdated } + if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $WYSIWYGCustomField = $env:wysiwygCustomFieldName } + if ($env:antivirusNameCustomField -and $env:antivirusNameCustomField -notlike "null") { $NameCustomField = $env:antivirusNameCustomField } + if ($env:statusCustomFieldName -and $env:statusCustomFieldName -notlike "null") { $StatusCustomField = $env:statusCustomFieldName } + if ($env:definitionDateAndStatusCustomField -and $env:definitionDateAndStatusCustomField -notlike "null") { $DefinitionDateAndStatusCustomField = $env:definitionDateAndStatusCustomField } + + # Trim leading and trailing whitespace from each specified variable, if it is set + if($DaysUntilConsideredOutdated) { $DaysUntilConsideredOutdated = $DaysUntilConsideredOutdated.Trim() } + if($WYSIWYGCustomField){ $WYSIWYGCustomField = $WYSIWYGCustomField.Trim() } + if($NameCustomField){ $NameCustomField = $NameCustomField.Trim() } + if($StatusCustomField){ $StatusCustomField = $StatusCustomField.Trim() } + if($DefinitionDateAndStatusCustomField){ $DefinitionDateAndStatusCustomField = $DefinitionDateAndStatusCustomField.Trim() } + + # Check if the $DaysUntilConsideredOutdated variable is not set or null + # Display an error message and exit if it is not set + if (!$DaysUntilConsideredOutdated) { + Write-Host -Object "[Error] Please specify a valid definition age limit that is a positive whole number greater than 0." + exit 1 + } + + # Validate that $DaysUntilConsideredOutdated contains only numeric characters + if ($DaysUntilConsideredOutdated -match "[^0-9]") { + Write-Host -Object "[Error] An invalid definition age limit of '$DaysUntilConsideredOutdated' was specified. Please specify a positive whole number greater than 0." + exit 1 + } + + # Attempt to convert $DaysUntilConsideredOutdated to a long integer + try { + $ErrorActionPreference = "Stop" + $DaysUntilConsideredOutdated = [long]$DaysUntilConsideredOutdated + $ErrorActionPreference = "Continue" + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] An invalid definition age limit of '$DaysUntilConsideredOutdated' was specified. Unable to convert '$DaysUntilConsideredOutdated' to an integer." + exit 1 + } + + # Check if the value of $DaysUntilConsideredOutdated is less than 1 + if ([long]$DaysUntilConsideredOutdated -lt 1) { + Write-Host -Object "[Error] An invalid definition age limit of '$DaysUntilConsideredOutdated' was specified. Please specify a positive whole number greater than 0." + exit 1 + } + + # Define a function to check if the current system is a server + function Test-IsServer { + # Determine the method to retrieve the operating system information based on PowerShell version + try { + $OS = if ($PSVersionTable.PSVersion.Major -lt 3) { + Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop + } + else { + Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop + } + } + catch { + Write-Host -Object "[Error] Unable to validate whether or not this device is a server." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Check if the ProductType is "2", which indicates that the system is a domain controller or is a server + if ($OS.ProductType -eq "2" -or $OS.ProductType -eq "3") { + return $true + } + } + + # Define a function to check if the script is running with elevated privileges + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + # Define a function to find the installation key of a specified program + function Find-InstallKey { + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $True)] + [String]$DisplayName, + [Parameter()] + [Switch]$UninstallString, + [Parameter()] + [String]$UserBaseKey + ) + process { + # Initialize a list to store found installation keys + $InstallList = New-Object System.Collections.Generic.List[Object] + + # If no custom user base key is provided, search in the standard HKLM paths + if (!$UserBaseKey) { + $ErrorActionPreference = "Stop" + # Search in the 32-bit uninstall registry key and add results to the list + try { + $Result = Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } + if ($Result) { $InstallList.Add($Result) } + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to retrieve registry keys at 'HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'." + exit 1 + } + + # Search in the 64-bit uninstall registry key and add results to the list + try { + $Result = Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } + if ($Result) { $InstallList.Add($Result) } + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to retrieve registry keys at 'HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'." + exit 1 + } + + $ErrorActionPreference = "Continue" + } + else { + $ErrorActionPreference = "Stop" + # If a custom user base key is provided, search in the corresponding Wow6432Node path and add results to the list + try { + $Result = Get-ChildItem -Path "$UserBaseKey\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } + if ($Result) { $InstallList.Add($Result) } + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to retrieve registry keys at '$UserBaseKey\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'." + exit 1 + } + + try { + # Search in the custom user base key for the standard uninstall path and add results to the list + $Result = Get-ChildItem -Path "$UserBaseKey\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } + if ($Result) { $InstallList.Add($Result) } + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to retrieve registry keys at '$UserBaseKey\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'." + exit 1 + } + + $ErrorActionPreference = "Continue" + } + + # If the UninstallString switch is set, return only the UninstallString property of the found keys + if ($UninstallString) { + $InstallList | Select-Object -ExpandProperty UninstallString -ErrorAction SilentlyContinue + } + else { + $InstallList + } + } + } + + # Define an array of custom objects representing different antivirus software to check + $AVsToCheck = @( + [PSCustomObject]@{ Name = "Avast Antivirus" ; ControlPanelName = "Avast Free Antivirus" ; InstallPath = "$env:ProgramFiles\Avast Software\Avast" ; RelevantProcesses = "AvastSvc", "aswEngSrv" ; RelevantServices = 'avast! Antivirus' } + [PSCustomObject]@{ Name = "AVG Antivirus Business Edition" ; ControlPanelName = "AVG Business Security" ; InstallPath = "$env:ProgramFiles\AVG\Antivirus" ; RelevantProcesses = "AVGSvc", "avgToolsSvc", "bcc", "bccavsvc" ; RelevantServices = "AVG Antivirus", "avgBcc", "AVG Business Console Client Antivirus Service" } + [PSCustomObject]@{ Name = "Bitdefender Endpoint Security Antimalware" ; ControlPanelName = "Bitdefender Endpoint Security Tools", "Bitdefender Agent" ; InstallPath = "$env:ProgramFiles\Bitdefender\Endpoint Security" ; RelevantProcesses = "EPSecurityService", "EPProtectedService" ; RelevantServices = "EPSecurityService", "EPProtectedService" } + [PSCustomObject]@{ Name = "CrowdStrike" ; ControlPanelName = "CrowdStrike Windows Sensor" ; InstallPath = "$env:ProgramFiles\CrowdStrike" ; RelevantProcesses = "CSFalconService" ; RelevantServices = "CSFalconService" } + [PSCustomObject]@{ Name = "Cylance"; ControlPanelName = "Cylance OPTICS", "Cylance Smart Antivirus", "Cylance PROTECT"; InstallPath = "$env:ProgramFiles\Cylance" } + [PSCustomObject]@{ Name = "Elastic Defend" ; ControlPanelName = "Elastic Agent" ; RelevantProcesses = "elastic-agent", "elastic-endpoint" ; RelevantServices = "Elastic Agent", "ElasticEndpoint" } + [PSCustomObject]@{ Name = "ESET Security" ; ControlPanelName = "ESET Security", "ESET Server Security" ; InstallPath = "$env:ProgramFiles\ESET\ESET Security" ; RelevantProcesses = "ekrn" ; RelevantServices = "ekrn", "efwd" } + [PSCustomObject]@{ Name = "F-Secure" ; ControlPanelName = "F-Secure" ; InstallPath = "$env:ProgramFiles\F-Secure" ; RelevantProcesses = "fshoster64" ; RelevantServices = "fshoster" } + [PSCustomObject]@{ Name = "Huntress" ; ControlPanelName = "Huntress Agent" ; InstallPath = "$env:ProgramFiles\Huntress" ; RelevantProcesses = "HuntressAgent", "HuntressRio" ; RelevantServices = "HuntressAgent", "HuntressRio" } + [PSCustomObject]@{ Name = "Kaspersky Endpoint Security for Windows" ; ControlPanelName = "Kaspersky Endpoint Security" ; InstallPath = "${env:ProgramFiles(x86)}\Kaspersky Lab\KES*" ; RelevantProcesses = "avp" ; RelevantServices = "AVP.KES*" } + [PSCustomObject]@{ Name = "Kaspersky Small Office Security" ; ControlPanelName = "Kaspersky Small Office Security" ; InstallPath = "${env:ProgramFiles(x86)}\Kaspersky Lab\Kaspersky Small Office Security*" ; RelevantProcesses = "avp" ; RelevantServices = "AVP*.*" } + [PSCustomObject]@{ Name = "MalwareBytes" ; InstallPath = "$env:ProgramFiles\Malwarebytes\Anti-Malware" ; ControlPanelName = "Malwarebytes" ; RelevantProcesses = "Malwarebytes" ; RelevantServices = "MBAMService" } + [PSCustomObject]@{ Name = "Sentinel Agent" ; ControlPanelName = "Sentinel Agent" ; InstallPath = "$env:ProgramFiles\SentinelOne" ; RelevantProcesses = "SentinelServiceHost", "SentinelStaticEngine", "SentinelStaticEngineScanner" ; RelevantServices = "SentinelAgent", "SentinelHelperService", "SentinelStaticEngine" } + [PSCustomObject]@{ Name = "Sophos Intercept X" ; ControlPanelName = "Sophos Endpoint Agent", "Sophos Endpoint Defense" ; InstallPath = "$env:ProgramFiles\Sophos\Sophos Endpoint Agent", "$env:ProgramFiles\Sophos\Endpoint Defense" ; RelevantProcesses = "SophosFileScanner", "SophosFS" ; RelevantServices = "Sophos Endpoint Defense Service", "Sophos System Protection Service" } + [PSCustomObject]@{ Name = "Trend Micro Maximum Security" ; ControlPanelName = "Trend Micro Maximum Security" ; InstallPath = "$env:ProgramFiles\Trend Micro" ; RelevantProcesses = "coreServiceShell" ; RelevantServices = "Amsp" } + [PSCustomObject]@{ Name = "Trend Micro Security Agent" ; ControlPanelName = "Trend Micro Worry-Free Business Security Agent" ; InstallPath = "${env:ProgramFiles(x86)}\Trend Micro\Security Agent" ; RelevantProcesses = "NTRTScan", "TmListen" ; RelevantServices = "ntrtscan", "TmCCSF" } + [PSCustomObject]@{ Name = "VIPRE Business Agent" ; ControlPanelName = "VIPRE Business Agent" ; InstallPath = "$env:ProgramFiles\VIPRE Business Agent" ; RelevantProcesses = "SBAMSvc" ; RelevantServices = "SBAMSvc" } + [PSCustomObject]@{ Name = "Webroot SecureAnywhere"; DisplayName = "Webroot SecureAnywhere" ; InstallPath = "$env:ProgramFiles\Webroot" ; RelevantProcesses = "WRSA" ; RelevantServices = "WRCoreService", "WRSkyClient", "WRSVC" } + [PSCustomObject]@{ Name = "Windows Defender" ; RelevantProcesses = "MsMpEng" ; RelevantServices = "WinDefend" } + ) + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # Check if the script is running with elevated privileges + if (!(Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Create a new list to store detected antivirus information + $DetectedAVs = New-Object System.Collections.Generic.List[object] + + # Check if the current system is not a server + if (!(Test-IsServer)) { + # Get antivirus products from the Security Center namespace based on PowerShell version + if ($PSVersionTable.PSVersion.Major -lt 3) { + $AVsInSecurityCenter = Get-WmiObject -Namespace root/SecurityCenter2 -Class AntivirusProduct + } + else { + $AVsInSecurityCenter = Get-CimInstance -Namespace root/SecurityCenter2 -Class AntivirusProduct + } + + # Iterate over each antivirus product found + $AVsInSecurityCenter | ForEach-Object { + $AVsToSkip = "Kaspersky Small Office Security", "AVG Antivirus" + if ($AVsToSkip -contains $_.displayName -or $AVsToCheck.Name -contains $_.displayName) { + return + } + + # Define enumerations with flag attributes for product states, signature statuses, product owners, and product flags + [Flags()] enum ProductState { + Off = 0x0000 + On = 0x1000 + Snoozed = 0x2000 + Expired = 0x3000 + CustomState = 0x4000 + } + + [Flags()] enum SignatureStatus { + UpToDate = 0x00 + OutOfDate = 0x10 + } + + [Flags()] enum ProductOwner { + NonMs = 0x000 + Windows = 0x100 + } + + [Flags()] enum ProductFlags { + SignatureStatus = 0x00F0 + ProductOwner = 0x0F00 + ProductState = 0xF000 + } + + # Get the current product state + [UInt32]$CurrentState = $_.ProductState + + try { + # Decode the product state and signature status by masking the relevant bits and converting + $ProductState = [ProductState]($CurrentState -band [ProductFlags]::ProductState) + $SignatureStatus = [SignatureStatus]($CurrentState -band [ProductFlags]::SignatureStatus) + } + catch { + Write-Host "[Error] Translating the product state for '$($_.DisplayName)' with a product state of '$CurrentState'." + } + + # Determine the running status based on the product state + $RunningStatus = switch ($ProductState) { + "On" { "Running" } + default { "Not Running" } + } + + # Determine if the definitions are up to date based on the hexadecimal value + $UpToDateWMI = switch ($SignatureStatus) { + "UpToDate" { $True } + default { $False } + } + + # Get the date of the last definition update + $DefinitionsDate = Get-Date $_.timestamp -ErrorAction SilentlyContinue + + # Determine if the antivirus definitions are up to date based on the date and WMI status + $UpToDate = if ($DefinitionsDate -and $DefinitionsDate -gt (Get-Date).AddDays(-$DaysUntilConsideredOutdated) -and ($UpToDateWMI -like "True")) { + "Up To Date" + } + else { + "Outdated" + } + + # Add the antivirus information to the detected list as a custom object + if ($Installed) { + $DetectedAVs.Add( + [PSCustomObject]@{ + "Antivirus Name" = $_.DisplayName + Status = $RunningStatus + "Definition Status" = $UpToDate + "Definition Date" = if ($DefinitionsDate) { "$($DefinitionsDate.ToShortDateString())" } + } + ) + } + } + } + + # Iterate through each antivirus in the $AVsToCheck array + foreach ($AntiVirus in $AVsToCheck) { + $InstallationKey = $null + $InstallPathExists = $null + $RunningServices = $null + $RunningProcesses = $null + $DefinitionStatus = $null + $DefinitionDate = $null + $installed = $Null + + # Find installation key based on ControlPanelName if available + if ($AntiVirus.ControlPanelName) { + $InstallationKey = $AntiVirus.ControlPanelName | ForEach-Object { Find-InstallKey -DisplayName $_ } + } + + # Check if the install path exists for the antivirus + if ($AntiVirus.InstallPath) { + $InstallPathExists = $AntiVirus.InstallPath | ForEach-Object { + if (Test-Path -Path $_ -ErrorAction SilentlyContinue) { + $InstallPathInfo = (Get-Item -Path $_ -ErrorAction SilentlyContinue) + + if (!$InstallPathInfo.PSIsContainer) { + $True + } + else { + if ((Get-ChildItem -Path $_ | Measure-Object).Count -gt 0) { + $True + } + } + } + } + } + + # Check if relevant services are running + if ($AntiVirus.RelevantServices) { + $RunningServices = $AntiVirus.RelevantServices | ForEach-Object { + if (!(Get-Service -Name $_ -ErrorAction SilentlyContinue | Where-Object { $_.Status -eq "Running" })) { + "Not Running" + } + } + + if (!$RunningServices) { + $RunningServices = "Running" + } + } + + # Check if relevant processes are running + if ($AntiVirus.RelevantProcesses) { + $RunningProcesses = $AntiVirus.RelevantProcesses | ForEach-Object { + if (!(Get-Process -Name $_ -ErrorAction SilentlyContinue)) { + "Not Running" + } + } + + if (!$RunningProcesses) { + $RunningProcesses = "Running" + } + } + + # Check specific antivirus products for their definition status and date + switch ($AntiVirus.Name) { + "Avast Antivirus" { + if (Test-Path -Path "$env:ProgramFiles\Avast Software\Avast\defs\aswdefs.ini" -ErrorAction SilentlyContinue) { + $DefinitionFile = Get-Content -Path "$env:ProgramFiles\Avast Software\Avast\defs\aswdefs.ini" -ErrorAction SilentlyContinue + $DefinitionFileDate = $DefinitionFile -replace '[^0-9]' | Where-Object { $_ } + + if ($DefinitionFileDate) { + $DefinitionDate = [datetime]::parseexact($DefinitionFileDate, 'yyMMddHH', $null) + $DefinitionDate = [System.TimeZoneInfo]::ConvertTimeFromUtc($DefinitionDate, [System.TimeZoneInfo]::Local) + } + } + } + "AVG Antivirus Business Edition" { + if (Test-Path -Path "$env:ProgramFiles\AVG\Antivirus\defs\aswdefs.ini" -ErrorAction SilentlyContinue) { + $DefinitionFile = Get-Content -Path "$env:ProgramFiles\AVG\Antivirus\defs\aswdefs.ini" -ErrorAction SilentlyContinue + $DefinitionFileDate = $DefinitionFile -replace '[^0-9]' | Where-Object { $_ } + + if ($DefinitionFileDate) { + $DefinitionDate = [datetime]::parseexact($DefinitionFileDate, 'yyMMddHH', $null) + $DefinitionDate = [System.TimeZoneInfo]::ConvertTimeFromUtc($DefinitionDate, [System.TimeZoneInfo]::Local) + } + } + } + "Bitdefender Endpoint Security Antimalware" { + if (Test-Path -Path "$env:ProgramFiles\Bitdefender\Bitdefender Endpoint Security\update_statistics.xml" -ErrorAction SilentlyContinue) { + $UpdateStatistics = New-Object -TypeName XML + $UpdateStatistics.Load("$env:ProgramFiles\Bitdefender\Bitdefender Endpoint Security\update_statistics.xml") + } + + if ($UpdateStatistics) { + [datetime]$UnixStart = '1970-01-01 00:00:00' + $BitDefenderUpdateDate = $UnixStart.AddSeconds($UpdateStatistics.UpdateStatistics.Antivirus.Update.succtime) + + if ($BitDefenderUpdateDate) { + $DefinitionDate = Get-Date ($BitDefenderUpdateDate.ToLocalTime()) + } + } + } + "Crowdstrike" { + $DefinitionStatus = "Not Applicable" + } + "Elastic Defend" { + $DefinitionStatus = "Not Applicable" + } + "ESET Security" { + if (Test-Path -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\ESET\ESET Security\CurrentVersion\Info" -ErrorAction SilentlyContinue) { + $ScannerVersion = Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\ESET\ESET Security\CurrentVersion\Info" -Name "ScannerVersion" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty ScannerVersion -ErrorAction SilentlyContinue + } + + if ($ScannerVersion) { + $ScannerDateString = $ScannerVersion -replace '^.*\(' -replace '\)' + if ($ScannerDateString) { + $DefinitionDate = [datetime]::parseexact($ScannerDateString, 'yyyyMMdd', $null) + } + } + } + "F-Secure" { + if (Test-Path -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\F-Secure\Ultralight\updates" -ErrorAction SilentlyContinue) { + $FSecureEngines = Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\F-Secure\Ultralight\updates" -ErrorAction SilentlyContinue + + $LatestFSecureEngines = New-Object System.Collections.Generic.List[String] + $FSecureEngines | ForEach-Object { + $FSecureEngineVersions = Get-ChildItem -Path "Registry::$($_.Name)" -ErrorAction SilentlyContinue + + $LatestEngine = $FsecureEngineVersions | Sort-Object Name -Descending | Select-Object -First 1 + $LatestEngine = ($LatestEngine | Select-Object -ExpandProperty Name) | ForEach-Object { $_ -replace "^.*\\" } + + if ($LatestEngine) { + $LatestFSecureEngines.Add($LatestEngine) + } + } + + $LatestFSecureEngine = $LatestFSecureEngines | Sort-Object -Descending | Select-Object -First 1 + + if ($LatestFSecureEngine) { + [datetime]$UnixStart = '1970-01-01 00:00:00' + $FSecureUpdateDate = $UnixStart.AddSeconds($LatestFSecureEngine) + $DefinitionDate = Get-Date ($FSecureUpdateDate.ToLocalTime()) + } + } + } + "Huntress" { + $DefinitionStatus = "Not Applicable" + } + "Kaspersky Endpoint Security for Windows" { + $KESConsole = Get-ChildItem "${env:ProgramFiles(x86)}\Kaspersky Lab\KES.*\kescli.exe" -File -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName -First 1 -ErrorAction SilentlyContinue + + if ($KESConsole) { + $KESprocess = Start-Process -FilePath $KESConsole -ArgumentList "--opswat", "GetDefinitionState" -RedirectStandardOutput "$env:TEMP\KESdata.txt" -NoNewWindow -Wait -PassThru + + if ($KESprocess.ExitCode -ne 0) { + Write-Host "Exit Code: $($KESprocess.ExitCode)" + Write-Host "[Error] Exit Code does not indicate success!" + } + + $KESdata = Get-Content -Path "$env:TEMP\KESdata.txt" + } + + if ($KESdata -match '\d+/\d+/\d{4}') { + $DefinitionDate = [datetime]::parseexact($KESdata, 'M/d/yyyy H:m:s', $null) + $DefinitionDate = $DefinitionDate.ToLocalTime() + + Remove-Item -Path "$env:TEMP\KESdata.txt" + } + else { + if (!$InstallPathExists -or !$InstallationKey -and ($RunningProcesses -or $RunningServices)) { + $RunningProcesses = $null + $RunningServices = $null + } + } + } + "Kaspersky Small Office Security" { + $AVPConsole = Get-ChildItem "${env:ProgramFiles(x86)}\Kaspersky Lab\Kaspersky Small Office Security *\avp.com" -File -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName -First 1 -ErrorAction SilentlyContinue + + if ($AVPConsole) { + $KSOprocess = Start-Process -FilePath $AVPConsole -ArgumentList "STATISTICS", "Updater" -RedirectStandardOutput "$env:TEMP\KSOdata.txt" -NoNewWindow -Wait -PassThru + + if ($KSOprocess.ExitCode -ne 0) { + Write-Host "Exit Code: $($KSOprocess.ExitCode)" + Write-Host "[Error] Exit Code does not indicate success!" + } + + $KSOdata = Get-Content -Path "$env:TEMP\KSOdata.txt" + } + + if ($KSOdata) { + $KsoTimeFinish = $KSOdata | Where-Object { $_ -match "Time Finish" } + if ($KsoTimeFinish) { + $KsoTimeFinish = ($KsoTimeFinish -replace "Time Finish:").Trim() + + $UpdateSucceeded = $KSOdata | Where-Object { $_ -match "Update succeeded" } + + if ($UpdateSucceeded) { + $DefinitionDate = [datetime]::parseexact($KsoTimeFinish, 'yyyy-MM-dd HH:mm:ss', $null) + } + } + + Remove-Item -Path "$env:TEMP\KSOdata.txt" + } + else { + if (!$InstallPathExists -or !$InstallationKey -and ($RunningProcesses -or $RunningServices)) { + $RunningProcesses = $null + $RunningServices = $null + } + } + } + "MalwareBytes" { + if (Test-Path -Path "$env:ProgramData\Malwarebytes\MBAMService\config\UpdateControllerConfig.json" -ErrorAction SilentlyContinue) { + $UpdateConfigFile = Get-Content -Path "$env:ProgramData\Malwarebytes\MBAMService\config\UpdateControllerConfig.json" | Select-Object -Skip 1 | ConvertFrom-Json + } + + if ($UpdateConfigFile) { + [datetime]$UnixStart = '1970-01-01 00:00:00' + $MalwarebytesUpdateDate = $UnixStart.AddSeconds($UpdateConfigFile.db_pub_date) + + if ($MalwarebytesUpdateDate) { + $DefinitionDate = Get-Date ($MalwarebytesUpdateDate.ToLocalTime()) + } + } + } + "Sentinel Agent" { + $DefinitionStatus = "Not Applicable" + } + "Sophos Intercept X" { + if (Test-Path -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Sophos\EndpointDefense\Acknowledged" -ErrorAction SilentlyContinue) { + $SophosVirusData = Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Sophos\EndpointDefense\Acknowledged" -Name "VirusDataVersion" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty VirusDataVersion -ErrorAction SilentlyContinue + } + + if ($SophosVirusData) { + $DefinitionDate = [datetime]::parseexact($SophosVirusData, 'yyyyMMddHH', $null) + } + } + "Trend Micro Maximum Security" { + if (Test-Path -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\TrendMicro\AMSP" -ErrorAction SilentlyContinue) { + $MaximumSecurityInstallTime = Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\TrendMicro\AMSP" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty InstallTime -ErrorAction SilentlyContinue + + } + + if ($MaximumSecurityInstallTime) { + [datetime]$UnixStart = '1970-01-01 00:00:00' + $MaximumSecurityUnixDate = $UnixStart.AddSeconds($MaximumSecurityInstallTime) + + if ($MaximumSecurityUnixDate) { + $DefinitionDate = Get-Date ($MaximumSecurityUnixDate.ToLocalTime()) + } + } + } + "Trend Micro Security Agent" { + if (Test-Path -Path "${env:ProgramFiles(x86)}\Trend Micro\Security Agent\ofcscan.ini" -ErrorAction SilentlyContinue) { + $ofcscanFile = Get-Content -Path "${env:ProgramFiles(x86)}\Trend Micro\Security Agent\ofcscan.ini" + + } + + if ($ofcscanFile) { + $ofcscanFile | ForEach-Object { + if ($_ -match '^\[(.+)\]') { + $Section = $_ + } + + if ($Section -match 'INI_PROGRAM_VERSION_SECTION' -and $_ -match '^Pattern_Last_Update') { + $ofcscanDateString = ($_ -replace 'Pattern_Last_Update=' -replace '').Trim() + $ofcscanDateString = $ofcscanDateString -replace "[^0-9]" + } + } + + if ($ofcscanDateString) { + $DefinitionDate = [datetime]::parseexact($ofcscanDateString, 'yyyyMMddHHmmss', $null) + } + } + } + "VIPRE Business Agent" { + if (Test-Path -Path "$env:ProgramFiles\VIPRE Business Agent\Definitions\DefVer.txt") { + $DefVerFile = Get-Content -Path "$env:ProgramFiles\VIPRE Business Agent\Definitions\DefVer.txt" + } + + if ($DefVerFile) { + $VipreDateString = ($DefVerFile -replace '.*,').Trim() + + if ($VipreDateString) { + $DefinitionDate = Get-Date $VipreDateString + } + } + } + "Webroot SecureAnywhere" { + $DefinitionStatus = "Not Applicable" + } + "Windows Defender" { + if ((Get-Command Get-MpComputerStatus -ErrorAction SilentlyContinue).Count -ne 0) { + try { + Get-MpComputerStatus -ErrorAction Stop | Out-Null + $Installed = $True + } + catch { + $Installed = $False + } + } + + if ($Installed) { + if (!((Get-MpComputerStatus -ErrorAction SilentlyContinue).RealTimeProtectionEnabled)) { + $RunningValue = "Not Running" + } + else { + $RunningValue = "Running" + } + + if ((Get-MpComputerStatus -ErrorAction SilentlyContinue).AntivirusSignatureLastUpdated) { + $DefinitionDate = (Get-MpComputerStatus).AntivirusSignatureLastUpdated + } + else { + $DefinitionStatus = "Outdated" + } + } + } + } + + # Determine if the antivirus is installed based on various check + if (!$Installed -and ($InstallPathExists -or $InstallationKey -or $RunningProcesses -eq "Running" -or $RunningServices -eq "Running")) { + + $RequireInstallationKey = "Trend Micro Maximum Security", "Trend Micro Security Agent", "Trend Micro Apex One Antivirus" + if ($RequireInstallationKey -contains $AntiVirus.Name -and !$InstallationKey) { + $Installed = $False + } + else { + $Installed = $True + } + } + elseif (!$Installed) { + $Installed = $False + } + + # Determine the running status based on process and service status + if ($RunningProcesses -eq "Running" -and $RunningServices -eq "Running") { + $RunningValue = "Running" + } + elseif (!$RunningProcesses -or !$RunningServices) { + $RunningValue = "Unable to Determine" + } + else { + $RunningValue = "Not Running" + } + + # Determine the definition status based on the date + if ($DefinitionDate) { + if ((Get-Date).AddDays(-$DaysUntilConsideredOutdated) -ge $DefinitionDate) { + $DefinitionStatus = "Outdated" + } + elseif ((Get-Date).AddDays(-$DaysUntilConsideredOutdated) -lt $DefinitionDate) { + $DefinitionStatus = "Up To Date" + } + else { + $DefinitionStatus = "Unable to Determine" + } + } + elseif (!$DefinitionStatus) { + $DefinitionStatus = "Unable to Determine" + } + + # Add the antivirus information to the list + if ($Installed) { + $DetectedAVs.Add( + [PSCustomObject]@{ + "Antivirus Name" = $AntiVirus.Name + Status = $RunningValue + "Definition Status" = $DefinitionStatus + "Definition Date" = if ($DefinitionDate) { "$($DefinitionDate.ToShortDateString())" } + } + ) + } + } + + # Check if no antivirus products were detected + if ($DetectedAVs.Count -eq 0) { + Write-Host "[Alert] No antivirus was detected." + } + + if ($WYSIWYGCustomField) { + Write-Host "" + Write-Host "Note: Custom field '$WYSIWYGCustomField' was specified but NinjaOne integration has been removed." + Write-Host "All output has been displayed above." + } + + if ($NameCustomField) { + Write-Host "" + Write-Host "Note: Custom field '$NameCustomField' was specified but NinjaOne integration has been removed." + Write-Host "All output has been displayed above." + } + + if ($StatusCustomField) { + Write-Host "" + Write-Host "Note: Custom field '$StatusCustomField' was specified but NinjaOne integration has been removed." + Write-Host "All output has been displayed above." + } + + if ($DefinitionDateAndStatusCustomField) { + Write-Host "" + Write-Host "Note: Custom field '$DefinitionDateAndStatusCustomField' was specified but NinjaOne integration has been removed." + Write-Host "All output has been displayed above." + } + + if ($DetectedAVs.Count -gt 0) { + Write-Host -Object "" + ($DetectedAVs | Sort-Object "Antivirus Name" | Format-Table -AutoSize | Out-String).Trim() | Write-Host + } + + exit $ExitCode +}end { + +} diff --git a/Powershell Scripts/Device Uptime Percentage Monitor.ps1 b/Powershell Scripts/Device Uptime Percentage Monitor.ps1 index 8c9b4f4..a7c74e6 100644 --- a/Powershell Scripts/Device Uptime Percentage Monitor.ps1 +++ b/Powershell Scripts/Device Uptime Percentage Monitor.ps1 @@ -1,514 +1,403 @@ # Get the Uptime percentage of a Windows device and show a list of boot events. -#Requires -Version 4.0 - -<# -.SYNOPSIS - Get the Uptime percentage of a Windows device and show a list of boot events. -.DESCRIPTION - Get the Uptime percentage of a Windows device and show a list of boot events. - - Unexpected Shutdowns can skew results slightly. - - Duration in results is in days. -.Example - (No Parameters) - - WARNING: No time frame specified. Checking uptime percentage for the last 30 days - Creating uptime entries based on event logs... - Uptime entries created! - WARNING: Estimating unexpected shutdown times. This will not be 100% accurate. - - Oldest uptime record: 06/29/2023 22:24:01 - - Filtering uptime records to your time frame... - Calculating uptime during time frame... - - ### Time Frame ### - Start Date: 12/26/2023 19:39:38 - End Date: 01/25/2024 19:39:38 - - ### Statistics ### - Percentage Online: 3.93% - Total Time Frame: 30d - Total Uptime: 1d 4h 19m 14s - - ### Uptime Entries ### - - BootType BootTime ShutdownTime Duration - -------- -------- ------------ -------- - Current Boot 1/24/2024 3:20:23 PM 1d 4h 19m 14s -.EXAMPLE - -Days 30 - - WARNING: No time frame specified. Checking uptime percentage for the last 30 days - Creating uptime entries based on event logs... - Uptime entries created! - WARNING: Estimating unexpected shutdown times. This will not be 100% accurate. - - Oldest uptime record: 06/29/2023 22:24:01 - - Filtering uptime records to your time frame... - Calculating uptime during time frame... - - ### Time Frame ### - Start Date: 12/26/2023 19:39:38 - End Date: 01/25/2024 19:39:38 - - ### Statistics ### - Percentage Online: 3.93% - Total Time Frame: 30d - Total Uptime: 1d 4h 19m 14s - - ### Uptime Entries ### - - BootType BootTime ShutdownTime Duration - -------- -------- ------------ -------- - Current Boot 1/24/2024 3:20:23 PM 1d 4h 19m 14s -.EXAMPLE - -StartDay "2023-07-01T00:00:00.000-07:00" -EndDay "2023-07-31T00:00:00.000-07:00" - - Creating uptime entries based on event logs... - Uptime entries created! - WARNING: Estimating unexpected shutdown times. This will not be 100% accurate. - - Oldest uptime record: 06/29/2023 22:24:01 - - Filtering uptime records to your time frame... - Calculating uptime during time frame... - - ### Time Frame ### - Start Date: 07/01/2023 00:00:00 - End Date: 07/31/2023 00:00:00 - - ### Statistics ### - Percentage Online: 89% - Total Time Frame: 30d - Total Uptime: 26d 16h 49m 17s - - ### Uptime Entries ### - - BootType BootTime ShutdownTime Duration - -------- -------- ------------ -------- - Normal 7/26/2023 12:16:34 PM 12/21/2023 11:24:17 AM 147d 23h 7m 42s - Normal 7/26/2023 11:52:47 AM 7/26/2023 12:16:26 PM 23m 39s - Normal 7/26/2023 11:22:46 AM 7/26/2023 11:52:40 AM 29m 53s - Normal 7/26/2023 10:55:08 AM 7/26/2023 11:22:38 AM 27m 29s - Unexpected Shutdown 7/26/2023 10:42:45 AM 7/26/2023 10:53:16 AM 10m 30s - Normal 7/24/2023 11:09:01 AM 7/24/2023 11:35:43 AM 26m 42s - Normal 7/24/2023 10:45:38 AM 7/24/2023 11:08:53 AM 23m 15s - Normal 7/24/2023 9:50:52 AM 7/24/2023 9:58:47 AM 7m 54s - Normal 7/24/2023 8:08:34 AM 7/24/2023 8:10:16 AM 1m 41s - Normal 7/20/2023 5:26:15 PM 7/24/2023 8:08:26 AM 3d 14h 42m 11s - Normal 7/20/2023 5:21:09 PM 7/20/2023 5:26:08 PM 4m 59s - Normal 7/20/2023 4:08:28 PM 7/20/2023 5:21:02 PM 1h 12m 33s - Unexpected Shutdown 6/29/2023 10:41:23 PM 7/19/2023 10:35:01 AM 19d 11h 53m 38s - -PARAMETER: -Days "replaceMeWithANumber" - Gets the uptime for the past X days. - -PARAMETER: -StartDay "2023-07-01T00:00:00.000-07:00" - Gets the uptime starting from the specified day. - -PARAMETER: -EndDay "2023-07-31T00:00:00.000-07:00" - Gets the uptime ending on the specified day. - -PARAMETER: -WysiwygCustomField "The name of your selected custom field." - Outputs the results to a WYSIWYG custom field. - -PARAMETER: -PercentageCustomField "The name of a text custom field" - Outputs the results to a text custom field. - -PARAMETER: -EstimateUnexpectedShutdown - Tells the script to estimate the shutdown time and date using the last known eventlog for that range. -.OUTPUTS - PSObject -.NOTES - Minimum OS Architecture Supported: Windows 8, Windows Server 2012 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [int]$Days, - [Parameter()] - $StartDay, - [Parameter()] - $EndDay, - [Parameter()] - [string]$WysiwygCustomField, - [Parameter()] - [String]$PercentageCustomField, - [Parameter()] - [Switch]$EstimateUnexpectedShutdown = [System.Convert]::ToBoolean($env:estimateUnexpectedShutdownTime) -) - -begin { - if ($env:uptimeForThePastXDays -and $env:uptimeForThePastXDays -notlike "null") { $Days = $env:uptimeForThePastXDays } - if ($env:startDay -and $env:startDay -notlike "null") { $StartDay = $env:startDay } - if ($env:endDay -and $env:endDay -notlike "null") { $EndDay = $env:endDay } - if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $WysiwygCustomField = $env:wysiwygCustomFieldName } - if ($env:percentageCustomFieldName -and $env:percentageCustomFieldName -notlike "null") { $PercentageCustomField = $env:percentageCustomFieldName } - - if ($StartDay -and $EndDay) { - $StartDay = Get-Date -Date $StartDay - $EndDay = Get-Date -Date $EndDay - } - - if ($Days -and ($StartDay -or $EndDay)) { Write-Host "[Error] You cannot use 'The Past X Days' and 'Start Day' or 'End Day' at the same time"; exit 1 } - if (-not $Days -and -not $StartDay -and -not $EndDay) { Write-Warning "No time frame specified. Checking uptime percentage for the last 30 days"; $Days = 30 } - if (($StartDay -and -not $EndDay) -or ($EndDay -and -not $StartDay)) { Write-Host "[Error] Start Day must be used with End Day."; exit 1 } - if ($StartDay -and $StartDay -ge $EndDay) { Write-Host "[Error] Start Day must be before End Day"; exit 1 } - - if ($Days) { - $StartDay = (Get-Date).AddDays(-$Days) - $EndDay = Get-Date - } - - function ConvertFrom-TimeSpan { - [CmdletBinding()] - [OutputType([string[]])] - param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] - [TimeSpan[]]$TimeSpan - ) - process { - $Days = if ($($_.Days)) { "$($_.Days)d" }else { "" } - $Hours = if ($($_.Hours)) { "$($_.Hours)h" }else { "" } - $Minutes = if ($($_.Minutes)) { "$($_.Minutes)m" }else { "" } - $Seconds = if ($($_.Seconds)) { "$($_.Seconds)s" }else { "" } - "$($(@($Days, $Hours, $Minutes, $Seconds) | Select-Object -Unique) -join ' ')".Trim() - } - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 10000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") - } - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to be set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received it will have an exception property, the function will exit with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - $ExitCode = 0 -} -process { - if ( - $PSCmdlet.ParameterSetName -like "StartEndDays" -and $StartDay -ge $EndDay -or - $($StartDay -and $EndDay -and $StartDay -ge $EndDay) - ) { - Write-Error "StartDay must be less than EndDay." - exit 1 - } - - # Gathers all the event logs pertaining to startup and shutdowns - $eventFilter = @{ - LogName = 'System' - ProviderName = @('Microsoft-Windows-Kernel-General', 'Microsoft-Windows-Eventlog') - ID = 12, 13, 6008 - } - $Events = Get-WinEvent -FilterHashtable $eventFilter - - $BootEntries = New-Object System.Collections.Generic.List[object] - $i = 0 - $BootTime = $null - $ShutdownTime = $null - - Write-Host "Creating uptime entries based on event logs..." - $Events | ForEach-Object { - # If this is the first event it is for our current boot - if ($i -eq 0) { - $BootEntries.Add( - [PSCustomObject]@{ - BootType = "Current Boot" - BootTime = $_.TimeCreated - ShutdownTime = $null - Duration = if ((((Get-Date) - $EndDay).TotalDays -lt 1)) { New-TimeSpan -Start $_.TimeCreated -End $EndDay }else { (New-TimeSpan -Start $_.TimeCreated -End $(Get-Date)) } - } - ) - $i++ - return - } - - # Check to see if the next event is either an Unexpected Shutdown or a Startup Entry - if ($_.Id -eq 13 -and ($Events[$($i + 1)].Id -eq 12 -or $Events[$($i + 1)].Id -eq 6008)) { - $ShutdownTime = $_.TimeCreated - $BootTime = $Events[$($i + 1)].TimeCreated - } - - # Check to see if the previous entry was something other than shutdown and the next event is a shutdown event. - if ($_.Id -eq 12 -and ($Events[$($i - 1)].Id -ne 13) -and ($Events[$($i + 1)].Id -eq 13)) { - # There's no end record for unexpected shutdowns so we'll need to record that - $BootEntries.Add( - [PSCustomObject]@{ - BootType = "Unexpected Shutdown" - BootTime = $_.TimeCreated - ShutdownTime = $null - Duration = $null - } - ) - } - - # If there are no special cases record the information - if ($BootTime -and $ShutdownTime) { - $BootEntries.Add( - [PSCustomObject]@{ - BootType = "Normal" - BootTime = $BootTime - ShutdownTime = $ShutdownTime - Duration = (New-TimeSpan -Start $BootTime -End $ShutdownTime) - } - ) - $BootTime = $null - $ShutdownTime = $null - } - - $i++ - } - - Write-Host "Uptime entries created!" - - # Warn about unexpected shutdown's effect on the report - if ($BootEntries.BootType -contains "Unexpected Shutdown" -and -not $EstimateUnexpectedShutdown) { - Write-Warning "There are unexpected shutdowns in your boot history (may or may not be within your timeframe)." - Write-Warning "Unexpected shutdowns do NOT have a shutdown time and will be excluded from all calculations." - } - elseif ($BootEntries.BootType -contains "Unexpected Shutdown") { - # If requested to estimate the shutdown time. Estimate it based on the last event log prior to the next bootup. - Write-Warning "Estimating unexpected shutdown times. This will not be 100% accurate." - $entry = 0 - $BootEntries | ForEach-Object { - if ($_.BootType -ne "Unexpected Shutdown") { - $entry++ - return - } - - $EventFilter = @{ - LogName = "*" - StartTime = $_.BootTime - EndTime = $BootEntries[$($entry - 1)].BootTime - } - - # We only want one event to minimize impact on performance - $LastEvent = Get-WinEvent -FilterHashtable $EventFilter -MaxEvents 1 - $_.ShutdownTime = $LastEvent.TimeCreated - $_.Duration = (New-TimeSpan -Start $_.BootTime -End $LastEvent.TimeCreated) - $entry++ - } - } - - Write-Host "`nOldest uptime record: $($BootEntries | Select-Object -ExpandProperty BootTime -Last 1)`n" - - # We now have all the information needed to decide what uptime records should be output. - Write-Host "Filtering uptime records to your time frame..." - $ReportableBootEntries = New-Object System.Collections.Generic.List[object] - $BootEntries | ForEach-Object { - if ($_.Duration) { - $_.Duration = $_.Duration | ConvertFrom-TimeSpan - } - - if ( $_.ShutdownTime -and $_.ShutdownTime -le $EndDay -and $_.ShutdownTime -ge $StartDay) { - $ReportableBootEntries.Add($_) - return - } - - if ( $_.BootType -eq "Current Boot" -and $_.BootTime -le $StartDay -and $_.BootTime -le $EndDay ) { - $ReportableBootEntries.Add($_) - return - } - - if ( $_.BootTime -and $_.BootTime -ge $StartDay -and $_.BootTime -le $EndDay) { - $ReportableBootEntries.Add($_) - return - } - } - - # Now let's use our information to calculate the total amount of time the system has been online in the time range. - Write-Host "Calculating uptime during time frame..." - $ReportableBootEntries | ForEach-Object { - # If the current boot is in the time range we'll need to use the End Date as the ending time frame. - if ($_.BootType -eq "Current Boot" -and $_.BootTime -gt $StartDay) { - $TotalTimeOnline = $TotalTimeOnline + (New-TimeSpan -Start $_.BootTime -End $EndDay) - return - } - elseif ($_.BootType -eq "Current Boot") { - # If the boot time is older than the start date we'll use the start date. - $TotalTimeOnline = $TotalTimeOnline + (New-TimeSpan -Start $StartDay -End $EndDay) - return - } - - # If we're missing the information required to make these calculations we should skip it. - if (-not $_.BootTime -or -not $_.ShutdownTime) { - return - } - - # If the uptime entry is in our time range we can add it straight in. - if ($_.BootTime -ge $StartDay -and $_.ShutdownTime -le $EndDay) { - $TotalTimeOnline = $TotalTimeOnline + (New-TimeSpan -Start $_.BootTime -End $_.ShutdownTime) - return - } - - # If the uptime entry starts to early then we need to use the start day as the starting point. - if ($_.BootTime -le $StartDay -and $_.ShutdownTime -le $EndDay) { - $TotalTimeOnline = $TotalTimeOnline + (New-TimeSpan -Start $StartDay -End $_.ShutdownTime) - return - } - - # If the uptime entry goes too long than we need to use the end date as a reference. - if ($_.ShutdownTime -ge $EndDay -and $_.BootTime -ge $StartDay) { - $TotalTimeOnline = $TotalTimeOnline + (New-TimeSpan -Start $_.BootTime -End $EndDay) - return - } - - # If the uptime entry is both before the start day and after the end day we'll use both the start and end day. - if ($_.ShutdownTime -ge $EndDay -and $_.BootTime -le $StartDay) { - $TotalTimeOnline = $TotalTimeOnline + (New-TimeSpan -Start $StartDay -End $EndDay) - return - } - } - - # We're now ready to output our results to the activity log - Write-Host "" - - $TotalTimeFrame = (New-TimeSpan -Start $StartDay -End $EndDay) - Write-Host "### Time Frame ###" - Write-Host "Start Date: $StartDay" - Write-Host "End Date: $EndDay`n" - - # For the percentage we'll take the total number of seconds it was online for and divide it by the total number of seconds possible - $Percentage = $([math]::Round(($TotalTimeOnline.TotalSeconds / $TotalTimeFrame.TotalSeconds * 100), 2)) - Write-Host "### Statistics ###" - Write-Host "Percentage Online: $Percentage%" - Write-Host "Total Time Frame: $($TotalTimeFrame | ConvertFrom-TimeSpan)" - if ($TotalTimeOnline) { - $HumanFriendlyTotalUptime = $($TotalTimeOnline | ConvertFrom-Timespan) - } - else { - $HumanFriendlyTotalUptime = "0d 0h 0m 0s" - } - Write-Host "Total Uptime: $HumanFriendlyTotalUptime" - - # Let's output our table as well - Write-Host "`n### Uptime Entries ###" - $ReportableBootEntries | Format-Table -AutoSize | Out-String | Write-Host - - if ($PercentageCustomField) { - try { - Write-Host "Attempting to set Custom Field '$PercentageCustomField'." - Set-NinjaProperty -Name $PercentageCustomField -Value "$Percentage%" - Write-Host "Successfully set Custom Field '$PercentageCustomField'!" - } - catch { - Write-Host "[Error] $($_.Message)" - $ExitCode = 1 - } - } - - if ($WysiwygCustomField) { - try { - Write-Host "Attempting to set Custom Field '$WysiwygCustomField'." - $htmlReport = New-Object System.Collections.Generic.List[String] - $htmlReport.Add(@" -

Uptime Statistics

- - - -"@) - $htmlTable = $ReportableBootEntries | ConvertTo-Html -Fragment - $htmlTable = $htmlTable -replace "", '' - $htmlTable = $htmlTable -replace "", '' - $htmlTable = $htmlTable -replace "", '' - $htmlTable | ForEach-Object { $htmlReport.Add($_) } - Set-NinjaProperty -Name $WysiwygCustomField -Value ($htmlReport | Out-String) - Write-Host "Successfully set Custom Field '$WysiwygCustomField'!" - } - catch { - Write-Host "[Error] $($_.Message)" - $ExitCode = 1 - } - } - - exit $ExitCode -} -end { - - - -} - - +#Requires -Version 4.0 + +<# +.SYNOPSIS + Get the Uptime percentage of a Windows device and show a list of boot events. +.DESCRIPTION + Get the Uptime percentage of a Windows device and show a list of boot events. + + Unexpected Shutdowns can skew results slightly. + + Duration in results is in days. +.Example + (No Parameters) + + WARNING: No time frame specified. Checking uptime percentage for the last 30 days + Creating uptime entries based on event logs... + Uptime entries created! + WARNING: Estimating unexpected shutdown times. This will not be 100% accurate. + + Oldest uptime record: 06/29/2023 22:24:01 + + Filtering uptime records to your time frame... + Calculating uptime during time frame... + + ### Time Frame ### + Start Date: 12/26/2023 19:39:38 + End Date: 01/25/2024 19:39:38 + + ### Statistics ### + Percentage Online: 3.93% + Total Time Frame: 30d + Total Uptime: 1d 4h 19m 14s + + ### Uptime Entries ### + + BootType BootTime ShutdownTime Duration + -------- -------- ------------ -------- + Current Boot 1/24/2024 3:20:23 PM 1d 4h 19m 14s +.EXAMPLE + -Days 30 + + WARNING: No time frame specified. Checking uptime percentage for the last 30 days + Creating uptime entries based on event logs... + Uptime entries created! + WARNING: Estimating unexpected shutdown times. This will not be 100% accurate. + + Oldest uptime record: 06/29/2023 22:24:01 + + Filtering uptime records to your time frame... + Calculating uptime during time frame... + + ### Time Frame ### + Start Date: 12/26/2023 19:39:38 + End Date: 01/25/2024 19:39:38 + + ### Statistics ### + Percentage Online: 3.93% + Total Time Frame: 30d + Total Uptime: 1d 4h 19m 14s + + ### Uptime Entries ### + + BootType BootTime ShutdownTime Duration + -------- -------- ------------ -------- + Current Boot 1/24/2024 3:20:23 PM 1d 4h 19m 14s +.EXAMPLE + -StartDay "2023-07-01T00:00:00.000-07:00" -EndDay "2023-07-31T00:00:00.000-07:00" + + Creating uptime entries based on event logs... + Uptime entries created! + WARNING: Estimating unexpected shutdown times. This will not be 100% accurate. + + Oldest uptime record: 06/29/2023 22:24:01 + + Filtering uptime records to your time frame... + Calculating uptime during time frame... + + ### Time Frame ### + Start Date: 07/01/2023 00:00:00 + End Date: 07/31/2023 00:00:00 + + ### Statistics ### + Percentage Online: 89% + Total Time Frame: 30d + Total Uptime: 26d 16h 49m 17s + + ### Uptime Entries ### + + BootType BootTime ShutdownTime Duration + -------- -------- ------------ -------- + Normal 7/26/2023 12:16:34 PM 12/21/2023 11:24:17 AM 147d 23h 7m 42s + Normal 7/26/2023 11:52:47 AM 7/26/2023 12:16:26 PM 23m 39s + Normal 7/26/2023 11:22:46 AM 7/26/2023 11:52:40 AM 29m 53s + Normal 7/26/2023 10:55:08 AM 7/26/2023 11:22:38 AM 27m 29s + Unexpected Shutdown 7/26/2023 10:42:45 AM 7/26/2023 10:53:16 AM 10m 30s + Normal 7/24/2023 11:09:01 AM 7/24/2023 11:35:43 AM 26m 42s + Normal 7/24/2023 10:45:38 AM 7/24/2023 11:08:53 AM 23m 15s + Normal 7/24/2023 9:50:52 AM 7/24/2023 9:58:47 AM 7m 54s + Normal 7/24/2023 8:08:34 AM 7/24/2023 8:10:16 AM 1m 41s + Normal 7/20/2023 5:26:15 PM 7/24/2023 8:08:26 AM 3d 14h 42m 11s + Normal 7/20/2023 5:21:09 PM 7/20/2023 5:26:08 PM 4m 59s + Normal 7/20/2023 4:08:28 PM 7/20/2023 5:21:02 PM 1h 12m 33s + Unexpected Shutdown 6/29/2023 10:41:23 PM 7/19/2023 10:35:01 AM 19d 11h 53m 38s + +PARAMETER: -Days "replaceMeWithANumber" + Gets the uptime for the past X days. + +PARAMETER: -StartDay "2023-07-01T00:00:00.000-07:00" + Gets the uptime starting from the specified day. + +PARAMETER: -EndDay "2023-07-31T00:00:00.000-07:00" + Gets the uptime ending on the specified day. + +PARAMETER: -WysiwygCustomField "The name of your selected custom field." + Outputs the results to a WYSIWYG custom field. + +PARAMETER: -PercentageCustomField "The name of a text custom field" + Outputs the results to a text custom field. + +PARAMETER: -EstimateUnexpectedShutdown + Tells the script to estimate the shutdown time and date using the last known eventlog for that range. +.OUTPUTS + PSObject +.NOTES + Minimum OS Architecture Supported: Windows 8, Windows Server 2012 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [int]$Days, + [Parameter()] + $StartDay, + [Parameter()] + $EndDay, + [Parameter()] + [string]$WysiwygCustomField, + [Parameter()] + [String]$PercentageCustomField, + [Parameter()] + [Switch]$EstimateUnexpectedShutdown = [System.Convert]::ToBoolean($env:estimateUnexpectedShutdownTime) +) + +begin { + if ($env:uptimeForThePastXDays -and $env:uptimeForThePastXDays -notlike "null") { $Days = $env:uptimeForThePastXDays } + if ($env:startDay -and $env:startDay -notlike "null") { $StartDay = $env:startDay } + if ($env:endDay -and $env:endDay -notlike "null") { $EndDay = $env:endDay } + if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $WysiwygCustomField = $env:wysiwygCustomFieldName } + if ($env:percentageCustomFieldName -and $env:percentageCustomFieldName -notlike "null") { $PercentageCustomField = $env:percentageCustomFieldName } + + if ($StartDay -and $EndDay) { + $StartDay = Get-Date -Date $StartDay + $EndDay = Get-Date -Date $EndDay + } + + if ($Days -and ($StartDay -or $EndDay)) { Write-Host "[Error] You cannot use 'The Past X Days' and 'Start Day' or 'End Day' at the same time"; exit 1 } + if (-not $Days -and -not $StartDay -and -not $EndDay) { Write-Warning "No time frame specified. Checking uptime percentage for the last 30 days"; $Days = 30 } + if (($StartDay -and -not $EndDay) -or ($EndDay -and -not $StartDay)) { Write-Host "[Error] Start Day must be used with End Day."; exit 1 } + if ($StartDay -and $StartDay -ge $EndDay) { Write-Host "[Error] Start Day must be before End Day"; exit 1 } + + if ($Days) { + $StartDay = (Get-Date).AddDays(-$Days) + $EndDay = Get-Date + } + + function ConvertFrom-TimeSpan { + [CmdletBinding()] + [OutputType([string[]])] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [TimeSpan[]]$TimeSpan + ) + process { + $Days = if ($($_.Days)) { "$($_.Days)d" }else { "" } + $Hours = if ($($_.Hours)) { "$($_.Hours)h" }else { "" } + $Minutes = if ($($_.Minutes)) { "$($_.Minutes)m" }else { "" } + $Seconds = if ($($_.Seconds)) { "$($_.Seconds)s" }else { "" } + "$($(@($Days, $Hours, $Minutes, $Seconds) | Select-Object -Unique) -join ' ')".Trim() + } + } + + $ExitCode = 0 +} +process { + if ( + $PSCmdlet.ParameterSetName -like "StartEndDays" -and $StartDay -ge $EndDay -or + $($StartDay -and $EndDay -and $StartDay -ge $EndDay) + ) { + Write-Error "StartDay must be less than EndDay." + exit 1 + } + + # Gathers all the event logs pertaining to startup and shutdowns + $eventFilter = @{ + LogName = 'System' + ProviderName = @('Microsoft-Windows-Kernel-General', 'Microsoft-Windows-Eventlog') + ID = 12, 13, 6008 + } + $Events = Get-WinEvent -FilterHashtable $eventFilter + + $BootEntries = New-Object System.Collections.Generic.List[object] + $i = 0 + $BootTime = $null + $ShutdownTime = $null + + Write-Host "Creating uptime entries based on event logs..." + $Events | ForEach-Object { + # If this is the first event it is for our current boot + if ($i -eq 0) { + $BootEntries.Add( + [PSCustomObject]@{ + BootType = "Current Boot" + BootTime = $_.TimeCreated + ShutdownTime = $null + Duration = if ((((Get-Date) - $EndDay).TotalDays -lt 1)) { New-TimeSpan -Start $_.TimeCreated -End $EndDay }else { (New-TimeSpan -Start $_.TimeCreated -End $(Get-Date)) } + } + ) + $i++ + return + } + + # Check to see if the next event is either an Unexpected Shutdown or a Startup Entry + if ($_.Id -eq 13 -and ($Events[$($i + 1)].Id -eq 12 -or $Events[$($i + 1)].Id -eq 6008)) { + $ShutdownTime = $_.TimeCreated + $BootTime = $Events[$($i + 1)].TimeCreated + } + + # Check to see if the previous entry was something other than shutdown and the next event is a shutdown event. + if ($_.Id -eq 12 -and ($Events[$($i - 1)].Id -ne 13) -and ($Events[$($i + 1)].Id -eq 13)) { + # There's no end record for unexpected shutdowns so we'll need to record that + $BootEntries.Add( + [PSCustomObject]@{ + BootType = "Unexpected Shutdown" + BootTime = $_.TimeCreated + ShutdownTime = $null + Duration = $null + } + ) + } + + # If there are no special cases record the information + if ($BootTime -and $ShutdownTime) { + $BootEntries.Add( + [PSCustomObject]@{ + BootType = "Normal" + BootTime = $BootTime + ShutdownTime = $ShutdownTime + Duration = (New-TimeSpan -Start $BootTime -End $ShutdownTime) + } + ) + $BootTime = $null + $ShutdownTime = $null + } + + $i++ + } + + Write-Host "Uptime entries created!" + + # Warn about unexpected shutdown's effect on the report + if ($BootEntries.BootType -contains "Unexpected Shutdown" -and -not $EstimateUnexpectedShutdown) { + Write-Warning "There are unexpected shutdowns in your boot history (may or may not be within your timeframe)." + Write-Warning "Unexpected shutdowns do NOT have a shutdown time and will be excluded from all calculations." + } + elseif ($BootEntries.BootType -contains "Unexpected Shutdown") { + # If requested to estimate the shutdown time. Estimate it based on the last event log prior to the next bootup. + Write-Warning "Estimating unexpected shutdown times. This will not be 100% accurate." + $entry = 0 + $BootEntries | ForEach-Object { + if ($_.BootType -ne "Unexpected Shutdown") { + $entry++ + return + } + + $EventFilter = @{ + LogName = "*" + StartTime = $_.BootTime + EndTime = $BootEntries[$($entry - 1)].BootTime + } + + # We only want one event to minimize impact on performance + $LastEvent = Get-WinEvent -FilterHashtable $EventFilter -MaxEvents 1 + $_.ShutdownTime = $LastEvent.TimeCreated + $_.Duration = (New-TimeSpan -Start $_.BootTime -End $LastEvent.TimeCreated) + $entry++ + } + } + + Write-Host "`nOldest uptime record: $($BootEntries | Select-Object -ExpandProperty BootTime -Last 1)`n" + + # We now have all the information needed to decide what uptime records should be output. + Write-Host "Filtering uptime records to your time frame..." + $ReportableBootEntries = New-Object System.Collections.Generic.List[object] + $BootEntries | ForEach-Object { + if ($_.Duration) { + $_.Duration = $_.Duration | ConvertFrom-TimeSpan + } + + if ( $_.ShutdownTime -and $_.ShutdownTime -le $EndDay -and $_.ShutdownTime -ge $StartDay) { + $ReportableBootEntries.Add($_) + return + } + + if ( $_.BootType -eq "Current Boot" -and $_.BootTime -le $StartDay -and $_.BootTime -le $EndDay ) { + $ReportableBootEntries.Add($_) + return + } + + if ( $_.BootTime -and $_.BootTime -ge $StartDay -and $_.BootTime -le $EndDay) { + $ReportableBootEntries.Add($_) + return + } + } + + # Now let's use our information to calculate the total amount of time the system has been online in the time range. + Write-Host "Calculating uptime during time frame..." + $ReportableBootEntries | ForEach-Object { + # If the current boot is in the time range we'll need to use the End Date as the ending time frame. + if ($_.BootType -eq "Current Boot" -and $_.BootTime -gt $StartDay) { + $TotalTimeOnline = $TotalTimeOnline + (New-TimeSpan -Start $_.BootTime -End $EndDay) + return + } + elseif ($_.BootType -eq "Current Boot") { + # If the boot time is older than the start date we'll use the start date. + $TotalTimeOnline = $TotalTimeOnline + (New-TimeSpan -Start $StartDay -End $EndDay) + return + } + + # If we're missing the information required to make these calculations we should skip it. + if (-not $_.BootTime -or -not $_.ShutdownTime) { + return + } + + # If the uptime entry is in our time range we can add it straight in. + if ($_.BootTime -ge $StartDay -and $_.ShutdownTime -le $EndDay) { + $TotalTimeOnline = $TotalTimeOnline + (New-TimeSpan -Start $_.BootTime -End $_.ShutdownTime) + return + } + + # If the uptime entry starts to early then we need to use the start day as the starting point. + if ($_.BootTime -le $StartDay -and $_.ShutdownTime -le $EndDay) { + $TotalTimeOnline = $TotalTimeOnline + (New-TimeSpan -Start $StartDay -End $_.ShutdownTime) + return + } + + # If the uptime entry goes too long than we need to use the end date as a reference. + if ($_.ShutdownTime -ge $EndDay -and $_.BootTime -ge $StartDay) { + $TotalTimeOnline = $TotalTimeOnline + (New-TimeSpan -Start $_.BootTime -End $EndDay) + return + } + + # If the uptime entry is both before the start day and after the end day we'll use both the start and end day. + if ($_.ShutdownTime -ge $EndDay -and $_.BootTime -le $StartDay) { + $TotalTimeOnline = $TotalTimeOnline + (New-TimeSpan -Start $StartDay -End $EndDay) + return + } + } + + # We're now ready to output our results to the activity log + Write-Host "" + + $TotalTimeFrame = (New-TimeSpan -Start $StartDay -End $EndDay) + Write-Host "### Time Frame ###" + Write-Host "Start Date: $StartDay" + Write-Host "End Date: $EndDay`n" + + # For the percentage we'll take the total number of seconds it was online for and divide it by the total number of seconds possible + $Percentage = $([math]::Round(($TotalTimeOnline.TotalSeconds / $TotalTimeFrame.TotalSeconds * 100), 2)) + Write-Host "### Statistics ###" + Write-Host "Percentage Online: $Percentage%" + Write-Host "Total Time Frame: $($TotalTimeFrame | ConvertFrom-TimeSpan)" + if ($TotalTimeOnline) { + $HumanFriendlyTotalUptime = $($TotalTimeOnline | ConvertFrom-Timespan) + } + else { + $HumanFriendlyTotalUptime = "0d 0h 0m 0s" + } + Write-Host "Total Uptime: $HumanFriendlyTotalUptime" + + # Let's output our table as well + Write-Host "`n### Uptime Entries ###" + $ReportableBootEntries | Format-Table -AutoSize | Out-String | Write-Host + + if ($PercentageCustomField) { + Write-Host "" + Write-Host "Note: Custom field '$PercentageCustomField' was specified but NinjaOne integration has been removed." + Write-Host "All output has been displayed above." + } + + if ($WysiwygCustomField) { + Write-Host "" + Write-Host "Note: Custom field '$WysiwygCustomField' was specified but NinjaOne integration has been removed." + Write-Host "All output has been displayed above." + } + + exit $ExitCode +} +end { + +} + diff --git a/Powershell Scripts/Disable Local Admin Tools.ps1 b/Powershell Scripts/Disable Local Admin Tools.ps1 index 9827a0c..e64ce0c 100644 --- a/Powershell Scripts/Disable Local Admin Tools.ps1 +++ b/Powershell Scripts/Disable Local Admin Tools.ps1 @@ -1,230 +1,228 @@ # This will disable the selected administrator tools depending on your selection (Defaults to all). Can be given a comma separated list of users to exclude from this action. -#Requires -Version 5.1 - -<# -.SYNOPSIS - This will disable the selected administrator tools depending on your selection (Defaults to all). Can be given a comma separated list of users to exclude from this action. -.DESCRIPTION - This will disable the selected administrator tools. The options are "All", the command prompt, the control panel, the microsoft management console, - the registry editor, the run command window and task manager. You can give it a comma separated list of items if you want to disable some but not all. - Exit 1 is usually an indicator of bad input but can also mean editing the registry is blocked. -.EXAMPLE - PS C:\> .\Disable-LocalAdminTools.ps1 -Tools "MMC,Cmd,TaskMgr,RegistryEditor" - Disabling MMC... - Set Registry::HKEY_USERS\DefaultProfile\Software\Policies\Microsoft\MMCRestrictToPermittedSnapins to... - Disabling Cmd... - Set Registry::HKEY_USERS\DefaultProfile\Software\Policies\Microsoft\WindowsDisableCMD to... - Disabling TaskMgr... - Set Registry::HKEY_USERS\DefaultProfile\Software\Microsoft\Windows\CurrentVersion\Policies\SystemDisableTaskMgr to... - Disabling RegistryEditor... - Set Registry::HKEY_USERS\DefaultProfile\Software\Microsoft\Windows\CurrentVersion\Policies\SystemDisableRegistryTools to... -.OUTPUTS - None -.NOTES - Minimum Supported OS: Windows 10, Windows Server 2016+ - Release Notes: Renamed script and added Script Variable support -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$Tools = "All", - [Parameter()] - [String]$ExcludedUsers -) - -begin { - - if ($env:excludeUsers -and $env:excludeUsers -notlike "null") { $ExcludedUsers = $env:excludeUsers } - - # Lets double check that this script is being run appropriately - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - function Test-IsSystem { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - return $id.Name -like "NT AUTHORITY*" -or $id.IsSystem - } - - if (!(Test-IsElevated) -and !(Test-IsSystem)) { - Write-Error -Message "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Setting up some functions to be used later. - function Set-HKProperty { - param ( - $Path, - $Name, - $Value, - [ValidateSet('DWord', 'QWord', 'String', 'ExpandedString', 'Binary', 'MultiString', 'Unknown')] - $PropertyType = 'DWord' - ) - if (-not $(Test-Path -Path $Path)) { - # Check if path does not exist and create the path - New-Item -Path $Path -Force | Out-Null - } - if ((Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore)) { - # Update property and print out what it was changed from and changed to - $CurrentValue = Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore - try { - Set-ItemProperty -Path $Path -Name $Name -Value $Value -Force -Confirm:$false -ErrorAction Stop | Out-Null - } - catch { - Write-Error "[Error] Unable to Set registry key for $Name please see below error!" - Write-Error $_ - exit 1 - } - Write-Host "$Path\$Name changed from $CurrentValue to $(Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore)" - } - else { - # Create property with value - try { - New-ItemProperty -Path $Path -Name $Name -Value $Value -PropertyType $PropertyType -Force -Confirm:$false -ErrorAction Stop | Out-Null - } - catch { - Write-Error "[Error] Unable to Set registry key for $Name please see below error!" - Write-Error $_ - exit 1 - } - Write-Host "Set $Path$Name to $(Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore)" - } - } - - # This will get all the registry path's for all actual users (not system or network service account but actual users.) - function Get-UserHives { - param ( - [Parameter()] - [ValidateSet('AzureAD', 'DomainAndLocal', 'All')] - [String]$Type = "All", - [Parameter()] - [String[]]$ExcludedUsers, - [Parameter()] - [switch]$IncludeDefault - ) - - # User account SID's follow a particular patter depending on if they're azure AD or a Domain account or a local "workgroup" account. - $Patterns = switch ($Type) { - "AzureAD" { "S-1-12-1-(\d+-?){4}$" } - "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" } - "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" } - } - - # We'll need the NTuser.dat file to load each users registry hive. So we grab it if their account sid matches the above pattern. - $UserProfiles = Foreach ($Pattern in $Patterns) { - Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | - Where-Object { $_.PSChildName -match $Pattern } | - Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, - @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, - @{Name = "UserName"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } } - } - - # There are some situations where grabbing the .Default user's info is needed. - switch ($IncludeDefault) { - $True { - $DefaultProfile = "" | Select-Object UserName, SID, UserHive - $DefaultProfile.UserName = "Default" - $DefaultProfile.SID = "DefaultProfile" - $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT" - - # It was easier to write-output twice than combine the two objects. - $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.UserName } | Write-Output - } - } - - $UserProfiles | Where-Object { $ExcludedUsers -notcontains $_.UserName } | Write-Output - } - - function Set-Tool { - [CmdletBinding()] - param( - [Parameter()] - [ValidateSet("All", "Cmd", "ControlPanel", "theControlPanel", "MMC", "RegistryEditor", "theRegistryEditor", "Run", "TaskMgr", "taskManager")] - [string]$Tool, - [string]$key - ) - process { - # Each option has a different registry key to change. Since this function only supports 1 item at a time I can check which option and set the regkey individually. - Write-Host "Disabling $Tool..." - switch ($Tool) { - "Cmd" { Set-HKProperty -Path $key\Software\Policies\Microsoft\Windows\System -Name DisableCMD -Value 1 } - "ControlPanel" { Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer -Name NoControlPanel -Value 1 } - "theControlPanel" { Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer -Name NoControlPanel -Value 1 } - "MMC" { Set-HKProperty -Path $key\Software\Policies\Microsoft\MMC -Name RestrictToPermittedSnapins -Value 1 } - "RegistryEditor" { Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\System -Name DisableRegistryTools -Value 1 } - "theRegistryEditor" { Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\System -Name DisableRegistryTools -Value 1 } - "Run" { Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer -Name NoRun -Value 1 } - "TaskMgr" { Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\System -Name DisableTaskMgr -Value 1 } - "taskManager" { Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\System -Name DisableTaskMgr -Value 1 } - "All" { - Set-HKProperty -Path $key\Software\Policies\Microsoft\Windows\System -Name DisableCMD -Value 1 - Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer -Name NoControlPanel -Value 1 - Set-HKProperty -Path $key\Software\Policies\Microsoft\MMC -Name RestrictToPermittedSnapins -Value 1 - Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\System -Name DisableRegistryTools -Value 1 - Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer -Name NoRun -Value 1 - Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\System -Name DisableTaskMgr -Value 1 - } - } - } - } -} -process { - - # Get each user profile SID and Path to the profile. If there are any exclusions we'll have to take them into account. - if ($ExcludedUsers) { - $ToBeExcluded = New-Object System.Collections.Generic.List[string] - $ExcludedUsers.split(",").trim() | ForEach-Object { if ($_) { $ToBeExcluded.Add($_) } } - Write-Warning "The Following Users will not have your selected tools disabled. $ToBeExcluded" - $UserProfiles = Get-UserHives -IncludeDefault -ExcludedUsers $ToBeExcluded - } - else { - $UserProfiles = Get-UserHives -IncludeDefault - } - - # Loop through each profile on the machine - Foreach ($UserProfile in $UserProfiles) { - # Load each user's registry hive if not already loaded. Backticked "UserProfile.UserHive" so that it accounts for spaces in the username. - If (($ProfileWasLoaded = Test-Path Registry::HKEY_USERS\$($UserProfile.SID)) -eq $false) { - Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe LOAD HKU\$($UserProfile.SID) `"$($UserProfile.UserHive)`"" -Wait -WindowStyle Hidden - } - # The path is different for each individual user. This is the base path. - $key = "Registry::HKEY_USERS\$($UserProfile.SID)" - - # List of checkbox items - $CheckboxItems = "cmd", "theControlPanel", "mmc", "theRegistryEditor", "run", "taskManager" - # Checkboxes come in as environmental variables. This'll grab the ones that were selected (if any) - $EnvItems = Get-ChildItem env:* | Where-Object { $CheckboxItems -contains $_.Name -and $_.Value -notlike "false" } - - # This will grab the tool selections from the parameter field. Since it comes in as a string we'll have to split it up. - $Tool = $Tools.split(",").trim() - - # If the checkbox for all was selected I can just run the function once instead of running it repeatedly for the same thing. - if ($env:allTools -and $env:allTools -notlike "false") { - Set-Tool -Tool "All" -Key $key - } - elseif ($EnvItems) { - # If checkboxes were used we should just use those. - $EnvItems | ForEach-Object { Set-Tool -Tool $_.Name -Key $key } - } - else { - $Tool | ForEach-Object { Set-Tool -Tool $_ -Key $key } - } - - # Unload NTuser.dat for user's we loaded previously. - If ($ProfileWasLoaded -eq $false) { - [gc]::Collect() - Start-Sleep -Seconds 1 - Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe UNLOAD HKU\$($UserProfile.SID)" -Wait -WindowStyle Hidden | Out-Null - } - } - -} -end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + This will disable the selected administrator tools depending on your selection (Defaults to all). Can be given a comma separated list of users to exclude from this action. +.DESCRIPTION + This will disable the selected administrator tools. The options are "All", the command prompt, the control panel, the microsoft management console, + the registry editor, the run command window and task manager. You can give it a comma separated list of items if you want to disable some but not all. + Exit 1 is usually an indicator of bad input but can also mean editing the registry is blocked. +.EXAMPLE + PS C:\> .\Disable-LocalAdminTools.ps1 -Tools "MMC,Cmd,TaskMgr,RegistryEditor" + Disabling MMC... + Set Registry::HKEY_USERS\DefaultProfile\Software\Policies\Microsoft\MMCRestrictToPermittedSnapins to... + Disabling Cmd... + Set Registry::HKEY_USERS\DefaultProfile\Software\Policies\Microsoft\WindowsDisableCMD to... + Disabling TaskMgr... + Set Registry::HKEY_USERS\DefaultProfile\Software\Microsoft\Windows\CurrentVersion\Policies\SystemDisableTaskMgr to... + Disabling RegistryEditor... + Set Registry::HKEY_USERS\DefaultProfile\Software\Microsoft\Windows\CurrentVersion\Policies\SystemDisableRegistryTools to... +.OUTPUTS + None +.NOTES + Minimum Supported OS: Windows 10, Windows Server 2016+ + Release Notes: Renamed script and added Script Variable support +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$Tools = "All", + [Parameter()] + [String]$ExcludedUsers +) + +begin { + + if ($env:excludeUsers -and $env:excludeUsers -notlike "null") { $ExcludedUsers = $env:excludeUsers } + + # Lets double check that this script is being run appropriately + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + function Test-IsSystem { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + return $id.Name -like "NT AUTHORITY*" -or $id.IsSystem + } + + if (!(Test-IsElevated) -and !(Test-IsSystem)) { + Write-Error -Message "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Setting up some functions to be used later. + function Set-HKProperty { + param ( + $Path, + $Name, + $Value, + [ValidateSet('DWord', 'QWord', 'String', 'ExpandedString', 'Binary', 'MultiString', 'Unknown')] + $PropertyType = 'DWord' + ) + if (-not $(Test-Path -Path $Path)) { + # Check if path does not exist and create the path + New-Item -Path $Path -Force | Out-Null + } + if ((Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore)) { + # Update property and print out what it was changed from and changed to + $CurrentValue = Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore + try { + Set-ItemProperty -Path $Path -Name $Name -Value $Value -Force -Confirm:$false -ErrorAction Stop | Out-Null + } + catch { + Write-Error "[Error] Unable to Set registry key for $Name please see below error!" + Write-Error $_ + exit 1 + } + Write-Host "$Path\$Name changed from $CurrentValue to $(Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore)" + } + else { + # Create property with value + try { + New-ItemProperty -Path $Path -Name $Name -Value $Value -PropertyType $PropertyType -Force -Confirm:$false -ErrorAction Stop | Out-Null + } + catch { + Write-Error "[Error] Unable to Set registry key for $Name please see below error!" + Write-Error $_ + exit 1 + } + Write-Host "Set $Path$Name to $(Get-ItemProperty -Path $Path -Name $Name -ErrorAction Ignore)" + } + } + + # This will get all the registry path's for all actual users (not system or network service account but actual users.) + function Get-UserHives { + param ( + [Parameter()] + [ValidateSet('AzureAD', 'DomainAndLocal', 'All')] + [String]$Type = "All", + [Parameter()] + [String[]]$ExcludedUsers, + [Parameter()] + [switch]$IncludeDefault + ) + + # User account SID's follow a particular patter depending on if they're azure AD or a Domain account or a local "workgroup" account. + $Patterns = switch ($Type) { + "AzureAD" { "S-1-12-1-(\d+-?){4}$" } + "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" } + "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" } + } + + # We'll need the NTuser.dat file to load each users registry hive. So we grab it if their account sid matches the above pattern. + $UserProfiles = Foreach ($Pattern in $Patterns) { + Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | + Where-Object { $_.PSChildName -match $Pattern } | + Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, + @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, + @{Name = "UserName"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } } + } + + # There are some situations where grabbing the .Default user's info is needed. + switch ($IncludeDefault) { + $True { + $DefaultProfile = "" | Select-Object UserName, SID, UserHive + $DefaultProfile.UserName = "Default" + $DefaultProfile.SID = "DefaultProfile" + $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT" + + # It was easier to write-output twice than combine the two objects. + $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.UserName } | Write-Output + } + } + + $UserProfiles | Where-Object { $ExcludedUsers -notcontains $_.UserName } | Write-Output + } + + function Set-Tool { + [CmdletBinding()] + param( + [Parameter()] + [ValidateSet("All", "Cmd", "ControlPanel", "theControlPanel", "MMC", "RegistryEditor", "theRegistryEditor", "Run", "TaskMgr", "taskManager")] + [string]$Tool, + [string]$key + ) + process { + # Each option has a different registry key to change. Since this function only supports 1 item at a time I can check which option and set the regkey individually. + Write-Host "Disabling $Tool..." + switch ($Tool) { + "Cmd" { Set-HKProperty -Path $key\Software\Policies\Microsoft\Windows\System -Name DisableCMD -Value 1 } + "ControlPanel" { Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer -Name NoControlPanel -Value 1 } + "theControlPanel" { Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer -Name NoControlPanel -Value 1 } + "MMC" { Set-HKProperty -Path $key\Software\Policies\Microsoft\MMC -Name RestrictToPermittedSnapins -Value 1 } + "RegistryEditor" { Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\System -Name DisableRegistryTools -Value 1 } + "theRegistryEditor" { Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\System -Name DisableRegistryTools -Value 1 } + "Run" { Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer -Name NoRun -Value 1 } + "TaskMgr" { Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\System -Name DisableTaskMgr -Value 1 } + "taskManager" { Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\System -Name DisableTaskMgr -Value 1 } + "All" { + Set-HKProperty -Path $key\Software\Policies\Microsoft\Windows\System -Name DisableCMD -Value 1 + Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer -Name NoControlPanel -Value 1 + Set-HKProperty -Path $key\Software\Policies\Microsoft\MMC -Name RestrictToPermittedSnapins -Value 1 + Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\System -Name DisableRegistryTools -Value 1 + Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer -Name NoRun -Value 1 + Set-HKProperty -Path $key\Software\Microsoft\Windows\CurrentVersion\Policies\System -Name DisableTaskMgr -Value 1 + } + } + } + } +} +process { + + # Get each user profile SID and Path to the profile. If there are any exclusions we'll have to take them into account. + if ($ExcludedUsers) { + $ToBeExcluded = New-Object System.Collections.Generic.List[string] + $ExcludedUsers.split(",").trim() | ForEach-Object { if ($_) { $ToBeExcluded.Add($_) } } + Write-Warning "The Following Users will not have your selected tools disabled. $ToBeExcluded" + $UserProfiles = Get-UserHives -IncludeDefault -ExcludedUsers $ToBeExcluded + } + else { + $UserProfiles = Get-UserHives -IncludeDefault + } + + # Loop through each profile on the machine + Foreach ($UserProfile in $UserProfiles) { + # Load each user's registry hive if not already loaded. Backticked "UserProfile.UserHive" so that it accounts for spaces in the username. + If (($ProfileWasLoaded = Test-Path Registry::HKEY_USERS\$($UserProfile.SID)) -eq $false) { + Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe LOAD HKU\$($UserProfile.SID) `"$($UserProfile.UserHive)`"" -Wait -WindowStyle Hidden + } + # The path is different for each individual user. This is the base path. + $key = "Registry::HKEY_USERS\$($UserProfile.SID)" + + # List of checkbox items + $CheckboxItems = "cmd", "theControlPanel", "mmc", "theRegistryEditor", "run", "taskManager" + # Checkboxes come in as environmental variables. This'll grab the ones that were selected (if any) + $EnvItems = Get-ChildItem env:* | Where-Object { $CheckboxItems -contains $_.Name -and $_.Value -notlike "false" } + + # This will grab the tool selections from the parameter field. Since it comes in as a string we'll have to split it up. + $Tool = $Tools.split(",").trim() + + # If the checkbox for all was selected I can just run the function once instead of running it repeatedly for the same thing. + if ($env:allTools -and $env:allTools -notlike "false") { + Set-Tool -Tool "All" -Key $key + } + elseif ($EnvItems) { + # If checkboxes were used we should just use those. + $EnvItems | ForEach-Object { Set-Tool -Tool $_.Name -Key $key } + } + else { + $Tool | ForEach-Object { Set-Tool -Tool $_ -Key $key } + } + + # Unload NTuser.dat for user's we loaded previously. + If ($ProfileWasLoaded -eq $false) { + [gc]::Collect() + Start-Sleep -Seconds 1 + Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe UNLOAD HKU\$($UserProfile.SID)" -Wait -WindowStyle Hidden | Out-Null + } + } + +} +end { + +} + diff --git a/Powershell Scripts/Disable Local User.ps1 b/Powershell Scripts/Disable Local User.ps1 index b916156..e6d400d 100644 --- a/Powershell Scripts/Disable Local User.ps1 +++ b/Powershell Scripts/Disable Local User.ps1 @@ -1,91 +1,89 @@ # Disable a local account -#Requires -Version 5.1 - -<# -.SYNOPSIS - Disable a local account -.DESCRIPTION - Disable a local account -.EXAMPLE - -UserName "AdminTest" - Disables the account AdminTest -.EXAMPLE - PS C:\> Disable-LocalAdminAccount.ps1 -UserName "Administrator" - Disables the account AdminTest -.OUTPUTS - None - String[] -.NOTES - Minimum Supported OS: Windows 10, Windows Server 2016+ - Release Notes: Renamed script and added Script Variable support -.COMPONENT - LocalBuiltInAccountManagement -#> - -[CmdletBinding()] -param ( - # User name of a local account - [Parameter()] - [String]$UserName -) - -begin { - if ($env:usernameToDisable -and $env:usernameToDisable -notlike "null") { $UserName = $env:usernameToDisable } - if (-not $UserName) { - Write-Host "UserName Parameter is required." - exit 1 - } - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - if ($p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)) - { Write-Output $true } - else - { Write-Output $false } - } -} -process { - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - if ($(Get-Command -Name "Disable-LocalUser" -ErrorAction SilentlyContinue)) { - # Disables $UserName using Disable-LocalUser - try { - Disable-LocalUser $UserName -Confirm:$false -ErrorAction Stop - if (-not $(Get-LocalUser $UserName | Select-Object -ExpandProperty Enabled)) { - Write-Host "Disabled Account: $UserName" - } - else { - Write-Host "[Error] Failed to Disabled Account: $UserName" - exit 1 - } - } - catch { - Write-Error $_ - Write-Host "[Error] Failed to Disabled Account: $UserName" - exit 1 - } - } - else { - # Disables $UserName using net.exe - net.exe user $UserName /active:no - if ($LASTEXITCODE -gt 0) { - Write-Host "[Error] Failed to Disabled Account: $UserName" - exit 1 - } - if ($(net.exe user $UserName | Select-String -Pattern "Account active") -like "*Yes*") { - Write-Host "Disabled Account: $UserName" - } - else { - Write-Host "[Error] Failed to Disabled Account: $UserName" - exit 1 - } - } -} -end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + Disable a local account +.DESCRIPTION + Disable a local account +.EXAMPLE + -UserName "AdminTest" + Disables the account AdminTest +.EXAMPLE + PS C:\> Disable-LocalAdminAccount.ps1 -UserName "Administrator" + Disables the account AdminTest +.OUTPUTS + None + String[] +.NOTES + Minimum Supported OS: Windows 10, Windows Server 2016+ + Release Notes: Renamed script and added Script Variable support +.COMPONENT + LocalBuiltInAccountManagement +#> + +[CmdletBinding()] +param ( + # User name of a local account + [Parameter()] + [String]$UserName +) + +begin { + if ($env:usernameToDisable -and $env:usernameToDisable -notlike "null") { $UserName = $env:usernameToDisable } + if (-not $UserName) { + Write-Host "UserName Parameter is required." + exit 1 + } + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + if ($p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)) + { Write-Output $true } + else + { Write-Output $false } + } +} +process { + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + if ($(Get-Command -Name "Disable-LocalUser" -ErrorAction SilentlyContinue)) { + # Disables $UserName using Disable-LocalUser + try { + Disable-LocalUser $UserName -Confirm:$false -ErrorAction Stop + if (-not $(Get-LocalUser $UserName | Select-Object -ExpandProperty Enabled)) { + Write-Host "Disabled Account: $UserName" + } + else { + Write-Host "[Error] Failed to Disabled Account: $UserName" + exit 1 + } + } + catch { + Write-Error $_ + Write-Host "[Error] Failed to Disabled Account: $UserName" + exit 1 + } + } + else { + # Disables $UserName using net.exe + net.exe user $UserName /active:no + if ($LASTEXITCODE -gt 0) { + Write-Host "[Error] Failed to Disabled Account: $UserName" + exit 1 + } + if ($(net.exe user $UserName | Select-String -Pattern "Account active") -like "*Yes*") { + Write-Host "Disabled Account: $UserName" + } + else { + Write-Host "[Error] Failed to Disabled Account: $UserName" + exit 1 + } + } +} +end { + +} + diff --git a/Powershell Scripts/Disable Weak TLS and SSL Protocols.ps1 b/Powershell Scripts/Disable Weak TLS and SSL Protocols.ps1 index 41be516..05b8bd7 100644 --- a/Powershell Scripts/Disable Weak TLS and SSL Protocols.ps1 +++ b/Powershell Scripts/Disable Weak TLS and SSL Protocols.ps1 @@ -1,101 +1,99 @@ # Disables TLS 1.0, TLS 1.1, SSL 2.0, SSL 3.0. Enables TLS 1.2. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Disables TLS 1.0, TLS 1.1, SSL 2.0, SSL 3.0. Enables TLS 1.2. -.DESCRIPTION - Disables TLS 1.0, TLS 1.1, SSL 2.0, SSL 3.0. Enables TLS 1.2. -.EXAMPLE - No Parameters Needed - Disables TLS 1.0, TLS 1.1, SSL 2.0, SSL 3.0. Enables TLS 1.2. -.EXAMPLE - -Restart - Disables TLS 1.0, TLS 1.1, SSL 2.0, SSL 3.0. Enables TLS 1.2. Does Restart the computer. -.OUTPUTS - String[] -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Could possibly run on Windows 7 and Server 2008 R2, but PowerShell 5.1 would be required. - Release Notes: Renamed script and added Script Variable support -#> - -[CmdletBinding()] -param ( - [Parameter()] - [Switch]$Restart = [System.Convert]::ToBoolean($env:forceRestart) -) - -begin { - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } -} -process { - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - @( - [PSCustomObject]@{ - Protocol = 'SSL 2.0' - Value = 0 - Default = 1 - } - [PSCustomObject]@{ - Protocol = 'SSL 3.0' - Value = 0 - Default = 1 - } - [PSCustomObject]@{ - Protocol = 'TLS 1.0' - Value = 0 - Default = 1 - } - [PSCustomObject]@{ - Protocol = 'TLS 1.1' - Value = 0 - Default = 1 - } - [PSCustomObject]@{ - Protocol = 'TLS 1.2' - Value = 1 - Default = 0 - } - ) | ForEach-Object { - $RegServerBase = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\$($_.Protocol)\Server" - $RegClientBase = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\$($_.Protocol)\Client" - - New-Item $RegServerBase -Force -ErrorAction SilentlyContinue | Out-Null - New-ItemProperty -Path $RegServerBase -Name 'Enabled' -Value $($_.Value) -PropertyType 'DWord' -Force -ErrorAction SilentlyContinue | Out-Null - New-ItemProperty -Path $RegServerBase -Name 'DisabledByDefault' -Value $($_.Default) -PropertyType 'DWord' -Force -ErrorAction SilentlyContinue | Out-Null - - New-Item $RegClientBase -Force -ErrorAction SilentlyContinue | Out-Null - New-ItemProperty -Path $RegClientBase -Name 'Enabled' -Value $($_.Value) -PropertyType 'DWord' -Force -ErrorAction SilentlyContinue | Out-Null - New-ItemProperty -Path $RegClientBase -Name 'DisabledByDefault' -Value $($_.Default) -PropertyType 'DWord' -Force -ErrorAction SilentlyContinue | Out-Null - - $State = if ( - $(Get-ItemPropertyValue -Path $RegServerBase -Name 'Enabled') -eq 0 -and - $(Get-ItemPropertyValue -Path $RegServerBase -Name 'DisabledByDefault') -eq 1 - ) { 'disabled' } else { 'enabled' } - - Write-Host "$($_.Protocol) has been $State." - } - - if (-not $Restart) { - Write-Host "Please reboot for settings to take effect." - } - else { - Write-Host "Scheduling reboot for 30 seconds from now!" - Start-Process cmd.exe -ArgumentList "/c shutdown.exe /r /t 30" - } -} -end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + Disables TLS 1.0, TLS 1.1, SSL 2.0, SSL 3.0. Enables TLS 1.2. +.DESCRIPTION + Disables TLS 1.0, TLS 1.1, SSL 2.0, SSL 3.0. Enables TLS 1.2. +.EXAMPLE + No Parameters Needed + Disables TLS 1.0, TLS 1.1, SSL 2.0, SSL 3.0. Enables TLS 1.2. +.EXAMPLE + -Restart + Disables TLS 1.0, TLS 1.1, SSL 2.0, SSL 3.0. Enables TLS 1.2. Does Restart the computer. +.OUTPUTS + String[] +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Could possibly run on Windows 7 and Server 2008 R2, but PowerShell 5.1 would be required. + Release Notes: Renamed script and added Script Variable support +#> + +[CmdletBinding()] +param ( + [Parameter()] + [Switch]$Restart = [System.Convert]::ToBoolean($env:forceRestart) +) + +begin { + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } +} +process { + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + @( + [PSCustomObject]@{ + Protocol = 'SSL 2.0' + Value = 0 + Default = 1 + } + [PSCustomObject]@{ + Protocol = 'SSL 3.0' + Value = 0 + Default = 1 + } + [PSCustomObject]@{ + Protocol = 'TLS 1.0' + Value = 0 + Default = 1 + } + [PSCustomObject]@{ + Protocol = 'TLS 1.1' + Value = 0 + Default = 1 + } + [PSCustomObject]@{ + Protocol = 'TLS 1.2' + Value = 1 + Default = 0 + } + ) | ForEach-Object { + $RegServerBase = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\$($_.Protocol)\Server" + $RegClientBase = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\$($_.Protocol)\Client" + + New-Item $RegServerBase -Force -ErrorAction SilentlyContinue | Out-Null + New-ItemProperty -Path $RegServerBase -Name 'Enabled' -Value $($_.Value) -PropertyType 'DWord' -Force -ErrorAction SilentlyContinue | Out-Null + New-ItemProperty -Path $RegServerBase -Name 'DisabledByDefault' -Value $($_.Default) -PropertyType 'DWord' -Force -ErrorAction SilentlyContinue | Out-Null + + New-Item $RegClientBase -Force -ErrorAction SilentlyContinue | Out-Null + New-ItemProperty -Path $RegClientBase -Name 'Enabled' -Value $($_.Value) -PropertyType 'DWord' -Force -ErrorAction SilentlyContinue | Out-Null + New-ItemProperty -Path $RegClientBase -Name 'DisabledByDefault' -Value $($_.Default) -PropertyType 'DWord' -Force -ErrorAction SilentlyContinue | Out-Null + + $State = if ( + $(Get-ItemPropertyValue -Path $RegServerBase -Name 'Enabled') -eq 0 -and + $(Get-ItemPropertyValue -Path $RegServerBase -Name 'DisabledByDefault') -eq 1 + ) { 'disabled' } else { 'enabled' } + + Write-Host "$($_.Protocol) has been $State." + } + + if (-not $Restart) { + Write-Host "Please reboot for settings to take effect." + } + else { + Write-Host "Scheduling reboot for 30 seconds from now!" + Start-Process cmd.exe -ArgumentList "/c shutdown.exe /r /t 30" + } +} +end { + +} + diff --git a/Powershell Scripts/Disk Space Alert.ps1 b/Powershell Scripts/Disk Space Alert.ps1 index 527c2c2..4fd4123 100644 --- a/Powershell Scripts/Disk Space Alert.ps1 +++ b/Powershell Scripts/Disk Space Alert.ps1 @@ -1,1763 +1,1598 @@ # Reports on the current volumes on the system. All volumes that are not the System Drive are considered data volumes. Raises an alert if one or more volumes have less than a specified percentage or data size. -#Requires -Version 2.0 - -<# -.SYNOPSIS - Reports on the current volumes on the system. All volumes that are not the 'System Drive' are considered data volumes. Raises an alert if one or more volumes have less than a specified percentage or data size. -.DESCRIPTION - Reports on the current volumes on the system. All volumes that are not the 'System Drive' are considered data volumes. Raises an alert if one or more volumes have less than a specified percentage or data size. -.EXAMPLE - -SystemVolumeMinFreePercent "99%" -SystemVolumeMinFreeSize "100TB" -DataVolumeMinFreePercent "99%" -DataVolumeMinFreeSize "100TB" - -ExcludeDataVolumesFromAlert "WINRETOOLS, ESP, DELLSUPPORT, Windows RE Tools, SYSTEM, System Reserved" - - Retrieving the system volume 'C:'. - Retrieving all data volumes. - - Converting the minimum free space percentage '99%' required for system volumes into bytes. - Converting the minimum free space required '100TB' for system volumes into bytes. - Converting the minimum free space percentage '99%' required into bytes for each data volume. - Converting the minimum free space required '100TB' for data volumes into bytes. - - [Alert] The system volume exceeds the 'Minimum Percent Free' limit of '99%'. - [Alert] The system volume exceeds the 'Minimum Free Size' limit of '100TB'. - - ### System Volume ### - Name DriveLetter FileSystemType FreeSpace Total PercentageFree - ---- ----------- -------------- --------- ----- -------------- - C: NTFS 115.26 GB 126.9 GB 90.83% - - ### Data Volumes ### - Name : System Reserved - DriveLetter : - FileSystemType : NTFS - Path : \\?\Volume{af2c6d55-e8c1-11ef-95b1-806e6f6e6963}\ - FreeSpace : 71.87 MB - Total : 100 MB - PercentageFree : 71.87% - -.PARAMETER -SystemVolumeMinFreePercent "10%" - Specifies the minimum percentage of free space that must remain available on the system volume to avoid triggering an alert. - For example, if set to 10, an alert will be raised if the system volume has less than 10% free space. - -.PARAMETER -SystemVolumeMinFreeSize "1TB" - Specifies the minimum amount of free space that must remain available on the system volume to avoid triggering an alert. - If no units are specified, gigabytes will be used. - For example, if set to 10, an alert will be raised if the system volume has less than 10GB of free space. - -.PARAMETER -DataVolumeMinFreePercent "10%" - Specifies the minimum percentage of free space that must remain available on each data volume to avoid triggering an alert. - For example, if set to 10, an alert will be raised if a data volume has less than 10% free space. - -.PARAMETER -DataVolumeMinFreeSize "1TB" - Specifies the minimum amount of free space that must remain available on each volume to avoid triggering an alert. - If no units are specified, gigabytes will be used. - For example, if set to 10, an alert will be raised if a data volume has less than 10GB of free space. - -.PARAMETER -ExcludeDataVolumesFromAlert "WINRETOOLS, ESP, DELLSUPPORT, Windows RE Tools, SYSTEM, System Reserved" - Allows you to specify a comma-separated list of data volumes you would like to exclude from the alert. - You can specify either the volume label, path, or drive letter. - -.PARAMETER -AlertOnlyForDataVolumes "E:\" - If you would only like an alert for specific volume(s), specify the volume(s) you would like the alert to trigger for. - You can specify either the volume label, path, or drive letter. - -.PARAMETER -MultilineCustomField "ReplaceMeWithAnyMultilineCustomField" - Stores a multiline report on the current volumes on the system. - -.PARAMETER -WYSIWYGCustomField "ReplaceMeWithAnyWYSIWYGCustomField" - Stores a WYSIWYG report on the current volumes on the system. - -.PARAMETER -SystemVolumeMinFreePercentCustomField "ReplaceMeWithAnyTextCustomField" - Optionally retrieves the 'SystemVolumeMinFreePercent' value from a custom field you specify if no value is currently set. - -.PARAMETER -SystemVolumeMinFreeSizeCustomField "ReplaceMeWithAnyTextCustomField" - Optionally retrieves the 'SystemVolumeMinFreeSize' value from a custom field you specify if no value is currently set. - -.PARAMETER -DataVolumeMinFreePercentCustomField "ReplaceMeWithAnyTextCustomField" - Optionally retrieves the 'DataVolumeMinFreePercent' value from a custom field you specify if no value is currently set. - -.PARAMETER -DataVolumeMinFreeSizeCustomField "ReplaceMeWithAnyTextCustomField" - Optionally retrieves the 'DataVolumeMinFreeSize' value from a custom field you specify if no value is currently set. - -.PARAMETER -ExcludeDataVolumeCustomField "ReplaceMeWithAnyTextCustomField" - Optionally retrieves the 'ExcludeDataVolumesFromAlert' value from a custom field you specify if no value is currently set. - -.PARAMETER -AlertOnlyCustomField "ReplaceMeWithAnyTextCustomField" - Optionally retrieves the 'AlertOnlyForDataVolumes' value from a custom field you specify if no value is currently set. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2012 R2 - Release Notes: Improved parameter validation, added the ability to save to custom fields, enhanced reporting to include all volumes - (not just those triggering an alert), switched from drives to volumes, introduced an include option, added unit conversion, and - simplified exclude options. -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$SystemVolumeMinFreePercent, - [Parameter()] - [String]$SystemVolumeMinFreeSize, - [Parameter()] - [String]$DataVolumeMinFreePercent, - [Parameter()] - [String]$DataVolumeMinFreeSize, - [Parameter()] - [String]$ExcludeDataVolumesFromAlert = "WINRETOOLS, ESP, DELLSUPPORT, Windows RE Tools, SYSTEM, System Reserved", - [Parameter()] - [String]$AlertOnlyForDataVolumes, - [Parameter()] - [String]$MultilineCustomField, - [Parameter()] - [String]$WYSIWYGCustomField, - [Parameter()] - [String]$SystemVolumeMinFreePercentCustomField, - [Parameter()] - [String]$SystemVolumeMinFreeSizeCustomField, - [Parameter()] - [String]$DataVolumeMinFreePercentCustomField, - [Parameter()] - [String]$DataVolumeMinFreeSizeCustomField, - [Parameter()] - [String]$ExcludeDataVolumeCustomField, - [Parameter()] - [String]$AlertOnlyCustomField -) - -begin { - # If script form variables are used, replace the command line parameters with their value. - if ($env:systemVolumeMinimumPercentageFree) { $SystemVolumeMinFreePercent = $env:systemVolumeMinimumPercentageFree } - if ($env:systemVolumeMinimumFreeSize) { $SystemVolumeMinFreeSize = $env:systemVolumeMinimumFreeSize } - if ($env:dataVolumeMinimumPercentageFree) { $DataVolumeMinFreePercent = $env:dataVolumeMinimumPercentageFree } - if ($env:dataVolumeMinimumFreeSize) { $DataVolumeMinFreeSize = $env:dataVolumeMinimumFreeSize } - if ($env:excludeDataVolumesFromAlert) { $ExcludeDataVolumesFromAlert = $env:excludeDataVolumesFromAlert } - if ($env:alertOnlyForDataVolumes) { $AlertOnlyForDataVolumes = $env:alertOnlyForDataVolumes } - if ($env:multilineCustomFieldName) { $MultilineCustomField = $env:multilineCustomFieldName } - if ($env:wysiwygCustomFieldName) { $WYSIWYGCustomField = $env:wysiwygCustomFieldName } - if ($env:retrieveSystemVolumeMinimumPercentageFreeFromCustomField) { $SystemVolumeMinFreePercentCustomField = $env:retrieveSystemVolumeMinimumPercentageFreeFromCustomField } - if ($env:retrieveSystemVolumeMinimumFreeSizeFromCustomField) { $SystemVolumeMinFreeSizeCustomField = $env:retrieveSystemVolumeMinimumFreeSizeFromCustomField } - if ($env:retrieveDataVolumeMinimumPercentageFreeFromCustomField) { $DataVolumeMinFreePercentCustomField = $env:retrieveDataVolumeMinimumPercentageFreeFromCustomField } - if ($env:retrieveDataVolumeMinimumFreeSizeFromCustomField) { $DataVolumeMinFreeSizeCustomField = $env:retrieveDataVolumeMinimumFreeSizeFromCustomField } - if ($env:retrieveExcludeDataVolumesFromCustomField) { $ExcludeDataVolumeCustomField = $env:retrieveExcludeDataVolumesFromCustomField } - if ($env:retrieveAlertOnlyForDataVolumesFromCustomField) { $AlertOnlyCustomField = $env:retrieveAlertOnlyForDataVolumesFromCustomField } - - - # Define an array of custom fields to validate - $CustomFields = @( - $SystemVolumeMinFreePercentCustomField, - $SystemVolumeMinFreeSizeCustomField, - $DataVolumeMinFreePercentCustomField, - $DataVolumeMinFreeSizeCustomField, - $ExcludeDataVolumeCustomField, - $AlertOnlyCustomField, - $MultilineCustomField, - $WYSIWYGCustomField - ) - - # Check if the PowerShell version is less than 3 and custom fields are being used - if ($PSVersionTable.PSVersion.Major -lt 3 -and ($CustomFields | Where-Object { $_ })) { - Write-Host -Object "[Error] Setting custom fields is not supported in PowerShell 2.0." - Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/4405408656013-Custom-Fields-and-Documentation-CLI-and-Scripting" - exit 1 - } - - # Validate the 'System Volume Minimum Percentage Free' custom field - if ($SystemVolumeMinFreePercentCustomField) { - $SystemVolumeMinFreePercentCustomField = $SystemVolumeMinFreePercentCustomField.Trim() - - if (!($SystemVolumeMinFreePercentCustomField)) { - Write-Host -Object "[Error] The 'Retrieve System Volume Minimum Percentage Free from Custom Field' is invalid." - Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'System Volume Minimum Percentage Free' value from, or leave it blank." - Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" - exit 1 - } - - if ($SystemVolumeMinFreePercentCustomField -match "[^0-9A-Z]") { - Write-Host -Object "[Error] The 'Retrieve System Volume Minimum Percentage Free from Custom Field' of '$SystemVolumeMinFreePercentCustomField' is invalid as it contains invalid characters." - Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'System Volume Minimum Percentage Free' value from, or leave it blank." - Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" - exit 1 - } - } - - # Validate the 'System Volume Minimum Free Size' custom field - if ($SystemVolumeMinFreeSizeCustomField) { - $SystemVolumeMinFreeSizeCustomField = $SystemVolumeMinFreeSizeCustomField.Trim() - - if (!($SystemVolumeMinFreeSizeCustomField)) { - Write-Host -Object "[Error] The 'Retrieve System Volume Minimum Free Size from Custom Field' is invalid." - Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'System Volume Minimum Free Size' value from, or leave it blank." - Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" - exit 1 - } - - if ($SystemVolumeMinFreeSizeCustomField -match "[^0-9A-Z]") { - Write-Host -Object "[Error] The 'Retrieve System Volume Minimum Free Size from Custom Field' of '$SystemVolumeMinFreeSizeCustomField' is invalid as it contains invalid characters." - Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'System Volume Minimum Free Size' value from, or leave it blank." - Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" - exit 1 - } - } - - # Validate the 'Data Volume Minimum Percentage Free' custom field - if ($DataVolumeMinFreePercentCustomField) { - $DataVolumeMinFreePercentCustomField = $DataVolumeMinFreePercentCustomField.Trim() - - if (!($DataVolumeMinFreePercentCustomField)) { - Write-Host -Object "[Error] The 'Retrieve Data Volume Minimum Percentage Free from Custom Field' is invalid." - Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'Data Volume Minimum Percentage Free' value from, or leave it blank." - Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" - exit 1 - } - - if ($DataVolumeMinFreePercentCustomField -match "[^0-9A-Z]") { - Write-Host -Object "[Error] The 'Retrieve Data Volume Minimum Percentage Free from Custom Field' of '$DataVolumeMinFreePercentCustomField' is invalid as it contains invalid characters." - Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'Data Volume Minimum Percentage Free' value from, or leave it blank." - Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" - exit 1 - } - } - - # Validate the 'Data Volume Minimum Free Size' custom field - if ($DataVolumeMinFreeSizeCustomField) { - $DataVolumeMinFreeSizeCustomField = $DataVolumeMinFreeSizeCustomField.Trim() - - if (!($DataVolumeMinFreeSizeCustomField)) { - Write-Host -Object "[Error] The 'Retrieve Data Volume Minimum Free Size from Custom Field' is invalid." - Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'Data Volume Minimum Free Size' value from, or leave it blank." - Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" - exit 1 - } - - if ($DataVolumeMinFreeSizeCustomField -match "[^0-9A-Z]") { - Write-Host -Object "[Error] The 'Retrieve Data Volume Minimum Free Size from Custom Field' of '$DataVolumeMinFreeSizeCustomField' is invalid as it contains invalid characters." - Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'Data Volume Minimum Free Size' value from, or leave it blank." - Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" - exit 1 - } - } - - # Validate the 'Exclude Data Volumes from Alert' custom field - if ($ExcludeDataVolumeCustomField) { - $ExcludeDataVolumeCustomField = $ExcludeDataVolumeCustomField.Trim() - - if (!($ExcludeDataVolumeCustomField)) { - Write-Host -Object "[Error] The 'Retrieve Exclude Data Volumes from Alert from Custom Field' is invalid." - Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'Exclude Data Volumes from Alert' value from, or leave it blank." - Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" - exit 1 - } - - if ($ExcludeDataVolumeCustomField -match "[^0-9A-Z]") { - Write-Host -Object "[Error] The 'Retrieve Exclude Data Volumes from Alert from Custom Field' of '$ExcludeDataVolumeCustomField' is invalid as it contains invalid characters." - Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'Exclude Data Volumes from Alert' value from, or leave it blank." - Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" - exit 1 - } - } - - # Validate the 'Alert Only for Data Volumes' custom field - if ($AlertOnlyCustomField) { - $AlertOnlyCustomField = $AlertOnlyCustomField.Trim() - - if (!($AlertOnlyCustomField)) { - Write-Host -Object "[Error] The 'Retrieve Alert Only for Data Volumes from Custom Field' is invalid." - Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'Alert Only for Data Volumes' value from, or leave it blank." - Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" - exit 1 - } - - if ($AlertOnlyCustomField -match "[^0-9A-Z]") { - Write-Host -Object "[Error] The 'Retrieve Alert Only for Data Volumes from Custom Field' of '$AlertOnlyCustomField' is invalid as it contains invalid characters." - Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'Alert Only for Data Volumes' value from, or leave it blank." - Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" - exit 1 - } - } - - # Validate the 'Multiline Custom Field Name' - if ($MultilineCustomField) { - $MultilineCustomField = $MultilineCustomField.Trim() - - if (!($MultilineCustomField)) { - Write-Host -Object "[Error] The 'Multiline Custom Field Name' is invalid." - Write-Host -Object "[Error] Please provide a valid multiline custom field name to save the results, or leave it blank." - Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" - exit 1 - } - - if ($MultilineCustomField -match "[^0-9A-Z]") { - Write-Host -Object "[Error] The 'Multiline Custom Field Name' of '$MultilineCustomField' is invalid as it contains invalid characters." - Write-Host -Object "[Error] Please provide a valid multiline custom field name to save the results, or leave it blank." - Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" - exit 1 - } - } - - # Validate the 'WYSIWYG Custom Field Name' - if ($WYSIWYGCustomField) { - $WYSIWYGCustomField = $WYSIWYGCustomField.Trim() - - if (!($WYSIWYGCustomField)) { - Write-Host -Object "[Error] The 'WYSIWYG Custom Field Name' is invalid." - Write-Host -Object "[Error] Please provide a valid WYSIWYG custom field name to save the results, or leave it blank." - Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" - exit 1 - } - - if ($WYSIWYGCustomField -match "[^0-9A-Z]") { - Write-Host -Object "[Error] The 'WYSIWYG Custom Field Name' of '$WYSIWYGCustomField' is invalid as it contains invalid characters." - Write-Host -Object "[Error] Please provide a valid WYSIWYG custom field name to save the results, or leave it blank." - Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" - exit 1 - } - } - - # Check for duplicate custom field names - $DuplicateFields = ($CustomFields | Where-Object { $_ } | Group-Object | Where-Object { $_.Count -gt 1 }).Group - if (($CustomFields | Where-Object { $_ } | Group-Object | Where-Object { $_.Count -gt 1 })) { - Write-Host -Object "[Error] You must provide a unique name for each custom field you would like to either retrieve the value from or save to." - Write-Host -Object "[Error] Or you can leave the value blank." - Write-Host -Object "[Error] Duplicate Field Names Given: $($DuplicateFields -join ', ')" - Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" - exit 1 - } - - # Convert file size strings (e.g., "50 GB") into their equivalent numeric byte values. - function ConvertTo-Bytes { - [CmdletBinding()] - param ( - [Parameter(ValueFromPipeline = $True)] - [String[]]$FileSize, - [Parameter()] - [String]$DefaultTo - ) - process { - # Check if the input is null or an empty string. - if ([String]::IsNullOrEmpty($FileSize)) { - throw (New-Object System.ArgumentNullException("You must provide a file size string to convert into bytes.")) - } - - # Define an array of valid default unit options. - $ValidDefaults = "PB", "TB", "GB", "MB", "KB", "B", "Bytes" - - # Check if $DefaultTo has a value and is not in the list of valid defaults. - if ($DefaultTo -and $ValidDefaults -notcontains $DefaultTo) { - throw (New-Object System.ArgumentOutOfRangeException("You cannot default to '$DefaultTo'. Valid default options include: 'PB', 'TB', 'GB', 'MB', 'KB', 'B', and 'Bytes'.")) - } - - # Create a generic list to store validated file size strings. - $FileSizesToConvert = New-Object System.Collections.Generic.List[String] - - # Iterate over each provided file size string. - $FileSize | ForEach-Object { - # Store the trimmed input string for easier reference. - $CurrentFileSizeObject = $_.Trim() - - if (!($CurrentFileSizeObject)) { - Write-Error -Category ObjectNotFound -Exception (New-Object System.ArgumentNullException("FileSize", "An empty file size string was given. Unable to convert null into bytes.")) - return - } - - # Validate the string for any characters not allowed. - # This regex permits digits, the period, whitespace, dashes, and valid unit strings (PB, TB, GB, MB, KB, B, Bytes). - if ($CurrentFileSizeObject -match "[^0-9. (PB|TB|GB|MB|KB|B|Bytes)-]") { - Write-Error -Category InvalidArgument -Exception (New-Object System.ArgumentException("The file size of '$CurrentFileSizeObject' is invalid; it contains invalid characters. Please specify a file size such as '50 GB'.")) - return - } - - # Validate the overall format of the file size string. - if ($CurrentFileSizeObject -notmatch "^-?[0-9]+\.?[0-9]*\s*(PB|TB|GB|MB|KB|B|Bytes)?$") { - Write-Error -Category InvalidArgument -Exception (New-Object System.ArgumentException("The file size of '$CurrentFileSizeObject' is invalid; it's in an invalid format. Please specify a file size such as '50 GB'")) - return - } - - # If the trimmed input does not end with one of the valid units (PB, TB, GB, MB, KB, B, or Bytes) - if ($CurrentFileSizeObject -notmatch "(PB|TB|GB|MB|KB|B|Bytes)$") { - - # Determine which unit to append based on the default unit ($DefaultTo) - $NewFileSize = switch ($DefaultTo) { - 'PB' { "$CurrentFileSizeObject PB" } - 'TB' { "$CurrentFileSizeObject TB" } - 'GB' { "$CurrentFileSizeObject GB" } - 'MB' { "$CurrentFileSizeObject MB" } - 'KB' { "$CurrentFileSizeObject KB" } - default { "$CurrentFileSizeObject Bytes" } - } - - # Add the newly formatted string to the list of file sizes to convert. - $FileSizesToConvert.Add($NewFileSize) - - return - } - - # Add the validated, trimmed file size string to our list. - $FileSizesToConvert.Add($CurrentFileSizeObject) - } - - # If no valid file sizes were found, throw an error. - if ($FileSizesToConvert.Count -lt 1) { - throw (New-Object System.ArgumentNullException("You must provide a file size string to convert into bytes.")) - } - - # Process each validated file size string and convert it into bytes. - $FileSizesToConvert | ForEach-Object { - $DigitCharacters = $Null - - try { - # Extract the numeric portion from the string (digits, decimal point, and optional minus sign) - # and convert it to a decimal. - [decimal]$DigitCharacters = $_ -replace '[^0-9.-]' - } - catch { - $_ - return - } - - # Determine the unit in the file size string using regex in a switch statement. - # Multiply the numeric value by the corresponding byte constant. - switch -regex ($_) { - 'PB$' { $DigitCharacters * 1PB; break } - 'TB$' { $DigitCharacters * 1TB; break } - 'GB$' { $DigitCharacters * 1GB; break } - 'MB$' { $DigitCharacters * 1MB; break } - 'KB$' { $DigitCharacters * 1KB; break } - 'B$' { $DigitCharacters * 1; break } - 'Bytes$' { $DigitCharacters * 1; break } - } - } - } - } - - # Convert file sizes given in bytes to a human-friendly string format (e.g., "2.8 MB"). - function ConvertTo-FriendlySize { - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline = $True)] - [long[]]$Bytes, - [Parameter()] - [long]$RoundTo = 2 - ) - process { - # Validate input: If $Bytes is null or an empty string set Bytes equal to 0. - if ([String]::IsNullOrEmpty($Bytes)) { - $Bytes = 0 - } - - # Process each file size in the input array. - $Bytes | ForEach-Object { - $ConvertedBytes = $Null - - # If the current file size is 0, immediately output "0 Bytes" and skip further processing. - if ($_ -eq 0) { - "0 Bytes" - return - } - - # Define an array of size units from Bytes to Zettabytes - $DataSizes = 'Bytes,KB,MB,GB,TB,PB,EB,ZB' -split ',' - - try { - # Initialize the conversion variable with the current byte value. - $ConvertedBytes = $_ - - # This loop repeats as long as the value is divisible by 1KB, incrementing the index by 1 each time. - # The index is later used to select the appropriate unit for the human-friendly string. - for ( $Index = 0; ($ConvertedBytes -ge 1KB -or $ConvertedBytes -le -1KB) -and $Index -lt $DataSizes.Count; $Index++ ) { - $ConvertedBytes = $ConvertedBytes / 1KB - } - } - catch { - $_ - return - } - - # If conversion resulted in a null or false value, write an error message. - if (!($ConvertedBytes)) { - Write-Error -Category ObjectNotFound -Exception (New-Object System.Data.ObjectNotFoundException("Failed to convert '$_' into a human-friendly string.")) - return - } - - # Format the converted value rounded to the specified number of decimal places, - # and append the corresponding unit from the $DataSizes array. - "$([System.Math]::Round($ConvertedBytes, $RoundTo)) $($DataSizes[$Index])" - } - } - } - - function Set-CustomField { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$Type, - [Parameter()] - [String]$DocumentName, - [Parameter()] - [Switch]$Piped - ) - - if ($Type -eq "Date Time") { $Type = "DateTime" } - if ($Type -match "[-]") { $Type = $Type -replace '-' } - if ($Type -match "[/]") { $Type = $Type -replace '/' } - - # Remove the non-breaking space character - if ($Type -eq "WYSIWYG") { - $Value = $Value -replace ' ', ' ' - } - - if ($Type -eq "DateTime" -or $Type -eq "Date") { - $Type = "Date or Date Time" - } - - # Measure the number of characters in the provided value - $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # Throw an error if the value exceeds the character limit of 200,000 characters - if ($Piped -and $Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - if (!$Piped -and $Characters -ge 45000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") - } - - # Initialize a hashtable for additional documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define a list of valid field types - $ValidFields = "Checkbox", "Date", "Date or Date Time", "DateTime", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", - "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - - # Warn the user if the provided type is not valid - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # Define types that require options to be retrieved - $NeedsOptions = "Dropdown", "MultiSelect" - - # If the property is being set in a document or field and the type needs options, retrieve them - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an error if there was an issue retrieving the property options - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Process the property value based on its type - switch ($Type) { - "Checkbox" { - # Convert the value to a boolean for Checkbox type - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Convert the value to a Unix timestamp for Date or Date Time type - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - [long]$NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Convert the dropdown value to its corresponding GUID - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - # Throw an error if the value is not present in the dropdown options - if (!($Selection)) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - "MultiSelect" { - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selections = New-Object System.Collections.Generic.List[String] - if ($Value -match "[,]") { - $Value = $Value -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } - } - - $Value | ForEach-Object { - $GivenValue = $_ - $Selection = $Options | Where-Object { $_.Name -eq $GivenValue } | Select-Object -ExpandProperty GUID - - # Throw an error if the value is not present in the dropdown options - if (!($Selection)) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $Selections.Add($Selection) - } - - $NinjaValue = $Selections -join "," - } - "Time" { - # Convert the value to a Unix timestamp for Date or Date Time type - $LocalTime = (Get-Date $Value) - $LocalTimeZone = [TimeZoneInfo]::Local - $UtcTime = [TimeZoneInfo]::ConvertTimeToUtc($LocalTime, $LocalTimeZone) - - [long]$NinjaValue = ($UtcTime.TimeOfDay).TotalSeconds - } - default { - # For other types, use the value as is - $NinjaValue = $Value - } - } - - # Set the property value in the document if a document name is provided - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - try { - # Otherwise, set the standard property value - if ($Piped) { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - } - catch { - throw $_.Exception.Message - } - } - - # Throw an error if setting the property failed - if ($CustomField.Exception) { - throw $CustomField - } - } - - function Get-CustomField { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter()] - [String]$DocumentName - ) - # Initialize a hashtable for documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - if ($Type -eq "Date Time") { $Type = "DateTime" } - if ($Type -match "[-]") { $Type = $Type -replace '-' } - if ($Type -match "[/]") { $Type = $Type -replace '/' } - - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "DateTime", "Decimal", "Device Dropdown", "Device MultiSelect", "Dropdown", - "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Organization Dropdown", "Organization Location Dropdown", - "Organization Location MultiSelect", "Organization MultiSelect", "Phone", "Secure", "Text", "Time", "WYSIWYG", "URL" - if ($Type -and $ValidFields -notcontains $Type) { - Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" - } - - # Define types that require options to be retrieved - $NeedsOptions = "DropDown", "MultiSelect" - - # If a document name is provided, retrieve the property value from the document - if ($DocumentName) { - - $NinjaPropertyValue = Ninja-Property-Docs-Get -AttributeName $Name @DocumentationParams 2>&1 - - # If the property type requires options, retrieve them - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - # If no document name is provided, retrieve the property value directly - $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1 - - # If the property type requires options, retrieve them - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an exception if there was an error retrieving the property value or options - if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Throw an error if the retrieved property value is null or empty - if (!($NinjaPropertyValue)) { - return - } - - # Handle the property value based on its type - switch ($Type) { - "Attachment" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Checkbox" { - # Convert the value to a boolean - [System.Convert]::ToBoolean([int]$NinjaPropertyValue) - } - "Date" { - # Convert a Unix timestamp to local date and time - $UnixTimeStamp = $NinjaPropertyValue - $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp) - $TimeZone = [TimeZoneInfo]::Local - $ConvertedDate = [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) - - Get-Date $ConvertedDate -DisplayHint Date - } - "Date or Date Time" { - # Convert a Unix timestamp to local date and time - $UnixTimeStamp = $NinjaPropertyValue - $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp) - $TimeZone = [TimeZoneInfo]::Local - [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) - } - "DateTime" { - # Convert a Unix timestamp to local date and time - $UnixTimeStamp = $NinjaPropertyValue - $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp) - $TimeZone = [TimeZoneInfo]::Local - $ConvertedDate = [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) - - Get-Date $ConvertedDate -DisplayHint DateTime - } - "Decimal" { - # Convert the value to a double (floating-point number) - [double]$NinjaPropertyValue - } - "Device Dropdown" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Device MultiSelect" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Dropdown" { - # Convert options to a CSV format and match the GUID to retrieve the display name - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name - } - "Integer" { - # Convert the value to an integer - [int]$NinjaPropertyValue - } - "MultiSelect" { - # Convert options to a CSV format, then match and return selected items - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = ($NinjaPropertyValue -split ',').trim() - - foreach ($Item in $Selection) { - $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name - } - } - "Organization Dropdown" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization Location Dropdown" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization Location MultiSelect" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization MultiSelect" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Time" { - $secondsSinceMidnightUTC = $NinjaPropertyValue - # Get midnight for today in UTC - $midnightUTC = [datetime]::UtcNow.Date - - # Add the seconds to midnight to get the UTC time - $utcTime = $midnightUTC.AddSeconds($secondsSinceMidnightUTC) - - # Convert the UTC time to the local time zone - $localTime = [System.TimeZoneInfo]::ConvertTimeFromUtc($utcTime, [System.TimeZoneInfo]::Local) - - # Display the result - Get-Date $localTime -DisplayHint Time - } - default { - # For any other types, return the raw value - $NinjaPropertyValue - } - } - } - - - function Test-IsElevated { - [CmdletBinding()] - param () - - # Get the current Windows identity of the user running the script - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - - # Create a WindowsPrincipal object based on the current identity - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - - # Check if the current user is in the Administrator role - # The function returns $True if the user has administrative privileges, $False otherwise - # 544 is the value for the Built-In Administrators role - # Reference: https://learn.microsoft.com/en-us/dotnet/api/system.security.principal.windowsbuiltinrole - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]'544') - } - - if (!($ExitCode)) { - $ExitCode = 0 - } -} -process { - # Attempt to determine if the current session is running with Administrator privileges. - try { - $IsElevated = Test-IsElevated -ErrorAction Stop - } - catch { - # Log an error if unable to determine admin privileges - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Unable to determine if the account '$env:Username' is running with Administrator privileges." - exit 1 - } - - # Exit if the script is not running with Administrator privileges - if (!($IsElevated)) { - Write-Host -Object "[Error] Access Denied: Please run with Administrator privileges." - exit 1 - } - - # Retrieve 'System Volume Minimum Percentage Free' from a custom field if specified and not already provided - if ($SystemVolumeMinFreePercentCustomField -and !($SystemVolumeMinFreePercent)) { - try { - Write-Host -Object "Attempting to retrieve the 'System Volume Minimum Percentage Free' value from the field '$SystemVolumeMinFreePercentCustomField'." - $SystemVolumeMinFreePercent = Get-CustomField -Name $SystemVolumeMinFreePercentCustomField -ErrorAction Stop - Write-Host -Object "Successfully retrieved the custom field contents.`n" - } - catch { - # Log an error if retrieval fails - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to retrieve the 'System Volume Minimum Percentage Free' from the custom field '$SystemVolumeMinFreePercentCustomField'." - exit 1 - } - - # Trim and validate the retrieved value - if ($SystemVolumeMinFreePercent) { - $SystemVolumeMinFreePercent = $SystemVolumeMinFreePercent.Trim() - } - - if (!($SystemVolumeMinFreePercent)) { - Write-Host -Object "[Error] The custom field '$SystemVolumeMinFreePercentCustomField' is empty." - Write-Host -Object "[Error] Please save a value to the custom field or leave 'Retrieve System Volume Minimum Percentage Free from Custom Field' blank." - exit 1 - } - } - - # Retrieve 'System Volume Minimum Free Size' from a custom field if specified and not already provided - if ($SystemVolumeMinFreeSizeCustomField -and !($SystemVolumeMinFreeSize)) { - try { - Write-Host -Object "Attempting to retrieve the 'System Volume Minimum Free Size' value from the field '$SystemVolumeMinFreeSizeCustomField'." - $SystemVolumeMinFreeSize = Get-CustomField -Name $SystemVolumeMinFreeSizeCustomField -ErrorAction Stop - Write-Host -Object "Successfully retrieved the custom field contents.`n" - } - catch { - # Log an error if retrieval fails - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to retrieve the 'System Volume Minimum Free Size' from the custom field '$SystemVolumeMinFreeSizeCustomField'." - exit 1 - } - - # Trim and validate the retrieved value - if ($SystemVolumeMinFreeSize) { - $SystemVolumeMinFreeSize = $SystemVolumeMinFreeSize.Trim() - } - - if (!($SystemVolumeMinFreeSize)) { - Write-Host -Object "[Error] The custom field '$SystemVolumeMinFreeSizeCustomField' is empty." - Write-Host -Object "[Error] Please save a value to the custom field or leave 'Retrieve System Volume Minimum Free Size from Custom Field' blank." - exit 1 - } - } - - # Retrieve 'Data Volume Minimum Percentage Free' from a custom field if specified and not already provided - if ($DataVolumeMinFreePercentCustomField -and !($DataVolumeMinFreePercent)) { - try { - Write-Host -Object "Attempting to retrieve the 'Data Volume Minimum Percentage Free' value from the field '$DataVolumeMinFreePercentCustomField'." - $DataVolumeMinFreePercent = Get-CustomField -Name $DataVolumeMinFreePercentCustomField -ErrorAction Stop - Write-Host -Object "Successfully retrieved the custom field contents.`n" - } - catch { - # Log an error if retrieval fails - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to retrieve the 'Data Volume Minimum Percentage Free' from the custom field '$DataVolumeMinFreePercentCustomField'." - exit 1 - } - - # Trim and validate the retrieved value - if ($DataVolumeMinFreePercent) { - $DataVolumeMinFreePercent = $DataVolumeMinFreePercent.Trim() - } - - if (!($DataVolumeMinFreePercent)) { - Write-Host -Object "[Error] The custom field '$DataVolumeMinFreePercentCustomField' is empty." - Write-Host -Object "[Error] Please save a value to the custom field or leave 'Retrieve Data Volume Minimum Percentage Free from Custom Field' blank." - exit 1 - } - } - - # Retrieve 'Data Volume Minimum Free Size' from a custom field if specified and not already provided - if ($DataVolumeMinFreeSizeCustomField -and !($DataVolumeMinFreeSize)) { - try { - Write-Host -Object "Attempting to retrieve the 'Data Volume Minimum Free Size' value from the field '$DataVolumeMinFreeSizeCustomField'." - $DataVolumeMinFreeSize = Get-CustomField -Name $DataVolumeMinFreeSizeCustomField -ErrorAction Stop - Write-Host -Object "Successfully retrieved the custom field contents.`n" - } - catch { - # Log an error if retrieval fails - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to retrieve the 'Data Volume Minimum Free Size' from the custom field '$DataVolumeMinFreeSizeCustomField'." - exit 1 - } - - # Trim and validate the retrieved value - if ($DataVolumeMinFreeSize) { - $DataVolumeMinFreeSize = $DataVolumeMinFreeSize.Trim() - } - - if (!($DataVolumeMinFreeSize)) { - Write-Host -Object "[Error] The custom field '$DataVolumeMinFreeSizeCustomField' is empty." - Write-Host -Object "[Error] Please save a value to the custom field or leave 'Retrieve Data Volume Minimum Free Size from Custom Field' blank." - exit 1 - } - } - - # Retrieve 'Exclude Data Volumes from Alert' from a custom field if specified and not already provided - if ($ExcludeDataVolumeCustomField) { - try { - Write-Host -Object "Attempting to retrieve the 'Exclude Data Volumes from Alert' value from the field '$ExcludeDataVolumeCustomField'." - $VolumesToExclude = Get-CustomField -Name $ExcludeDataVolumeCustomField -ErrorAction Stop - Write-Host -Object "Successfully retrieved the custom field contents.`n" - } - catch { - # Log an error if retrieval fails - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to retrieve the 'Exclude Data Volumes from Alert' from the custom field '$ExcludeDataVolumeCustomField'." - exit 1 - } - - # Trim and validate the retrieved value - if ($VolumesToExclude) { - $VolumesToExclude = $VolumesToExclude.Trim() - } - - if (!($VolumesToExclude)) { - Write-Host -Object "[Error] The custom field '$ExcludeDataVolumeCustomField' is empty." - Write-Host -Object "[Error] Please save a value to the custom field or leave 'Retrieve Exclude Data Volumes from Alert from Custom Field' blank." - exit 1 - } - - if($ExcludeDataVolumesFromAlert -match "[^, ]"){ - $ExcludeDataVolumesFromAlert = "$ExcludeDataVolumesFromAlert, $VolumesToExclude" - }else{ - $ExcludeDataVolumesFromAlert = $VolumesToExclude - } - } - - # Retrieve 'Alert Only for Data Volumes' from a custom field if specified and not already provided - if ($AlertOnlyCustomField -and !($AlertOnlyForDataVolumes)) { - try { - Write-Host -Object "Attempting to retrieve the 'Alert Only for Data Volumes' value from the field '$AlertOnlyCustomField'." - $AlertOnlyForDataVolumes = Get-CustomField -Name $AlertOnlyCustomField -ErrorAction Stop - Write-Host -Object "Successfully retrieved the custom field contents.`n" - } - catch { - # Log an error if retrieval fails - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to retrieve the 'Alert Only for Data Volumes' from the custom field '$AlertOnlyCustomField'." - exit 1 - } - - # Trim and validate the retrieved value - if ($AlertOnlyForDataVolumes) { - $AlertOnlyForDataVolumes = $AlertOnlyForDataVolumes.Trim() - } - - if (!($AlertOnlyForDataVolumes)) { - Write-Host -Object "[Error] The custom field '$AlertOnlyCustomField' is empty." - Write-Host -Object "[Error] Please save a value to the custom field or leave 'Retrieve Alert Only for Data Volumes from Custom Field' blank." - exit 1 - } - } - - # Retrieve the system volume based on the system drive - Write-Host -Object "Retrieving the system volume '$env:SystemDrive'." - try { - if ($PSVersionTable.PSVersion.Major -lt 3) { - # Use WMI for PowerShell versions < 3 - $SystemVolume = Get-WmiObject -Class Win32_Volume -ErrorAction Stop | Where-Object { $_.DriveLetter -eq "$env:SystemDrive" } | - Select-Object "DriveLetter", "Label", "Name", "FileSystem", "FreeSpace", "Capacity" | Sort-Object Label - } - else { - # Use CIM for PowerShell versions >= 3 - $SystemVolume = Get-CimInstance -ClassName Win32_Volume -ErrorAction Stop | Where-Object { $_.DriveLetter -eq "$env:SystemDrive" } | - Select-Object "DriveLetter", "Label", "Name", "FileSystem", "FreeSpace", "Capacity" | Sort-Object Label - } - - # Validate the retrieved system volume - if (!($SystemVolume)) { - throw "No system volume with a drive letter that matches '$env:SystemDrive'." - } - - if ($SystemVolume.Count -gt 1) { - throw "Multiple volumes detected with the drive letter '$env:SystemDrive'." - } - } - catch { - # Log an error if retrieval fails - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to retrieve the system volume. Unable to calculate the free space remaining." - exit 1 - } - - # Initialize a list to store system volume minimum free space data - $SystemVolumeMinFree = New-Object System.Collections.Generic.List[object] - - # Retrieve all data volumes excluding the system drive - Write-Host -Object "Retrieving all data volumes.`n" - try { - if ($PSVersionTable.PSVersion.Major -lt 3) { - # Use WMI for PowerShell versions < 3 - $DataVolumes = Get-WmiObject -Class Win32_Volume -ErrorAction Stop | Where-Object { $_.DriveType -eq 3 -and $_.DriveLetter -ne "$env:SystemDrive" -and $_.Capacity -gt 0 -and $_.FreeSpace -gt 0 } | - Select-Object "DriveLetter", "Label", "Name", "FileSystem", "FreeSpace", "Capacity" | Sort-Object Label - } - else { - # Use CIM for PowerShell versions >= 3 - $DataVolumes = Get-CimInstance -ClassName Win32_Volume -ErrorAction Stop | Where-Object { $_.DriveType -eq 3 -and $_.DriveLetter -ne "$env:SystemDrive" -and $_.Capacity -gt 0 -and $_.FreeSpace -gt 0 } | - Select-Object "DriveLetter", "Label", "Name", "FileSystem", "FreeSpace", "Capacity" | Sort-Object Label - } - - # Log a warning if no data volumes are detected - if (!($DataVolumes)) { - Write-Host -Object "[Warning] No data volumes detected." - } - } - catch { - # Log an error if retrieval fails - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to retrieve data volumes. Unable to calculate free space remaining." - exit 1 - } - - # Initialize a list to store data volume minimum free space data - $DataVolumeMinFree = New-Object System.Collections.Generic.List[object] - - # Process volumes to exclude from alerts - if ($ExcludeDataVolumesFromAlert) { - if (!($ExcludeDataVolumesFromAlert.Trim())) { - Write-Host -Object "[Error] Please specify a valid volume label or drive letter or file path to exclude." - exit 1 - } - - if (!($DataVolumeMinFreePercent) -and !($DataVolumeMinFreeSize)) { - Write-Host -Object "[Warning] You must have a data volume limit in order to exclude it from the alert." - } - - # Initialize a list to store volumes to exclude - $VolumesToExclude = New-Object System.Collections.Generic.List[string] - - try { - if ($PSVersionTable.PSVersion.Major -lt 3) { - # Retrieve system volume labels using WMI for PowerShell versions < 3 - $SystemVolumeLabels = (Get-WmiObject -Class Win32_Volume -ErrorAction Stop | Where-Object { $_.DriveLetter -eq "$env:SystemDrive" } | - Select-Object -Property "Label" -ErrorAction SilentlyContinue).Label - } - else { - # Retrieve system volume labels using CIM for PowerShell versions >= 3 - $SystemVolumeLabels = (Get-CimInstance -ClassName Win32_Volume -ErrorAction Stop | Where-Object { $_.DriveLetter -eq "$env:SystemDrive" } | - Select-Object -Property "Label" -ErrorAction SilentlyContinue).Label - } - } - catch { - # Log an error if retrieval fails - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to retrieve the system volume labels." - exit 1 - } - - # Process each volume to exclude - $ExcludeDataVolumesFromAlert -split "," | ForEach-Object { - # Normalize the volume format - $PotentialVolume = $_.Trim() - - if (!($PotentialVolume)) { - Write-Host -Object "[Error] '$ExcludeDataVolumesFromAlert' contains an invalid volume to exclude. There is an empty item in the comma-separated list." - exit 1 - } - - if ($PotentialVolume -match "^[A-Z]$") { - $PotentialVolume = "$PotentialVolume" + ":" - } - - # Prevent excluding the system volume - $SystemVolumes = "$env:SystemDrive", "$($env:SystemDrive -replace '[^A-Z:]')", "$env:SystemDrive\" - if ($SystemVolumes -contains $PotentialVolume) { - Write-Host -Object "[Warning] The system volume '$PotentialVolume' cannot be excluded." - } - - if ($SystemVolumeLabels -contains $PotentialVolume) { - Write-Host -Object "[Warning] The system volume '$PotentialVolume' cannot be excluded." - } - - if ($DataVolumes.DriveLetter -notcontains $PotentialVolume -and $DataVolumes.Label -notcontains $PotentialVolume -and $DataVolumes.Name -notcontains $PotentialVolume) { - Write-Host -Object "[Warning] The volume '$PotentialVolume' is not the label, drive letter, or path of any data volume on this system." - $AddAnExtraNewLine = $True - } - - # Add the volume to the exclusion list - $VolumesToExclude.Add($PotentialVolume) - } - } - - if($AddAnExtraNewLine){ - Write-Host -Object "" - } - - # Process volumes to include in alerts - if ($AlertOnlyForDataVolumes) { - if (!($AlertOnlyForDataVolumes.Trim())) { - Write-Host -Object "[Error] Please specify a valid volume label or drive letter or file path to alert on." - exit 1 - } - - if (!($DataVolumeMinFreePercent) -and !($DataVolumeMinFreeSize)) { - Write-Host -Object "[Warning] You must have a data volume limit in order to include it for the alert." - } - - # Initialize a list to store volumes to include - $VolumesToInclude = New-Object System.Collections.Generic.List[String] - - # Process each volume to include - $AlertOnlyForDataVolumes -split "," | ForEach-Object { - # Normalize the volume format - $PotentialVolume = $_.Trim() - - if (!($PotentialVolume)) { - Write-Host -Object "[Error] '$AlertOnlyForDataVolumes' contains an invalid volume to alert on. There is an empty item in the comma-separated list." - exit 1 - } - - # Normalize the volume format - $PotentialVolume = $_.Trim() - - if ($PotentialVolume -match "^[A-Z]$") { - $PotentialVolume = "$PotentialVolume" + ":" - } - - # Prevent including volumes that are also excluded - if ($ExcludeDataVolumesFromAlert.Count -ge 1 -and $ExcludeDataVolumesFromAlert -contains $PotentialVolume) { - Write-Host -Object "[Error] Cannot alert only on '$($_.Trim())' and exclude it from the alert." - exit 1 - } - - if ($DataVolumes.DriveLetter -notcontains $PotentialVolume -and $DataVolumes.Label -notcontains $PotentialVolume -and $DataVolumes.Name -notcontains $PotentialVolume) { - Write-Host -Object "[Warning] The volume '$PotentialVolume' is not the label, drive letter, or path of any data volume on this system.`n" - $AddAnExtraNewLine = $True - } - - # Add the volume to the inclusion list - $VolumesToInclude.Add($PotentialVolume) - } - } - - if($AddAnExtraNewLine){ - Write-Host -Object "" - } - - # Check if a minimum percentage of free space for the system volume is specified - if ($SystemVolumeMinFreePercent) { - $SystemVolumeMinFreePercent = $SystemVolumeMinFreePercent.Trim() - - # Validate that the percentage is not empty - if (!($SystemVolumeMinFreePercent)) { - Write-Host -Object "[Error] The minimum system volume free size percentage of '$SystemVolumeMinFreePercent' is invalid." - Write-Host -Object "[Error] A percentage that is greater than 0 and less than 100 was expected." - exit 1 - } - - # Validate that the percentage contains only valid characters - if ($SystemVolumeMinFreePercent -match "[^0-9.% ]") { - Write-Host -Object "[Error] The minimum system volume free size percentage of '$SystemVolumeMinFreePercent' is invalid." - Write-Host -Object "[Error] It contains invalid characters. The only characters supported (besides whitespace) are '.','%' and numeric characters." - exit 1 - } - - # Validate the format of the percentage - if ($SystemVolumeMinFreePercent -notmatch "^\d{1,}(\.\d{1,})?\s?%?$") { - Write-Host -Object "[Error] The minimum system volume free size percentage of '$SystemVolumeMinFreePercent' is invalid. It is in an invalid format." - Write-Host -Object "[Error] A percentage was expected such as '33%', '77.23%' or '77.23 %'." - exit 1 - } - - # Attempt to extract the numeric value from the percentage - try { - $SystemVolumeMinFreePercent = [decimal]$(($SystemVolumeMinFreePercent -replace '%').Trim()) - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to extract the percentage from '$SystemVolumeMinFreePercent'" - exit 1 - } - - # Ensure the percentage is within a valid range - if ([decimal]$SystemVolumeMinFreePercent -le 0 -or [decimal]$SystemVolumeMinFreePercent -ge 100) { - Write-Host -Object "[Error] The minimum system volume free size percentage of '$SystemVolumeMinFreePercent' is invalid." - Write-Host -Object "[Error] A value between 0 and 100 is expected." - exit 1 - } - - # Convert the percentage into bytes based on the system volume's capacity - try { - Write-Host -Object "Converting the minimum free space percentage '$($SystemVolumeMinFreePercent -replace "[^0-9.%]")%' required for system volumes into bytes." - $SystemVolumeMinFreeBytes = $SystemVolume.Capacity * ([decimal]$($SystemVolumeMinFreePercent -replace "[^0-9.]") / 100) - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to convert the percentage into the minimum free space required in bytes using '$($SystemVolume.Used)' and '$($SystemVolume.Free)'." - exit 1 - } - - # Add the calculated minimum free space requirement to the list - $SystemVolumeMinFree.Add(( - New-Object PSObject -Property @{ - Type = "Minimum Percent Free" - Path = $SystemVolume.Name - Limit = "$($SystemVolumeMinFreePercent -replace '[^0-9.]')%" - MinimumInBytes = $SystemVolumeMinFreeBytes - } - )) - } - - # Check if a minimum free size for the system volume is specified - if ($SystemVolumeMinFreeSize) { - $SystemVolumeMinFreeSize = $SystemVolumeMinFreeSize.Trim() - - # Validate that the size is not empty - if (!($SystemVolumeMinFreeSize)) { - Write-Host -Object "[Error] The minimum system volume free size of '$SystemVolumeMinFreeSize' is invalid." - Write-Host -Object "[Error] A value representing the amount of free space that needs to be available was expected. For example: '50GB', '50000MB', or '53687091200 Bytes'" - exit 1 - } - - # Validate that the size contains only valid characters - if ($SystemVolumeMinFreeSize -match "[^0-9. (PB|TB|GB|MB|KB|B|Bytes)]") { - Write-Host -Object "[Error] The minimum system volume free size of '$SystemVolumeMinFreeSize' is invalid." - Write-Host -Object "[Error] It contains invalid characters. The only characters supported (besides whitespace) are '.', data size characters (PB, TB, GB, MB, KB, B, Bytes) and numeric characters." - exit 1 - } - - # Validate the format of the size - if ($SystemVolumeMinFreeSize -notmatch "^?[0-9]+\.?[0-9]*\s*(PB|TB|GB|MB|KB|B|Bytes)?$") { - Write-Host -Object "[Error] The minimum system volume free size of '$SystemVolumeMinFreeSize' is invalid." - Write-Host -Object "[Error] It is in an invalid format. Please specify a data size such as '50GB', '50000MB', or '53687091200 Bytes'." - exit 1 - } - - # Default to GB if no unit is specified - if ($SystemVolumeMinFreeSize -match "^[0-9]+$") { - $SystemVolumeMinFreeSize = "$SystemVolumeMinFreeSize" + "GB" - } - - # Convert the size into bytes - try { - $SystemVolumeMinFreeBytes = ConvertTo-Bytes -FileSize $SystemVolumeMinFreeSize -DefaultTo "GB" -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to convert '$SystemVolumeMinFreeSize' to bytes. Unable to determine the minimum free space." - exit 1 - } - - # Ensure the size is greater than 0 - if ($SystemVolumeMinFreeBytes -le 0) { - Write-Host -Object "[Error] The minimum system volume free size of '$SystemVolumeMinFreeSize' is invalid." - Write-Host -Object "[Error] A value greater than 0 is expected." - exit 1 - } - - # Add the calculated minimum free size requirement to the list - Write-Host -Object "Converting the minimum free space required '$SystemVolumeMinFreeSize' for system volumes into bytes." - $SystemVolumeMinFree.Add(( - New-Object PSObject -Property @{ - Type = "Minimum Free Size" - Path = $SystemVolume.Name - Limit = $SystemVolumeMinFreeSize - MinimumInBytes = $SystemVolumeMinFreeBytes - } - )) - } - - # Check if a minimum percentage of free space for data volumes is specified - if ($DataVolumeMinFreePercent) { - $DataVolumeMinFreePercent = $DataVolumeMinFreePercent.Trim() - - # Validate that the percentage is not empty - if (!($DataVolumeMinFreePercent)) { - Write-Host -Object "[Error] The minimum data volume free size percentage of '$DataVolumeMinFreePercent' is invalid." - Write-Host -Object "[Error] A percentage that is greater than 0 and less than 100 was expected." - exit 1 - } - - # Validate that the percentage contains only valid characters - if ($DataVolumeMinFreePercent -match "[^0-9.% ]") { - Write-Host -Object "[Error] The minimum data volume free size percentage of '$DataVolumeMinFreePercent' is invalid." - Write-Host -Object "[Error] It contains invalid characters. The only characters supported (besides whitespace) are '.','%' and numeric characters." - exit 1 - } - - # Validate the format of the percentage - if ($DataVolumeMinFreePercent -notmatch "^\d{1,}(\.\d{1,})?\s?%?$") { - Write-Host -Object "[Error] The minimum data volume free size percentage of '$DataVolumeMinFreePercent' is invalid. It is in an invalid format." - Write-Host -Object "[Error] A percentage was expected such as '33%', '77.23%' or '77.23 %'." - exit 1 - } - - # Attempt to extract the numeric value from the percentage - try { - $DataVolumeMinFreePercent = [decimal]$(($DataVolumeMinFreePercent -replace '%').Trim()) - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to extract the percentage from '$DataVolumeMinFreePercent'" - exit 1 - } - - # Ensure the percentage is within a valid range - if ([decimal]$DataVolumeMinFreePercent -le 0 -or [decimal]$DataVolumeMinFreePercent -ge 100) { - Write-Host -Object "[Error] The minimum data volume free size percentage of '$DataVolumeMinFreePercent' is invalid." - Write-Host -Object "[Error] A value between 0 and 100 is expected." - exit 1 - } - - # Convert the percentage into bytes for each data volume - try { - Write-Host -Object "Converting the minimum free space percentage '$($DataVolumeMinFreePercent -replace '[^0-9.]')%' required into bytes for each data volume." - $DataVolumes | ForEach-Object { - # Skip volumes that are excluded - if ($VolumesToExclude.Count -ge 1) { - if ($VolumesToExclude -contains $_.DriveLetter) { - return - } - - if ($VolumesToExclude -contains $_.Label) { - return - } - - if ($VolumesToExclude -contains $_.Name) { - return - } - } - - # Skip volumes that are not included - if ($VolumesToInclude.Count -ge 1) { - if ($VolumesToInclude -notcontains $_.DriveLetter -and $VolumesToInclude -notcontains $_.Label -and $VolumesToInclude -notcontains $_.Name) { - return - } - } - - # Calculate the minimum free space in bytes for the current volume - Write-Verbose -Message "Converting the minimum free space percentage '$($DataVolumeMinFreePercent -replace '[^0-9.]')%' required into bytes for the volume '$($_.Label)'." - $DataVolumeMinFreeBytes = $_.Capacity * ([decimal]$($DataVolumeMinFreePercent -replace "[^0-9.]") / 100) - - # Add the calculated minimum free space requirement to the list - $DataVolumeMinFree.Add(( - New-Object PSObject -Property @{ - Name = $_.Label - DriveLetter = $_.DriveLetter - Path = $_.Name - FreeSpace = $_.FreeSpace - Total = $_.Capacity - Type = "Minimum Percent Free" - Limit = "$($DataVolumeMinFreePercent -replace '[^0-9.]')%" - MinimumInBytes = $DataVolumeMinFreeBytes - } - )) - } - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to convert the percentage into the minimum data volume free space required in bytes." - exit 1 - } - } - - # Check if a minimum free size for data volumes is specified - if ($DataVolumeMinFreeSize) { - $DataVolumeMinFreeSize = $DataVolumeMinFreeSize.Trim() - - # Validate that the size is not empty - if (!($DataVolumeMinFreeSize)) { - Write-Host -Object "[Error] The minimum data volume free size of '$DataVolumeMinFreeSize' is invalid." - Write-Host -Object "[Error] A value representing the amount of free space that needs to be available was expected. For example: '50GB', '50000MB', or '53687091200 Bytes'" - exit 1 - } - - # Validate that the size contains only valid characters - if ($DataVolumeMinFreeSize -match "[^0-9. (PB|TB|GB|MB|KB|B|Bytes)]") { - Write-Host -Object "[Error] The minimum data volume free size of '$DataVolumeMinFreeSize' is invalid." - Write-Host -Object "[Error] It contains invalid characters. The only characters supported (besides whitespace) are '.', data size characters (PB, TB, GB, MB, KB, B, Bytes) and numeric characters." - exit 1 - } - - # Validate the format of the size - if ($DataVolumeMinFreeSize -notmatch "^?[0-9]+\.?[0-9]*\s*(PB|TB|GB|MB|KB|B|Bytes)?$") { - Write-Host -Object "[Error] The minimum data volume free size of '$DataVolumeMinFreeSize' is invalid." - Write-Host -Object "[Error] It is in an invalid format. Please specify a data size such as '50GB', '50000MB', or '53687091200 Bytes'." - exit 1 - } - - # Default to GB if no unit is specified - if ($DataVolumeMinFreeSize -match "^[0-9]+$") { - $DataVolumeMinFreeSize = "$DataVolumeMinFreeSize" + "GB" - } - - # Convert the size into bytes - Write-Host -Object "Converting the minimum free space required '$DataVolumeMinFreeSize' for data volumes into bytes." - try { - $DataVolumeMinFreeBytes = ConvertTo-Bytes -FileSize $DataVolumeMinFreeSize -DefaultTo "GB" -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to convert '$DataVolumeMinFreeSize' to bytes. Unable to determine the minimum free space." - exit 1 - } - - # Ensure the size is greater than 0 - if ($DataVolumeMinFreeBytes -le 0) { - Write-Host -Object "[Error] The minimum data volume free size of '$DataVolumeMinFreeSize' is invalid." - Write-Host -Object "[Error] A value greater than 0 is expected." - exit 1 - } - - # Add the calculated minimum free size requirement to the list for each data volume - $DataVolumes | ForEach-Object { - # Skip volumes that are excluded - if ($VolumesToExclude.Count -ge 1) { - if ($VolumesToExclude -contains $_.DriveLetter) { - return - } - - if ($VolumesToExclude -contains $_.Label) { - return - } - - if ($VolumesToExclude -contains $_.Name) { - return - } - } - - # Skip volumes that are not included - if ($VolumesToInclude.Count -ge 1) { - if ($VolumesToInclude -notcontains $_.DriveLetter -and $VolumesToInclude -notcontains $_.Label -and $VolumesToInclude -notcontains $_.Name) { - return - } - } - - # Add the calculated minimum free size requirement to the list - $DataVolumeMinFree.Add(( - New-Object PSObject -Property @{ - Name = $_.Label - DriveLetter = $_.DriveLetter - Path = $_.Name - FreeSpace = $_.FreeSpace - Total = $_.Capacity - Type = "Minimum Free Size" - Limit = $DataVolumeMinFreeSize - MinimumInBytes = $DataVolumeMinFreeBytes - } - )) - } - } - - # Check if there are any alerts for system or data volumes - if ($SystemVolumeMinFree.Count -gt 0 -or $DataVolumeMinFree.Count -gt 0) { - Write-Host -Object "" - } - - # Process alerts for the system volume - if ($SystemVolumeMinFree.Count -gt 0) { - $SystemVolumeMinFree | ForEach-Object { - if ($SystemVolume.FreeSpace -lt $_.MinimumInBytes) { - # Alert if the system volume free space is below the specified limit - Write-Host -Object "[Alert] The system volume exceeds the '$($_.Type)' limit of '$($_.Limit)'." - } - } - } - - # Process alerts for data volumes - if ($DataVolumeMinFree.Count -gt 0) { - $DataVolumeMinFree | ForEach-Object { - if ($_.FreeSpace -lt $_.MinimumInBytes) { - # Alert if a data volume free space is below the specified limit - Write-Host -Object "[Alert] The data volume labeled '$($_.Name)' exceeds the '$($_.Type)' limit of '$($_.Limit)'." - } - } - } - - # Display system volume details in a human-readable format - Write-Host -Object "`n### System Volume ###" - try { - # Convert system volume metrics to friendly sizes and calculate percentages - $SystemVolumeFreeSpace = ConvertTo-FriendlySize -Bytes $SystemVolume.FreeSpace -RoundTo 2 -ErrorAction Stop - $SystemVolumeTotal = ConvertTo-FriendlySize -Bytes $SystemVolume.Capacity -RoundTo 2 -ErrorAction Stop - $SystemVolumeUsed = ConvertTo-FriendlySize -Bytes $($SystemVolume.Capacity - $SystemVolume.FreeSpace) -RoundTo 2 -ErrorAction Stop - $SystemPercentFree = [System.Math]::Round((($SystemVolume.FreeSpace / $SystemVolume.Capacity) * 100), 2) - $SystemPercentUsed = [System.Math]::Round((100 - $SystemPercentFree), 2) - - # Create a formatted object for system volume details - $FormattedSystemVolume = New-Object PSObject -Property @{ - Name = $SystemVolume.Label - DriveLetter = $SystemVolume.DriveLetter - FileSystemType = $SystemVolume.FileSystem - Path = $SystemVolume.Name - FreeSpace = $SystemVolumeFreeSpace - UsedSpace = $SystemVolumeUsed - Total = $SystemVolumeTotal - PercentageFree = "$SystemPercentFree%" - PercentageUsed = "$SystemPercentUsed%" - } - - # Output the formatted system volume details as a table - ($FormattedSystemVolume | Format-Table -Property Name, DriveLetter, FileSystemType, FreeSpace, Total, PercentageFree -AutoSize | Out-String).Trim() | Write-Host - } - catch { - # Handle errors during system volume formatting - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to convert the system volume into a human-readable table." - exit 1 - } - - # Display data volume details in a human-readable format - if ($DataVolumes) { - Write-Host -Object "`n### Data Volumes ###" - try { - # Convert data volume metrics to friendly sizes and calculate percentages - $FormattedDataVolumes = $DataVolumes | ForEach-Object { - $DataVolumeFreeSpace = ConvertTo-FriendlySize -Bytes $_.FreeSpace -RoundTo 2 -ErrorAction Stop - $DataVolumeTotal = ConvertTo-FriendlySize -Bytes $_.Capacity -RoundTo 2 -ErrorAction Stop - $DataVolumeUsed = ConvertTo-FriendlySize -Bytes $($_.Capacity - $_.FreeSpace) -RoundTo 2 -ErrorAction Stop - $DataVolumePercentFree = [System.Math]::Round((($_.FreeSpace / $_.Capacity) * 100), 2) - $DataVolumePercentUsed = [System.Math]::Round((100 - $DataVolumePercentFree), 2) - - # Create a formatted object for each data volume - New-Object PSObject -Property @{ - Name = $_.Label - DriveLetter = $_.DriveLetter - FileSystemType = $_.FileSystem - Path = $_.Name - FreeSpace = $DataVolumeFreeSpace - UsedSpace = $DataVolumeUsed - Total = $DataVolumeTotal - PercentageFree = "$DataVolumePercentFree%" - PercentageUsed = "$DataVolumePercentUsed%" - } - } - - # Output the formatted data volume details as a list - ($FormattedDataVolumes | Format-List -Property Name, DriveLetter, FileSystemType, Path, FreeSpace, Total, PercentageFree | Out-String).Trim() | Write-Host - } - catch { - # Handle errors during data volume formatting - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to convert the data volumes into a human-readable table." - exit 1 - } - } - - # Update a multiline custom field with volume details and alerts - if ($MultilineCustomField) { - $CustomFieldValue = New-Object System.Collections.Generic.List[String] - - # Add system volume alerts to the custom field - if ($SystemVolumeMinFree.Count -gt 0) { - $SystemVolumeMinFree | ForEach-Object { - if ($SystemVolume.FreeSpace -lt $_.MinimumInBytes) { - $CustomFieldValue.Add("[Alert] The system volume exceeds the '$($_.Type)' limit of '$($_.Limit)'.`n") - } - } - } - - # Add data volume alerts to the custom field - if ($DataVolumeMinFree.Count -gt 0) { - $DataVolumeMinFree | ForEach-Object { - if ($_.FreeSpace -lt $_.MinimumInBytes) { - $CustomFieldValue.Add("[Alert] The data volume labeled '$($_.Name)' exceeds the '$($_.Type)' limit of '$($_.Limit)'.`n") - } - } - } - - # Add formatted volume details to the custom field - if ($SystemVolumeMinFree.Count -gt 0 -or $DataVolumeMinFree.Count -gt 0) { - $CustomFieldValue.Add("`n`n") - } - - $CustomFieldValue.Add(($FormattedSystemVolume | Format-List -Property Name, DriveLetter, FileSystemType, Path, FreeSpace, Total, PercentageFree | Out-String).Trim()) - $CustomFieldValue.Add("`n`n") - $CustomFieldValue.Add(($FormattedDataVolumes | Format-List -Property Name, DriveLetter, FileSystemType, Path, FreeSpace, Total, PercentageFree | Out-String).Trim()) - - try { - # Attempt to set the multiline custom field - Write-Host -Object "`nAttempting to set Custom Field '$MultilineCustomField'." - Set-CustomField -Name $MultilineCustomField -Value $CustomFieldValue - Write-Host -Object "Successfully set Custom Field '$MultilineCustomField'!" - } - catch { - # Handle errors during custom field update - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to set the multiline custom field." - $ExitCode = 1 - } - } - - # Update a WYSIWYG custom field with volume details and alerts - if ($WYSIWYGCustomField) { - $CustomFieldValue = New-Object System.Collections.Generic.List[object] - - # Create WYSIWYG cards for the system volume - $FormattedSystemVolume | ForEach-Object { - - $SystemCard = "
-
-
  $($_.Name)
-
-
-
-
-
-

Drive Letter: $($_.DriveLetter)

-

Path: $($_.Path)

-
-
-

File System: $($_.FileSystemType)

-
-
-
-
-
-
-
-
    -
  • - - Used Space ($($_.UsedSpace) | $($_.PercentageUsed)) -
  • -
  • - - Free Space ($($_.FreeSpace) | $($_.PercentageFree)) -
  • -
-
-
-

Total Space: $($_.Total)

-
-
-
-
`n" - - # Highlight the card if the system volume exceeds limits - foreach ($Volume in $SystemVolumeMinFree) { - if ($Volume.Path -eq $_.Path -and $Volume.FreeSpace -lt $Volume.MinimumInBytes) { - $SystemCard = $SystemCard -replace "class='fa-solid fa-hard-drive'", "class='fa-solid fa-circle-exclamation' style='color: #C6313A;'" - $SystemCard = $SystemCard -replace "class='card flex-grow-1'", "class='card flex-grow-1' style='background-color: #FBEBED;'" - } - } - - $CustomFieldValue.Add($SystemCard) - } - - # Create WYSIWYG cards for data volumes - $FormattedDataVolumes | ForEach-Object { - - $DataCard = "
-
-
  $($_.Name)
-
-
-
-
-
-

Drive Letter: $($_.DriveLetter)

-

Path: $($_.Path)

-
-
-

File System: $($_.FileSystemType)

-
-
-
-
-
-
-
-
    -
  • - - Used Space ($($_.UsedSpace) | $($_.PercentageUsed)) -
  • -
  • - - Free Space ($($_.FreeSpace) | $($_.PercentageFree)) -
  • -
-
-
-

Total Space: $($_.Total)

-
-
-
-
`n" - - # Highlight the card if the data volume exceeds limits - foreach ($Volume in $DataVolumeMinFree) { - if ($Volume.Path -eq $_.Path -and $Volume.FreeSpace -lt $Volume.MinimumInBytes) { - $DataCard = $DataCard -replace "class='fa-solid fa-hard-drive'", "class='fa-solid fa-circle-exclamation' style='color: #C6313A;'" - $DataCard = $DataCard -replace "class='card flex-grow-1'", "class='card flex-grow-1' style='background-color: #FBEBED;'" - } - } - - $CustomFieldValue.Add($DataCard) - } - - try { - # Attempt to set the WYSIWYG custom field - Write-Host -Object "`nAttempting to set Custom Field '$WYSIWYGCustomField'." - Set-CustomField -Name $WYSIWYGCustomField -Value $CustomFieldValue - Write-Host -Object "Successfully set Custom Field '$WYSIWYGCustomField'!" - } - catch { - # Handle errors during custom field update - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to set the WYSIWYG custom field." - $ExitCode = 1 - } - } - - exit $ExitCode -} -end { - - - -} +#Requires -Version 2.0 + +<# +.SYNOPSIS + Reports on the current volumes on the system. All volumes that are not the 'System Drive' are considered data volumes. Raises an alert if one or more volumes have less than a specified percentage or data size. +.DESCRIPTION + Reports on the current volumes on the system. All volumes that are not the 'System Drive' are considered data volumes. Raises an alert if one or more volumes have less than a specified percentage or data size. +.EXAMPLE + -SystemVolumeMinFreePercent "99%" -SystemVolumeMinFreeSize "100TB" -DataVolumeMinFreePercent "99%" -DataVolumeMinFreeSize "100TB" + -ExcludeDataVolumesFromAlert "WINRETOOLS, ESP, DELLSUPPORT, Windows RE Tools, SYSTEM, System Reserved" + + Retrieving the system volume 'C:'. + Retrieving all data volumes. + + Converting the minimum free space percentage '99%' required for system volumes into bytes. + Converting the minimum free space required '100TB' for system volumes into bytes. + Converting the minimum free space percentage '99%' required into bytes for each data volume. + Converting the minimum free space required '100TB' for data volumes into bytes. + + [Alert] The system volume exceeds the 'Minimum Percent Free' limit of '99%'. + [Alert] The system volume exceeds the 'Minimum Free Size' limit of '100TB'. + + ### System Volume ### + Name DriveLetter FileSystemType FreeSpace Total PercentageFree + ---- ----------- -------------- --------- ----- -------------- + C: NTFS 115.26 GB 126.9 GB 90.83% + + ### Data Volumes ### + Name : System Reserved + DriveLetter : + FileSystemType : NTFS + Path : \\?\Volume{af2c6d55-e8c1-11ef-95b1-806e6f6e6963}\ + FreeSpace : 71.87 MB + Total : 100 MB + PercentageFree : 71.87% + +.PARAMETER -SystemVolumeMinFreePercent "10%" + Specifies the minimum percentage of free space that must remain available on the system volume to avoid triggering an alert. + For example, if set to 10, an alert will be raised if the system volume has less than 10% free space. + +.PARAMETER -SystemVolumeMinFreeSize "1TB" + Specifies the minimum amount of free space that must remain available on the system volume to avoid triggering an alert. + If no units are specified, gigabytes will be used. + For example, if set to 10, an alert will be raised if the system volume has less than 10GB of free space. + +.PARAMETER -DataVolumeMinFreePercent "10%" + Specifies the minimum percentage of free space that must remain available on each data volume to avoid triggering an alert. + For example, if set to 10, an alert will be raised if a data volume has less than 10% free space. + +.PARAMETER -DataVolumeMinFreeSize "1TB" + Specifies the minimum amount of free space that must remain available on each volume to avoid triggering an alert. + If no units are specified, gigabytes will be used. + For example, if set to 10, an alert will be raised if a data volume has less than 10GB of free space. + +.PARAMETER -ExcludeDataVolumesFromAlert "WINRETOOLS, ESP, DELLSUPPORT, Windows RE Tools, SYSTEM, System Reserved" + Allows you to specify a comma-separated list of data volumes you would like to exclude from the alert. + You can specify either the volume label, path, or drive letter. + +.PARAMETER -AlertOnlyForDataVolumes "E:\" + If you would only like an alert for specific volume(s), specify the volume(s) you would like the alert to trigger for. + You can specify either the volume label, path, or drive letter. + +.PARAMETER -MultilineCustomField "ReplaceMeWithAnyMultilineCustomField" + Stores a multiline report on the current volumes on the system. + +.PARAMETER -WYSIWYGCustomField "ReplaceMeWithAnyWYSIWYGCustomField" + Stores a WYSIWYG report on the current volumes on the system. + +.PARAMETER -SystemVolumeMinFreePercentCustomField "ReplaceMeWithAnyTextCustomField" + Optionally retrieves the 'SystemVolumeMinFreePercent' value from a custom field you specify if no value is currently set. + +.PARAMETER -SystemVolumeMinFreeSizeCustomField "ReplaceMeWithAnyTextCustomField" + Optionally retrieves the 'SystemVolumeMinFreeSize' value from a custom field you specify if no value is currently set. + +.PARAMETER -DataVolumeMinFreePercentCustomField "ReplaceMeWithAnyTextCustomField" + Optionally retrieves the 'DataVolumeMinFreePercent' value from a custom field you specify if no value is currently set. + +.PARAMETER -DataVolumeMinFreeSizeCustomField "ReplaceMeWithAnyTextCustomField" + Optionally retrieves the 'DataVolumeMinFreeSize' value from a custom field you specify if no value is currently set. + +.PARAMETER -ExcludeDataVolumeCustomField "ReplaceMeWithAnyTextCustomField" + Optionally retrieves the 'ExcludeDataVolumesFromAlert' value from a custom field you specify if no value is currently set. + +.PARAMETER -AlertOnlyCustomField "ReplaceMeWithAnyTextCustomField" + Optionally retrieves the 'AlertOnlyForDataVolumes' value from a custom field you specify if no value is currently set. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2012 R2 + Release Notes: Improved parameter validation, added the ability to save to custom fields, enhanced reporting to include all volumes + (not just those triggering an alert), switched from drives to volumes, introduced an include option, added unit conversion, and + simplified exclude options. +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$SystemVolumeMinFreePercent, + [Parameter()] + [String]$SystemVolumeMinFreeSize, + [Parameter()] + [String]$DataVolumeMinFreePercent, + [Parameter()] + [String]$DataVolumeMinFreeSize, + [Parameter()] + [String]$ExcludeDataVolumesFromAlert = "WINRETOOLS, ESP, DELLSUPPORT, Windows RE Tools, SYSTEM, System Reserved", + [Parameter()] + [String]$AlertOnlyForDataVolumes, + [Parameter()] + [String]$MultilineCustomField, + [Parameter()] + [String]$WYSIWYGCustomField, + [Parameter()] + [String]$SystemVolumeMinFreePercentCustomField, + [Parameter()] + [String]$SystemVolumeMinFreeSizeCustomField, + [Parameter()] + [String]$DataVolumeMinFreePercentCustomField, + [Parameter()] + [String]$DataVolumeMinFreeSizeCustomField, + [Parameter()] + [String]$ExcludeDataVolumeCustomField, + [Parameter()] + [String]$AlertOnlyCustomField +) + +begin { + # If script form variables are used, replace the command line parameters with their value. + if ($env:systemVolumeMinimumPercentageFree) { $SystemVolumeMinFreePercent = $env:systemVolumeMinimumPercentageFree } + if ($env:systemVolumeMinimumFreeSize) { $SystemVolumeMinFreeSize = $env:systemVolumeMinimumFreeSize } + if ($env:dataVolumeMinimumPercentageFree) { $DataVolumeMinFreePercent = $env:dataVolumeMinimumPercentageFree } + if ($env:dataVolumeMinimumFreeSize) { $DataVolumeMinFreeSize = $env:dataVolumeMinimumFreeSize } + if ($env:excludeDataVolumesFromAlert) { $ExcludeDataVolumesFromAlert = $env:excludeDataVolumesFromAlert } + if ($env:alertOnlyForDataVolumes) { $AlertOnlyForDataVolumes = $env:alertOnlyForDataVolumes } + if ($env:multilineCustomFieldName) { $MultilineCustomField = $env:multilineCustomFieldName } + if ($env:wysiwygCustomFieldName) { $WYSIWYGCustomField = $env:wysiwygCustomFieldName } + if ($env:retrieveSystemVolumeMinimumPercentageFreeFromCustomField) { $SystemVolumeMinFreePercentCustomField = $env:retrieveSystemVolumeMinimumPercentageFreeFromCustomField } + if ($env:retrieveSystemVolumeMinimumFreeSizeFromCustomField) { $SystemVolumeMinFreeSizeCustomField = $env:retrieveSystemVolumeMinimumFreeSizeFromCustomField } + if ($env:retrieveDataVolumeMinimumPercentageFreeFromCustomField) { $DataVolumeMinFreePercentCustomField = $env:retrieveDataVolumeMinimumPercentageFreeFromCustomField } + if ($env:retrieveDataVolumeMinimumFreeSizeFromCustomField) { $DataVolumeMinFreeSizeCustomField = $env:retrieveDataVolumeMinimumFreeSizeFromCustomField } + if ($env:retrieveExcludeDataVolumesFromCustomField) { $ExcludeDataVolumeCustomField = $env:retrieveExcludeDataVolumesFromCustomField } + if ($env:retrieveAlertOnlyForDataVolumesFromCustomField) { $AlertOnlyCustomField = $env:retrieveAlertOnlyForDataVolumesFromCustomField } + + # Define an array of custom fields to validate + $CustomFields = @( + $SystemVolumeMinFreePercentCustomField, + $SystemVolumeMinFreeSizeCustomField, + $DataVolumeMinFreePercentCustomField, + $DataVolumeMinFreeSizeCustomField, + $ExcludeDataVolumeCustomField, + $AlertOnlyCustomField, + $MultilineCustomField, + $WYSIWYGCustomField + ) + + # Check if the PowerShell version is less than 3 and custom fields are being used + if ($PSVersionTable.PSVersion.Major -lt 3 -and ($CustomFields | Where-Object { $_ })) { + Write-Host -Object "[Error] Setting custom fields is not supported in PowerShell 2.0." + Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/4405408656013-Custom-Fields-and-Documentation-CLI-and-Scripting" + exit 1 + } + + # Validate the 'System Volume Minimum Percentage Free' custom field + if ($SystemVolumeMinFreePercentCustomField) { + $SystemVolumeMinFreePercentCustomField = $SystemVolumeMinFreePercentCustomField.Trim() + + if (!($SystemVolumeMinFreePercentCustomField)) { + Write-Host -Object "[Error] The 'Retrieve System Volume Minimum Percentage Free from Custom Field' is invalid." + Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'System Volume Minimum Percentage Free' value from, or leave it blank." + Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" + exit 1 + } + + if ($SystemVolumeMinFreePercentCustomField -match "[^0-9A-Z]") { + Write-Host -Object "[Error] The 'Retrieve System Volume Minimum Percentage Free from Custom Field' of '$SystemVolumeMinFreePercentCustomField' is invalid as it contains invalid characters." + Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'System Volume Minimum Percentage Free' value from, or leave it blank." + Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" + exit 1 + } + } + + # Validate the 'System Volume Minimum Free Size' custom field + if ($SystemVolumeMinFreeSizeCustomField) { + $SystemVolumeMinFreeSizeCustomField = $SystemVolumeMinFreeSizeCustomField.Trim() + + if (!($SystemVolumeMinFreeSizeCustomField)) { + Write-Host -Object "[Error] The 'Retrieve System Volume Minimum Free Size from Custom Field' is invalid." + Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'System Volume Minimum Free Size' value from, or leave it blank." + Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" + exit 1 + } + + if ($SystemVolumeMinFreeSizeCustomField -match "[^0-9A-Z]") { + Write-Host -Object "[Error] The 'Retrieve System Volume Minimum Free Size from Custom Field' of '$SystemVolumeMinFreeSizeCustomField' is invalid as it contains invalid characters." + Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'System Volume Minimum Free Size' value from, or leave it blank." + Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" + exit 1 + } + } + + # Validate the 'Data Volume Minimum Percentage Free' custom field + if ($DataVolumeMinFreePercentCustomField) { + $DataVolumeMinFreePercentCustomField = $DataVolumeMinFreePercentCustomField.Trim() + + if (!($DataVolumeMinFreePercentCustomField)) { + Write-Host -Object "[Error] The 'Retrieve Data Volume Minimum Percentage Free from Custom Field' is invalid." + Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'Data Volume Minimum Percentage Free' value from, or leave it blank." + Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" + exit 1 + } + + if ($DataVolumeMinFreePercentCustomField -match "[^0-9A-Z]") { + Write-Host -Object "[Error] The 'Retrieve Data Volume Minimum Percentage Free from Custom Field' of '$DataVolumeMinFreePercentCustomField' is invalid as it contains invalid characters." + Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'Data Volume Minimum Percentage Free' value from, or leave it blank." + Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" + exit 1 + } + } + + # Validate the 'Data Volume Minimum Free Size' custom field + if ($DataVolumeMinFreeSizeCustomField) { + $DataVolumeMinFreeSizeCustomField = $DataVolumeMinFreeSizeCustomField.Trim() + + if (!($DataVolumeMinFreeSizeCustomField)) { + Write-Host -Object "[Error] The 'Retrieve Data Volume Minimum Free Size from Custom Field' is invalid." + Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'Data Volume Minimum Free Size' value from, or leave it blank." + Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" + exit 1 + } + + if ($DataVolumeMinFreeSizeCustomField -match "[^0-9A-Z]") { + Write-Host -Object "[Error] The 'Retrieve Data Volume Minimum Free Size from Custom Field' of '$DataVolumeMinFreeSizeCustomField' is invalid as it contains invalid characters." + Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'Data Volume Minimum Free Size' value from, or leave it blank." + Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" + exit 1 + } + } + + # Validate the 'Exclude Data Volumes from Alert' custom field + if ($ExcludeDataVolumeCustomField) { + $ExcludeDataVolumeCustomField = $ExcludeDataVolumeCustomField.Trim() + + if (!($ExcludeDataVolumeCustomField)) { + Write-Host -Object "[Error] The 'Retrieve Exclude Data Volumes from Alert from Custom Field' is invalid." + Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'Exclude Data Volumes from Alert' value from, or leave it blank." + Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" + exit 1 + } + + if ($ExcludeDataVolumeCustomField -match "[^0-9A-Z]") { + Write-Host -Object "[Error] The 'Retrieve Exclude Data Volumes from Alert from Custom Field' of '$ExcludeDataVolumeCustomField' is invalid as it contains invalid characters." + Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'Exclude Data Volumes from Alert' value from, or leave it blank." + Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" + exit 1 + } + } + + # Validate the 'Alert Only for Data Volumes' custom field + if ($AlertOnlyCustomField) { + $AlertOnlyCustomField = $AlertOnlyCustomField.Trim() + + if (!($AlertOnlyCustomField)) { + Write-Host -Object "[Error] The 'Retrieve Alert Only for Data Volumes from Custom Field' is invalid." + Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'Alert Only for Data Volumes' value from, or leave it blank." + Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" + exit 1 + } + + if ($AlertOnlyCustomField -match "[^0-9A-Z]") { + Write-Host -Object "[Error] The 'Retrieve Alert Only for Data Volumes from Custom Field' of '$AlertOnlyCustomField' is invalid as it contains invalid characters." + Write-Host -Object "[Error] Please provide a valid text custom field name to retrieve the 'Alert Only for Data Volumes' value from, or leave it blank." + Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" + exit 1 + } + } + + # Validate the 'Multiline Custom Field Name' + if ($MultilineCustomField) { + $MultilineCustomField = $MultilineCustomField.Trim() + + if (!($MultilineCustomField)) { + Write-Host -Object "[Error] The 'Multiline Custom Field Name' is invalid." + Write-Host -Object "[Error] Please provide a valid multiline custom field name to save the results, or leave it blank." + Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" + exit 1 + } + + if ($MultilineCustomField -match "[^0-9A-Z]") { + Write-Host -Object "[Error] The 'Multiline Custom Field Name' of '$MultilineCustomField' is invalid as it contains invalid characters." + Write-Host -Object "[Error] Please provide a valid multiline custom field name to save the results, or leave it blank." + Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" + exit 1 + } + } + + # Validate the 'WYSIWYG Custom Field Name' + if ($WYSIWYGCustomField) { + $WYSIWYGCustomField = $WYSIWYGCustomField.Trim() + + if (!($WYSIWYGCustomField)) { + Write-Host -Object "[Error] The 'WYSIWYG Custom Field Name' is invalid." + Write-Host -Object "[Error] Please provide a valid WYSIWYG custom field name to save the results, or leave it blank." + Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" + exit 1 + } + + if ($WYSIWYGCustomField -match "[^0-9A-Z]") { + Write-Host -Object "[Error] The 'WYSIWYG Custom Field Name' of '$WYSIWYGCustomField' is invalid as it contains invalid characters." + Write-Host -Object "[Error] Please provide a valid WYSIWYG custom field name to save the results, or leave it blank." + Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" + exit 1 + } + } + + # Check for duplicate custom field names + $DuplicateFields = ($CustomFields | Where-Object { $_ } | Group-Object | Where-Object { $_.Count -gt 1 }).Group + if (($CustomFields | Where-Object { $_ } | Group-Object | Where-Object { $_.Count -gt 1 })) { + Write-Host -Object "[Error] You must provide a unique name for each custom field you would like to either retrieve the value from or save to." + Write-Host -Object "[Error] Or you can leave the value blank." + Write-Host -Object "[Error] Duplicate Field Names Given: $($DuplicateFields -join ', ')" + Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/articles/360060920631-Custom-Field-Setup" + exit 1 + } + + # Convert file size strings (e.g., "50 GB") into their equivalent numeric byte values. + function ConvertTo-Bytes { + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $True)] + [String[]]$FileSize, + [Parameter()] + [String]$DefaultTo + ) + process { + # Check if the input is null or an empty string. + if ([String]::IsNullOrEmpty($FileSize)) { + throw (New-Object System.ArgumentNullException("You must provide a file size string to convert into bytes.")) + } + + # Define an array of valid default unit options. + $ValidDefaults = "PB", "TB", "GB", "MB", "KB", "B", "Bytes" + + # Check if $DefaultTo has a value and is not in the list of valid defaults. + if ($DefaultTo -and $ValidDefaults -notcontains $DefaultTo) { + throw (New-Object System.ArgumentOutOfRangeException("You cannot default to '$DefaultTo'. Valid default options include: 'PB', 'TB', 'GB', 'MB', 'KB', 'B', and 'Bytes'.")) + } + + # Create a generic list to store validated file size strings. + $FileSizesToConvert = New-Object System.Collections.Generic.List[String] + + # Iterate over each provided file size string. + $FileSize | ForEach-Object { + # Store the trimmed input string for easier reference. + $CurrentFileSizeObject = $_.Trim() + + if (!($CurrentFileSizeObject)) { + Write-Error -Category ObjectNotFound -Exception (New-Object System.ArgumentNullException("FileSize", "An empty file size string was given. Unable to convert null into bytes.")) + return + } + + # Validate the string for any characters not allowed. + # This regex permits digits, the period, whitespace, dashes, and valid unit strings (PB, TB, GB, MB, KB, B, Bytes). + if ($CurrentFileSizeObject -match "[^0-9. (PB|TB|GB|MB|KB|B|Bytes)-]") { + Write-Error -Category InvalidArgument -Exception (New-Object System.ArgumentException("The file size of '$CurrentFileSizeObject' is invalid; it contains invalid characters. Please specify a file size such as '50 GB'.")) + return + } + + # Validate the overall format of the file size string. + if ($CurrentFileSizeObject -notmatch "^-?[0-9]+\.?[0-9]*\s*(PB|TB|GB|MB|KB|B|Bytes)?$") { + Write-Error -Category InvalidArgument -Exception (New-Object System.ArgumentException("The file size of '$CurrentFileSizeObject' is invalid; it's in an invalid format. Please specify a file size such as '50 GB'")) + return + } + + # If the trimmed input does not end with one of the valid units (PB, TB, GB, MB, KB, B, or Bytes) + if ($CurrentFileSizeObject -notmatch "(PB|TB|GB|MB|KB|B|Bytes)$") { + + # Determine which unit to append based on the default unit ($DefaultTo) + $NewFileSize = switch ($DefaultTo) { + 'PB' { "$CurrentFileSizeObject PB" } + 'TB' { "$CurrentFileSizeObject TB" } + 'GB' { "$CurrentFileSizeObject GB" } + 'MB' { "$CurrentFileSizeObject MB" } + 'KB' { "$CurrentFileSizeObject KB" } + default { "$CurrentFileSizeObject Bytes" } + } + + # Add the newly formatted string to the list of file sizes to convert. + $FileSizesToConvert.Add($NewFileSize) + + return + } + + # Add the validated, trimmed file size string to our list. + $FileSizesToConvert.Add($CurrentFileSizeObject) + } + + # If no valid file sizes were found, throw an error. + if ($FileSizesToConvert.Count -lt 1) { + throw (New-Object System.ArgumentNullException("You must provide a file size string to convert into bytes.")) + } + + # Process each validated file size string and convert it into bytes. + $FileSizesToConvert | ForEach-Object { + $DigitCharacters = $Null + + try { + # Extract the numeric portion from the string (digits, decimal point, and optional minus sign) + # and convert it to a decimal. + [decimal]$DigitCharacters = $_ -replace '[^0-9.-]' + } + catch { + $_ + return + } + + # Determine the unit in the file size string using regex in a switch statement. + # Multiply the numeric value by the corresponding byte constant. + switch -regex ($_) { + 'PB$' { $DigitCharacters * 1PB; break } + 'TB$' { $DigitCharacters * 1TB; break } + 'GB$' { $DigitCharacters * 1GB; break } + 'MB$' { $DigitCharacters * 1MB; break } + 'KB$' { $DigitCharacters * 1KB; break } + 'B$' { $DigitCharacters * 1; break } + 'Bytes$' { $DigitCharacters * 1; break } + } + } + } + } + + # Convert file sizes given in bytes to a human-friendly string format (e.g., "2.8 MB"). + function ConvertTo-FriendlySize { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline = $True)] + [long[]]$Bytes, + [Parameter()] + [long]$RoundTo = 2 + ) + process { + # Validate input: If $Bytes is null or an empty string set Bytes equal to 0. + if ([String]::IsNullOrEmpty($Bytes)) { + $Bytes = 0 + } + + # Process each file size in the input array. + $Bytes | ForEach-Object { + $ConvertedBytes = $Null + + # If the current file size is 0, immediately output "0 Bytes" and skip further processing. + if ($_ -eq 0) { + "0 Bytes" + return + } + + # Define an array of size units from Bytes to Zettabytes + $DataSizes = 'Bytes,KB,MB,GB,TB,PB,EB,ZB' -split ',' + + try { + # Initialize the conversion variable with the current byte value. + $ConvertedBytes = $_ + + # This loop repeats as long as the value is divisible by 1KB, incrementing the index by 1 each time. + # The index is later used to select the appropriate unit for the human-friendly string. + for ( $Index = 0; ($ConvertedBytes -ge 1KB -or $ConvertedBytes -le -1KB) -and $Index -lt $DataSizes.Count; $Index++ ) { + $ConvertedBytes = $ConvertedBytes / 1KB + } + } + catch { + $_ + return + } + + # If conversion resulted in a null or false value, write an error message. + if (!($ConvertedBytes)) { + Write-Error -Category ObjectNotFound -Exception (New-Object System.Data.ObjectNotFoundException("Failed to convert '$_' into a human-friendly string.")) + return + } + + # Format the converted value rounded to the specified number of decimal places, + # and append the corresponding unit from the $DataSizes array. + "$([System.Math]::Round($ConvertedBytes, $RoundTo)) $($DataSizes[$Index])" + } + } + } + + function Set-CustomField { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True)] + [String]$Name, + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + $Value, + [Parameter()] + [String]$Type, + [Parameter()] + [String]$DocumentName, + [Parameter()] + [Switch]$Piped + ) + + if ($Type -eq "Date Time") { $Type = "DateTime" } + if ($Type -match "[-]") { $Type = $Type -replace '-' } + if ($Type -match "[/]") { $Type = $Type -replace '/' } + + # Remove the non-breaking space character + if ($Type -eq "WYSIWYG") { + $Value = $Value -replace ' ', ' ' + } + + if ($Type -eq "DateTime" -or $Type -eq "Date") { + $Type = "Date or Date Time" + } + + # Measure the number of characters in the provided value + $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters + + # Throw an error if the value exceeds the character limit of 200,000 characters + if ($Piped -and $Characters -ge 200000) { + throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") + } + + if (!$Piped -and $Characters -ge 45000) { + throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") + } + + # Initialize a hashtable for additional documentation parameters + $DocumentationParams = @{} + + # If a document name is provided, add it to the documentation parameters + if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } + + # Define a list of valid field types + $ValidFields = "Checkbox", "Date", "Date or Date Time", "DateTime", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", + "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" + + # Warn the user if the provided type is not valid + if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } + + # Define types that require options to be retrieved + $NeedsOptions = "Dropdown", "MultiSelect" + + # If the property is being set in a document or field and the type needs options, retrieve them + if ($DocumentName) { + if ($NeedsOptions -contains $Type) { + } + } + else { + if ($NeedsOptions -contains $Type) { + } + } + + # Throw an error if there was an issue retrieving the property options + if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } + + # Process the property value based on its type + switch ($Type) { + "Checkbox" { + # Convert the value to a boolean for Checkbox type + $NinjaValue = [System.Convert]::ToBoolean($Value) + } + "Date or Date Time" { + # Convert the value to a Unix timestamp for Date or Date Time type + $Date = (Get-Date $Value).ToUniversalTime() + $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date + [long]$NinjaValue = $TimeSpan.TotalSeconds + } + "Dropdown" { + # Convert the dropdown value to its corresponding GUID + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID + + # Throw an error if the value is not present in the dropdown options + if (!($Selection)) { + throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") + } + + $NinjaValue = $Selection + } + "MultiSelect" { + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Selections = New-Object System.Collections.Generic.List[String] + if ($Value -match "[,]") { + $Value = $Value -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } + } + + $Value | ForEach-Object { + $GivenValue = $_ + $Selection = $Options | Where-Object { $_.Name -eq $GivenValue } | Select-Object -ExpandProperty GUID + + # Throw an error if the value is not present in the dropdown options + if (!($Selection)) { + throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") + } + + $Selections.Add($Selection) + } + + $NinjaValue = $Selections -join "," + } + "Time" { + # Convert the value to a Unix timestamp for Date or Date Time type + $LocalTime = (Get-Date $Value) + $LocalTimeZone = [TimeZoneInfo]::Local + $UtcTime = [TimeZoneInfo]::ConvertTimeToUtc($LocalTime, $LocalTimeZone) + + [long]$NinjaValue = ($UtcTime.TimeOfDay).TotalSeconds + } + default { + # For other types, use the value as is + $NinjaValue = $Value + } + } + + # Set the property value in the document if a document name is provided + if ($DocumentName) { + } + else { + try { + # NinjaOne integration removed - property setting skipped + # Otherwise, set the standard property value + if ($Piped) { + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 + } + else { + } + } + catch { + throw $_.Exception.Message + } + } + + # Throw an error if setting the property failed + if ($CustomField.Exception) { + throw $CustomField + } + } + + function Get-CustomField { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [String]$Name, + [Parameter()] + [String]$Type, + [Parameter()] + [String]$DocumentName + ) + # Initialize a hashtable for documentation parameters + $DocumentationParams = @{} + + # If a document name is provided, add it to the documentation parameters + if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } + + if ($Type -eq "Date Time") { $Type = "DateTime" } + if ($Type -match "[-]") { $Type = $Type -replace '-' } + if ($Type -match "[/]") { $Type = $Type -replace '/' } + + $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "DateTime", "Decimal", "Device Dropdown", "Device MultiSelect", "Dropdown", + "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Organization Dropdown", "Organization Location Dropdown", + "Organization Location MultiSelect", "Organization MultiSelect", "Phone", "Secure", "Text", "Time", "WYSIWYG", "URL" + if ($Type -and $ValidFields -notcontains $Type) { + Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" + } + + # Define types that require options to be retrieved + $NeedsOptions = "DropDown", "MultiSelect" + + # If a document name is provided, retrieve the property value from the document + if ($DocumentName) { + + # If the property type requires options, retrieve them + if ($NeedsOptions -contains $Type) { + } + } + else { + # If no document name is provided, retrieve the property value directly + + # If the property type requires options, retrieve them + if ($NeedsOptions -contains $Type) { + } + } + + # Throw an exception if there was an error retrieving the property value or options + if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } + if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } + + # Throw an error if the retrieved property value is null or empty + if (!($NinjaPropertyValue)) { + return + } + + # Handle the property value based on its type + switch ($Type) { + "Attachment" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Checkbox" { + # Convert the value to a boolean + [System.Convert]::ToBoolean([int]$NinjaPropertyValue) + } + "Date" { + # Convert a Unix timestamp to local date and time + $UnixTimeStamp = $NinjaPropertyValue + $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp) + $TimeZone = [TimeZoneInfo]::Local + $ConvertedDate = [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) + + Get-Date $ConvertedDate -DisplayHint Date + } + "Date or Date Time" { + # Convert a Unix timestamp to local date and time + $UnixTimeStamp = $NinjaPropertyValue + $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp) + $TimeZone = [TimeZoneInfo]::Local + [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) + } + "DateTime" { + # Convert a Unix timestamp to local date and time + $UnixTimeStamp = $NinjaPropertyValue + $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp) + $TimeZone = [TimeZoneInfo]::Local + $ConvertedDate = [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) + + Get-Date $ConvertedDate -DisplayHint DateTime + } + "Decimal" { + # Convert the value to a double (floating-point number) + [double]$NinjaPropertyValue + } + "Device Dropdown" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Device MultiSelect" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Dropdown" { + # Convert options to a CSV format and match the GUID to retrieve the display name + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name + } + "Integer" { + # Convert the value to an integer + [int]$NinjaPropertyValue + } + "MultiSelect" { + # Convert options to a CSV format, then match and return selected items + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Selection = ($NinjaPropertyValue -split ',').trim() + + foreach ($Item in $Selection) { + $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name + } + } + "Organization Dropdown" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization Location Dropdown" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization Location MultiSelect" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization MultiSelect" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Time" { + $secondsSinceMidnightUTC = $NinjaPropertyValue + # Get midnight for today in UTC + $midnightUTC = [datetime]::UtcNow.Date + + # Add the seconds to midnight to get the UTC time + $utcTime = $midnightUTC.AddSeconds($secondsSinceMidnightUTC) + + # Convert the UTC time to the local time zone + $localTime = [System.TimeZoneInfo]::ConvertTimeFromUtc($utcTime, [System.TimeZoneInfo]::Local) + + # Display the result + Get-Date $localTime -DisplayHint Time + } + default { + # For any other types, return the raw value + $NinjaPropertyValue + } + } + } + + function Test-IsElevated { + [CmdletBinding()] + param () + + # Get the current Windows identity of the user running the script + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + + # Create a WindowsPrincipal object based on the current identity + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + + # Check if the current user is in the Administrator role + # The function returns $True if the user has administrative privileges, $False otherwise + # 544 is the value for the Built-In Administrators role + # Reference: https://learn.microsoft.com/en-us/dotnet/api/system.security.principal.windowsbuiltinrole + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]'544') + } + + if (!($ExitCode)) { + $ExitCode = 0 + } +} +process { + # Attempt to determine if the current session is running with Administrator privileges. + try { + $IsElevated = Test-IsElevated -ErrorAction Stop + } + catch { + # Log an error if unable to determine admin privileges + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Unable to determine if the account '$env:Username' is running with Administrator privileges." + exit 1 + } + + # Exit if the script is not running with Administrator privileges + if (!($IsElevated)) { + Write-Host -Object "[Error] Access Denied: Please run with Administrator privileges." + exit 1 + } + + # Retrieve 'System Volume Minimum Percentage Free' from a custom field if specified and not already provided + if ($SystemVolumeMinFreePercentCustomField -and !($SystemVolumeMinFreePercent)) { + try { + Write-Host -Object "Attempting to retrieve the 'System Volume Minimum Percentage Free' value from the field '$SystemVolumeMinFreePercentCustomField'." + $SystemVolumeMinFreePercent = Get-CustomField -Name $SystemVolumeMinFreePercentCustomField -ErrorAction Stop + Write-Host -Object "Successfully retrieved the custom field contents.`n" + } + catch { + # Log an error if retrieval fails + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to retrieve the 'System Volume Minimum Percentage Free' from the custom field '$SystemVolumeMinFreePercentCustomField'." + exit 1 + } + + # Trim and validate the retrieved value + if ($SystemVolumeMinFreePercent) { + $SystemVolumeMinFreePercent = $SystemVolumeMinFreePercent.Trim() + } + + if (!($SystemVolumeMinFreePercent)) { + Write-Host -Object "[Error] The custom field '$SystemVolumeMinFreePercentCustomField' is empty." + Write-Host -Object "[Error] Please save a value to the custom field or leave 'Retrieve System Volume Minimum Percentage Free from Custom Field' blank." + exit 1 + } + } + + # Retrieve 'System Volume Minimum Free Size' from a custom field if specified and not already provided + if ($SystemVolumeMinFreeSizeCustomField -and !($SystemVolumeMinFreeSize)) { + try { + Write-Host -Object "Attempting to retrieve the 'System Volume Minimum Free Size' value from the field '$SystemVolumeMinFreeSizeCustomField'." + $SystemVolumeMinFreeSize = Get-CustomField -Name $SystemVolumeMinFreeSizeCustomField -ErrorAction Stop + Write-Host -Object "Successfully retrieved the custom field contents.`n" + } + catch { + # Log an error if retrieval fails + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to retrieve the 'System Volume Minimum Free Size' from the custom field '$SystemVolumeMinFreeSizeCustomField'." + exit 1 + } + + # Trim and validate the retrieved value + if ($SystemVolumeMinFreeSize) { + $SystemVolumeMinFreeSize = $SystemVolumeMinFreeSize.Trim() + } + + if (!($SystemVolumeMinFreeSize)) { + Write-Host -Object "[Error] The custom field '$SystemVolumeMinFreeSizeCustomField' is empty." + Write-Host -Object "[Error] Please save a value to the custom field or leave 'Retrieve System Volume Minimum Free Size from Custom Field' blank." + exit 1 + } + } + + # Retrieve 'Data Volume Minimum Percentage Free' from a custom field if specified and not already provided + if ($DataVolumeMinFreePercentCustomField -and !($DataVolumeMinFreePercent)) { + try { + Write-Host -Object "Attempting to retrieve the 'Data Volume Minimum Percentage Free' value from the field '$DataVolumeMinFreePercentCustomField'." + $DataVolumeMinFreePercent = Get-CustomField -Name $DataVolumeMinFreePercentCustomField -ErrorAction Stop + Write-Host -Object "Successfully retrieved the custom field contents.`n" + } + catch { + # Log an error if retrieval fails + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to retrieve the 'Data Volume Minimum Percentage Free' from the custom field '$DataVolumeMinFreePercentCustomField'." + exit 1 + } + + # Trim and validate the retrieved value + if ($DataVolumeMinFreePercent) { + $DataVolumeMinFreePercent = $DataVolumeMinFreePercent.Trim() + } + + if (!($DataVolumeMinFreePercent)) { + Write-Host -Object "[Error] The custom field '$DataVolumeMinFreePercentCustomField' is empty." + Write-Host -Object "[Error] Please save a value to the custom field or leave 'Retrieve Data Volume Minimum Percentage Free from Custom Field' blank." + exit 1 + } + } + + # Retrieve 'Data Volume Minimum Free Size' from a custom field if specified and not already provided + if ($DataVolumeMinFreeSizeCustomField -and !($DataVolumeMinFreeSize)) { + try { + Write-Host -Object "Attempting to retrieve the 'Data Volume Minimum Free Size' value from the field '$DataVolumeMinFreeSizeCustomField'." + $DataVolumeMinFreeSize = Get-CustomField -Name $DataVolumeMinFreeSizeCustomField -ErrorAction Stop + Write-Host -Object "Successfully retrieved the custom field contents.`n" + } + catch { + # Log an error if retrieval fails + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to retrieve the 'Data Volume Minimum Free Size' from the custom field '$DataVolumeMinFreeSizeCustomField'." + exit 1 + } + + # Trim and validate the retrieved value + if ($DataVolumeMinFreeSize) { + $DataVolumeMinFreeSize = $DataVolumeMinFreeSize.Trim() + } + + if (!($DataVolumeMinFreeSize)) { + Write-Host -Object "[Error] The custom field '$DataVolumeMinFreeSizeCustomField' is empty." + Write-Host -Object "[Error] Please save a value to the custom field or leave 'Retrieve Data Volume Minimum Free Size from Custom Field' blank." + exit 1 + } + } + + # Retrieve 'Exclude Data Volumes from Alert' from a custom field if specified and not already provided + if ($ExcludeDataVolumeCustomField) { + try { + Write-Host -Object "Attempting to retrieve the 'Exclude Data Volumes from Alert' value from the field '$ExcludeDataVolumeCustomField'." + $VolumesToExclude = Get-CustomField -Name $ExcludeDataVolumeCustomField -ErrorAction Stop + Write-Host -Object "Successfully retrieved the custom field contents.`n" + } + catch { + # Log an error if retrieval fails + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to retrieve the 'Exclude Data Volumes from Alert' from the custom field '$ExcludeDataVolumeCustomField'." + exit 1 + } + + # Trim and validate the retrieved value + if ($VolumesToExclude) { + $VolumesToExclude = $VolumesToExclude.Trim() + } + + if (!($VolumesToExclude)) { + Write-Host -Object "[Error] The custom field '$ExcludeDataVolumeCustomField' is empty." + Write-Host -Object "[Error] Please save a value to the custom field or leave 'Retrieve Exclude Data Volumes from Alert from Custom Field' blank." + exit 1 + } + + if($ExcludeDataVolumesFromAlert -match "[^, ]"){ + $ExcludeDataVolumesFromAlert = "$ExcludeDataVolumesFromAlert, $VolumesToExclude" + }else{ + $ExcludeDataVolumesFromAlert = $VolumesToExclude + } + } + + # Retrieve 'Alert Only for Data Volumes' from a custom field if specified and not already provided + if ($AlertOnlyCustomField -and !($AlertOnlyForDataVolumes)) { + try { + Write-Host -Object "Attempting to retrieve the 'Alert Only for Data Volumes' value from the field '$AlertOnlyCustomField'." + $AlertOnlyForDataVolumes = Get-CustomField -Name $AlertOnlyCustomField -ErrorAction Stop + Write-Host -Object "Successfully retrieved the custom field contents.`n" + } + catch { + # Log an error if retrieval fails + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to retrieve the 'Alert Only for Data Volumes' from the custom field '$AlertOnlyCustomField'." + exit 1 + } + + # Trim and validate the retrieved value + if ($AlertOnlyForDataVolumes) { + $AlertOnlyForDataVolumes = $AlertOnlyForDataVolumes.Trim() + } + + if (!($AlertOnlyForDataVolumes)) { + Write-Host -Object "[Error] The custom field '$AlertOnlyCustomField' is empty." + Write-Host -Object "[Error] Please save a value to the custom field or leave 'Retrieve Alert Only for Data Volumes from Custom Field' blank." + exit 1 + } + } + + # Retrieve the system volume based on the system drive + Write-Host -Object "Retrieving the system volume '$env:SystemDrive'." + try { + if ($PSVersionTable.PSVersion.Major -lt 3) { + # Use WMI for PowerShell versions < 3 + $SystemVolume = Get-WmiObject -Class Win32_Volume -ErrorAction Stop | Where-Object { $_.DriveLetter -eq "$env:SystemDrive" } | + Select-Object "DriveLetter", "Label", "Name", "FileSystem", "FreeSpace", "Capacity" | Sort-Object Label + } + else { + # Use CIM for PowerShell versions >= 3 + $SystemVolume = Get-CimInstance -ClassName Win32_Volume -ErrorAction Stop | Where-Object { $_.DriveLetter -eq "$env:SystemDrive" } | + Select-Object "DriveLetter", "Label", "Name", "FileSystem", "FreeSpace", "Capacity" | Sort-Object Label + } + + # Validate the retrieved system volume + if (!($SystemVolume)) { + throw "No system volume with a drive letter that matches '$env:SystemDrive'." + } + + if ($SystemVolume.Count -gt 1) { + throw "Multiple volumes detected with the drive letter '$env:SystemDrive'." + } + } + catch { + # Log an error if retrieval fails + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to retrieve the system volume. Unable to calculate the free space remaining." + exit 1 + } + + # Initialize a list to store system volume minimum free space data + $SystemVolumeMinFree = New-Object System.Collections.Generic.List[object] + + # Retrieve all data volumes excluding the system drive + Write-Host -Object "Retrieving all data volumes.`n" + try { + if ($PSVersionTable.PSVersion.Major -lt 3) { + # Use WMI for PowerShell versions < 3 + $DataVolumes = Get-WmiObject -Class Win32_Volume -ErrorAction Stop | Where-Object { $_.DriveType -eq 3 -and $_.DriveLetter -ne "$env:SystemDrive" -and $_.Capacity -gt 0 -and $_.FreeSpace -gt 0 } | + Select-Object "DriveLetter", "Label", "Name", "FileSystem", "FreeSpace", "Capacity" | Sort-Object Label + } + else { + # Use CIM for PowerShell versions >= 3 + $DataVolumes = Get-CimInstance -ClassName Win32_Volume -ErrorAction Stop | Where-Object { $_.DriveType -eq 3 -and $_.DriveLetter -ne "$env:SystemDrive" -and $_.Capacity -gt 0 -and $_.FreeSpace -gt 0 } | + Select-Object "DriveLetter", "Label", "Name", "FileSystem", "FreeSpace", "Capacity" | Sort-Object Label + } + + # Log a warning if no data volumes are detected + if (!($DataVolumes)) { + Write-Host -Object "[Warning] No data volumes detected." + } + } + catch { + # Log an error if retrieval fails + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to retrieve data volumes. Unable to calculate free space remaining." + exit 1 + } + + # Initialize a list to store data volume minimum free space data + $DataVolumeMinFree = New-Object System.Collections.Generic.List[object] + + # Process volumes to exclude from alerts + if ($ExcludeDataVolumesFromAlert) { + if (!($ExcludeDataVolumesFromAlert.Trim())) { + Write-Host -Object "[Error] Please specify a valid volume label or drive letter or file path to exclude." + exit 1 + } + + if (!($DataVolumeMinFreePercent) -and !($DataVolumeMinFreeSize)) { + Write-Host -Object "[Warning] You must have a data volume limit in order to exclude it from the alert." + } + + # Initialize a list to store volumes to exclude + $VolumesToExclude = New-Object System.Collections.Generic.List[string] + + try { + if ($PSVersionTable.PSVersion.Major -lt 3) { + # Retrieve system volume labels using WMI for PowerShell versions < 3 + $SystemVolumeLabels = (Get-WmiObject -Class Win32_Volume -ErrorAction Stop | Where-Object { $_.DriveLetter -eq "$env:SystemDrive" } | + Select-Object -Property "Label" -ErrorAction SilentlyContinue).Label + } + else { + # Retrieve system volume labels using CIM for PowerShell versions >= 3 + $SystemVolumeLabels = (Get-CimInstance -ClassName Win32_Volume -ErrorAction Stop | Where-Object { $_.DriveLetter -eq "$env:SystemDrive" } | + Select-Object -Property "Label" -ErrorAction SilentlyContinue).Label + } + } + catch { + # Log an error if retrieval fails + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to retrieve the system volume labels." + exit 1 + } + + # Process each volume to exclude + $ExcludeDataVolumesFromAlert -split "," | ForEach-Object { + # Normalize the volume format + $PotentialVolume = $_.Trim() + + if (!($PotentialVolume)) { + Write-Host -Object "[Error] '$ExcludeDataVolumesFromAlert' contains an invalid volume to exclude. There is an empty item in the comma-separated list." + exit 1 + } + + if ($PotentialVolume -match "^[A-Z]$") { + $PotentialVolume = "$PotentialVolume" + ":" + } + + # Prevent excluding the system volume + $SystemVolumes = "$env:SystemDrive", "$($env:SystemDrive -replace '[^A-Z:]')", "$env:SystemDrive\" + if ($SystemVolumes -contains $PotentialVolume) { + Write-Host -Object "[Warning] The system volume '$PotentialVolume' cannot be excluded." + } + + if ($SystemVolumeLabels -contains $PotentialVolume) { + Write-Host -Object "[Warning] The system volume '$PotentialVolume' cannot be excluded." + } + + if ($DataVolumes.DriveLetter -notcontains $PotentialVolume -and $DataVolumes.Label -notcontains $PotentialVolume -and $DataVolumes.Name -notcontains $PotentialVolume) { + Write-Host -Object "[Warning] The volume '$PotentialVolume' is not the label, drive letter, or path of any data volume on this system." + $AddAnExtraNewLine = $True + } + + # Add the volume to the exclusion list + $VolumesToExclude.Add($PotentialVolume) + } + } + + if($AddAnExtraNewLine){ + Write-Host -Object "" + } + + # Process volumes to include in alerts + if ($AlertOnlyForDataVolumes) { + if (!($AlertOnlyForDataVolumes.Trim())) { + Write-Host -Object "[Error] Please specify a valid volume label or drive letter or file path to alert on." + exit 1 + } + + if (!($DataVolumeMinFreePercent) -and !($DataVolumeMinFreeSize)) { + Write-Host -Object "[Warning] You must have a data volume limit in order to include it for the alert." + } + + # Initialize a list to store volumes to include + $VolumesToInclude = New-Object System.Collections.Generic.List[String] + + # Process each volume to include + $AlertOnlyForDataVolumes -split "," | ForEach-Object { + # Normalize the volume format + $PotentialVolume = $_.Trim() + + if (!($PotentialVolume)) { + Write-Host -Object "[Error] '$AlertOnlyForDataVolumes' contains an invalid volume to alert on. There is an empty item in the comma-separated list." + exit 1 + } + + # Normalize the volume format + $PotentialVolume = $_.Trim() + + if ($PotentialVolume -match "^[A-Z]$") { + $PotentialVolume = "$PotentialVolume" + ":" + } + + # Prevent including volumes that are also excluded + if ($ExcludeDataVolumesFromAlert.Count -ge 1 -and $ExcludeDataVolumesFromAlert -contains $PotentialVolume) { + Write-Host -Object "[Error] Cannot alert only on '$($_.Trim())' and exclude it from the alert." + exit 1 + } + + if ($DataVolumes.DriveLetter -notcontains $PotentialVolume -and $DataVolumes.Label -notcontains $PotentialVolume -and $DataVolumes.Name -notcontains $PotentialVolume) { + Write-Host -Object "[Warning] The volume '$PotentialVolume' is not the label, drive letter, or path of any data volume on this system.`n" + $AddAnExtraNewLine = $True + } + + # Add the volume to the inclusion list + $VolumesToInclude.Add($PotentialVolume) + } + } + + if($AddAnExtraNewLine){ + Write-Host -Object "" + } + + # Check if a minimum percentage of free space for the system volume is specified + if ($SystemVolumeMinFreePercent) { + $SystemVolumeMinFreePercent = $SystemVolumeMinFreePercent.Trim() + + # Validate that the percentage is not empty + if (!($SystemVolumeMinFreePercent)) { + Write-Host -Object "[Error] The minimum system volume free size percentage of '$SystemVolumeMinFreePercent' is invalid." + Write-Host -Object "[Error] A percentage that is greater than 0 and less than 100 was expected." + exit 1 + } + + # Validate that the percentage contains only valid characters + if ($SystemVolumeMinFreePercent -match "[^0-9.% ]") { + Write-Host -Object "[Error] The minimum system volume free size percentage of '$SystemVolumeMinFreePercent' is invalid." + Write-Host -Object "[Error] It contains invalid characters. The only characters supported (besides whitespace) are '.','%' and numeric characters." + exit 1 + } + + # Validate the format of the percentage + if ($SystemVolumeMinFreePercent -notmatch "^\d{1,}(\.\d{1,})?\s?%?$") { + Write-Host -Object "[Error] The minimum system volume free size percentage of '$SystemVolumeMinFreePercent' is invalid. It is in an invalid format." + Write-Host -Object "[Error] A percentage was expected such as '33%', '77.23%' or '77.23 %'." + exit 1 + } + + # Attempt to extract the numeric value from the percentage + try { + $SystemVolumeMinFreePercent = [decimal]$(($SystemVolumeMinFreePercent -replace '%').Trim()) + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to extract the percentage from '$SystemVolumeMinFreePercent'" + exit 1 + } + + # Ensure the percentage is within a valid range + if ([decimal]$SystemVolumeMinFreePercent -le 0 -or [decimal]$SystemVolumeMinFreePercent -ge 100) { + Write-Host -Object "[Error] The minimum system volume free size percentage of '$SystemVolumeMinFreePercent' is invalid." + Write-Host -Object "[Error] A value between 0 and 100 is expected." + exit 1 + } + + # Convert the percentage into bytes based on the system volume's capacity + try { + Write-Host -Object "Converting the minimum free space percentage '$($SystemVolumeMinFreePercent -replace "[^0-9.%]")%' required for system volumes into bytes." + $SystemVolumeMinFreeBytes = $SystemVolume.Capacity * ([decimal]$($SystemVolumeMinFreePercent -replace "[^0-9.]") / 100) + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to convert the percentage into the minimum free space required in bytes using '$($SystemVolume.Used)' and '$($SystemVolume.Free)'." + exit 1 + } + + # Add the calculated minimum free space requirement to the list + $SystemVolumeMinFree.Add(( + New-Object PSObject -Property @{ + Type = "Minimum Percent Free" + Path = $SystemVolume.Name + Limit = "$($SystemVolumeMinFreePercent -replace '[^0-9.]')%" + MinimumInBytes = $SystemVolumeMinFreeBytes + } + )) + } + + # Check if a minimum free size for the system volume is specified + if ($SystemVolumeMinFreeSize) { + $SystemVolumeMinFreeSize = $SystemVolumeMinFreeSize.Trim() + + # Validate that the size is not empty + if (!($SystemVolumeMinFreeSize)) { + Write-Host -Object "[Error] The minimum system volume free size of '$SystemVolumeMinFreeSize' is invalid." + Write-Host -Object "[Error] A value representing the amount of free space that needs to be available was expected. For example: '50GB', '50000MB', or '53687091200 Bytes'" + exit 1 + } + + # Validate that the size contains only valid characters + if ($SystemVolumeMinFreeSize -match "[^0-9. (PB|TB|GB|MB|KB|B|Bytes)]") { + Write-Host -Object "[Error] The minimum system volume free size of '$SystemVolumeMinFreeSize' is invalid." + Write-Host -Object "[Error] It contains invalid characters. The only characters supported (besides whitespace) are '.', data size characters (PB, TB, GB, MB, KB, B, Bytes) and numeric characters." + exit 1 + } + + # Validate the format of the size + if ($SystemVolumeMinFreeSize -notmatch "^?[0-9]+\.?[0-9]*\s*(PB|TB|GB|MB|KB|B|Bytes)?$") { + Write-Host -Object "[Error] The minimum system volume free size of '$SystemVolumeMinFreeSize' is invalid." + Write-Host -Object "[Error] It is in an invalid format. Please specify a data size such as '50GB', '50000MB', or '53687091200 Bytes'." + exit 1 + } + + # Default to GB if no unit is specified + if ($SystemVolumeMinFreeSize -match "^[0-9]+$") { + $SystemVolumeMinFreeSize = "$SystemVolumeMinFreeSize" + "GB" + } + + # Convert the size into bytes + try { + $SystemVolumeMinFreeBytes = ConvertTo-Bytes -FileSize $SystemVolumeMinFreeSize -DefaultTo "GB" -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to convert '$SystemVolumeMinFreeSize' to bytes. Unable to determine the minimum free space." + exit 1 + } + + # Ensure the size is greater than 0 + if ($SystemVolumeMinFreeBytes -le 0) { + Write-Host -Object "[Error] The minimum system volume free size of '$SystemVolumeMinFreeSize' is invalid." + Write-Host -Object "[Error] A value greater than 0 is expected." + exit 1 + } + + # Add the calculated minimum free size requirement to the list + Write-Host -Object "Converting the minimum free space required '$SystemVolumeMinFreeSize' for system volumes into bytes." + $SystemVolumeMinFree.Add(( + New-Object PSObject -Property @{ + Type = "Minimum Free Size" + Path = $SystemVolume.Name + Limit = $SystemVolumeMinFreeSize + MinimumInBytes = $SystemVolumeMinFreeBytes + } + )) + } + + # Check if a minimum percentage of free space for data volumes is specified + if ($DataVolumeMinFreePercent) { + $DataVolumeMinFreePercent = $DataVolumeMinFreePercent.Trim() + + # Validate that the percentage is not empty + if (!($DataVolumeMinFreePercent)) { + Write-Host -Object "[Error] The minimum data volume free size percentage of '$DataVolumeMinFreePercent' is invalid." + Write-Host -Object "[Error] A percentage that is greater than 0 and less than 100 was expected." + exit 1 + } + + # Validate that the percentage contains only valid characters + if ($DataVolumeMinFreePercent -match "[^0-9.% ]") { + Write-Host -Object "[Error] The minimum data volume free size percentage of '$DataVolumeMinFreePercent' is invalid." + Write-Host -Object "[Error] It contains invalid characters. The only characters supported (besides whitespace) are '.','%' and numeric characters." + exit 1 + } + + # Validate the format of the percentage + if ($DataVolumeMinFreePercent -notmatch "^\d{1,}(\.\d{1,})?\s?%?$") { + Write-Host -Object "[Error] The minimum data volume free size percentage of '$DataVolumeMinFreePercent' is invalid. It is in an invalid format." + Write-Host -Object "[Error] A percentage was expected such as '33%', '77.23%' or '77.23 %'." + exit 1 + } + + # Attempt to extract the numeric value from the percentage + try { + $DataVolumeMinFreePercent = [decimal]$(($DataVolumeMinFreePercent -replace '%').Trim()) + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to extract the percentage from '$DataVolumeMinFreePercent'" + exit 1 + } + + # Ensure the percentage is within a valid range + if ([decimal]$DataVolumeMinFreePercent -le 0 -or [decimal]$DataVolumeMinFreePercent -ge 100) { + Write-Host -Object "[Error] The minimum data volume free size percentage of '$DataVolumeMinFreePercent' is invalid." + Write-Host -Object "[Error] A value between 0 and 100 is expected." + exit 1 + } + + # Convert the percentage into bytes for each data volume + try { + Write-Host -Object "Converting the minimum free space percentage '$($DataVolumeMinFreePercent -replace '[^0-9.]')%' required into bytes for each data volume." + $DataVolumes | ForEach-Object { + # Skip volumes that are excluded + if ($VolumesToExclude.Count -ge 1) { + if ($VolumesToExclude -contains $_.DriveLetter) { + return + } + + if ($VolumesToExclude -contains $_.Label) { + return + } + + if ($VolumesToExclude -contains $_.Name) { + return + } + } + + # Skip volumes that are not included + if ($VolumesToInclude.Count -ge 1) { + if ($VolumesToInclude -notcontains $_.DriveLetter -and $VolumesToInclude -notcontains $_.Label -and $VolumesToInclude -notcontains $_.Name) { + return + } + } + + # Calculate the minimum free space in bytes for the current volume + Write-Verbose -Message "Converting the minimum free space percentage '$($DataVolumeMinFreePercent -replace '[^0-9.]')%' required into bytes for the volume '$($_.Label)'." + $DataVolumeMinFreeBytes = $_.Capacity * ([decimal]$($DataVolumeMinFreePercent -replace "[^0-9.]") / 100) + + # Add the calculated minimum free space requirement to the list + $DataVolumeMinFree.Add(( + New-Object PSObject -Property @{ + Name = $_.Label + DriveLetter = $_.DriveLetter + Path = $_.Name + FreeSpace = $_.FreeSpace + Total = $_.Capacity + Type = "Minimum Percent Free" + Limit = "$($DataVolumeMinFreePercent -replace '[^0-9.]')%" + MinimumInBytes = $DataVolumeMinFreeBytes + } + )) + } + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to convert the percentage into the minimum data volume free space required in bytes." + exit 1 + } + } + + # Check if a minimum free size for data volumes is specified + if ($DataVolumeMinFreeSize) { + $DataVolumeMinFreeSize = $DataVolumeMinFreeSize.Trim() + + # Validate that the size is not empty + if (!($DataVolumeMinFreeSize)) { + Write-Host -Object "[Error] The minimum data volume free size of '$DataVolumeMinFreeSize' is invalid." + Write-Host -Object "[Error] A value representing the amount of free space that needs to be available was expected. For example: '50GB', '50000MB', or '53687091200 Bytes'" + exit 1 + } + + # Validate that the size contains only valid characters + if ($DataVolumeMinFreeSize -match "[^0-9. (PB|TB|GB|MB|KB|B|Bytes)]") { + Write-Host -Object "[Error] The minimum data volume free size of '$DataVolumeMinFreeSize' is invalid." + Write-Host -Object "[Error] It contains invalid characters. The only characters supported (besides whitespace) are '.', data size characters (PB, TB, GB, MB, KB, B, Bytes) and numeric characters." + exit 1 + } + + # Validate the format of the size + if ($DataVolumeMinFreeSize -notmatch "^?[0-9]+\.?[0-9]*\s*(PB|TB|GB|MB|KB|B|Bytes)?$") { + Write-Host -Object "[Error] The minimum data volume free size of '$DataVolumeMinFreeSize' is invalid." + Write-Host -Object "[Error] It is in an invalid format. Please specify a data size such as '50GB', '50000MB', or '53687091200 Bytes'." + exit 1 + } + + # Default to GB if no unit is specified + if ($DataVolumeMinFreeSize -match "^[0-9]+$") { + $DataVolumeMinFreeSize = "$DataVolumeMinFreeSize" + "GB" + } + + # Convert the size into bytes + Write-Host -Object "Converting the minimum free space required '$DataVolumeMinFreeSize' for data volumes into bytes." + try { + $DataVolumeMinFreeBytes = ConvertTo-Bytes -FileSize $DataVolumeMinFreeSize -DefaultTo "GB" -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to convert '$DataVolumeMinFreeSize' to bytes. Unable to determine the minimum free space." + exit 1 + } + + # Ensure the size is greater than 0 + if ($DataVolumeMinFreeBytes -le 0) { + Write-Host -Object "[Error] The minimum data volume free size of '$DataVolumeMinFreeSize' is invalid." + Write-Host -Object "[Error] A value greater than 0 is expected." + exit 1 + } + + # Add the calculated minimum free size requirement to the list for each data volume + $DataVolumes | ForEach-Object { + # Skip volumes that are excluded + if ($VolumesToExclude.Count -ge 1) { + if ($VolumesToExclude -contains $_.DriveLetter) { + return + } + + if ($VolumesToExclude -contains $_.Label) { + return + } + + if ($VolumesToExclude -contains $_.Name) { + return + } + } + + # Skip volumes that are not included + if ($VolumesToInclude.Count -ge 1) { + if ($VolumesToInclude -notcontains $_.DriveLetter -and $VolumesToInclude -notcontains $_.Label -and $VolumesToInclude -notcontains $_.Name) { + return + } + } + + # Add the calculated minimum free size requirement to the list + $DataVolumeMinFree.Add(( + New-Object PSObject -Property @{ + Name = $_.Label + DriveLetter = $_.DriveLetter + Path = $_.Name + FreeSpace = $_.FreeSpace + Total = $_.Capacity + Type = "Minimum Free Size" + Limit = $DataVolumeMinFreeSize + MinimumInBytes = $DataVolumeMinFreeBytes + } + )) + } + } + + # Check if there are any alerts for system or data volumes + if ($SystemVolumeMinFree.Count -gt 0 -or $DataVolumeMinFree.Count -gt 0) { + Write-Host -Object "" + } + + # Process alerts for the system volume + if ($SystemVolumeMinFree.Count -gt 0) { + $SystemVolumeMinFree | ForEach-Object { + if ($SystemVolume.FreeSpace -lt $_.MinimumInBytes) { + # Alert if the system volume free space is below the specified limit + Write-Host -Object "[Alert] The system volume exceeds the '$($_.Type)' limit of '$($_.Limit)'." + } + } + } + + # Process alerts for data volumes + if ($DataVolumeMinFree.Count -gt 0) { + $DataVolumeMinFree | ForEach-Object { + if ($_.FreeSpace -lt $_.MinimumInBytes) { + # Alert if a data volume free space is below the specified limit + Write-Host -Object "[Alert] The data volume labeled '$($_.Name)' exceeds the '$($_.Type)' limit of '$($_.Limit)'." + } + } + } + + # Display system volume details in a human-readable format + Write-Host -Object "`n### System Volume ###" + try { + # Convert system volume metrics to friendly sizes and calculate percentages + $SystemVolumeFreeSpace = ConvertTo-FriendlySize -Bytes $SystemVolume.FreeSpace -RoundTo 2 -ErrorAction Stop + $SystemVolumeTotal = ConvertTo-FriendlySize -Bytes $SystemVolume.Capacity -RoundTo 2 -ErrorAction Stop + $SystemVolumeUsed = ConvertTo-FriendlySize -Bytes $($SystemVolume.Capacity - $SystemVolume.FreeSpace) -RoundTo 2 -ErrorAction Stop + $SystemPercentFree = [System.Math]::Round((($SystemVolume.FreeSpace / $SystemVolume.Capacity) * 100), 2) + $SystemPercentUsed = [System.Math]::Round((100 - $SystemPercentFree), 2) + + # Create a formatted object for system volume details + $FormattedSystemVolume = New-Object PSObject -Property @{ + Name = $SystemVolume.Label + DriveLetter = $SystemVolume.DriveLetter + FileSystemType = $SystemVolume.FileSystem + Path = $SystemVolume.Name + FreeSpace = $SystemVolumeFreeSpace + UsedSpace = $SystemVolumeUsed + Total = $SystemVolumeTotal + PercentageFree = "$SystemPercentFree%" + PercentageUsed = "$SystemPercentUsed%" + } + + # Output the formatted system volume details as a table + ($FormattedSystemVolume | Format-Table -Property Name, DriveLetter, FileSystemType, FreeSpace, Total, PercentageFree -AutoSize | Out-String).Trim() | Write-Host + } + catch { + # Handle errors during system volume formatting + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to convert the system volume into a human-readable table." + exit 1 + } + + # Display data volume details in a human-readable format + if ($DataVolumes) { + Write-Host -Object "`n### Data Volumes ###" + try { + # Convert data volume metrics to friendly sizes and calculate percentages + $FormattedDataVolumes = $DataVolumes | ForEach-Object { + $DataVolumeFreeSpace = ConvertTo-FriendlySize -Bytes $_.FreeSpace -RoundTo 2 -ErrorAction Stop + $DataVolumeTotal = ConvertTo-FriendlySize -Bytes $_.Capacity -RoundTo 2 -ErrorAction Stop + $DataVolumeUsed = ConvertTo-FriendlySize -Bytes $($_.Capacity - $_.FreeSpace) -RoundTo 2 -ErrorAction Stop + $DataVolumePercentFree = [System.Math]::Round((($_.FreeSpace / $_.Capacity) * 100), 2) + $DataVolumePercentUsed = [System.Math]::Round((100 - $DataVolumePercentFree), 2) + + # Create a formatted object for each data volume + New-Object PSObject -Property @{ + Name = $_.Label + DriveLetter = $_.DriveLetter + FileSystemType = $_.FileSystem + Path = $_.Name + FreeSpace = $DataVolumeFreeSpace + UsedSpace = $DataVolumeUsed + Total = $DataVolumeTotal + PercentageFree = "$DataVolumePercentFree%" + PercentageUsed = "$DataVolumePercentUsed%" + } + } + + # Output the formatted data volume details as a list + ($FormattedDataVolumes | Format-List -Property Name, DriveLetter, FileSystemType, Path, FreeSpace, Total, PercentageFree | Out-String).Trim() | Write-Host + } + catch { + # Handle errors during data volume formatting + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to convert the data volumes into a human-readable table." + exit 1 + } + } + + # Update a multiline custom field with volume details and alerts + if ($MultilineCustomField) { + Write-Host "" + Write-Host "Note: Custom field '$MultilineCustomField' was specified but NinjaOne integration has been removed." + Write-Host "All output has been displayed above." + } + + # Update a WYSIWYG custom field with volume details and alerts + if ($WYSIWYGCustomField) { + Write-Host "" + Write-Host "Note: Custom field '$WYSIWYGCustomField' was specified but NinjaOne integration has been removed." + Write-Host "All output has been displayed above." + } + + exit $ExitCode +} +end { + +} diff --git a/Powershell Scripts/Display PopUp.ps1 b/Powershell Scripts/Display PopUp.ps1 index af93483..b2e122c 100644 --- a/Powershell Scripts/Display PopUp.ps1 +++ b/Powershell Scripts/Display PopUp.ps1 @@ -1,788 +1,786 @@ # Displays a popup on the end users screen. The script needs to be run as the Current Logged In User. Use Restart Reminder to display a request to the end user to restart their computer. -<# -.SYNOPSIS - Displays a popup on the end user's screen. The script needs to be run as the 'Current Logged In User'. Use "Restart Reminder" to display a request to the end user to restart their computer. -.DESCRIPTION - Displays a popup on the end user's screen. The script needs to be run as the 'Current Logged In User'. Use "Restart Reminder" to display a request to the end user to restart their computer. - See the comment block in this script's code for extra options such as changing the logo or the window title. - Uses Windows Presentation Framework to accommodate different DPIs and image scaling. On Windows 7 and Server 2008, the script will use the older Windows Form UI. - - You can also convert an image to base64 and replace either lines 164 or 168 if you'd prefer to not have to download an image to a machine. -.EXAMPLE - (No Parameters on Windows 11) - - Result: Success - -PARAMETER: -RestartReminder - Displays a generic prompt requesting the user restart their machine. - This parameter is equivalent to the below options. - -Text "Your IT Administrator is requesting that you restart your computer. Click 'Restart Now' after saving your work." - -ButtonLText "Restart Now" - -ButtonLaction "shutdown.exe /r /t 30" - -ButtonRText "Ignore" - -ButtonRcountdown 900 - -ButtonRDefault -.EXAMPLE - -RestartReminder (Server 2008) - - WARNING: PowerShell 2 cannot import the presentation framework switching to winform... - Ignore (895) Button Clicked! - -PARAMETER: -ApplicationID "ReplaceWithWindowTitle/ApplicationID" - This will replace the window title and it will also change the taskbar overlay icon and logo/image used in the popup - with the Notification Icon used by that Application ID if it exists and is not overridden by another parameter. - This is the same application ID used by the Send-UserPrompt and the built-in Windows notification system. -.EXAMPLE - -ApplicationID "Contoso Inc" - - Application ID with Icon found! Checking if the icon and logo were supplied elsewhere... - The icon was not specified elsewhere in the script switching from the default icon to the Applications Notification Icon. - The logo/image was not specified elsewhere in the script switching from the default image to the Applications Notification Icon. - You can also switch the default Application ID on line 128 - -PARAMETER: -LogoPath "C:\Replace\This\Path.png" - The location of an image you would like to use in the large image window. - This script is running as the end-user so their account will need permission to this path. - Can be given a URL. Just keep in mind that Ninja doesn't support these special characters: &|;$><`!. - Can be given any size image however the script will center it and then scale it into a 250px x 125px rectangle. - The image will always be centered and a square 1:1 ratio will work. - Scaling is not done on Server 2008 and Windows 7 (The image is simply centered and you may see it cut off the image if the image is too large). - - Supported image formats: .png, .jpg, .ico, .gif (GIFs will be static and not animated.) - - You can also convert an image to base64 and replace either lines 165 or 169 if you'd prefer to not have to download an image and give the script a path to it. - -PARAMETER: -IconPath "C:\Replace\This\Path.png" - The location of an icon you would like to use for the taskbar overlay and window title bar. - This script is running as the end-user so their account will need permission to this path. - Can be given a URL. Just keep in mind that Ninja doesn't support these special characters: &|;$><`!. - Can be given any icon size however the script will convert it to 64px x 64px so it is recommended to keep to the 1:1 ratio so that the image is not squished. - - Supported image formats: .png, .jpg, .ico, .gif (GIFs will be static and not animated.) - - You can also convert an image to base64 and replace either lines 164 or 168 if you'd prefer to not have to download an image and give the script a path to it. - -PARAMETER: -Text "ReplaceMeWithTextYouWantInsideThePopUp" - The text you would like displayed inside the popup. - If too much text is written, a scrollbar will automatically appear, allowing the end-user to view all the text. - -PARAMETER: -AllowResize - By default, resizing the popup is not allowed. Use -AllowResize to allow resizing the window. - -PARAMETER: -ButtonR - Add a button to the bottom right (can only be specified once). - -PARAMETER: -ButtonRdefault - Set the right button as the default button. This will allow end-users to simply hit the enter key when the window is in focus to perform a click. - -PARAMETER: -ButtonRtext "TextYouWouldLikeInsideTheButton" - Change the text from 'Right Button' to whatever you put encased in quotes. -.EXAMPLE - -ButtonRtext "Later" - - Later Button Clicked! - -PARAMETER: -ButtonRaction "ReplaceWithAnyCMDcommand" - This will set the action the right button performs when clicked. Can be given any command that will work in cmd.exe as well as parameters. ex. logoff.exe - -PARAMETER: -ButtonRcountdown "160" - This will add a countdown to the right button with your input in seconds. When the countdown reaches 0 it'll click the button for the end user. - The left and right button countdown cannot be used at the same time. - -PARAMETER: -ButtonL - Add a button to the bottom left (can only be specified once). - -PARAMETER: -ButtonLdefault - Set the left button as the default button. This will allow end-users to simply hit the enter key when the window is in focus to perform a click. - -PARAMETER: -ButtonLtext "TextYouWouldLikeInsideTheButton" - Change the text from 'Left Button' to whatever you put encased in quotes. -.EXAMPLE - -ButtonLtext "Later" - - Later Button Clicked! - -PARAMETER: -ButtonLaction "ReplaceWithAnyCMDcommand" - This will set the action the left button performs when clicked. Can be given any command that will work in cmd.exe as well as parameters. ex. logoff.exe - -PARAMETER: -ButtonLcountdown - This will add a countdown to the left button with your input in seconds. When the countdown reaches 0 it'll click the button for the end user. - The left and right button countdown cannot be used at the same time. - -PARAMETER: -Verbose - More verbose output (useful for troubleshooting). -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 7+, Server 2008+ - Release Notes: Updated calculated name -#> - -[CmdletBinding()] -param ( - [Parameter()] - [Switch]$AllowResize = [System.Convert]::ToBoolean($env:allowEnduserToResizeWindow), - # You can set line 127 to [String]$ApplicationID = "$env:NINJA_COMPANY_NAME" to automatically use your company name. - [Parameter()] - [String]$ApplicationID = "NinjaOne RMM", - [Parameter()] - [Switch]$ButtonR, - [Parameter()] - [Switch]$ButtonRdefault, - [Parameter()] - [String]$ButtonRtext, - [Parameter()] - [String]$ButtonRaction, - [Parameter()] - [int]$ButtonRcountdown, - [Parameter()] - [Switch]$ButtonL, - [Parameter()] - [Switch]$ButtonLDefault, - [Parameter()] - [String]$ButtonLtext, - [Parameter()] - [String]$ButtonLaction, - [Parameter()] - [int]$ButtonLcountdown, - [Parameter()] - [String]$IconPath, - [Parameter()] - [String]$LogoPath, - [Parameter()] - [String]$Text, - [Parameter()] - [Switch]$winForm, - [Parameter()] - [Switch]$RestartReminder = [System.Convert]::ToBoolean($env:restartReminder) -) - -begin { - - # You can replace the below line with $IconBase64 = "ReplaceThisWithYourBase64encodedimageEncasedInQuotes" and the script will decode the image and use it - # for the taskbar overlay icon and title bar icon. - $IconBase64 = "iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAJFBMVEUARF0Apc0AmL8ApM0Aos0Aps7///8Am8ia1ug9rtLd8/jw+/2tMDHwAAAABXRSTlMBrBTIcce4nvwAAAIeSURBVHic7dvrcoMgEAXgiOAivv/7Fm+JBpCLwk7bsz86rcNkPw+Y0Gl5vd4lGtbLKSG7vmF18mwQnWpe3YcghP2Z1svU8OtbIOihm8op25M2gWBov9UqYJj/vSRzAGsEkhMglxngWINbdbxLAAAAAAAAAAAAAKAI8Oz2KRtApPWThEyAbT8NZwDZGpeav6sLIKXNMBwAtuGotTGTvTpMRms9qkxEBsDe/dz+A7B3rufeS/utrCKPkAywzfYmK8BeOHY+lBkzBImALfwDgA4XnNLphCTA4e43AKmL9vNMJD8pCQAna20nP5D+SfkQgJyp1qS9PYsEKQDnpVP627WYJCgBmGj+GRmUAFIraSXWBAwDcwJJk1AXMIzcgHgElQHxCGoDohHcBsybgIvPpei70S2A0csuaNkTBRBTbA7uAOb271E0+gWxOSgHfG87yD+wGsCz7fGONNf9iwGTb89DnlkwkUVQCPD2t1sXz9A6gMDT5YsgsggKARljI/vTMkDo7cU3B1USCL+oOwdVAMGF5RlcAxB+tBoBwq/JDlDcAPYEAGgDuPiNBwkgASSABJAAEkACSAAJIAEkgASQABL4JwlcA9w/9N4GTOZcl1OQMTgRoEannhv9O/+PCAAAAAAAAAAAAACAPwhgP+7HeOCR1jOfjBHI9dBrz9W/34/d9jyHLvvPweP2GdCx/3zyvLlAfZ8+l13LktJzAJ+nfgAP50EVLvPsRgAAAABJRU5ErkJggg==" - - # You can replace the below line with $LogoBase64 = "ReplaceThisWithYourBase64encodedimageEncasedInQuotes" and the script will decode the image and use it - # for the main large image / logo - $LogoBase64 = "iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAJFBMVEUARF0Apc0AmL8ApM0Aos0Aps7///8Am8ia1ug9rtLd8/jw+/2tMDHwAAAABXRSTlMBrBTIcce4nvwAAAIeSURBVHic7dvrcoMgEAXgiOAivv/7Fm+JBpCLwk7bsz86rcNkPw+Y0Gl5vd4lGtbLKSG7vmF18mwQnWpe3YcghP2Z1svU8OtbIOihm8op25M2gWBov9UqYJj/vSRzAGsEkhMglxngWINbdbxLAAAAAAAAAAAAAKAI8Oz2KRtApPWThEyAbT8NZwDZGpeav6sLIKXNMBwAtuGotTGTvTpMRms9qkxEBsDe/dz+A7B3rufeS/utrCKPkAywzfYmK8BeOHY+lBkzBImALfwDgA4XnNLphCTA4e43AKmL9vNMJD8pCQAna20nP5D+SfkQgJyp1qS9PYsEKQDnpVP627WYJCgBmGj+GRmUAFIraSXWBAwDcwJJk1AXMIzcgHgElQHxCGoDohHcBsybgIvPpei70S2A0csuaNkTBRBTbA7uAOb271E0+gWxOSgHfG87yD+wGsCz7fGONNf9iwGTb89DnlkwkUVQCPD2t1sXz9A6gMDT5YsgsggKARljI/vTMkDo7cU3B1USCL+oOwdVAMGF5RlcAxB+tBoBwq/JDlDcAPYEAGgDuPiNBwkgASSABJAAEkACSAAJIAEkgASQABL4JwlcA9w/9N4GTOZcl1OQMTgRoEannhv9O/+PCAAAAAAAAAAAAACAPwhgP+7HeOCR1jOfjBHI9dBrz9W/34/d9jyHLvvPweP2GdCx/3zyvLlAfZ8+l13LktJzAJ+nfgAP50EVLvPsRgAAAABJRU5ErkJggg==" - - # If script form is used replace the parameters - if ($env:applicationId -and $env:applicationId -notlike "null") { $ApplicationID = $env:applicationId } - if ($env:logoPath -and $env:logoPath -notlike "null") { $LogoPath = $env:logoPath } - if ($env:iconPath -and $env:iconPath -notlike "null") { $IconPath = $env:iconPath } - if ($env:popupMessage -and $env:popupMessage -notlike "null") { $Text = $env:popupMessage } - if ($env:rightButtonText -and $env:rightButtonText -notlike "null") { $ButtonRtext = $env:rightButtonText } - if ($env:rightButtonAction -and $env:rightButtonAction -notlike "null") { $ButtonRaction = $env:rightButtonAction } - if ($env:rightButtonCountdown -and $env:rightButtonCountdown -notlike "null") { $ButtonRcountdown = $env:rightButtonCountdown } - if ($env:leftButtonText -and $env:leftButtonText -notlike "null") { $ButtonLText = $env:leftButtonText } - if ($env:leftButtonAction -and $env:leftButtonAction -notlike "null") { $ButtonLaction = $env:leftButtonAction } - if ($env:leftButtonCountdown -and $env:leftButtonCountdown -notlike "null") { $ButtonLcountdown = $env:leftButtonCountdown } - if ($env:defaultButton -and $env:defaultButton -notlike "null") { - if ($env:defaultButton -eq "Right Button") { $ButtonRdefault = $True } - if ($env:defaultButton -eq "Left Button") { $ButtonLdefault = $True } - } - - # Sets the parameters for a generic restart prompt - if ($RestartReminder) { - if (-not $ButtonLtext) { $ButtonLtext = "Restart Now" } - if (-not $ButtonLaction) { - if (([System.Environment]::OSVersion.Version).Major -ge 10) { - $ButtonLaction = "shutdown.exe /r /soft /t 30" - } - else { - $ButtonLaction = "shutdown.exe /r /t 30" - } - } - if (-not $ButtonRtext) { $ButtonRtext = "Ignore" } - if (-not $ButtonRcountdown) { $ButtonRcountdown = 900 } - if (-not $Text) { $Text = "Your IT Administrator is requesting that you restart your computer. Click 'Restart Now' after saving your work." } - } - - # These Assemblies are needed to prepare the images for the form - Write-Verbose "Adding required assemblies..." - Add-Type -AssemblyName System.Windows.Forms - Add-Type -AssemblyName System.Drawing - - # Check if the script was run as the default System User - function Test-IsSystem { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - return $id.Name -like "NT AUTHORITY*" -or $id.IsSystem - } - - function Test-PSVersion { - return ($PSVersionTable.PSVersion.Major) - } - - function ConvertFrom-Base64 { - param( - $Base64, - $Path - ) - $bytes = [Convert]::FromBase64String($Base64) - - # This section of code will error out when ran multiple times in the same session. This is from wpf holding onto the file after closing. - # The file is unlocked when the powershell session is closed. - $ErrorActionPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue - [IO.File]::WriteAllBytes($Path, $bytes) - $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Continue - } - - # There's a lot of ways to create icon files the below method creates a png and then creates an ico file in binary form by creating the header and adding the png's binary at the bottom. - # Once this has been done we can simply write all the bytes to our new file. - function ConvertFrom-Image { - param( - $ImagePath, - $Path - ) - - # Grab an instance of the image and blank bitmap - $image = [Drawing.Image]::FromFile($ImagePath) - - # Resize the image to 255px by 255px while maintaining quality. - # If you want transparency you'll need an Alpha channel in the pixel format - $bitmap = New-Object System.Drawing.Bitmap (64, 64, [system.drawing.imaging.PixelFormat]::Format32bppArgb) - $bitmap.SetResolution(64, 64) - - # Create a graphics object which will be used to resize the image to 255px by 255px - $graphics = [System.Drawing.Graphics]::FromImage($bitmap) - - # Set some quality settings for the resize operation - $graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality - $graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic - $graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality - - # Draw the image onto the bitmap - $graphics.DrawImage($Image, 0, 0, 64, 64) - - # Temporarily save the image as a png - $RandomNumber = Get-Random -Maximum 1000000 - $bitmap.Save("$env:TEMP\image-$RandomNumber.png", [System.Drawing.Imaging.ImageFormat]::Png) - $png = "$env:TEMP\image-$RandomNumber.png" - - # Build the ico file using the png binary. - if ($PSVersionTable.PSVersion.Major -gt 5) { - $pngBytes = Get-Content -Path $png -AsByteStream - } - elseif ($PSVersionTable.PSVersion.Major -gt 2) { - $pngBytes = Get-Content -Path $png -Encoding Byte -Raw - } - else { - $pngBytes = [System.IO.File]::ReadAllBytes($png) - } - $icoHeader = [byte[]] @(0, 0, 1, 0, 1, 0) - $imageDataSize = $pngBytes.Length - $icoDirectory = [byte[]] @( - 64, 64, # icon size - 0, 0, # color count - 0, 0, # reserved - 0, 0, # hotspot x, hotspot y - ($imageDataSize -band 0xFF), - ([Math]::Floor($imageDataSize / [Math]::Pow(2, 8)) -band 0xFF), - ([Math]::Floor($imageDataSize / [Math]::Pow(2, 16)) -band 0xFF), - ([Math]::Floor($imageDataSize / [Math]::Pow(2, 24)) -band 0xFF), - 22, 0, 0, 0 # offset to image data - ) - $iconData = $icoHeader + $icoDirectory + $pngBytes - - # Save the completed icon file and clean up any temporary files. - # This section of code will error out when ran multiple times in the same session. This is from wpf holding onto the file after closing. - # The file is unlocked when the powershell session is closed. - $ErrorActionPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue - if (Test-Path $Path -ErrorAction SilentlyContinue) { Remove-Item $Path -Force } - [System.IO.File]::WriteAllBytes($Path, $iconData) - - if (Test-Path $png -ErrorAction SilentlyContinue) { Remove-Item $png -Force } - $bitmap.Dispose() - $image.Dispose() - $graphics.Dispose() - [System.GC]::Collect() - - $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Continue - } - - function Invoke-Download { - param( - [Parameter()] - [String]$URL, - [Parameter()] - [String]$BaseName, - [Parameter()] - [Switch]$SkipSleep - ) - Write-Host "URL Given, Downloading the file..." - - $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType') - if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12 - } - elseif ( $SupportedTLSversions -contains 'Tls12' ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 - } - else { - # Not everything requires TLS 1.2, but we'll try anyways. - Write-Warning "TLS 1.2 and or TLS 1.3 isn't supported on this system. This download may fail!" - if ($PSVersionTable.PSVersion.Major -lt 3) { - Write-Warning "PowerShell 2 / .NET 2.0 doesn't support TLS 1.2." - } - } - - $i = 1 - While ($i -lt 4) { - if (-not ($SkipSleep)) { - $SleepTime = Get-Random -Minimum 3 -Maximum 30 - Start-Sleep -Seconds $SleepTime - } - - Write-Host "Download Attempt $i" - - try { - $WebClient = New-Object System.Net.WebClient - $Response = $WebClient.OpenRead($Url) - $MimeType = $WebClient.ResponseHeaders["Content-Type"] - $DesiredExtension = switch -regex ($MimeType) { - "image/jpeg|image/jpg" { "jpg" } - "image/png" { "png" } - "image/gif" { "gif" } - "image/bmp|image/x-windows-bmp|image/x-bmp" { "bmp" } - "image/x-icon" { "ico" } - default { - Write-Error "The URL you provided does not provide a supported image type. Image Types Supported: jpg, jpeg, ico, bmp, png and gif. Image Type detected: $MimeType" - Exit 1 - } - } - $Path = "$BaseName.$DesiredExtension" - $WebClient.DownloadFile($URL, $Path) - $File = Test-Path -Path $Path -ErrorAction SilentlyContinue - $Response.Close() - } - catch { - if ($Response) { $Response.Close() } - Write-Warning "An error has occured while downloading!" - Write-Warning $_.Exception.Message - } - - if ($File) { - $i = 4 - } - else { - $i++ - } - } - - if (-not (Test-Path $Path)) { - Write-Error "Failed to download file!" - Exit 1 - } - - $Path - } - - function Build-WPFform { - # This is xml I created using visual studio community edition https://visualstudio.microsoft.com/downloads/ I removed some of the lines in "Window" so PowerShell could load it. - [XML]$form = @" - - - - - - - - - - - - - - - - - ', '' - $HTMLTable = $HTMLTable -replace '', '' + $HTMLTable = $HTMLTable -replace ' + +[CmdletBinding()] +param ( + [Parameter()] + [String]$MultilineCustomField, + [Parameter()] + [String]$WysiwygCustomField +) + +begin { + # Replace parameters with the dynamic script variables. + if ($env:multilineCustomFieldName -and $env:multilineCustomFieldName -notlike "null") { $MultilineCustomField = $env:multilineCustomFieldName } + if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $WysiwygCustomField = $env:wysiwygCustomFieldName } + + # Check if $MultilineCustomField and $WysiwygCustomField are both not null and have the same value + if ($MultilineCustomField -and $WysiwygCustomField -and $MultilineCustomField -eq $WysiwygCustomField) { + Write-Host "[Error] Custom Fields of different types cannot have the same name." + Write-Host "https://ninjarmm.zendesk.com/hc/en-us/articles/360060920631-Custom-Fields-Configuration-Device-Role-Fields" + exit 1 + } + + # Function to get user registry hives based on the type of account + function Get-UserHives { + param ( + [Parameter()] + [ValidateSet('AzureAD', 'DomainAndLocal', 'All')] + [String]$Type = "All", + [Parameter()] + [String[]]$ExcludedUsers, + [Parameter()] + [switch]$IncludeDefault + ) + + # Patterns for user SID depending on account type + $Patterns = switch ($Type) { + "AzureAD" { "S-1-12-1-(\d+-?){4}$" } + "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" } + "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" } + } + + # Fetch user profiles whose SIDs match the defined patterns and prepare objects with their details + $UserProfiles = Foreach ($Pattern in $Patterns) { + Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | + Where-Object { $_.PSChildName -match $Pattern } | + Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, + @{Name = "UserName"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } }, + @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, + @{Name = "Path"; Expression = { $_.ProfileImagePath } } + } + + # Handle inclusion of the default user profile if requested + switch ($IncludeDefault) { + $True { + $DefaultProfile = "" | Select-Object UserName, SID, UserHive, Path + $DefaultProfile.UserName = "Default" + $DefaultProfile.SID = "DefaultProfile" + $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT" + $DefaultProfile.Path = "C:\Users\Default" + + $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.UserName } + } + } + + # Return user profiles, excluding any specified users + $UserProfiles | Where-Object { $ExcludedUsers -notcontains $_.UserName } + } + + # Function to check if the current PowerShell session is running with elevated permissions + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + # if ($Characters -ge 200000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded; the value is greater than or equal to 200,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If requested to set the field value for a Ninja document, we'll specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # This is a list of valid fields that can be set. If no type is specified, it is assumed that the input does not need to be changed. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # The field below requires additional information to be set. # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # Redirect error output to the success stream to make it easier to handle errors if nothing is found or if something else goes wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If an error is received with an exception property, the function will exit with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # The below types require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # Although it's highly likely we were given a value like "True" or a boolean datatype, it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # if (-not $Selection) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # We'll need to set the field differently depending on if its a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # Function to find installation keys based on the display name, optionally returning uninstall strings + function Find-InstallKey { + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $True)] + [String]$DisplayName, + [Parameter()] + [Switch]$UninstallString, + [Parameter()] + [String]$UserBaseKey + ) + process { + # Initialize an empty list to hold installation objects + $InstallList = New-Object System.Collections.Generic.List[Object] + + # If no user base key is specified, search in the default system-wide uninstall paths + if (!$UserBaseKey) { + # Search for programs in 32-bit and 64-bit locations. Then add them to the list if they match the display name + $Result = Get-ChildItem -Path "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } + if ($Result) { $InstallList.Add($Result) } + + $Result = Get-ChildItem -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } + if ($Result) { $InstallList.Add($Result) } + } + else { + # If a user base key is specified, search in the user-specified 64-bit and 32-bit paths. + $Result = Get-ChildItem -Path "$UserBaseKey\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } + if ($Result) { $InstallList.Add($Result) } + + $Result = Get-ChildItem -Path "$UserBaseKey\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } + if ($Result) { $InstallList.Add($Result) } + } + + # If the UninstallString switch is specified, return only the uninstall strings; otherwise, return the full installation objects. + if ($UninstallString) { + $InstallList | Select-Object -ExpandProperty UninstallString -ErrorAction SilentlyContinue + } + else { + $InstallList + } + } + } + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # Check if the script is running with elevated permissions (administrator rights) + if (!(Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Search for Chrome installations on the system and enable chrome extension search if found. + Find-InstallKey -DisplayName "Chrome" | ForEach-Object { + $ChromeInstallations = $True + } + + # Search for Firefox installations on the system and enable firefox extension search if found. + Find-InstallKey -DisplayName "Firefox" | ForEach-Object { + $FireFoxInstallations = $True + } + + # Search for Edge installations on the system and flag if found and enable edge extension search if found. + Find-InstallKey -DisplayName "Edge" | ForEach-Object { + $EdgeInstallations = $True + } + + # Retrieve all user profiles from the system + $UserProfiles = Get-UserHives -Type "All" + # Loop through each profile on the machine + Foreach ($UserProfile in $UserProfiles) { + # Load User ntuser.dat if it's not already loaded + If (($ProfileWasLoaded = Test-Path Registry::HKEY_USERS\$($UserProfile.SID)) -eq $false) { + Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe LOAD HKU\$($UserProfile.SID) `"$($UserProfile.UserHive)`"" -Wait -WindowStyle Hidden + } + + # Repeat search for installations of browsers but in the user's registry context + Find-InstallKey -UserBaseKey "Registry::HKEY_USERS\$($UserProfile.SID)" -DisplayName "Chrome" | ForEach-Object { + $ChromeInstallations = $True + } + Find-InstallKey -UserBaseKey "Registry::HKEY_USERS\$($UserProfile.SID)" -DisplayName "Firefox" | ForEach-Object { + $FireFoxInstallations = $True + } + Find-InstallKey -UserBaseKey "Registry::HKEY_USERS\$($UserProfile.SID)" -DisplayName "Edge" | ForEach-Object { + $EdgeInstallations = $True + } + + # Unload NTuser.dat + If ($ProfileWasLoaded -eq $false) { + [gc]::Collect() + Start-Sleep 1 + Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe UNLOAD HKU\$($UserProfile.SID)" -Wait -WindowStyle Hidden | Out-Null + } + } + + # Initialize a list to store details of detected browser extensions + $BrowserExtensions = New-Object System.Collections.Generic.List[object] + + # If Chrome was found, search for Chrome extensions in each user's profile + if ($ChromeInstallations) { + Write-Host -Object "A Google Chrome installation was detected. Searching Chrome for browser extensions..." + $UserProfiles | ForEach-Object { + if (!(Test-Path -Path "$($_.Path)\AppData\Local\Google\Chrome\User Data" -ErrorAction SilentlyContinue)) { + return + } + + if(Test-Path -Path "$($_.Path)\AppData\Local\Google\Chrome\User Data\Local State" -ErrorAction SilentlyContinue){ + $AllProfiles = Get-Content -Path "$($_.Path)\AppData\Local\Google\Chrome\User Data\Local State" | ConvertFrom-JSON + } + + $PreferenceFiles = Get-ChildItem "$($_.Path)\AppData\Local\Google\Chrome\User Data\*\Preferences" -Exclude "System Profile" | Select-Object -ExpandProperty Fullname + + foreach ($PreferenceFile in $PreferenceFiles) { + + $GooglePreferences = Get-Content -Path $PreferenceFile | ConvertFrom-Json + if($AllProfiles){ + $ProfileLocation = $PreferenceFile | Get-Item | Select-Object -ExpandProperty Directory | Split-Path -Leaf + $ProfileName = $AllProfiles.profile.info_cache | Select-Object -ExpandProperty $ProfileLocation | Select-Object -ExpandProperty Name + }else{ + $ProfileName = $GooglePreferences.profile.name + } + + foreach ($Extension in $GooglePreferences.extensions.settings.PSObject.Properties) { + $BrowserExtensions.Add( + [PSCustomObject]@{ + Browser = "Chrome" + User = $_.UserName + Profile = $ProfileName + Name = $Extension.Value.manifest.name + "Extension ID" = $Extension.name + Description = $Extension.Value.manifest.description + } + ) + } + } + } + } + + # If Edge was found, search for Edge extensions in each user's profile + if ($EdgeInstallations) { + Write-Host -Object "A Microsoft Edge installation was detected. Searching Microsoft Edge for browser extensions..." + $UserProfiles | ForEach-Object { + if (!(Test-Path -Path "$($_.Path)\AppData\Local\Microsoft\Edge\User Data" -ErrorAction SilentlyContinue)) { + return + } + + if(Test-Path -Path "$($_.Path)\AppData\Local\Microsoft\Edge\User Data\Local State" -ErrorAction SilentlyContinue){ + $AllProfiles = Get-Content -Path "$($_.Path)\AppData\Local\Microsoft\Edge\User Data\Local State" | ConvertFrom-JSON + } + + $PreferenceFiles = Get-ChildItem "$($_.Path)\AppData\Local\Microsoft\Edge\User Data\*\Preferences" -Exclude "System Profile" | Select-Object -ExpandProperty Fullname + + foreach ($PreferenceFile in $PreferenceFiles) { + + $EdgePreferences = Get-Content -Path $PreferenceFile | ConvertFrom-Json + if($AllProfiles){ + $ProfileLocation = $PreferenceFile | Get-Item | Select-Object -ExpandProperty Directory | Split-Path -Leaf + $ProfileName = $AllProfiles.profile.info_cache | Select-Object -ExpandProperty $ProfileLocation | Select-Object -ExpandProperty Name + }else{ + $ProfileName = $EdgePreferences.profile.name + } + + foreach ($Extension in $EdgePreferences.extensions.settings.PSObject.Properties) { + if ($Extension.Value.active_bit -like "False" ) { continue } + if (!$Extension.Value.manifest.name) { continue } + $BrowserExtensions.Add( + [PSCustomObject]@{ + Browser = "Edge" + User = $_.UserName + Profile = $ProfileName + Name = $Extension.Value.manifest.name + "Extension ID" = $Extension.name + Description = $Extension.Value.manifest.description + } + ) + } + } + } + } + + # If Firefox was found, search for Firefox extensions in each user's profile + if ($FireFoxInstallations) { + Write-Host -Object "A Firefox installation was detected. Searching Firefox for browser extensions..." + $UserProfiles | ForEach-Object { + if (!(Test-Path -Path "$($_.Path)\AppData\Roaming\Mozilla\Firefox\Profiles" -ErrorAction SilentlyContinue)) { + return + } + + $FirefoxProfileFolders = Get-ChildItem -Path "$($_.Path)\AppData\Roaming\Mozilla\Firefox\Profiles" -Directory | Where-Object { $_.Name -match "\.default-release$" } | Select-Object -ExpandProperty Fullname + + foreach ( $FirefoxProfile in $FirefoxProfileFolders ) { + + if (!(Test-Path -Path "$FirefoxProfile\extensions.json")) { + continue + } + + $Extensions = Get-Content -Path "$FirefoxProfile\extensions.json" | ConvertFrom-Json + + foreach ($Extension in $Extensions.addons) { + $BrowserExtensions.Add( + [PSCustomObject]@{ + Browser = "Firefox" + User = $_.UserName + Profile = "N/A" + Name = $Extension.defaultlocale.name + "Extension ID" = $Extension.id + Description = $Extension.defaultlocale.description + } + ) + } + } + } + } + + # Check if there are any browser extensions to process + if ($BrowserExtensions.Count -gt 0) { + # Format the BrowserExtensions list to include a shortened description if the description is too long. + $BrowserExtensions = $BrowserExtensions | Select-Object Browser, User, Profile, Name, "Extension ID", @{ + Name = "Description" + Expression = { + $Characters = $_.Description | Measure-Object -Character | Select-Object -ExpandProperty Characters + if ($Characters -gt 75) { + "$(($_.Description).SubString(0,75))(...)" + } + else { + $_.Description + } + } + } + } + + # Check if extensions were found and if we were requested to set a multiline custom field + if ($BrowserExtensions.Count -gt 0 -and $MultilineCustomField) { + try { + Write-Host "Attempting to set Custom Field '$MultilineCustomField'." + $CustomFieldValue = New-Object System.Collections.Generic.List[string] + + # Sort and format the list of extensions for output + $CustomFieldList = $BrowserExtensions | Sort-Object Browser, User, Profile, Name | Select-Object Browser, User, Profile, Name, "Extension ID", Description + $CustomFieldValue.Add(($CustomFieldList | Format-List | Out-String)) + + # Measure the total character count of the formatted string + $Characters = $CustomFieldValue | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters + if ($Characters -ge 9500) { + Write-Warning "10,000 Character Limit has been reached! Trimming output until the character limit is satisfied..." + + # If it doesn't comply with the limits we'll need to recreate it with some adjustments. + $i = 0 + do { + # Recreate the custom field output starting with a warning that we truncated the output. + $CustomFieldValue = New-Object System.Collections.Generic.List[string] + $CustomFieldValue.Add("This info has been truncated to accommodate the 10,000 character limit.") + + # Flip the array so that the last entry is on top. + [array]::Reverse($CustomFieldList) + + # Remove the next item. + $CustomFieldList[$i] = $null + $i++ + + # We'll flip the array back to right side up. + [array]::Reverse($CustomFieldList) + + # Add it back to the output. + $CustomFieldValue.Add(($CustomFieldList | Format-List | Out-String)) + + # Check that we now comply with the character limit. If not restart the do loop. + $Characters = $CustomFieldValue | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters + }while ($Characters -ge 9500) + } + + # Set-NinjaProperty -Name $MultilineCustomField -Value $CustomFieldValue # Removed NinjaOne dependency + Write-Host "Successfully set Custom Field '$MultilineCustomField'!" + } + catch { + Write-Host "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + } + + # Check if extensions were found and if we were requested to set a WYSIWYG custom field. + if ($BrowserExtensions.Count -gt 0 -and $WysiwygCustomField) { + try { + Write-Host "Attempting to set Custom Field '$WysiwygCustomField'." + + # Prepare the custom field output. + $CustomFieldValue = New-Object System.Collections.Generic.List[string] + + # Convert the matching events into an html report. + $htmlTable = $BrowserExtensions | Sort-Object Browser, User, Profile, Name | Select-Object Browser, User, Profile, Name, "Extension ID", Description | ConvertTo-Html -Fragment + + # Add the newly created html into the custom field output. + $CustomFieldValue.Add($htmlTable) + + # Check that the output complies with the hard character limits. + $Characters = $CustomFieldValue | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters + if ($Characters -ge 199500) { + Write-Warning "200,000 Character Limit has been reached! Trimming output until the character limit is satisfied..." + + # If it doesn't comply with the limits we'll need to recreate it with some adjustments. + $i = 0 + do { + # Recreate the custom field output starting with a warning that we truncated the output. + $CustomFieldValue = New-Object System.Collections.Generic.List[string] + $CustomFieldValue.Add("

This info has been truncated to accommodate the 200,000 character limit.

") + + # Flip the array so that the last entry is on top. + [array]::Reverse($htmlTable) + # If the next entry is a row we'll delete it. + if ($htmlTable[$i] -match ' - -[CmdletBinding()] -param ( - [Parameter()] - [String]$CustomField = "LocalAdmins", - [Parameter()] - [String]$Delimiter = ', ' -) - -begin { - if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } - if ($env:delimiter -and $env:delimiter -notlike "null") { $Delimiter = $env:delimiter } - $CheckNinjaCommand = "Ninja-Property-Set" -} -process { - # Get objects in the Administrators group, includes user objects and groups - $Users = net.exe localgroup "Administrators" | Where-Object { $_ -AND $_ -notmatch "command completed successfully" } | Select-Object -Skip 4 - - if (-not $Users) { - Write-Error "[Error] No user's found! This is extremely unlikely is something blocking access to 'net localgroup administrators'?" - exit 1 - } - - Write-Host "Local Admins Found (Users & Groups): $($Users -join $Delimiter)" - if ($(Get-Command $CheckNinjaCommand -ErrorAction SilentlyContinue).Name -like $CheckNinjaCommand -and -not [string]::IsNullOrEmpty($CustomField) -and -not [string]::IsNullOrWhiteSpace($CustomField)) { - Write-Host "Attempting to set Custom Field: $CustomField" - Ninja-Property-Set -Name $CustomField -Value $($Users -join $Delimiter) - } - else { - Write-Warning "Unable to set customfield either due to legacy OS or this script is not running as an elevated user." - } -} -end { - - - -} - - + +<# +.SYNOPSIS + Updates a custom field with a list of local admins. +.DESCRIPTION + Updates a custom field with a list of local admins. +.EXAMPLE + No parameter needed + + Local Admins Found: Administrator, kbohlander, TEST\Domain Admins + Attempting to set Custom Field: LocalAdmins + +PARAMETER: -CustomField "ReplaceWithAnyTextCustomField" + Updates the custom field you specified (defaults to "LocalAdmins"). The Custom Field needs to be writable by scripts (otherwise the script will report it as not found). + +PARAMETER: -Delimiter "ReplaceWithYourDesiredDelimiter" + Places whatever is entered encased of quotes between each user name. See below example. +.EXAMPLE + -Delimiter " - " + + Local Admins Found: Administrator - kbohlander - TEST\Domain Admins + Attempting to set Custom Field: LocalAdmins +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 7, Windows Server 2008 + Release Notes: + Switched to using net localgroup as it's the most reliable. Removed PowerShell 5.1 requirement. +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$CustomField = "LocalAdmins", + [Parameter()] + [String]$Delimiter = ', ' +) + +begin { + if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } + if ($env:delimiter -and $env:delimiter -notlike "null") { $Delimiter = $env:delimiter } + # NinjaOne integration removed + # $CheckNinjaCommand = "Ninja-Property-Set" +} +process { + # Get objects in the Administrators group, includes user objects and groups + $Users = net.exe localgroup "Administrators" | Where-Object { $_ -AND $_ -notmatch "command completed successfully" } | Select-Object -Skip 4 + + if (-not $Users) { + Write-Error "[Error] No user's found! This is extremely unlikely is something blocking access to 'net localgroup administrators'?" + exit 1 + } + + Write-Host "Local Admins Found (Users & Groups): $($Users -join $Delimiter)" + if ($(Get-Command $CheckNinjaCommand -ErrorAction SilentlyContinue).Name -like $CheckNinjaCommand -and -not [string]::IsNullOrEmpty($CustomField) -and -not [string]::IsNullOrWhiteSpace($CustomField)) { + Write-Host "Attempting to set Custom Field: $CustomField" + # Ninja-Property-Set -Name $CustomField -Value $($Users -join $Delimiter) # Removed NinjaOne dependency + } + else { + Write-Warning "Unable to set customfield either due to legacy OS or this script is not running as an elevated user." + } +} +end { + + + +} + + diff --git a/Powershell Scripts/Local Certificate Expiration Alert.ps1 b/Powershell Scripts/Local Certificate Expiration Alert.ps1 index 5247f06..1bd1138 100644 --- a/Powershell Scripts/Local Certificate Expiration Alert.ps1 +++ b/Powershell Scripts/Local Certificate Expiration Alert.ps1 @@ -1,137 +1,137 @@ # Alerts when a local certificate will expire in a configurable number of days. Can optionally ignore self-signed certificates, certificates that have been expired for a long time and certificates that were only valid for an extremely short time frame. -<# -.SYNOPSIS - Alerts when a local certificate will expire in a configurable number of days. Can optionally ignore self-signed certificates, certificates that have been expired for a long time and certificates that were only valid for an extremely short time frame. -.DESCRIPTION - Alerts when a local certificate will expire in a configurable number of days. - Can optionally ignore self-signed certificates, certificates that have been expired for a long time - and certificates that were only valid for an extremely short time frame. -.EXAMPLE - (No Parameters) - - Checking for certificates that were valid before 10/10/2023 09:07:23 and will expire before 11/11/2023 09:07:23. - No Certificates were found with an expiration date before 11/11/2023 09:07:23 and after 07/13/2023 09:07:23. - -PARAMETER: -DaysUntilExpiration "ReplaceWithNumber" - Alerts if a certificate is set to expire within the specified number of days. -.EXAMPLE - -DaysUntilExpiration "366" - - Checking for certificates that were valid before 10/10/2023 09:08:14 and will expire before 10/12/2024 09:08:14. - - WARNING: Expired Certificates found! - - ### Expired Certificates ### - - SerialNumber HasPrivateKey ExpirationDate Subject - ------------ ------------- -------------- ------- - 0AA60783EBB5076EBC2D12DA9B04C290 False 6/10/2024 4:59:59 PM CN=Insecure.Com LLC, O=Insecure.Com... - 619DCC976458E38D471DC3DCE3603C2C True 3/29/2024 10:19:00 AM CN=KYLE-SRV22-TEST.test.lan - 0AA60783EBB5076EBC2D12DA9B04C290 False 6/10/2024 4:59:59 PM CN=Insecure.Com LLC, O=Insecure.Com... - 7D5FC733E3A8CF9344CDDFC0AB01CCB9 True 4/9/2024 9:53:53 AM CN=KYLE-SRV22-TEST.test.lan - 4EDC0A79D6CD5A8D4D1E3705BC20C206 True 4/9/2024 9:58:06 AM CN=KYLLE-SRV22-TEST.test.lan - -PARAMETER: -MustBeValidBefore "ReplaceWithNumber" - Only alert on certificates that are older than X days. This is primarily to silence alerts about certificates that were only valid for 24 hours in their entire lifetime. - -PARAMETER: -Cutoff "ReplaceWithNumber" - Don't alert on certificates that have been expired for longer than X days (default is 91 days). - -PARAMETER: -IgnoreSelfSignedCerts - Ignore certificates where the subject of the certificate and the issuer of the certificate are identical. - -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 7, Server 2008 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$ExpirationFromCustomField = "certExpirationAlertDays", - [Parameter()] - [int]$DaysUntilExpiration = 30, - [Parameter()] - [int]$MustBeValidBefore = 2, - [Parameter()] - [int]$Cutoff = 91, - [Parameter()] - [Switch]$IgnoreSelfSignedCerts = [System.Convert]::ToBoolean($env:ignoreSelfSignedCerts) -) -begin { - # Retrieve script variables from the dynamic script form. - if ($env:expirationFromCustomFieldName -and $env:expirationFromCustomFieldName -notlike "null") { $ExpirationFromCustomField = $env:expirationFromCustomFieldName } - if ($env:daysUntilExpiration -and $env:daysUntilExpiration -notlike "null") { $DaysUntilExpiration = $env:daysUntilExpiration } - if ($env:certificateMustBeOlderThanXDays -and $env:certificateMustBeOlderThanXDays -notlike "null") { $MustBeValidBefore = $env:certificateMustBeOlderThanXDays } - if ($env:skipCertsExpiredForMoreThanXDays -and $env:skipCertsExpiredForMoreThanXDays -notlike "null") { $Cutoff = $env:skipCertsExpiredForMoreThanXDays } - - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - # If using the custom field option, check for the default value and replace it if necessary. - if ($PSVersionTable.PSVersion.Major -gt 2) { - $CustomField = Ninja-Property-Get -Name $ExpirationFromCustomField 2>$Null - } - - if ($CustomField -and $DaysUntilExpiration -eq 30 -and (Test-IsElevated) -and $PSVersionTable.PSVersion.Major -gt 2) { - Write-Host "Retrieved value of $CustomField days from Custom Field $ExpirationFromCustomField. Using it for expiration value." - $DaysUntilExpiration = $CustomField - } - elseif (-not (Test-IsElevated) -or $PSVersionTable.PSVersion.Major -le 2) { - Write-Warning "Skipping CustomField retrieval due to either incompatible PowerShell version or lack of elevation." - } -} -process { - # Calculate expiration and cutoff dates. - $ExpirationDate = (Get-Date "11:59pm").AddDays($DaysUntilExpiration) - $CutoffDate = (Get-Date "12am").AddDays(-$Cutoff) - $MustBeValidBeforeDate = (Get-Date "12am").AddDays(-$MustBeValidBefore) - - # Retrieve all certificates. - $Certificates = Get-ChildItem -Path "Cert:\" -Recurse - - Write-Host "Checking for certificates that were valid before $MustBeValidBeforeDate and will expire before $ExpirationDate." - - # Filter down to certificates that are expired in our desired date range - $ExpiredCertificates = $Certificates | Where-Object { $_.NotAfter -le $ExpirationDate -and $_.NotAfter -gt $CutoffDate -and $_.NotBefore -lt $MustBeValidBeforeDate } - - # If we're asked to ignore self signed certs we'll filter them out - if ($IgnoreSelfSignedCerts -and $ExpiredCertificates) { - Write-Host "Removing Self-Signed certificates from list." - $ExpiredCertificates = $ExpiredCertificates | Where-Object { $_.Subject -ne $_.Issuer } - } - - if ($ExpiredCertificates) { - Write-Host "" - Write-Warning "Expired Certificates found!" - Write-Host "" - - $Report = $ExpiredCertificates | ForEach-Object { - # Subject can be a long property, we'll truncate it to maintain readability - New-Object PSObject -Property @{ - SerialNumber = $_.SerialNumber - HasPrivateKey = $_.HasPrivateKey - ExpirationDate = $_.NotAfter - Subject = if ($_.Subject.Length -gt 35) { $_.Subject.Substring(0, 35) + "..." }else { $_.Subject } - } - } - - Write-Host "### Expired Certificates ###" - $Report | Format-Table -AutoSize | Out-String | Write-Host - - exit 1 - } - else { - Write-Host "No Certificates were found with an expiration date before $ExpirationDate and after $CutoffDate." - } -} -end { - - - -} +<# +.SYNOPSIS + Alerts when a local certificate will expire in a configurable number of days. Can optionally ignore self-signed certificates, certificates that have been expired for a long time and certificates that were only valid for an extremely short time frame. +.DESCRIPTION + Alerts when a local certificate will expire in a configurable number of days. + Can optionally ignore self-signed certificates, certificates that have been expired for a long time + and certificates that were only valid for an extremely short time frame. +.EXAMPLE + (No Parameters) + + Checking for certificates that were valid before 10/10/2023 09:07:23 and will expire before 11/11/2023 09:07:23. + No Certificates were found with an expiration date before 11/11/2023 09:07:23 and after 07/13/2023 09:07:23. + +PARAMETER: -DaysUntilExpiration "ReplaceWithNumber" + Alerts if a certificate is set to expire within the specified number of days. +.EXAMPLE + -DaysUntilExpiration "366" + + Checking for certificates that were valid before 10/10/2023 09:08:14 and will expire before 10/12/2024 09:08:14. + + WARNING: Expired Certificates found! + + ### Expired Certificates ### + + SerialNumber HasPrivateKey ExpirationDate Subject + ------------ ------------- -------------- ------- + 0AA60783EBB5076EBC2D12DA9B04C290 False 6/10/2024 4:59:59 PM CN=Insecure.Com LLC, O=Insecure.Com... + 619DCC976458E38D471DC3DCE3603C2C True 3/29/2024 10:19:00 AM CN=KYLE-SRV22-TEST.test.lan + 0AA60783EBB5076EBC2D12DA9B04C290 False 6/10/2024 4:59:59 PM CN=Insecure.Com LLC, O=Insecure.Com... + 7D5FC733E3A8CF9344CDDFC0AB01CCB9 True 4/9/2024 9:53:53 AM CN=KYLE-SRV22-TEST.test.lan + 4EDC0A79D6CD5A8D4D1E3705BC20C206 True 4/9/2024 9:58:06 AM CN=KYLLE-SRV22-TEST.test.lan + +PARAMETER: -MustBeValidBefore "ReplaceWithNumber" + Only alert on certificates that are older than X days. This is primarily to silence alerts about certificates that were only valid for 24 hours in their entire lifetime. + +PARAMETER: -Cutoff "ReplaceWithNumber" + Don't alert on certificates that have been expired for longer than X days (default is 91 days). + +PARAMETER: -IgnoreSelfSignedCerts + Ignore certificates where the subject of the certificate and the issuer of the certificate are identical. + +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 7, Server 2008 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$ExpirationFromCustomField = "certExpirationAlertDays", + [Parameter()] + [int]$DaysUntilExpiration = 30, + [Parameter()] + [int]$MustBeValidBefore = 2, + [Parameter()] + [int]$Cutoff = 91, + [Parameter()] + [Switch]$IgnoreSelfSignedCerts = [System.Convert]::ToBoolean($env:ignoreSelfSignedCerts) +) +begin { + # Retrieve script variables from the dynamic script form. + if ($env:expirationFromCustomFieldName -and $env:expirationFromCustomFieldName -notlike "null") { $ExpirationFromCustomField = $env:expirationFromCustomFieldName } + if ($env:daysUntilExpiration -and $env:daysUntilExpiration -notlike "null") { $DaysUntilExpiration = $env:daysUntilExpiration } + if ($env:certificateMustBeOlderThanXDays -and $env:certificateMustBeOlderThanXDays -notlike "null") { $MustBeValidBefore = $env:certificateMustBeOlderThanXDays } + if ($env:skipCertsExpiredForMoreThanXDays -and $env:skipCertsExpiredForMoreThanXDays -notlike "null") { $Cutoff = $env:skipCertsExpiredForMoreThanXDays } + + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + # If using the custom field option, check for the default value and replace it if necessary. + if ($PSVersionTable.PSVersion.Major -gt 2) { + # $CustomField = Ninja-Property-Get -Name $ExpirationFromCustomField 2>$Null # Removed NinjaOne dependency + } + + if ($CustomField -and $DaysUntilExpiration -eq 30 -and (Test-IsElevated) -and $PSVersionTable.PSVersion.Major -gt 2) { + Write-Host "Retrieved value of $CustomField days from Custom Field $ExpirationFromCustomField. Using it for expiration value." + $DaysUntilExpiration = $CustomField + } + elseif (-not (Test-IsElevated) -or $PSVersionTable.PSVersion.Major -le 2) { + Write-Warning "Skipping CustomField retrieval due to either incompatible PowerShell version or lack of elevation." + } +} +process { + # Calculate expiration and cutoff dates. + $ExpirationDate = (Get-Date "11:59pm").AddDays($DaysUntilExpiration) + $CutoffDate = (Get-Date "12am").AddDays(-$Cutoff) + $MustBeValidBeforeDate = (Get-Date "12am").AddDays(-$MustBeValidBefore) + + # Retrieve all certificates. + $Certificates = Get-ChildItem -Path "Cert:\" -Recurse + + Write-Host "Checking for certificates that were valid before $MustBeValidBeforeDate and will expire before $ExpirationDate." + + # Filter down to certificates that are expired in our desired date range + $ExpiredCertificates = $Certificates | Where-Object { $_.NotAfter -le $ExpirationDate -and $_.NotAfter -gt $CutoffDate -and $_.NotBefore -lt $MustBeValidBeforeDate } + + # If we're asked to ignore self signed certs we'll filter them out + if ($IgnoreSelfSignedCerts -and $ExpiredCertificates) { + Write-Host "Removing Self-Signed certificates from list." + $ExpiredCertificates = $ExpiredCertificates | Where-Object { $_.Subject -ne $_.Issuer } + } + + if ($ExpiredCertificates) { + Write-Host "" + Write-Warning "Expired Certificates found!" + Write-Host "" + + $Report = $ExpiredCertificates | ForEach-Object { + # Subject can be a long property, we'll truncate it to maintain readability + New-Object PSObject -Property @{ + SerialNumber = $_.SerialNumber + HasPrivateKey = $_.HasPrivateKey + ExpirationDate = $_.NotAfter + Subject = if ($_.Subject.Length -gt 35) { $_.Subject.Substring(0, 35) + "..." }else { $_.Subject } + } + } + + Write-Host "### Expired Certificates ###" + $Report | Format-Table -AutoSize | Out-String | Write-Host + + exit 1 + } + else { + Write-Host "No Certificates were found with an expiration date before $ExpirationDate and after $CutoffDate." + } +} +end { + + + +} diff --git a/Powershell Scripts/Local Users Report.ps1 b/Powershell Scripts/Local Users Report.ps1 index 36bcf94..ac51f2d 100644 --- a/Powershell Scripts/Local Users Report.ps1 +++ b/Powershell Scripts/Local Users Report.ps1 @@ -1,527 +1,520 @@ # List all local accounts on the machine and optionally save the results to a WYSIWYG custom field. -#Requires -Version 4 - -<# -.SYNOPSIS - List all local accounts on the machine and optionally save the results to a WYSIWYG custom field. -.DESCRIPTION - List all local accounts on the machine and optionally save the results to a WYSIWYG custom field. -.EXAMPLE - (No Parameters) - Retrieving list of local users. - ExitCode: 1 - Parsing username list into machine-readable format. - Retrieving additional information on individual user accounts. - - Username FullName Enabled PasswordLastSet LastLogon - -------- -------- ------- --------------- --------- - helpdesk True 9/13/2024 9:01:22 AM 9/13/2024 9:20:25 AM - -PARAMETER: -IncludeDisabledUsers - Include disabled user accounts in the results. - -PARAMETER: -WysiwygCustomField "ReplaceMeWithAnyWysiwygCustomField" - Optionally specify the name of a WYSIWYG custom field to store the results in. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2012 R2 - Release Notes: Added WYSIWYG support, switched to using only "net user", made more verbose, and improved error handling. -#> - -[CmdletBinding()] -param ( - [Parameter()] - [Switch]$IncludeDisabledUsers = [System.Convert]::ToBoolean($env:includeDisabledUsers), - [Parameter()] - [String]$WysiwygCustomField -) - -begin { - # If script form variables are used, replace the command line parameters with their value. - if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $WysiwygCustomField = $env:wysiwygCustomFieldName } - - function Test-IsDomainController { - # Determine the method to retrieve the operating system information based on PowerShell version - try { - $OS = if ($PSVersionTable.PSVersion.Major -lt 5) { - Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop - } - else { - Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop - } - } - catch { - Write-Host -Object "[Error] Unable to validate whether or not this device is a domain controller." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Check if the ProductType is "2", which indicates that the system is a domain controller - if ($OS.ProductType -eq "2") { - return $true - } - } - - function Test-IsDomainJoined { - # Check the PowerShell version to determine the appropriate cmdlet to use - try { - if ($PSVersionTable.PSVersion.Major -lt 5) { - return $(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain - } - else { - return $(Get-CimInstance -Class Win32_ComputerSystem).PartOfDomain - } - } - catch { - Write-Host -Object "[Error] Unable to validate whether or not this device is a part of a domain." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - } - - function Test-IsEntraJoined { - # Check if the operating system version is Windows 10 or higher - if ([environment]::OSVersion.Version.Major -ge 10) { - # Run the dsregcmd.exe tool to check Entra join status and look for "AzureAdJoined : YES" - $dsreg = dsregcmd.exe /status | Select-String "AzureAdJoined : YES" - } - - # If the search found the "AzureAdJoined : YES" string, return True, otherwise return False - if ($dsreg) { return $True }else { return $False } - } - - # If running on a domain controller, display an error message and exit - if (Test-IsDomainController) { - Write-Host -Object "[Error] This script is not compatible with domain controllers." - exit 1 - } - - # If running on a domain joined machine, warn that only local accounts will be displayed. - if (Test-IsDomainJoined) { - Write-Warning -Message "This script will only display local accounts. It will not display Active Directory accounts." - } - - # If running on an Entra joined machine, warn that only local accounts will be displayed. - if (Test-IsEntraJoined) { - Write-Warning -Message "This script will only display local accounts. It will not display Microsoft Entra accounts." - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - # Measure the number of characters in the provided value - $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # Throw an error if the value exceeds the character limit of 200,000 characters - if ($Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - # Initialize a hashtable for additional documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define a list of valid field types - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - - # Warn the user if the provided type is not valid - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # Define types that require options to be retrieved - $NeedsOptions = "Dropdown" - - # If the property is being set in a document or field and the type needs options, retrieve them - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an error if there was an issue retrieving the property options - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Process the property value based on its type - switch ($Type) { - "Checkbox" { - # Convert the value to a boolean for Checkbox type - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Convert the value to a Unix timestamp for Date or Date Time type - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Convert the dropdown value to its corresponding GUID - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - # Throw an error if the value is not present in the dropdown options - if (!($Selection)) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # For other types, use the value as is - $NinjaValue = $Value - } - } - - # Set the property value in the document if a document name is provided - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - # Otherwise, set the standard property value - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - - # Throw an error if setting the property failed - if ($CustomField.Exception) { - throw $CustomField - } - } - - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # Check if the script is running with elevated (administrator) privileges. If not elevated, display an error and exit with code 1. - if (!(Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Define paths for standard output and error logs, with random names to avoid conflicts - $StandardOutLog = "$env:TEMP\$(Get-Random)_stdout.log" - $StandardErrLog = "$env:TEMP\$(Get-Random)_stderr.log" - - # Define the arguments for the "net user" command to list all users - $NetUserArguments = @( - "user" - ) - - # Configure the process start parameters for the "net.exe" command - $ProcessArguments = @{ - FilePath = "$env:SystemRoot\System32\net.exe" - ArgumentList = $NetUserArguments - RedirectStandardOutput = $StandardOutLog - RedirectStandardError = $StandardErrLog - PassThru = $True - NoNewWindow = $True - Wait = $True - } - - # Inform the user that the script is retrieving the list of local users - Write-Host -Object "Retrieving list of local users." - - # Try to start the "net.exe" process and catch any errors - try { - $NetUserProcess = Start-Process @ProcessArguments -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] Failed to start net.exe" - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Output the exit code of the net.exe process - Write-Host -Object "ExitCode: $($NetUserProcess.ExitCode)" - - # Check if the exit code indicates success (0 or 1) - if ($NetUserProcess.ExitCode -ne 0 -and $NetUserProcess.ExitCode -ne 1) { - Write-Warning "Exit code of $($NetUserProcess.ExitCode) does not indicate success." - } - - # Check if the standard error log exists, indicating an error occurred - if (Test-Path -Path $StandardErrLog -ErrorAction SilentlyContinue) { - - # Attempt to read the error log - try { - $ErrorLog = Get-Content -Path $StandardErrLog -ErrorAction Stop - } - catch { - # If reading the log fails, display an error and exit - Write-Host -Object "[Error] Failed to open error log at '$StandardErrLog'." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Remove the error log file after reading - try { - Remove-Item -Path $StandardErrLog -ErrorAction Stop - } - catch { - # If removing the log file fails, display an error - Write-Host -Object "[Error] Failed to remove standard error log at '$StandardErrLog'." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - - # If there is any content in the error log, display it and exit - if ($ErrorLog) { - Write-Host -Object "[Error] An error has occurred." - $ErrorLog | ForEach-Object { - Write-Host -Object "[Error] $_" - } - exit 1 - } - - # Check if the standard output log exists, which contains the user list - if (!(Test-Path -Path $StandardOutLog -ErrorAction SilentlyContinue)) { - Write-Host -Object "[Error] No net user output detected." - exit 1 - } - - # Try to read the standard output log for user data - try { - $NetUserOutput = Get-Content -Path $StandardOutLog -ErrorAction Stop - } - catch { - # If reading the log fails, display an error and exit - Write-Host -Object "[Error] Failed to open output log at '$StandardOutLog'." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Try to remove the standard output log after reading - try { - Remove-Item -Path $StandardOutLog -ErrorAction Stop - } - catch { - # If removing the log file fails, display an error - Write-Host -Object "[Error] Failed to remove standard output log at '$StandardOutLog'." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - - # Inform the user that the username list is being parsed - Write-Host -Object "Parsing username list into machine-readable format." - - # Skip the first 4 lines of the output and filter out any empty lines or the completion message - $NetUserOutput = $NetUserOutput | Select-Object -Skip 4 | Where-Object { $_ -and $_ -notmatch 'command completed' } - - # Split the usernames by 4 or more spaces and trim whitespace - $Usernames = $NetUserOutput -split '\s{4,}' | ForEach-Object { $_.Trim() } | Where-Object { $_ } - - # Create a list to hold user account information - $LocalUserAccounts = New-Object System.Collections.Generic.List[object] - - # Inform the user that additional information is being retrieved for each account - Write-Host -Object "Retrieving additional information on individual user accounts." - - # For each username in the list, retrieve more details - $Usernames | ForEach-Object { - $Username = if ($_ -notmatch '"') { "`"$_`"" }else { $_ } - $StandardOutLog = "$env:TEMP\$(Get-Random)_stdout.log" - $StandardErrLog = "$env:TEMP\$(Get-Random)_stderr.log" - - # Define the arguments for the "net user" command for a specific user - $NetUserArguments = @( - "user" - $Username - ) - - # Configure the process start parameters for the "net.exe" command - $ProcessArguments = @{ - FilePath = "$env:SystemRoot\System32\net.exe" - ArgumentList = $NetUserArguments - RedirectStandardOutput = $StandardOutLog - RedirectStandardError = $StandardErrLog - PassThru = $True - NoNewWindow = $True - Wait = $True - } - - # Try to start the "net.exe" process for the current user - try { - $NetUserProcess = Start-Process @ProcessArguments -ErrorAction Stop - } - catch { - # If the process fails, display an error and return to the next iteration - Write-Host -Object "[Error] Failed to start net.exe for user '$_'" - Write-Host -Object "[Error] $($_.Exception.Message)" - return - } - - # Check if the exit code of the net.exe process indicates failure - if ($NetUserProcess.ExitCode -ne 0) { - Write-Warning "Exit code of $($NetUserProcess.ExitCode) does not indicate success." - } - - # Check if the standard error log exists, indicating an error occurred - if (Test-Path -Path $StandardErrLog -ErrorAction SilentlyContinue) { - try { - $ErrorLog = Get-Content -Path $StandardErrLog -ErrorAction Stop - } - catch { - # If reading the log fails, display an error and set the exit code to 1 - Write-Host -Object "[Error] Failed to open error log at '$StandardErrLog'." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - - # Remove the error log file after reading - try { - Remove-Item -Path $StandardErrLog -ErrorAction Stop - } - catch { - # If removing the log file fails, display an error and set the exit code to 1 - Write-Host -Object "[Error] Failed to remove standard error log at '$StandardErrLog'." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - - # If there is any content in the error log, display it and set the exit code to 1 - if ($ErrorLog) { - Write-Host -Object "[Error] An error has occurred." - $ErrorLog | ForEach-Object { - Write-Host -Object "[Error] $_" - } - $ExitCode = 1 - } - - # Check if the standard output log exists, which contains the user details - if (!(Test-Path -Path $StandardOutLog -ErrorAction SilentlyContinue)) { - Write-Host -Object "[Error] No net user output detected for '$_'." - return - } - - # Try to read the standard output log for user details - try { - $NetUserOutput = Get-Content -Path $StandardOutLog -ErrorAction Stop - } - catch { - # If reading the log fails, display an error and return - Write-Host -Object "[Error] Failed to open output log at '$StandardOutLog'." - Write-Host -Object "[Error] $($_.Exception.Message)" - return - } - - # Try to remove the standard output log after reading - try { - Remove-Item -Path $StandardOutLog -ErrorAction Stop - } - catch { - # If removing the log file fails, display an error and set the exit code to 1 - Write-Host -Object "[Error] Failed to remove standard output log at '$StandardOutLog'." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - - # Extract relevant information from the output log for each user account - $LastSet = "$(($NetUserOutput | Select-String 'Password last set') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - $Expired = "$(($NetUserOutput | Select-String 'Password expires') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - $Changeable = "$(($NetUserOutput | Select-String 'Password changeable') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - $LastLogon = "$(($NetUserOutput | Select-String 'Last logon') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - - # Try to add user account details to the list of local user accounts - try { - $ErrorActionPreference = "Stop" - $LocalUserAccounts.Add( - [PSCustomObject]@{ - Username = $_ - FullName = "$(($NetUserOutput | Select-String 'Full Name') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - Comment = "$(($NetUserOutput | Select-String 'Comment') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - Enabled = if ("$(($NetUserOutput | Select-String 'Account active') -split '\s{4,}' | Select-Object -Skip 1)".Trim() -like "Yes") { $true }else { $false } - AccountExpires = "$(($NetUserOutput | Select-String 'Account expires') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - PasswordLastSet = if ($LastSet) { Get-Date -Date $LastSet }else { $null } - PasswordExpires = if ($Expired -notmatch "Never" -and $Expired -notlike "") { Get-Date -Date $Expired }else { $Expired } - PasswordChangeable = if ($Changeable) { Get-Date -Date $Changeable }else { $null } - PasswordRequired = if ("$(($NetUserOutput | Select-String 'Password required') -split '\s{4,}' | Select-Object -Skip 1)".Trim() -like "Yes") { $true }else { $false } - UserMayChangePassword = if ("$(($NetUserOutput | Select-String 'User may change password') -split '\s{4,}' | Select-Object -Skip 1)".Trim() -like "Yes") { $true }else { $false } - WorkstationsAllowed = "$(($NetUserOutput | Select-String 'Workstations allowed') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - LogonScript = "$(($NetUserOutput | Select-String 'Logon script') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - UserProfile = "$(($NetUserOutput | Select-String 'User profile') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - LastLogon = if ($LastLogon -notmatch "Never" -and $LastLogon -notlike "") { Get-Date -Date $LastLogon }else { $LastLogon } - LogonHoursAllowed = "$(($NetUserOutput | Select-String 'Logon hours allowed') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - } - ) - $ErrorActionPreference = "Continue" - } - catch { - # If adding the account details fails, display an error and set the exit code to 1 - Write-Host -Object "[Error] Failed to parse account '$_'" - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - - # Silently continue and add a basic object with just the username to the list - $ErrorActionPreference = "SilentlyContinue" - $LocalUserAccounts.Add( - [PSCustomObject]@{ - Username = $_ - } - ) - $ErrorActionPreference = "Continue" - } - - } - - # Filter out disabled users if $IncludeDisabledUsers is not specified - if (!$IncludeDisabledUsers) { - $LocalUserAccounts = $LocalUserAccounts | Where-Object { $_.Enabled } - } - - # Display the final list of users in a table format - Write-Host -Object "" - ($LocalUserAccounts | Sort-Object -Property Username | Format-Table -Property Username, FullName, Enabled, PasswordLastSet, LastLogon -AutoSize | Out-String).Trim() | Write-Host - Write-Host -Object "" - - # If a custom field is specified, try to set it - if ($WysiwygCustomField) { - try { - Write-Host "Attempting to set Custom Field '$WysiwygCustomField'." - - # Generate HTML content from the user accounts and format the output - $CustomFieldValue = $LocalUserAccounts | Sort-Object -Property Username | Select-Object -Property Username, @{ Name = "Full Name" ; Expression = { $_.FullName } }, Enabled, @{ Name = "Password Last Set" ; Expression = { $_.PasswordLastSet } }, @{ Name = "Last Logon" ; Expression = { $_.LastLogon } } | ConvertTo-Html -Fragment - $CustomFieldValue = $CustomFieldValue -replace "", "" - $CustomFieldValue = $CustomFieldValue -replace "
Detected Antivirus Details
Current Boot
Current Boot
Normal
Normal
Unexpected Shutdown
Unexpected Shutdown
') { - $htmlTable[$i] = $null - } - $i++ - # We'll flip the array back to right side up. - [array]::Reverse($htmlTable) - - # Add it back to the output. - $CustomFieldValue.Add($htmlTable) - # Finish with adding any errors we encountered during the search. - $CustomFieldErrorInfo | ForEach-Object { - $CustomFieldValue.Add($_) - } - # Check that we now comply with the character limit. If not restart the do loop. - $Characters = $CustomFieldValue | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters - }while ($Characters -ge 199500) - } - - # Set the custom field. - Set-NinjaProperty -Name $WysiwygField -Value $CustomFieldValue - Write-Host "Successfully set Custom Field '$WysiwygField'!" - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - - # Output the results of our search into the activity log. - if (!$MatchingItems) { - Write-Host "No files found with extension $Extension!" - } - else { - Write-Host "Files found!" - $MatchingItems | Format-List -Property Name, FullName, CreationTime, LastWriteTime, Size | Out-String | Write-Host - } - - # If we encountered any errors during the search we'll output them here. - if ($JobErrors -or $FailedJobs) { - Write-Host "" - Write-Host "[Error] Failed to search certain directories due to an error." - - if ($JobErrors) { - Write-Host "" - - $JobErrors | ForEach-Object { - Write-Host "[Error] $($_.Exception.Message)" - } - } - $ExitCode = 1 - } - - # Remove all jobs to clean up. - $SearchJobs | Get-Job | Remove-Job -Force - - # Exit the script with the appropriate exit code - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Creates a report based on the files found in the directory or subdirectory you specified with your desired extension. +.DESCRIPTION + Creates a report based on the files found in the directory or subdirectory you specified with your desired extension. +.EXAMPLE + -Extensions ".exe" -SearchPaths "C:\Users\tuser\Downloads" + + Searching C:\Users\tuser\Downloads for files with extension '.exe'... + No files found with extension .exe! + + +PARAMETER: -Extensions "exe, .ico" + A comma-separated list of extensions to search for. You can use the * character as a wildcard. + +PARAMETER: -SearchPaths "C:\Replace\Me\With\Valid\Path" + Enter the starting directories for the search, separated by commas. This will include all subdirectories as well. + +PARAMETER: -MultiLineField "ReplaceMeWithNameOfMultilineCustomField" + Optional multiline field to record search results. Leave blank if unused. + +PARAMETER: -WysiwygField "ReplaceMeWithNameOfWYSIWYGCustomField" + Optional WYSIWYG field to record search results. Leave blank if unused. + +PARAMETER: -ScanSystemDrive + This will set the system drive (usually drive C:\) as the starting point for the search. + +PARAMETER: -ScanAllDrives + This will set all drives (including flash drives) as the starting point for the search. +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Updated checkbox script variables. +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$Extensions, + [Parameter()] + [String]$SearchPaths, + [Parameter()] + [String]$MultiLineField, + [Parameter()] + [String]$WysiwygField, + [Parameter()] + [Switch]$ScanSystemDrive = [System.Convert]::ToBoolean($env:scanSystemDrive), + [Parameter()] + [Switch]$ScanAllDrives = [System.Convert]::ToBoolean($env:scanAllDrives) +) +begin { + # Set parameters using dynamic script variables. + if ($env:fileExtension -and $env:fileExtension -notlike "null") { $Extensions = $env:fileExtension } + if ($env:searchPath -and $env:searchPath -notlike "null") { $SearchPaths = $env:searchPath } + if ($env:multilineCustomField -and $env:multilineCustomField -notlike "null") { $MultiLineField = $env:multilineCustomField } + if ($env:wysiwygCustomField -and $env:wysiwygCustomField -notlike "null") { $WysiwygField = $env:wysiwygCustomField } + + # Check if no extensions were specified and exit with an error if true. + if (-not $Extensions) { + Write-Host -Object "[Error] Missing extension to search for!" + exit 1 + } + + # Verify that WysiwygField and MultiLineField are not the same, exiting with an error if they are. + if ($WysiwygField -and $MultiLineField -and ($WysiwygField -eq $MultiLineField)) { + Write-Host -Object "[Error] Wysiwyg Field and Multiline Field are the same! Custom fields cannot be the same type." + Write-Host -Object "https://ninjarmm.zendesk.com/hc/en-us/articles/18601842971789-Custom-Fields-by-Type-and-Functionality" + exit 1 + } + + # Initialize a list to store the extensions to search for. + $ExtensionsToSearch = New-Object System.Collections.Generic.List[string] + # Split the extensions if they are comma-separated and trim whitespace. + if ($Extensions -match ",") { + $Extensions -split "," | ForEach-Object { $ExtensionsToSearch.Add($_.Trim()) } + } + else { + $ExtensionsToSearch.Add($Extensions.Trim()) + } + + # Initialize a list to keep track of extensions that need to be replaced (adding a leading dot if missing). + $ExtensionsToReplace = New-Object System.Collections.Generic.List[object] + $ExtensionsToSearch | ForEach-Object { + if ($_ -notmatch "^\.") { + $NewExtension = ".$_" + + $ExtensionsToReplace.Add( + [PSCustomObject]@{ + Index = $ExtensionsToSearch.IndexOf("$_") + NewExtension = $NewExtension + } + ) + + Write-Warning "Missing . for extension. Changing extension search to '$NewExtension'." + } + } + + # Apply the replacements for extensions that were missing a leading dot. + $ExtensionsToReplace | ForEach-Object { + $ExtensionsToSearch[$_.index] = $_.NewExtension + } + + # Check if no search locations were specified and exit with an error if true. + if (!$SearchPaths -and !$ScanSystemDrive -and !$ScanAllDrives) { + Write-Host -Object "[Error] Missing somewhere to search!" + exit 1 + } + + # If scanning all drives, ignore specific paths and the system drive flag. + if ($ScanAllDrives) { + $ScanSystemDrive = $false + $SearchPaths = $Null + } + + # Initialize a list for paths to search. + $PathsToSearch = New-Object System.Collections.Generic.List[string] + # Split the search paths if they are comma-separated and trim whitespace. + if ($SearchPaths -match ",") { + $SearchPaths -split "," | ForEach-Object { $PathsToSearch.Add($_.Trim()) } + } + elseif ($SearchPaths) { + $PathsToSearch.Add($SearchPaths) + } + + # Add the system drive to the search paths if specified. + if ($ScanSystemDrive) { + if ($env:SystemDrive -notmatch '^[A-Z]:\\$' -and $env:SystemDrive -match '^[A-Z]:$') { + $PathsToSearch.Add("$env:SystemDrive\") + } + else { + $PathsToSearch.Add($env:SystemDrive) + } + } + + # Add all filesystem drives to the search paths if scanning all drives. + if ($ScanAllDrives) { + Get-PSDrive -PSProvider FileSystem | Where-Object { $_.Free -and $_.Used } | ForEach-Object { + if ($_.Root -notmatch '^[A-Z]:\\$' -and $_.Root -match '^[A-Z]:$') { + $PathsToSearch.Add("$($_.Root)\") + } + else { + $PathsToSearch.Add($_.Root) + } + } + } + + # Initialize a list for paths that need to be corrected (adding a trailing backslash if missing). + $ReplacementPaths = New-Object System.Collections.Generic.List[Object] + + # Check each path and add a backslash if it's missing. + $PathsToSearch | ForEach-Object { + if ($_ -notmatch '^[A-Z]:\\$' -and $_ -match '^[A-Z]:$') { + $NewPath = "$_\" + $ReplacementPaths.Add( + [PSCustomObject]@{ + Index = $PathsToSearch.IndexOf("$_") + NewPath = $NewPath + } + ) + + Write-Warning "Backslash missing from the search path. Changing it to $NewPath." + } + } + + # Apply the path corrections. + $ReplacementPaths | ForEach-Object { + $PathsToSearch[$_.index] = $_.NewPath + } + + # Function to test if the script is running with elevated permissions. + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + # Handy function to set a custom field. + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + # if ($Characters -ge 200000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than or equal to 200,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If we're requested to set the field value for a Ninja document we'll specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # The field below requires additional information to be set # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If an error is received it will have an exception property, the function will exit with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # if (-not $Selection) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # We'll need to set the field differently depending on if its a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + $ExitCode = 0 +} +process { + # Check if the script is running with Administrator privileges. Exit with an error message if not. + if (!(Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Remove illegal extensions + $ExtensionsToRemove = New-Object System.Collections.Generic.List[String] + $invalidExtensions = '[<>:"/\\|\x00-\x1F]|\.$' + $ExtensionsToSearch | ForEach-Object { + if($_ -match $invalidExtensions){ + Write-Host -Object "[Error] Extension $_ contains one of the following invalid characters or ends in a period. '\:<>`"/|'" + $ExtensionsToRemove.Add($_) + $ExitCode = 1 + } + } + + # Actual removal + $ExtensionsToRemove | ForEach-Object { + $ExtensionsToSearch.Remove($_) | Out-Null + } + + # Exit the script if there are no valid extensions left to search. + if ($ExtensionsToSearch.Count -eq 0) { + Write-Host "[Error] No valid extensions to search!" + exit 1 + } + + # Initialize lists to store information about paths and errors. + $CustomFieldErrorInfo = New-Object System.Collections.Generic.List[string] + + # These characters are not valid for a search path. + $invalidSearchPathCharacters = '[<>"/|?\x00-\x1F]' + + # Initialize a generic list to store paths that don't exist and should be removed from the search. + $PathsToRemove = New-Object System.Collections.Generic.List[String] + # Check each path in the search list to ensure it exists. Collect paths that don't exist for removal. + $PathsToSearch | ForEach-Object { + if($_ -match $invalidSearchPathCharacters){ + Write-Host -Object "[Error] Path $_ contains one of the following invalid characters. '<>`"/|'" + $PathsToRemove.Add($_) + $ExitCode = 1 + return + } + + if (!(Test-Path $_)) { + Write-Host -Object "[Error] $_ does not exist!" + $PathsToRemove.Add($_) + $ExitCode = 1 + } + } + + # Remove non-existing paths from the search list. + $PathsToRemove | ForEach-Object { + $PathsToSearch.Remove($_) | Out-Null + } + + # Exit the script if there are no valid paths left to search. + if ($PathsToSearch.Count -eq 0) { + Write-Host "[Error] No valid paths to search!" + exit 1 + } + + # Initialize a list to keep track of the search jobs created. + $SearchJobs = New-Object System.Collections.Generic.List[object] + + # Create and start a PowerShell job for each path and extension combination. + foreach ($Path in $PathsToSearch) { + foreach ($Extension in $ExtensionsToSearch) { + Write-Host "Searching '$Path' for files with extension '$Extension'..." + $SearchJobs.Add( + ( + Start-Job -ScriptBlock { + param($Path, $Extension) + + # Defines a function to convert file sizes to a human-readable format. + function Get-FriendlySize { + param($Bytes) + # Converts Bytes to the highest matching unit + $Sizes = 'Bytes,KB,MB,GB,TB,PB,EB,ZB' -split ',' + for ($i = 0; ($Bytes -ge 1kb) -and ($i -lt $Sizes.Count); $i++) { $Bytes /= 1kb } + $N = 2 + if ($i -eq 0) { $N = 0 } + if ($Bytes) { "$([System.Math]::Round($Bytes,$N)) $($Sizes[$i])" }else { "0 B" } + } + + # Search for files matching the extension and output their details in CSV format. + Get-ChildItem -Path $Path -Filter "*$Extension" -Recurse -File -Force | Select-Object Name, FullName, CreationTime, LastWriteTime, Length, @{Name = "Size"; Expression = { Get-FriendlySize $_.Length } } | ConvertTo-Csv + } -ArgumentList $Path, $Extension + ) + ) + } + } + + # Wait for all search jobs to complete or timeout after 9000 seconds (2.5 hours). + $SearchJobs | Wait-Job -Timeout 9000 | Out-Null + + # Check for incomplete jobs due to timeout and log an error. + $IncompleteJobs = $SearchJobs | Get-Job | Where-Object { $_.State -eq "Running" } + if ($IncompleteJobs) { + Write-Host "[Error] The timeout period of 2.5 hours has been reached, but not all files or directories have been searched!" + $CustomFieldErrorInfo.Add("[Error] The timeout period of 2.5 hours has been reached, but not all files or directories have been searched!") + $ExitCode = 1 + } + + # Collect and process the output from each search job. + $MatchingItems = $SearchJobs | ForEach-Object { + $_ | Get-Job | Receive-Job -ErrorAction SilentlyContinue -ErrorVariable JobErrors | ConvertFrom-Csv + } + + # Clear out duplicate entries + if ($MatchingItems) { + $MatchingItems = $MatchingItems | Sort-Object FullName -Unique + } + + # Check for jobs that failed to complete successfully and log errors. + $FailedJobs = $SearchJobs | Get-Job | Where-Object { $_.State -ne "Completed" } + if ($JobErrors -or $FailedJobs) { + $CustomFieldErrorInfo.Add("[Error] Failed to search certain directories due to an error.") + + if ($JobErrors) { + $JobErrors | ForEach-Object { + $CustomFieldErrorInfo.Add("[Error] $($_.Exception.Message)") + } + } + $ExitCode = 1 + } + + # Process and attempt to set custom field values based on search results and errors, with specific handling for multiline fields. + # Truncate data if it exceeds character limits for the fields. + if ($MultiLineField -and $MatchingItems) { + try { + Write-Host "Attempting to set Custom Field '$MultiLineField'." + + # Prepare the custom field output. + $CustomFieldValue = New-Object System.Collections.Generic.List[string] + + # We don't want to edit the matching items array if we have to truncate later so we'll create a duplicate here. + $CustomFieldList = $MatchingItems | Select-Object -Property Name, FullName, CreationTime, LastWriteTime, Size + + # Format the matching items into a nice list with the relevant properties. + $CustomFieldValue.Add(($CustomFieldList | Format-List -Property Name, FullName, CreationTime, LastWriteTime, Size | Out-String)) + + # If any errors were encountered in the search add them to the bottom of the custom field output. + $CustomFieldErrorInfo | ForEach-Object { + $CustomFieldValue.Add($_) + } + + # Check that the output complies with the hard character limits. + $Characters = $CustomFieldValue | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters + if ($Characters -ge 9500) { + Write-Warning "10,000 Character Limit has been reached! Trimming output until the character limit is satisified..." + + # If it doesn't comply with the limits we'll need to recreate it with some adjustments. + $i = 0 + do { + # Recreate the custom field output starting with a warning that we truncated the output. + $CustomFieldValue = New-Object System.Collections.Generic.List[string] + $CustomFieldValue.Add("This info has been truncated to accommodate the 10,000 character limit.") + + # The custom field information is sorted in alphabetical order. We'll flip the array upside down to sort it in reverse alphabetical. + [array]::Reverse($CustomFieldList) + + # Remove the next item which in this case will be the smallest item. + $CustomFieldList[$i] = $null + $i++ + + # We'll flip the array back to right side up. + [array]::Reverse($CustomFieldList) + + # Add it back to the output. + $CustomFieldValue.Add(($CustomFieldList | Format-List -Property Name, FullName, CreationTime, LastWriteTime, Size | Out-String)) + # Finish with adding any errors we encountered during the search. + $CustomFieldErrorInfo | ForEach-Object { + $CustomFieldValue.Add($_) + } + + # Check that we now comply with the character limit. If not restart the do loop. + $Characters = $CustomFieldValue | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters + }while ($Characters -ge 9500) + } + + # Set the custom field. + # Set-NinjaProperty -Name $MultiLineField -Value $CustomFieldValue # Removed NinjaOne dependency + Write-Host "Successfully set Custom Field '$MultiLineField'!" + } + catch { + Write-Host "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + } + + # Process and attempt to set custom field values based on search results and errors, with specific handling for WYSIWYG fields. + # Truncate data if it exceeds character limits for the fields. + if ($WysiwygField -and $MatchingItems) { + try { + Write-Host "Attempting to set Custom Field '$WysiwygField'." + + # Prepare the custom field output. + $CustomFieldValue = New-Object System.Collections.Generic.List[string] + + # Convert the matching items into an html report. + $htmlTable = $MatchingItems | Select-Object -Property Name, FullName, CreationTime, LastWriteTime, Size | ConvertTo-Html -Fragment + + # Add the newly created html into the custom field output. + $CustomFieldValue.Add($htmlTable) + # If any errors were encountered in the search add them to the bottom of the custom field output. + $CustomFieldErrorInfo | ForEach-Object { + $CustomFieldValue.Add($_) + } + + # Check that the output complies with the hard character limits. + $Characters = $CustomFieldValue | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters + if ($Characters -ge 199500) { + Write-Warning "200,000 Character Limit has been reached! Trimming output until the character limit is satisified..." + + # If it doesn't comply with the limits we'll need to recreate it with some adjustments. + $i = 0 + do { + # Recreate the custom field output starting with a warning that we truncated the output. + $CustomFieldValue = New-Object System.Collections.Generic.List[string] + $CustomFieldValue.Add("

This info has been truncated to accommodate the 200,000 character limit.

") + + # The custom field information is sorted in alphabetical order. We'll sort it into reverse alphabetical by flipping the array upside down. + [array]::Reverse($htmlTable) + # If the next entry is a row we'll delete it. + if ($htmlTable[$i] -match '
') { + $htmlTable[$i] = $null + } + $i++ + # We'll flip the array back to right side up. + [array]::Reverse($htmlTable) + + # Add it back to the output. + $CustomFieldValue.Add($htmlTable) + # Finish with adding any errors we encountered during the search. + $CustomFieldErrorInfo | ForEach-Object { + $CustomFieldValue.Add($_) + } + # Check that we now comply with the character limit. If not restart the do loop. + $Characters = $CustomFieldValue | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters + }while ($Characters -ge 199500) + } + + # Set the custom field. + # Set-NinjaProperty -Name $WysiwygField -Value $CustomFieldValue # Removed NinjaOne dependency + Write-Host "Successfully set Custom Field '$WysiwygField'!" + } + catch { + Write-Host "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + } + + # Output the results of our search into the activity log. + if (!$MatchingItems) { + Write-Host "No files found with extension $Extension!" + } + else { + Write-Host "Files found!" + $MatchingItems | Format-List -Property Name, FullName, CreationTime, LastWriteTime, Size | Out-String | Write-Host + } + + # If we encountered any errors during the search we'll output them here. + if ($JobErrors -or $FailedJobs) { + Write-Host "" + Write-Host "[Error] Failed to search certain directories due to an error." + + if ($JobErrors) { + Write-Host "" + + $JobErrors | ForEach-Object { + Write-Host "[Error] $($_.Exception.Message)" + } + } + $ExitCode = 1 + } + + # Remove all jobs to clean up. + $SearchJobs | Get-Job | Remove-Job -Force + + # Exit the script with the appropriate exit code + exit $ExitCode +} +end { + + + +} diff --git a/Powershell Scripts/Find Registry Key.ps1 b/Powershell Scripts/Find Registry Key.ps1 index 2ff6ba7..7b89994 100644 --- a/Powershell Scripts/Find Registry Key.ps1 +++ b/Powershell Scripts/Find Registry Key.ps1 @@ -1,455 +1,455 @@ # Find a registry key path, property or value that contains your given search text. Larger depth values may increase script runtime. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Find a registry key path, property or value that contains your given search text. Larger depth values may increase script runtime. -.DESCRIPTION - Find a registry key path, property or value that contains your given search text. Larger depth values may increase script runtime. -.EXAMPLE - -RootKey "HKEY_USERS" -SearchPath "*\Software" -Search "Microsoft" -Path -Property -Value - - WARNING: Matching registry path names found! - WARNING: Matching registry properties found! - WARNING: Matching registry key values found! - - - Path : HKEY_USERS\.DEFAULT\Software\AppDataLow\Software\Microsoft - Property : N/A - Value : N/A - - Path : HKEY_USERS\.DEFAULT\Software\Classes\Local Settings\MrtCache\C:%5CProgram Files%5CWindowsApps%5CClipchamp.Clipchamp_2.9.1.0_neutral__yxz26nhyzhsrt%5Cresources.pri\1da6c1775fdf538\a37dfe62 - Property : @{C:\Program Files\WindowsApps\Clipchamp.Clipchamp_2.9.1.0_neutral__yxz26nhyzhsrt\resources.pri? ms-resource:///resources/Clipchamp/AppName} - Value : Microsoft Clipchamp - - Path : HKEY_USERS\.DEFAULT\Software\Classes\Local Settings\MrtCache\C:%5CProgram Files%5CWindowsApps%5CMicrosoft.BingNews_4.55.62231.0_x64__8wekyb3d8bbwe%5Cresources.pri\1da6c1719ed8ee6\a37dfe62 - Property : @{C:\Program Files\WindowsApps\Microsoft.BingNews_4.55.62231.0_x64__8wekyb3d8bbwe\resources.pri? ms-resource:///resources/ApplicationTitleWithTagline} - Value : News - - Path : HKEY_USERS\.DEFAULT\Software\Classes\Local Settings\MrtCache\C:%5CProgram Files%5CWindowsApps%5CMicrosoft.BingWeather_1.0.6.0_x64__8wekyb3d8bbwe%5Cresources.pri\1d861e9fdbc0f2\a37dfe62 - Property : @{C:\Program Files\WindowsApps\Microsoft.BingWeather_1.0.6.0_x64__8wekyb3d8bbwe\resources.pri? ms-resource:///resources/ApplicationTitleWithBranding} - Value : MSN W... - -PARAMETER: -RootKey "HKEY_LOCAL_MACHINE" - Enter the root registry key where your search will begin. - -PARAMETER: -SearchPath "SOFTWARE\ReplaceMe" - Specify the subpath within the selected root key where the registry search should start. Exclude the root key from this path. - -PARAMETER: -Search "ReplaceMe" - Enter the text that must be present in the registry path, property, or value for it to be considered a match in the search results. - -PARAMETER: -Depth "3" - Set the maximum number of levels deep to search within the registry from the specified path. Increasing this value can significantly impact script performance due to deeper searches. - -PARAMETER: -CustomField "ReplaceMeWithAnyMultilineCustomField" - Specifies the name of an optional multiline custom field where results can be sent. Leave blank if not applicable. - -PARAMETER: -Path - If selected, the search will include registry key paths that contain the specified 'Search For' text as part of the search results. - -PARAMETER: -Property - If selected, the search will include registry key properties (names) that contain the specified 'Search For' text as part of the search results. - -PARAMETER: -Value - If selected, the search will include registry key values that contain the specified 'Search For' text as part of the search results. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$RootKey = "HKEY_LOCAL_MACHINE", - [Parameter()] - [String]$SearchPath, - [Parameter()] - [String]$Search, - [Parameter()] - [int]$Depth = 4, - [Parameter()] - [String]$CustomField, - [Parameter()] - [Switch]$Path = [System.Convert]::ToBoolean($env:searchForMatchingKeyPaths), - [Parameter()] - [Switch]$Property = [System.Convert]::ToBoolean($env:searchForMatchingKeyProperties), - [Parameter()] - [Switch]$Value = [System.Convert]::ToBoolean($env:searchForMatchingKeyValues) -) - -begin { - if ($env:rootKeyToSearch -and $env:rootKeyToSearch -notlike "null") { $RootKey = $env:rootKeyToSearch } - if ($env:searchPath -and $env:searchPath -notlike "null") { $SearchPath = $env:searchPath } - if ($env:searchFor -and $env:searchFor -notlike "null") { $Search = $env:searchFor } - if ($env:searchDepth -and $env:searchDepth -notlike "null") { $Depth = $env:searchDepth } - if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } - - # Error out if we're not told to match the search string with anything. - if (-not $Path -and -not $Property -and -not $Value) { - Write-Host "[Error] You must select the option to either match based on the key path, the property name, or the value." - exit 1 - } - - # If no search string is given error out. - if ( -not $Search) { - Write-Host "[Error] You must specify something to search for." - exit 1 - } - - # If we're not given a search path error out. - if ( -not $SearchPath) { - Write-Host "[Error] You must specify a path to search, e.g., 'SOFTWARE\Microsoft'." - exit 1 - } - - # If no root key is given error out. - if ( -not $RootKey) { - Write-Host "[Error] You must specify a root key to search in." - exit 1 - } - - # Valid root keys for the search. - $ValidRootKeys = "HKEY_LOCAL_MACHINE", "HKEY_CLASSES_ROOT", "HKEY_USERS", "HKEY_CURRENT_CONFIG", "HKEY_CURRENT_USER" - if ($ValidRootKeys -notcontains $RootKey) { - Write-Host "[Error] You must specify a valid root key! Valid root keys are 'HKEY_LOCAL_MACHINE', 'HKEY_CLASSES_ROOT', 'HKEY_USERS', 'HKEY_CURRENT_CONFIG', and 'HKEY_CURRENT_USER'." - exit 1 - } - - # Remove accidental backslashes. - if ($SearchPath -match "^\\") { - $SearchPath = $SearchPath -replace "^\\" - Write-Warning "An extra backslash was detected; changing the search path to $SearchPath." - } - - # If the search path is not valid error out. - if (-not (Test-Path "Registry::$RootKey\$SearchPath")) { - Write-Host "[Error] Search path $RootKey\$SearchPath does not exist! Please specify an existing registry path to start the search from!" - exit 1 - } - - # Depth must be greater than 0. - if ( -not $Depth -or $Depth -lt 1) { - Write-Host "[Error] Depth must be greater than 0." - exit 1 - } - - # If depth is 5 or higher, output a warning. - if ($Depth -ge 5) { - Write-Warning "Executing deep registry searches may significantly extend script runtime." - } - - # If HKEY_USERS is used we'll need a list of User Profiles and where to mount the corresponding registry hives. - function Get-UserHives { - param ( - [Parameter()] - [ValidateSet('AzureAD', 'DomainAndLocal', 'All')] - [String]$Type = "All", - [Parameter()] - [String[]]$ExcludedUsers, - [Parameter()] - [switch]$IncludeDefault - ) - - # User account SID's follow a particular pattern depending on if they're Azure AD, a Domain account, or a local "workgroup" account. - $Patterns = switch ($Type) { - "AzureAD" { "S-1-12-1-(\d+-?){4}$" } - "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" } - "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" } - } - - # We'll need the NTUSER.DAT file to load each user's registry hive. So we grab it if their account SID matches the above pattern. - $UserProfiles = Foreach ($Pattern in $Patterns) { - Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | - Where-Object { $_.PSChildName -match $Pattern } | - Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, - @{Name = "UserName"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } }, - @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, - @{Name = "Path"; Expression = { $_.ProfileImagePath } } - } - - # There are some situations where grabbing the .Default user's info is needed. - switch ($IncludeDefault) { - $True { - $DefaultProfile = "" | Select-Object UserName, SID, UserHive, Path - $DefaultProfile.UserName = "Default" - $DefaultProfile.SID = "DefaultProfile" - $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT" - $DefaultProfile.Path = "C:\Users\Default" - - $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.UserName } - } - } - - $UserProfiles | Where-Object { $ExcludedUsers -notcontains $_.UserName } - } - - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - # This function makes it easier to set Custom Fields. - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 10000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") - } - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to be set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received it will have an exception property, the function will exit with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the Date-Time to be in Unix Epoch time so we'll convert it here. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - - $ExitCode = 0 -} -process { - # Test for local administrator rights. - if (-not (Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Load unloaded profiles if asked to search in HKEY_USERS. - if ($RootKey -eq "HKEY_USERS") { - $UserProfiles = Get-UserHives -Type "All" - $ProfileWasLoaded = New-Object System.Collections.Generic.List[string] - - # Loop through each profile on the machine. - Foreach ($UserProfile in $UserProfiles) { - # Load user's NTUSER.DAT if it's not already loaded. - If ((Test-Path Registry::HKEY_USERS\$($UserProfile.SID)) -eq $false) { - Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe LOAD HKU\$($UserProfile.SID) `"$($UserProfile.UserHive)`"" -Wait -WindowStyle Hidden - $ProfileWasLoaded.Add("$($UserProfile.SID)") - } - } - } - - # Retrieve all the registry keys with the given parameters. - $RegistryKeys = Get-ChildItem -Path "Registry::$RootKey\$SearchPath" -Depth $Depth -Recurse -ErrorAction SilentlyContinue -ErrorVariable RegistryErrors - - if ($RootKey -eq "HKEY_USERS") { - # Unload all hives that were loaded for this script. - ForEach ($UserHive in $ProfileWasLoaded) { - If ($ProfileWasLoaded -eq $false) { - [gc]::Collect() - Start-Sleep 1 - Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe UNLOAD HKU\$($UserHive)" -Wait -WindowStyle Hidden | Out-Null - } - } - } - - # Initialize generic lists. - $AllKeys = New-Object System.Collections.Generic.List[object] - $MatchingKeys = New-Object System.Collections.Generic.List[object] - $CustomFieldValue = New-Object System.Collections.Generic.List[string] - - # For each registry key, retrieve all properties and values if available. - $RegistryKeys | ForEach-Object { - $RegistryPath = $_.PSPATH -replace "Microsoft.PowerShell.Core\\Registry::" - try { - $ErrorActionPreference = "Stop" - $Properties = New-Object System.Collections.Generic.List[string] - $_.GetValueNames() | ForEach-Object { $Properties.Add($_) } - $Properties.Add("(default)") - } - catch { - $Properties = $Null - } - $ErrorActionPreference = "Continue" - - if (-not $Properties) { - $AllKeys.Add( - [PSCustomObject]@{ - Path = $RegistryPath - Property = "N/A" - Value = "N/A" - } - ) - return - } - - foreach ($PropertyName in $Properties) { - $ErrorActionPreference = "SilentlyContinue" - $RegValue = ($_ | Get-ItemProperty -ErrorVariable RegistryErrors).$PropertyName - $ErrorActionPreference = "Continue" - $AllKeys.Add( - [PSCustomObject]@{ - Path = $RegistryPath - Property = $PropertyName - Value = $RegValue - } - ) - } - } - - $MatchingValues = $False - $MatchingProperties = $False - $MatchingPaths = $False - - # Match the registry keys based on the key path, property, or value. Add the results to the MatchingKeys generic list. - if ($Value) { - $AllKeys | Where-Object { $_.Value -match [regex]::Escape($Search) } | ForEach-Object { - $MatchingValues = $True - $MatchingKeys.Add($_) - } - } - - if ($Property) { - $AllKeys | Where-Object { $_.Property -match [regex]::Escape($Search) } | ForEach-Object { - $MatchingProperties = $True - $MatchingKeys.Add($_) - } - } - - if ($Path) { - $AllKeys | Where-Object { $_.Path -match $([regex]::Escape($Search)) } | ForEach-Object { - $MatchingPaths = $True - $MatchingKeys.Add($_) - } - } - - if (-not $MatchingPaths -and -not $MatchingProperties -and -not $MatchingValues) { - $CustomFieldValue.Add("No matching registry keys found!") - Write-Host "No matching registry keys found!" - } - - # If we have any matches, output to Write-Warning. - if ($MatchingPaths) { - Write-Warning -Message "Matching registry path names found!" - $CustomFieldValue.Add("WARNING: Matching registry path names found!") - } - - if ($MatchingProperties) { - Write-Warning -Message "Matching registry properties found!" - $CustomFieldValue.Add("WARNING: Matching registry properties found!") - } - - if ($MatchingValues) { - Write-Warning -Message "Matching registry key values found!" - $CustomFieldValue.Add("WARNING: Matching registry key values found!") - } - - if ($MatchingKeys) { - $KeysToReport = $MatchingKeys | Format-List Path, Property, Value | Out-String - $CustomFieldValue.Add($KeysToReport) - } - - # For each error, output them at the bottom. Most of these errors are not going to be relevant. - $RegistryErrors | ForEach-Object { - $CustomFieldValue.Add("[Error] $($_.TargetObject)") - $CustomFieldValue.Add("[Error] $($_.Exception.Message)") - } - - # Save the output to a custom field if a field name is provided. - if ($CustomField) { - try { - Write-Host "Attempting to set Custom Field '$CustomField'." - Set-NinjaProperty -Name $CustomField -Value (($CustomFieldValue | Out-String) -replace "`n") - Write-Host "Successfully set Custom Field '$CustomField'!" - } - catch { - if ($_.Exception.Message) { - Write-Host "[Error] $($_.Exception.Message)" - } - - if ($_.Message) { - Write-Host "[Error] $($_.Message)" - } - $ExitCode = 1 - } - } - - # Activity Log output - if($MatchingKeys){ - $KeysToReport | Write-Host - } - - $RegistryErrors | ForEach-Object { - Write-Host "[Error] $($_.TargetObject)" - Write-Host "[Error] $($_.Exception.Message)" - } - - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Find a registry key path, property or value that contains your given search text. Larger depth values may increase script runtime. +.DESCRIPTION + Find a registry key path, property or value that contains your given search text. Larger depth values may increase script runtime. +.EXAMPLE + -RootKey "HKEY_USERS" -SearchPath "*\Software" -Search "Microsoft" -Path -Property -Value + + WARNING: Matching registry path names found! + WARNING: Matching registry properties found! + WARNING: Matching registry key values found! + + + Path : HKEY_USERS\.DEFAULT\Software\AppDataLow\Software\Microsoft + Property : N/A + Value : N/A + + Path : HKEY_USERS\.DEFAULT\Software\Classes\Local Settings\MrtCache\C:%5CProgram Files%5CWindowsApps%5CClipchamp.Clipchamp_2.9.1.0_neutral__yxz26nhyzhsrt%5Cresources.pri\1da6c1775fdf538\a37dfe62 + Property : @{C:\Program Files\WindowsApps\Clipchamp.Clipchamp_2.9.1.0_neutral__yxz26nhyzhsrt\resources.pri? ms-resource:///resources/Clipchamp/AppName} + Value : Microsoft Clipchamp + + Path : HKEY_USERS\.DEFAULT\Software\Classes\Local Settings\MrtCache\C:%5CProgram Files%5CWindowsApps%5CMicrosoft.BingNews_4.55.62231.0_x64__8wekyb3d8bbwe%5Cresources.pri\1da6c1719ed8ee6\a37dfe62 + Property : @{C:\Program Files\WindowsApps\Microsoft.BingNews_4.55.62231.0_x64__8wekyb3d8bbwe\resources.pri? ms-resource:///resources/ApplicationTitleWithTagline} + Value : News + + Path : HKEY_USERS\.DEFAULT\Software\Classes\Local Settings\MrtCache\C:%5CProgram Files%5CWindowsApps%5CMicrosoft.BingWeather_1.0.6.0_x64__8wekyb3d8bbwe%5Cresources.pri\1d861e9fdbc0f2\a37dfe62 + Property : @{C:\Program Files\WindowsApps\Microsoft.BingWeather_1.0.6.0_x64__8wekyb3d8bbwe\resources.pri? ms-resource:///resources/ApplicationTitleWithBranding} + Value : MSN W... + +PARAMETER: -RootKey "HKEY_LOCAL_MACHINE" + Enter the root registry key where your search will begin. + +PARAMETER: -SearchPath "SOFTWARE\ReplaceMe" + Specify the subpath within the selected root key where the registry search should start. Exclude the root key from this path. + +PARAMETER: -Search "ReplaceMe" + Enter the text that must be present in the registry path, property, or value for it to be considered a match in the search results. + +PARAMETER: -Depth "3" + Set the maximum number of levels deep to search within the registry from the specified path. Increasing this value can significantly impact script performance due to deeper searches. + +PARAMETER: -CustomField "ReplaceMeWithAnyMultilineCustomField" + Specifies the name of an optional multiline custom field where results can be sent. Leave blank if not applicable. + +PARAMETER: -Path + If selected, the search will include registry key paths that contain the specified 'Search For' text as part of the search results. + +PARAMETER: -Property + If selected, the search will include registry key properties (names) that contain the specified 'Search For' text as part of the search results. + +PARAMETER: -Value + If selected, the search will include registry key values that contain the specified 'Search For' text as part of the search results. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$RootKey = "HKEY_LOCAL_MACHINE", + [Parameter()] + [String]$SearchPath, + [Parameter()] + [String]$Search, + [Parameter()] + [int]$Depth = 4, + [Parameter()] + [String]$CustomField, + [Parameter()] + [Switch]$Path = [System.Convert]::ToBoolean($env:searchForMatchingKeyPaths), + [Parameter()] + [Switch]$Property = [System.Convert]::ToBoolean($env:searchForMatchingKeyProperties), + [Parameter()] + [Switch]$Value = [System.Convert]::ToBoolean($env:searchForMatchingKeyValues) +) + +begin { + if ($env:rootKeyToSearch -and $env:rootKeyToSearch -notlike "null") { $RootKey = $env:rootKeyToSearch } + if ($env:searchPath -and $env:searchPath -notlike "null") { $SearchPath = $env:searchPath } + if ($env:searchFor -and $env:searchFor -notlike "null") { $Search = $env:searchFor } + if ($env:searchDepth -and $env:searchDepth -notlike "null") { $Depth = $env:searchDepth } + if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } + + # Error out if we're not told to match the search string with anything. + if (-not $Path -and -not $Property -and -not $Value) { + Write-Host "[Error] You must select the option to either match based on the key path, the property name, or the value." + exit 1 + } + + # If no search string is given error out. + if ( -not $Search) { + Write-Host "[Error] You must specify something to search for." + exit 1 + } + + # If we're not given a search path error out. + if ( -not $SearchPath) { + Write-Host "[Error] You must specify a path to search, e.g., 'SOFTWARE\Microsoft'." + exit 1 + } + + # If no root key is given error out. + if ( -not $RootKey) { + Write-Host "[Error] You must specify a root key to search in." + exit 1 + } + + # Valid root keys for the search. + $ValidRootKeys = "HKEY_LOCAL_MACHINE", "HKEY_CLASSES_ROOT", "HKEY_USERS", "HKEY_CURRENT_CONFIG", "HKEY_CURRENT_USER" + if ($ValidRootKeys -notcontains $RootKey) { + Write-Host "[Error] You must specify a valid root key! Valid root keys are 'HKEY_LOCAL_MACHINE', 'HKEY_CLASSES_ROOT', 'HKEY_USERS', 'HKEY_CURRENT_CONFIG', and 'HKEY_CURRENT_USER'." + exit 1 + } + + # Remove accidental backslashes. + if ($SearchPath -match "^\\") { + $SearchPath = $SearchPath -replace "^\\" + Write-Warning "An extra backslash was detected; changing the search path to $SearchPath." + } + + # If the search path is not valid error out. + if (-not (Test-Path "Registry::$RootKey\$SearchPath")) { + Write-Host "[Error] Search path $RootKey\$SearchPath does not exist! Please specify an existing registry path to start the search from!" + exit 1 + } + + # Depth must be greater than 0. + if ( -not $Depth -or $Depth -lt 1) { + Write-Host "[Error] Depth must be greater than 0." + exit 1 + } + + # If depth is 5 or higher, output a warning. + if ($Depth -ge 5) { + Write-Warning "Executing deep registry searches may significantly extend script runtime." + } + + # If HKEY_USERS is used we'll need a list of User Profiles and where to mount the corresponding registry hives. + function Get-UserHives { + param ( + [Parameter()] + [ValidateSet('AzureAD', 'DomainAndLocal', 'All')] + [String]$Type = "All", + [Parameter()] + [String[]]$ExcludedUsers, + [Parameter()] + [switch]$IncludeDefault + ) + + # User account SID's follow a particular pattern depending on if they're Azure AD, a Domain account, or a local "workgroup" account. + $Patterns = switch ($Type) { + "AzureAD" { "S-1-12-1-(\d+-?){4}$" } + "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" } + "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" } + } + + # We'll need the NTUSER.DAT file to load each user's registry hive. So we grab it if their account SID matches the above pattern. + $UserProfiles = Foreach ($Pattern in $Patterns) { + Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | + Where-Object { $_.PSChildName -match $Pattern } | + Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, + @{Name = "UserName"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } }, + @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, + @{Name = "Path"; Expression = { $_.ProfileImagePath } } + } + + # There are some situations where grabbing the .Default user's info is needed. + switch ($IncludeDefault) { + $True { + $DefaultProfile = "" | Select-Object UserName, SID, UserHive, Path + $DefaultProfile.UserName = "Default" + $DefaultProfile.SID = "DefaultProfile" + $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT" + $DefaultProfile.Path = "C:\Users\Default" + + $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.UserName } + } + } + + $UserProfiles | Where-Object { $ExcludedUsers -notcontains $_.UserName } + } + + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + # This function makes it easier to set Custom Fields. + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + # if ($Characters -ge 10000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If we're requested to set the field value for a Ninja document we'll specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # The field below requires additional information to be set # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If an error is received it will have an exception property, the function will exit with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the Date-Time to be in Unix Epoch time so we'll convert it here. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # if (-not $Selection) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # We'll need to set the field differently depending on if its a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + $ExitCode = 0 +} +process { + # Test for local administrator rights. + if (-not (Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Load unloaded profiles if asked to search in HKEY_USERS. + if ($RootKey -eq "HKEY_USERS") { + $UserProfiles = Get-UserHives -Type "All" + $ProfileWasLoaded = New-Object System.Collections.Generic.List[string] + + # Loop through each profile on the machine. + Foreach ($UserProfile in $UserProfiles) { + # Load user's NTUSER.DAT if it's not already loaded. + If ((Test-Path Registry::HKEY_USERS\$($UserProfile.SID)) -eq $false) { + Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe LOAD HKU\$($UserProfile.SID) `"$($UserProfile.UserHive)`"" -Wait -WindowStyle Hidden + $ProfileWasLoaded.Add("$($UserProfile.SID)") + } + } + } + + # Retrieve all the registry keys with the given parameters. + $RegistryKeys = Get-ChildItem -Path "Registry::$RootKey\$SearchPath" -Depth $Depth -Recurse -ErrorAction SilentlyContinue -ErrorVariable RegistryErrors + + if ($RootKey -eq "HKEY_USERS") { + # Unload all hives that were loaded for this script. + ForEach ($UserHive in $ProfileWasLoaded) { + If ($ProfileWasLoaded -eq $false) { + [gc]::Collect() + Start-Sleep 1 + Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe UNLOAD HKU\$($UserHive)" -Wait -WindowStyle Hidden | Out-Null + } + } + } + + # Initialize generic lists. + $AllKeys = New-Object System.Collections.Generic.List[object] + $MatchingKeys = New-Object System.Collections.Generic.List[object] + $CustomFieldValue = New-Object System.Collections.Generic.List[string] + + # For each registry key, retrieve all properties and values if available. + $RegistryKeys | ForEach-Object { + $RegistryPath = $_.PSPATH -replace "Microsoft.PowerShell.Core\\Registry::" + try { + $ErrorActionPreference = "Stop" + $Properties = New-Object System.Collections.Generic.List[string] + $_.GetValueNames() | ForEach-Object { $Properties.Add($_) } + $Properties.Add("(default)") + } + catch { + $Properties = $Null + } + $ErrorActionPreference = "Continue" + + if (-not $Properties) { + $AllKeys.Add( + [PSCustomObject]@{ + Path = $RegistryPath + Property = "N/A" + Value = "N/A" + } + ) + return + } + + foreach ($PropertyName in $Properties) { + $ErrorActionPreference = "SilentlyContinue" + $RegValue = ($_ | Get-ItemProperty -ErrorVariable RegistryErrors).$PropertyName + $ErrorActionPreference = "Continue" + $AllKeys.Add( + [PSCustomObject]@{ + Path = $RegistryPath + Property = $PropertyName + Value = $RegValue + } + ) + } + } + + $MatchingValues = $False + $MatchingProperties = $False + $MatchingPaths = $False + + # Match the registry keys based on the key path, property, or value. Add the results to the MatchingKeys generic list. + if ($Value) { + $AllKeys | Where-Object { $_.Value -match [regex]::Escape($Search) } | ForEach-Object { + $MatchingValues = $True + $MatchingKeys.Add($_) + } + } + + if ($Property) { + $AllKeys | Where-Object { $_.Property -match [regex]::Escape($Search) } | ForEach-Object { + $MatchingProperties = $True + $MatchingKeys.Add($_) + } + } + + if ($Path) { + $AllKeys | Where-Object { $_.Path -match $([regex]::Escape($Search)) } | ForEach-Object { + $MatchingPaths = $True + $MatchingKeys.Add($_) + } + } + + if (-not $MatchingPaths -and -not $MatchingProperties -and -not $MatchingValues) { + $CustomFieldValue.Add("No matching registry keys found!") + Write-Host "No matching registry keys found!" + } + + # If we have any matches, output to Write-Warning. + if ($MatchingPaths) { + Write-Warning -Message "Matching registry path names found!" + $CustomFieldValue.Add("WARNING: Matching registry path names found!") + } + + if ($MatchingProperties) { + Write-Warning -Message "Matching registry properties found!" + $CustomFieldValue.Add("WARNING: Matching registry properties found!") + } + + if ($MatchingValues) { + Write-Warning -Message "Matching registry key values found!" + $CustomFieldValue.Add("WARNING: Matching registry key values found!") + } + + if ($MatchingKeys) { + $KeysToReport = $MatchingKeys | Format-List Path, Property, Value | Out-String + $CustomFieldValue.Add($KeysToReport) + } + + # For each error, output them at the bottom. Most of these errors are not going to be relevant. + $RegistryErrors | ForEach-Object { + $CustomFieldValue.Add("[Error] $($_.TargetObject)") + $CustomFieldValue.Add("[Error] $($_.Exception.Message)") + } + + # Save the output to a custom field if a field name is provided. + if ($CustomField) { + try { + Write-Host "Attempting to set Custom Field '$CustomField'." + # Set-NinjaProperty -Name $CustomField -Value (($CustomFieldValue | Out-String) -replace "`n") # Removed NinjaOne dependency + Write-Host "Successfully set Custom Field '$CustomField'!" + } + catch { + if ($_.Exception.Message) { + Write-Host "[Error] $($_.Exception.Message)" + } + + if ($_.Message) { + Write-Host "[Error] $($_.Message)" + } + $ExitCode = 1 + } + } + + # Activity Log output + if($MatchingKeys){ + $KeysToReport | Write-Host + } + + $RegistryErrors | ForEach-Object { + Write-Host "[Error] $($_.TargetObject)" + Write-Host "[Error] $($_.Exception.Message)" + } + + exit $ExitCode +} +end { + + + +} diff --git a/Powershell Scripts/Find Rogue DHCP Servers Using Nmap.ps1 b/Powershell Scripts/Find Rogue DHCP Servers Using Nmap.ps1 index 2076daf..8525e95 100644 --- a/Powershell Scripts/Find Rogue DHCP Servers Using Nmap.ps1 +++ b/Powershell Scripts/Find Rogue DHCP Servers Using Nmap.ps1 @@ -1,195 +1,197 @@ # Runs an nmap scan to find rogue dhcp servers on a network. This script will not install nmap and nmap is required for this script to work. -#Requires -Version 4.0 - -<# -.SYNOPSIS - Runs an nmap scan to find rogue dhcp servers on a network. This script will not install nmap and nmap is required for this script to work. -.DESCRIPTION - Runs an nmap scan to find rogue dhcp servers on a network. This script will not install nmap and nmap is required for this script to work. -.EXAMPLE - (No Parameters) - - DHCP Servers found. - - Mac Address IP Address - ----------- ---------- - 00:15:5D:FF:93:C3 172.17.240.1 - 172.17.242.16 - 00:15:5D:45:D5:07 172.17.251.231 - - - - Checking allowed servers list... - C:\ProgramData\NinjaRMMAgent\scripting\customscript_gen_14.ps1 : Rogue DHCP Server Found! 172.17.240.1 is not on the - list of allowed DHCP Servers. - + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException - + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,customscript_gen_14.ps1 - -PARAMETER: -AllowedServers "172.17.240.1" - Lists 172.17.240.1 as an allowed dhcp server. - -PARAMETER: -CustomField "ReplaceMeWithAnyMultilineCustomField" - Output results to a custom field of your choice. - -PARAMETER: -AllowedServersField "ReplaceMeWithAnyTextCustomField" - Will retrieve a list of allowed servers from a custom field. - -.OUTPUTS - None -.NOTES - Minimum Supported OS: Windows 8, Server 2012 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String[]]$AllowedServers, - [Parameter()] - [String]$CustomField = "rogueDHCPServers", - [Parameter()] - [String]$AllowedServersField = "allowedDHCPServers" -) - -begin { - - # If script variables are used set them here - if($env:allowedServersCustomField -and $env:allowedServersCustomField -notlike "null"){ - $AllowedServersField = $env:allowedServersCustomField - } - - if($AllowedServersField -and -not ($AllowedServers)){ - $AllowedServers = (Ninja-Property-Get $AllowedServersField) -split ',' | ForEach-Object { ($_).trim() } - } - - if($env:allowedServers -and $env:allowedServers -notlike "null"){ - $AllowedServers = $env:AllowedServers -split ',' | ForEach-Object { ($_).trim() } - } - - if($env:customFieldName -and $env:customFieldName -notlike "null"){ - $CustomField = $env:customFieldName - } - - # Parses out the subnet info into cidr format - function Get-Subnet { - $DefaultGateways = (Get-NetIPConfiguration).IPv4DefaultGateway - - $Subnets = $DefaultGateways | ForEach-Object { - $Index = $_.ifIndex - $PrefixLength = (Get-NetIPAddress | Where-Object { $_.AddressFamily -eq 'IPv4' -and $_.PrefixOrigin -ne 'WellKnown' -and $Index -eq $_.InterfaceIndex } | Select-Object -ExpandProperty PrefixLength) - if ($_.NextHop -and $PrefixLength) { - "$($_.NextHop)/$PrefixLength" - } - } - - if ($Subnets) { - $Subnets | Select-Object -Unique - } - } - - # Handy uninstall string finder - function Find-UninstallKey { - [CmdletBinding()] - param ( - [Parameter(ValueFromPipeline = $True)] - [String]$DisplayName, - [Parameter()] - [Switch]$UninstallString - ) - process { - $UninstallList = New-Object System.Collections.Generic.List[Object] - - $Result = Get-ChildItem HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } - if ($Result) { $UninstallList.Add($Result) } - - $Result = Get-ChildItem HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } - if ($Result) { $UninstallList.Add($Result) } - - # Programs don't always have an uninstall string listed here so to account for that I made this optional. - if ($UninstallString) { - $UninstallList | Select-Object -ExpandProperty UninstallString -ErrorAction SilentlyContinue - } - else { - $UninstallList - } - } - } - - $Nmap = (Find-UninstallKey -DisplayName "Nmap" -UninstallString) -replace '"' -replace 'uninstall.exe', 'nmap.exe' - if (-not $Nmap) { - Write-Error "Nmap is not installed! Please install nmap prior to running this script. https://nmap.org/download.html" - exit 1 - } -} -process { - - # Get's a list of subnets - $Subnets = Get-Subnet - if (-not $Subnets) { - Write-Error "Unable to get list of subnets?" - exit 1 - } - - # nmap arguments - $Arguments = @( - "-sU" - "-p" - "67" - "-d" - $Subnets - "--open" - "-oX" - "$env:TEMP\nmap-results.xml" - ) - try { - Start-Process -FilePath $Nmap -ArgumentList $Arguments -WindowStyle Hidden -Wait - [xml]$result = Get-Content -Path "$env:Temp\nmap-results.xml" - } - catch { - Write-Error "Nmap scan failed to run! Ensure nmap is installed prior to running this script." - exit 1 - } - - # Parse the xml results - if ($result) { - $resultObject = $result.DocumentElement.host | ForEach-Object { - New-Object psobject -Property @{ - "IP Address" = ($_.address | Where-Object { $_.addrtype -match "ip" } | Select-Object -ExpandProperty "addr") - "Mac Address" = ($_.address | Where-Object { $_.addrtype -match "mac" } | Select-Object -ExpandProperty "addr") - } - } - } - else { - Write-Error "Nmap results are empty?" - exit 1 - } - - # Check if the dhcp servers found are on the list. If so simply report back what were found otherwise indicate that they're Rogue DHCP Servers. - if ($resultObject) { - Write-Host "DHCP Servers found." - $resultObject | Sort-Object -Property "IP Address" -Unique | Format-Table | Out-String | Write-Host - Remove-Item -Path "$env:Temp\nmap-results.xml" -Force - - Write-Host "Checking allowed servers list..." - $ErrorOut = $False - $resultObject | ForEach-Object { - if ($AllowedServers -notcontains $_."IP Address") { - Write-Error "Rogue DHCP Server Found! $($_.'IP Address') is not on the list of allowed DHCP Servers." - $ErrorOut = $True - } - } - - Ninja-Property-Set -Name $CustomField -Value ($resultObject | Where-Object { $AllowedServers -notcontains $_."IP Address" } | Format-List | Out-String) - - if($ErrorOut -eq $True){ - exit 1 - } - - Write-Host "No rogue dhcp servers found." - } -} -end { - - - -} +#Requires -Version 4.0 + +<# +.SYNOPSIS + Runs an nmap scan to find rogue dhcp servers on a network. This script will not install nmap and nmap is required for this script to work. +.DESCRIPTION + Runs an nmap scan to find rogue dhcp servers on a network. This script will not install nmap and nmap is required for this script to work. +.EXAMPLE + (No Parameters) + + DHCP Servers found. + + Mac Address IP Address + ----------- ---------- + 00:15:5D:FF:93:C3 172.17.240.1 + 172.17.242.16 + 00:15:5D:45:D5:07 172.17.251.231 + + + + Checking allowed servers list... + C:\ProgramData\NinjaRMMAgent\scripting\customscript_gen_14.ps1 : Rogue DHCP Server Found! 172.17.240.1 is not on the + list of allowed DHCP Servers. + + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException + + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,customscript_gen_14.ps1 + +PARAMETER: -AllowedServers "172.17.240.1" + Lists 172.17.240.1 as an allowed dhcp server. + +PARAMETER: -CustomField "ReplaceMeWithAnyMultilineCustomField" + Output results to a custom field of your choice. + +PARAMETER: -AllowedServersField "ReplaceMeWithAnyTextCustomField" + Will retrieve a list of allowed servers from a custom field. + +.OUTPUTS + None +.NOTES + Minimum Supported OS: Windows 8, Server 2012 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String[]]$AllowedServers, + [Parameter()] + [String]$CustomField = "rogueDHCPServers", + [Parameter()] + [String]$AllowedServersField = "allowedDHCPServers" +) + +begin { + + # If script variables are used set them here + if($env:allowedServersCustomField -and $env:allowedServersCustomField -notlike "null"){ + $AllowedServersField = $env:allowedServersCustomField + } + + if($AllowedServersField -and -not ($AllowedServers)){ + # NinjaOne integration removed - cannot retrieve from custom field + Write-Warning "AllowedServersField specified but NinjaOne integration has been removed. Please use -AllowedServers parameter instead." + # $AllowedServers = (Ninja-Property-Get $AllowedServersField) -split ',' | ForEach-Object { ($_).trim() } + } + + if($env:allowedServers -and $env:allowedServers -notlike "null"){ + $AllowedServers = $env:AllowedServers -split ',' | ForEach-Object { ($_).trim() } + } + + if($env:customFieldName -and $env:customFieldName -notlike "null"){ + $CustomField = $env:customFieldName + } + + # Parses out the subnet info into cidr format + function Get-Subnet { + $DefaultGateways = (Get-NetIPConfiguration).IPv4DefaultGateway + + $Subnets = $DefaultGateways | ForEach-Object { + $Index = $_.ifIndex + $PrefixLength = (Get-NetIPAddress | Where-Object { $_.AddressFamily -eq 'IPv4' -and $_.PrefixOrigin -ne 'WellKnown' -and $Index -eq $_.InterfaceIndex } | Select-Object -ExpandProperty PrefixLength) + if ($_.NextHop -and $PrefixLength) { + "$($_.NextHop)/$PrefixLength" + } + } + + if ($Subnets) { + $Subnets | Select-Object -Unique + } + } + + # Handy uninstall string finder + function Find-UninstallKey { + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $True)] + [String]$DisplayName, + [Parameter()] + [Switch]$UninstallString + ) + process { + $UninstallList = New-Object System.Collections.Generic.List[Object] + + $Result = Get-ChildItem HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } + if ($Result) { $UninstallList.Add($Result) } + + $Result = Get-ChildItem HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } + if ($Result) { $UninstallList.Add($Result) } + + # Programs don't always have an uninstall string listed here so to account for that I made this optional. + if ($UninstallString) { + $UninstallList | Select-Object -ExpandProperty UninstallString -ErrorAction SilentlyContinue + } + else { + $UninstallList + } + } + } + + $Nmap = (Find-UninstallKey -DisplayName "Nmap" -UninstallString) -replace '"' -replace 'uninstall.exe', 'nmap.exe' + if (-not $Nmap) { + Write-Error "Nmap is not installed! Please install nmap prior to running this script. https://nmap.org/download.html" + exit 1 + } +} +process { + + # Get's a list of subnets + $Subnets = Get-Subnet + if (-not $Subnets) { + Write-Error "Unable to get list of subnets?" + exit 1 + } + + # nmap arguments + $Arguments = @( + "-sU" + "-p" + "67" + "-d" + $Subnets + "--open" + "-oX" + "$env:TEMP\nmap-results.xml" + ) + try { + Start-Process -FilePath $Nmap -ArgumentList $Arguments -WindowStyle Hidden -Wait + [xml]$result = Get-Content -Path "$env:Temp\nmap-results.xml" + } + catch { + Write-Error "Nmap scan failed to run! Ensure nmap is installed prior to running this script." + exit 1 + } + + # Parse the xml results + if ($result) { + $resultObject = $result.DocumentElement.host | ForEach-Object { + New-Object psobject -Property @{ + "IP Address" = ($_.address | Where-Object { $_.addrtype -match "ip" } | Select-Object -ExpandProperty "addr") + "Mac Address" = ($_.address | Where-Object { $_.addrtype -match "mac" } | Select-Object -ExpandProperty "addr") + } + } + } + else { + Write-Error "Nmap results are empty?" + exit 1 + } + + # Check if the dhcp servers found are on the list. If so simply report back what were found otherwise indicate that they're Rogue DHCP Servers. + if ($resultObject) { + Write-Host "DHCP Servers found." + $resultObject | Sort-Object -Property "IP Address" -Unique | Format-Table | Out-String | Write-Host + Remove-Item -Path "$env:Temp\nmap-results.xml" -Force + + Write-Host "Checking allowed servers list..." + $ErrorOut = $False + $resultObject | ForEach-Object { + if ($AllowedServers -notcontains $_."IP Address") { + Write-Error "Rogue DHCP Server Found! $($_.'IP Address') is not on the list of allowed DHCP Servers." + $ErrorOut = $True + } + } + + # Ninja-Property-Set -Name $CustomField -Value ($resultObject | Where-Object { $AllowedServers -notcontains $_."IP Address" } | Format-List | Out-String) # Removed NinjaOne dependency + + if($ErrorOut -eq $True){ + exit 1 + } + + Write-Host "No rogue dhcp servers found." + } +} +end { + + + +} diff --git a/Powershell Scripts/Firewall - Audit Status.ps1 b/Powershell Scripts/Firewall - Audit Status.ps1 index 1c5c75a..81403d3 100644 --- a/Powershell Scripts/Firewall - Audit Status.ps1 +++ b/Powershell Scripts/Firewall - Audit Status.ps1 @@ -1,266 +1,266 @@ # Get the current status of the specified Windows firewall profile. -#Requires -Version 4 - -<# -.SYNOPSIS - Get the current status of the specified Windows firewall profile. -.DESCRIPTION - Get the current status of the specified Windows firewall profile. -.EXAMPLE - -Domain -Private -Public - - Retrieving current firewall status. - Checking for disabled firewall profiles or those that allow all inbound connections. - - [Alert] The 'Private' firewall profile is disabled! - ### Firewall Status ### - Name Enabled DefaultInboundAction - ---- ------- -------------------- - Domain True Block - Private False Block - Public True Block - -PARAMETER: -Domain - Check the Domain Firewall Profile. - -PARAMETER: -Private - Check the Private Firewall Profile. - -PARAMETER: -Public - Check the Public Firewall Profile. - -PARAMETER: -CustomField "ReplaceMeWithNameOfTextCustomField" - Optionally specify the name of a text custom field to store the results in. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2012 R2 - Release Notes: Code cleanup and reorganization; removed exit codes for non-errors and added a custom field option. -#> - -[CmdletBinding()] -param ( - [Parameter()] - [Switch]$Domain = [System.Convert]::ToBoolean($env:domainProfile), - [Parameter()] - [Switch]$Private = [System.Convert]::ToBoolean($env:privateProfile), - [Parameter()] - [Switch]$Public = [System.Convert]::ToBoolean($env:publicProfile), - [Parameter()] - [String]$CustomField -) -begin { - # If script form variables are used, replace the command-line parameters with their values. - if ($env:firewallStatusCustomFieldName -and $env:firewallStatusCustomFieldName -notlike "null") { $CustomField = $env:firewallStatusCustomFieldName } - - # If no firewall profile is given, display an error message and exit the script. - if (!$Domain -and !$Private -and !$Public) { - Write-Host -Object "[Error] You must select the firewall profile you would like to audit." - exit 1 - } - - function Test-IsElevated { - # Get the current Windows identity of the user running the script - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - - # Create a WindowsPrincipal object based on the current identity - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - - # Check if the current user is in the Administrator role - # The function returns $True if the user has administrative privileges, $False otherwise - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - # If setting a custom field is requested, check whether the script is elevated. - if ($CustomField -and !(Test-IsElevated)) { - Write-Host -Object "[Error] Setting a custom field requires the script to be run with Administrator privileges." - exit 1 - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - # Measure the number of characters in the provided value - $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # Throw an error if the value exceeds the character limit of 200,000 characters - if ($Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - # Initialize a hashtable for additional documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define a list of valid field types - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - - # Warn the user if the provided type is not valid - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # Define types that require options to be retrieved - $NeedsOptions = "Dropdown" - - # If the property is being set in a document or field and the type needs options, retrieve them - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an error if there was an issue retrieving the property options - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Process the property value based on its type - switch ($Type) { - "Checkbox" { - # Convert the value to a boolean for Checkbox type - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Convert the value to a Unix timestamp for Date or Date Time type - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Convert the dropdown value to its corresponding GUID - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - # Throw an error if the value is not present in the dropdown options - if (!($Selection)) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # For other types, use the value as is - $NinjaValue = $Value - } - } - - # Set the property value in the document if a document name is provided - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - # Otherwise, set the standard property value - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - - # Throw an error if setting the property failed - if ($CustomField.Exception) { - throw $CustomField - } - } - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # Create a new list to store firewall profiles to audit - $ProfilesToAudit = New-Object -TypeName System.Collections.Generic.List[string] - - # Add the corrosponding profile to the list if $Domain, $Private or $Public is set - if ($Domain) { $ProfilesToAudit.Add("Domain") } - if ($Private) { $ProfilesToAudit.Add("Private") } - if ($Public) { $ProfilesToAudit.Add("Public") } - - try { - # Inform the user that the script is retrieving the current firewall status - Write-Host -Object "Retrieving current firewall status." - - # Retrieve firewall profiles from the ActiveStore and select specific properties: Name, Enabled, and DefaultInboundAction - $NetProfile = Get-NetFirewallProfile -All -PolicyStore ActiveStore -ErrorAction Stop | Select-Object "Name", "Enabled", "DefaultInboundAction" | Where-Object { $ProfilesToAudit -contains $_.Name } - } - catch { - # Display an error message if the firewall status retrieval fails and exit the script - Write-Host -Object "[Error] Failed to retrieve the current firewall status!" - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Inform the user that the script is checking for disabled profiles or profiles allowing all inbound connections - Write-Host -Object "Checking for disabled firewall profiles or those that allow all inbound connections.`n" - - # Loop through each profile in $NetProfile where the profile name is in $ProfilesToAudit - $NetProfile | ForEach-Object { - # Check if the profile is disabled and alert the user if so - if (!([System.Convert]::ToBoolean($_.Enabled))) { - Write-Host -Object "[Alert] The '$($_.Name)' firewall profile is disabled!" - } - - # Check if the profile allows all inbound connections and alert the user if so - if ($_.DefaultInboundAction -like "Allow") { - Write-Host -Object "[Alert] The '$($_.Name)' firewall profile is set to allow all inbound connections!" - } - } - - # Display the status of the firewall profiles in a formatted table - Write-Host -Object "### Firewall Status ###" - ($NetProfile | Format-Table -AutoSize | Out-String).Trim() | Write-Host - Write-Host -Object "" - - # If $CustomField is set, update the status for the custom field - if ($CustomField) { - - # Loop through profiles to update the custom field status - $NetProfile | ForEach-Object { - # Set the status to "Off" if the profile is disabled or allows all inbound connections, otherwise set it to "On" - if (!$_.Enabled -or $_.DefaultInboundAction -like "Allow") { - $Status = "Off" - } - else { - $Status = "On" - } - - # Update the $CustomFieldValue with the profile name and status - if ($CustomFieldValue) { - $CustomFieldValue = "$CustomFieldValue | $($_.Name): $Status" - } - else { - $CustomFieldValue = "$($_.Name): $Status" - } - } - - # Try to set the custom field value using the Set-NinjaProperty command - try { - Write-Host "Attempting to set Custom Field '$CustomField'." - Set-NinjaProperty -Name $CustomField -Value $CustomFieldValue - Write-Host "Successfully set Custom Field '$CustomField'!" - } - catch { - # If setting the custom field fails, display an error message and exit the script - Write-Host "[Error] $($_.Exception.Message)" - exit 1 - } - } - - # Exit the script with the specified exit code - exit $ExitCode -} -end { - - - -} - +#Requires -Version 4 + +<# +.SYNOPSIS + Get the current status of the specified Windows firewall profile. +.DESCRIPTION + Get the current status of the specified Windows firewall profile. +.EXAMPLE + -Domain -Private -Public + + Retrieving current firewall status. + Checking for disabled firewall profiles or those that allow all inbound connections. + + [Alert] The 'Private' firewall profile is disabled! + ### Firewall Status ### + Name Enabled DefaultInboundAction + ---- ------- -------------------- + Domain True Block + Private False Block + Public True Block + +PARAMETER: -Domain + Check the Domain Firewall Profile. + +PARAMETER: -Private + Check the Private Firewall Profile. + +PARAMETER: -Public + Check the Public Firewall Profile. + +PARAMETER: -CustomField "ReplaceMeWithNameOfTextCustomField" + Optionally specify the name of a text custom field to store the results in. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2012 R2 + Release Notes: Code cleanup and reorganization; removed exit codes for non-errors and added a custom field option. +#> + +[CmdletBinding()] +param ( + [Parameter()] + [Switch]$Domain = [System.Convert]::ToBoolean($env:domainProfile), + [Parameter()] + [Switch]$Private = [System.Convert]::ToBoolean($env:privateProfile), + [Parameter()] + [Switch]$Public = [System.Convert]::ToBoolean($env:publicProfile), + [Parameter()] + [String]$CustomField +) +begin { + # If script form variables are used, replace the command-line parameters with their values. + if ($env:firewallStatusCustomFieldName -and $env:firewallStatusCustomFieldName -notlike "null") { $CustomField = $env:firewallStatusCustomFieldName } + + # If no firewall profile is given, display an error message and exit the script. + if (!$Domain -and !$Private -and !$Public) { + Write-Host -Object "[Error] You must select the firewall profile you would like to audit." + exit 1 + } + + function Test-IsElevated { + # Get the current Windows identity of the user running the script + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + + # Create a WindowsPrincipal object based on the current identity + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + + # Check if the current user is in the Administrator role + # The function returns $True if the user has administrative privileges, $False otherwise + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + # If setting a custom field is requested, check whether the script is elevated. + if ($CustomField -and !(Test-IsElevated)) { + Write-Host -Object "[Error] Setting a custom field requires the script to be run with Administrator privileges." + exit 1 + } + + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # # Measure the number of characters in the provided value # Removed NinjaOne dependency + # $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + + # # Throw an error if the value exceeds the character limit of 200,000 characters # Removed NinjaOne dependency + # if ($Characters -ge 200000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Initialize a hashtable for additional documentation parameters # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + + # # If a document name is provided, add it to the documentation parameters # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # Define a list of valid field types # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + + # # Warn the user if the provided type is not valid # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # Define types that require options to be retrieved # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + + # # If the property is being set in a document or field and the type needs options, retrieve them # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if there was an issue retrieving the property options # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # Process the property value based on its type # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # Convert the value to a boolean for Checkbox type # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Convert the value to a Unix timestamp for Date or Date Time type # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Convert the dropdown value to its corresponding GUID # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # # Throw an error if the value is not present in the dropdown options # Removed NinjaOne dependency + # if (!($Selection)) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # For other types, use the value as is # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Set the property value in the document if a document name is provided # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # # Otherwise, set the standard property value # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if setting the property failed # Removed NinjaOne dependency + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # Create a new list to store firewall profiles to audit + $ProfilesToAudit = New-Object -TypeName System.Collections.Generic.List[string] + + # Add the corrosponding profile to the list if $Domain, $Private or $Public is set + if ($Domain) { $ProfilesToAudit.Add("Domain") } + if ($Private) { $ProfilesToAudit.Add("Private") } + if ($Public) { $ProfilesToAudit.Add("Public") } + + try { + # Inform the user that the script is retrieving the current firewall status + Write-Host -Object "Retrieving current firewall status." + + # Retrieve firewall profiles from the ActiveStore and select specific properties: Name, Enabled, and DefaultInboundAction + $NetProfile = Get-NetFirewallProfile -All -PolicyStore ActiveStore -ErrorAction Stop | Select-Object "Name", "Enabled", "DefaultInboundAction" | Where-Object { $ProfilesToAudit -contains $_.Name } + } + catch { + # Display an error message if the firewall status retrieval fails and exit the script + Write-Host -Object "[Error] Failed to retrieve the current firewall status!" + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Inform the user that the script is checking for disabled profiles or profiles allowing all inbound connections + Write-Host -Object "Checking for disabled firewall profiles or those that allow all inbound connections.`n" + + # Loop through each profile in $NetProfile where the profile name is in $ProfilesToAudit + $NetProfile | ForEach-Object { + # Check if the profile is disabled and alert the user if so + if (!([System.Convert]::ToBoolean($_.Enabled))) { + Write-Host -Object "[Alert] The '$($_.Name)' firewall profile is disabled!" + } + + # Check if the profile allows all inbound connections and alert the user if so + if ($_.DefaultInboundAction -like "Allow") { + Write-Host -Object "[Alert] The '$($_.Name)' firewall profile is set to allow all inbound connections!" + } + } + + # Display the status of the firewall profiles in a formatted table + Write-Host -Object "### Firewall Status ###" + ($NetProfile | Format-Table -AutoSize | Out-String).Trim() | Write-Host + Write-Host -Object "" + + # If $CustomField is set, update the status for the custom field + if ($CustomField) { + + # Loop through profiles to update the custom field status + $NetProfile | ForEach-Object { + # Set the status to "Off" if the profile is disabled or allows all inbound connections, otherwise set it to "On" + if (!$_.Enabled -or $_.DefaultInboundAction -like "Allow") { + $Status = "Off" + } + else { + $Status = "On" + } + + # Update the $CustomFieldValue with the profile name and status + if ($CustomFieldValue) { + $CustomFieldValue = "$CustomFieldValue | $($_.Name): $Status" + } + else { + $CustomFieldValue = "$($_.Name): $Status" + } + } + + # # Try to set the custom field value using the Set-NinjaProperty command # Removed NinjaOne dependency + try { + Write-Host "Attempting to set Custom Field '$CustomField'." + # Set-NinjaProperty -Name $CustomField -Value $CustomFieldValue # Removed NinjaOne dependency + Write-Host "Successfully set Custom Field '$CustomField'!" + } + catch { + # If setting the custom field fails, display an error message and exit the script + Write-Host "[Error] $($_.Exception.Message)" + exit 1 + } + } + + # Exit the script with the specified exit code + exit $ExitCode +} +end { + + + +} + diff --git a/Powershell Scripts/Get Device Description.ps1 b/Powershell Scripts/Get Device Description.ps1 index f4bffb6..56ac7b9 100644 --- a/Powershell Scripts/Get Device Description.ps1 +++ b/Powershell Scripts/Get Device Description.ps1 @@ -1,194 +1,194 @@ # Retrieves the current device description and optionally saves it to a custom field. - -<# -.SYNOPSIS - Retrieves the current device description and optionally saves it to a custom field. -.DESCRIPTION - Retrieves the current device description and optionally saves it to a custom field. -.EXAMPLE - -CustomField "text" - - Current device description: 'Kitchen Computer' - Attempting to set custom field 'text'. - Successfully set custom field 'text'! - -PARAMETER: -CustomField "ExampleInput" - Optionally specify the name of a custom field you would like to save the results to. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2012 R2 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$CustomField -) - -begin { - if ($env:nameOfCustomField -and $env:nameOfCustomField -notlike "null") { $CustomField = $env:nameOfCustomField } - - # PowerShell 3 or higher is required for custom field functionality - if ($PSVersionTable.PSVersion.Major -lt 3 -and $CustomField) { - Write-Host -Object "[Error] Setting custom fields requires powershell version 3 or higher." - Write-Host -Object "https://ninjarmm.zendesk.com/hc/en-us/articles/4405408656013-Custom-Fields-and-Documentation-CLI-and-Scripting" - exit 1 - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - # If requested to set the field value for a Ninja document, specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is specified, assume that the input does not need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to set. - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # Redirect error output to the success stream to handle errors more easily if nothing is found or something else goes wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received with an exception property, exit the function with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The types below require values not typically given to be set. The code below will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # Although it's highly likely we were given a value like "True" or a boolean data type, it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, match the given value with a GUID. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli expects the GUID of the option we're trying to select, so match the value we were given with a GUID. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # Set the field differently depending on whether it's a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # Check if the script is being run with elevated (administrator) privileges - if (!(Test-IsElevated)) { - Write-Host -Object "[Error] Access denied. Please run with administrator privileges." - exit 1 - } - - try { - # Determine the PowerShell version and get the operating system description accordingly - if ($PSVersionTable.PSVersion.Major -lt 5) { - # Use Get-WmiObject for PowerShell versions less than 5 - $Description = $(Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop).Description - } - else { - # Use Get-CimInstance for PowerShell version 5 or greater - $Description = $(Get-CimInstance -Class Win32_OperatingSystem -ErrorAction Stop).Description - } - - # Trim any leading or trailing whitespace from the description - if ($Description) { - $Description = $Description.Trim() - } - } - catch { - # Handle any errors that occur while retrieving the device description - Write-Host -Object "[Error] Failed to retrieve current device description." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Check if the description is empty or not - if (!$Description) { - Write-Host -Object "[Alert] No device description is currently set." - $CustomFieldValue = "No device description is currently set." - } - else { - Write-Host -Object "Current device description: '$Description'" - $CustomFieldValue = $Description - } - - # If a custom field is specified, attempt to set its value - if ($CustomField) { - try { - Write-Host "Attempting to set custom field '$CustomField'." - Set-NinjaProperty -Name $CustomField -Value $CustomFieldValue - Write-Host "Successfully set custom field '$CustomField'!" - } - catch { - Write-Host -Object "[Error] Failed to set custom field '$CustomField'" - Write-Host "[Error] $($_.Exception.Message)" - exit 1 - } - } - - exit $ExitCode -} -end { - - - -} + +<# +.SYNOPSIS + Retrieves the current device description and optionally saves it to a custom field. +.DESCRIPTION + Retrieves the current device description and optionally saves it to a custom field. +.EXAMPLE + -CustomField "text" + + Current device description: 'Kitchen Computer' + Attempting to set custom field 'text'. + Successfully set custom field 'text'! + +PARAMETER: -CustomField "ExampleInput" + Optionally specify the name of a custom field you would like to save the results to. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2012 R2 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$CustomField +) + +begin { + if ($env:nameOfCustomField -and $env:nameOfCustomField -notlike "null") { $CustomField = $env:nameOfCustomField } + + # PowerShell 3 or higher is required for custom field functionality + if ($PSVersionTable.PSVersion.Major -lt 3 -and $CustomField) { + Write-Host -Object "[Error] Setting custom fields requires powershell version 3 or higher." + Write-Host -Object "https://ninjarmm.zendesk.com/hc/en-us/articles/4405408656013-Custom-Fields-and-Documentation-CLI-and-Scripting" + exit 1 + } + + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + # if ($Characters -ge 200000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If requested to set the field value for a Ninja document, specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # This is a list of valid fields that can be set. If no type is specified, assume that the input does not need to be changed. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # The field below requires additional information to set. # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # Redirect error output to the success stream to handle errors more easily if nothing is found or something else goes wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If an error is received with an exception property, exit the function with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # The types below require values not typically given to be set. The code below will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # Although it's highly likely we were given a value like "True" or a boolean data type, it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, match the given value with a GUID. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the GUID of the option we're trying to select, so match the value we were given with a GUID. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # if (-not $Selection) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Set the field differently depending on whether it's a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # Check if the script is being run with elevated (administrator) privileges + if (!(Test-IsElevated)) { + Write-Host -Object "[Error] Access denied. Please run with administrator privileges." + exit 1 + } + + try { + # Determine the PowerShell version and get the operating system description accordingly + if ($PSVersionTable.PSVersion.Major -lt 5) { + # Use Get-WmiObject for PowerShell versions less than 5 + $Description = $(Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop).Description + } + else { + # Use Get-CimInstance for PowerShell version 5 or greater + $Description = $(Get-CimInstance -Class Win32_OperatingSystem -ErrorAction Stop).Description + } + + # Trim any leading or trailing whitespace from the description + if ($Description) { + $Description = $Description.Trim() + } + } + catch { + # Handle any errors that occur while retrieving the device description + Write-Host -Object "[Error] Failed to retrieve current device description." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Check if the description is empty or not + if (!$Description) { + Write-Host -Object "[Alert] No device description is currently set." + $CustomFieldValue = "No device description is currently set." + } + else { + Write-Host -Object "Current device description: '$Description'" + $CustomFieldValue = $Description + } + + # If a custom field is specified, attempt to set its value + if ($CustomField) { + try { + Write-Host "Attempting to set custom field '$CustomField'." + # Set-NinjaProperty -Name $CustomField -Value $CustomFieldValue # Removed NinjaOne dependency + Write-Host "Successfully set custom field '$CustomField'!" + } + catch { + Write-Host -Object "[Error] Failed to set custom field '$CustomField'" + Write-Host "[Error] $($_.Exception.Message)" + exit 1 + } + } + + exit $ExitCode +} +end { + + + +} diff --git a/Powershell Scripts/Get Direct Link To ConnectWise ScreenConnect.ps1 b/Powershell Scripts/Get Direct Link To ConnectWise ScreenConnect.ps1 index 7dda0ce..37d81ad 100644 --- a/Powershell Scripts/Get Direct Link To ConnectWise ScreenConnect.ps1 +++ b/Powershell Scripts/Get Direct Link To ConnectWise ScreenConnect.ps1 @@ -1,157 +1,157 @@ # Get ConnectWise ScreenConnect Launch URL and save to custom field (defaults to screenconnectUrl). Requires the domain used for ScreenConnect and a Session Group the machine is a part of to successfully build URL. - -<# -.SYNOPSIS - Get ConnectWise ScreenConnect Launch URL and save to custom field (defaults to screenconnectUrl). Requires the domain used for ScreenConnect and a Session Group the machine is a part of to successfully build URL. -.DESCRIPTION - Get ConnectWise ScreenConnect Launch URL and save to custom field (defaults to screenconnectUrl). - Requires the domain used for ScreenConnect and a Session Group the machine is a part of to successfully build URL. -.EXAMPLE - -ScreenConnectDomain "replace.me" -InstanceID "1111111111" - - Building Launch URL(s)... - Launch URL(s) Created - - - Instance : 1111111111 - LaunchURL : https://replace.me/Host#Access/All%20Machines//555555-555-555-5555-55555/Join - SessionId : 555555-555-555-5555-55555 - -PARAMETER: -ScreenConnectDomain "ExampleInput" - The domain used for your Connectwise ScreenConnect Instance. - -PARAMETER: -SessionGroup "ExampleInput" - The Session Group in which the machine would normally be found. Defaults to "All Machines". - -PARAMETER: -InstanceID "ExampleInput" - The Instance ID for your instance of ScreenConnect. Used to differentiate between multiple installed ScreenConnect Instances. - To get the instance id you can see it in the program's name in Control Panel e.g. ScreenConnect Client (yourinstanceidishere) - or in ScreenConnect itself (Admin > Advanced > Server Information > Instance Identifier Fingerprint). - -PARAMETER: -CustomField "ReplaceWithAnyMultilineCustomField" - The custom field you would like to write the results to. Defaults to screenconnectUrl - -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 7+, Server 2008+ - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$ScreenConnectDomain, - [Parameter()] - [String]$SessionGroup = "All Machines", - [Parameter()] - [String]$InstanceID, - [Parameter()] - [String]$CustomField = "screenconnectUrl" -) - -begin { - if ($env:screenconnectDomain -and $env:screenconnectDomain -notlike "null") { $ScreenConnectDomain = $env:screenconnectDomain } - if ($env:sessionGroup -and $env:sessionGroup -notlike "null") { $SessionGroup = $env:sessionGroup } - if ($env:instanceId -and $env:instanceId -notlike "null") { $InstanceID = $env:instanceId } - if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } - - # Warn end-user if we're not provided an instance id - if (-not ($InstanceID)) { - Write-Warning "Without the instance id we will be unable to tell which ScreenConnect instance is yours if multiple are installed resulting in the wrong URL being displayed." - Write-Warning "To get the instance id you can see it in the programs name in Control Panel ex. ScreenConnect Client (yourinstanceidishere) or in Control itself (Admin > Advanced > Server Information > Instance Identifier Fingerprint)" - } - - # These two are actually necessary to build the URL - if (-not ($ScreenConnectDomain) -or -not ($SessionGroup)) { - Write-Error "Unable to build URL without the domain or Session Group." - exit 1 - } - - # Test for elevation - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - # Checks the two Uninstall registry keys to see if the app is installed. Needs the name as it would appear in Control Panel. - function Find-UninstallKey { - [CmdletBinding()] - param ( - [Parameter(ValueFromPipeline = $True)] - [String]$DisplayName, - [Parameter()] - [Switch]$UninstallString - ) - process { - $UninstallList = New-Object System.Collections.Generic.List[Object] - - $Result = Get-ChildItem HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } - if ($Result) { $UninstallList.Add($Result) } - - $Result = Get-ChildItem HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } - if ($Result) { $UninstallList.Add($Result) } - - # Programs don't always have an uninstall string listed here so to account for that I made this optional. - if ($UninstallString) { - $UninstallList | Select-Object -ExpandProperty UninstallString -ErrorAction SilentlyContinue - } - else { - $UninstallList - } - } - } - - # Define the name of the software we are searching for and look for it in both the 64 bit and 32 bit registry nodes. - if (-not $InstanceID) { $SoftwareName = "ScreenConnect Client" }else { $SoftwareName = "ScreenConnect Client ($InstanceID)" } - $ControlInstallation = Find-UninstallKey -DisplayName $SoftwareName - - # If its not installed lets error out. - if (-not ($ControlInstallation)) { - Write-Error "Connectwise ScreenConnect is not installed!" - exit 1 - } - - # Elevation is required to write to custom fields. - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } -} -process { - # The Image Path Registry Key contains the unique session id needed to generate the URL - Write-Host "Building Launch URL(s)..." - $ControlInstances = $ControlInstallation.DisplayName | ForEach-Object { - $ImagePath = Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\$_" | Select-Object -Property ImagePath -ExpandProperty ImagePath - $Id = ($ImagePath -split '&' | Where-Object { $_ -match 's=(.*-){4}' }) -replace "s=" - $Instance = ($_ -replace "ScreenConnect Client \(" -replace "\)").trim() - New-Object psobject -Property @{ - Instance = $Instance - LaunchURL = [URI]::EscapeUriString("https://$ScreenConnectDomain/Host#Access/$SessionGroup//$Id/Join") - SessionId = $Id - } - } - - # Create a Table/List of our results - Write-Host "Launch URL(s) Created" - $ControlInstances | Format-List -Property Instance, LaunchURL, SessionId | Out-String | Write-Host - - # PowerShell 2.0 does not support ninjarmm-cli - if ($PSVersionTable.PSVersion.Major -gt 2) { - if ($ControlInstances.LaunchURL.Count -gt 1) { - Ninja-Property-Set -Name $CustomField -Value ($ControlInstances | Format-List -Property Instance, LaunchURL | Out-String) - } - else { - Ninja-Property-Set -Name $CustomField -Value ($ControlInstances.LaunchURL | Out-String) - } - } - else { - Write-Host "ninjarmm-cli does not support PowerShell 1 & 2. Refer to https://ninjarmm.zendesk.com/hc/en-us/articles/4405408656013 ." - } -} -end { - - - -} + +<# +.SYNOPSIS + Get ConnectWise ScreenConnect Launch URL and save to custom field (defaults to screenconnectUrl). Requires the domain used for ScreenConnect and a Session Group the machine is a part of to successfully build URL. +.DESCRIPTION + Get ConnectWise ScreenConnect Launch URL and save to custom field (defaults to screenconnectUrl). + Requires the domain used for ScreenConnect and a Session Group the machine is a part of to successfully build URL. +.EXAMPLE + -ScreenConnectDomain "replace.me" -InstanceID "1111111111" + + Building Launch URL(s)... + Launch URL(s) Created + + + Instance : 1111111111 + LaunchURL : https://replace.me/Host#Access/All%20Machines//555555-555-555-5555-55555/Join + SessionId : 555555-555-555-5555-55555 + +PARAMETER: -ScreenConnectDomain "ExampleInput" + The domain used for your Connectwise ScreenConnect Instance. + +PARAMETER: -SessionGroup "ExampleInput" + The Session Group in which the machine would normally be found. Defaults to "All Machines". + +PARAMETER: -InstanceID "ExampleInput" + The Instance ID for your instance of ScreenConnect. Used to differentiate between multiple installed ScreenConnect Instances. + To get the instance id you can see it in the program's name in Control Panel e.g. ScreenConnect Client (yourinstanceidishere) + or in ScreenConnect itself (Admin > Advanced > Server Information > Instance Identifier Fingerprint). + +PARAMETER: -CustomField "ReplaceWithAnyMultilineCustomField" + The custom field you would like to write the results to. Defaults to screenconnectUrl + +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 7+, Server 2008+ + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$ScreenConnectDomain, + [Parameter()] + [String]$SessionGroup = "All Machines", + [Parameter()] + [String]$InstanceID, + [Parameter()] + [String]$CustomField = "screenconnectUrl" +) + +begin { + if ($env:screenconnectDomain -and $env:screenconnectDomain -notlike "null") { $ScreenConnectDomain = $env:screenconnectDomain } + if ($env:sessionGroup -and $env:sessionGroup -notlike "null") { $SessionGroup = $env:sessionGroup } + if ($env:instanceId -and $env:instanceId -notlike "null") { $InstanceID = $env:instanceId } + if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } + + # Warn end-user if we're not provided an instance id + if (-not ($InstanceID)) { + Write-Warning "Without the instance id we will be unable to tell which ScreenConnect instance is yours if multiple are installed resulting in the wrong URL being displayed." + Write-Warning "To get the instance id you can see it in the programs name in Control Panel ex. ScreenConnect Client (yourinstanceidishere) or in Control itself (Admin > Advanced > Server Information > Instance Identifier Fingerprint)" + } + + # These two are actually necessary to build the URL + if (-not ($ScreenConnectDomain) -or -not ($SessionGroup)) { + Write-Error "Unable to build URL without the domain or Session Group." + exit 1 + } + + # Test for elevation + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + # Checks the two Uninstall registry keys to see if the app is installed. Needs the name as it would appear in Control Panel. + function Find-UninstallKey { + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $True)] + [String]$DisplayName, + [Parameter()] + [Switch]$UninstallString + ) + process { + $UninstallList = New-Object System.Collections.Generic.List[Object] + + $Result = Get-ChildItem HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } + if ($Result) { $UninstallList.Add($Result) } + + $Result = Get-ChildItem HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } + if ($Result) { $UninstallList.Add($Result) } + + # Programs don't always have an uninstall string listed here so to account for that I made this optional. + if ($UninstallString) { + $UninstallList | Select-Object -ExpandProperty UninstallString -ErrorAction SilentlyContinue + } + else { + $UninstallList + } + } + } + + # Define the name of the software we are searching for and look for it in both the 64 bit and 32 bit registry nodes. + if (-not $InstanceID) { $SoftwareName = "ScreenConnect Client" }else { $SoftwareName = "ScreenConnect Client ($InstanceID)" } + $ControlInstallation = Find-UninstallKey -DisplayName $SoftwareName + + # If its not installed lets error out. + if (-not ($ControlInstallation)) { + Write-Error "Connectwise ScreenConnect is not installed!" + exit 1 + } + + # Elevation is required to write to custom fields. + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } +} +process { + # The Image Path Registry Key contains the unique session id needed to generate the URL + Write-Host "Building Launch URL(s)..." + $ControlInstances = $ControlInstallation.DisplayName | ForEach-Object { + $ImagePath = Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\$_" | Select-Object -Property ImagePath -ExpandProperty ImagePath + $Id = ($ImagePath -split '&' | Where-Object { $_ -match 's=(.*-){4}' }) -replace "s=" + $Instance = ($_ -replace "ScreenConnect Client \(" -replace "\)").trim() + New-Object psobject -Property @{ + Instance = $Instance + LaunchURL = [URI]::EscapeUriString("https://$ScreenConnectDomain/Host#Access/$SessionGroup//$Id/Join") + SessionId = $Id + } + } + + # Create a Table/List of our results + Write-Host "Launch URL(s) Created" + $ControlInstances | Format-List -Property Instance, LaunchURL, SessionId | Out-String | Write-Host + + # PowerShell 2.0 does not support ninjarmm-cli + if ($PSVersionTable.PSVersion.Major -gt 2) { + if ($ControlInstances.LaunchURL.Count -gt 1) { + # Ninja-Property-Set -Name $CustomField -Value ($ControlInstances | Format-List -Property Instance, LaunchURL | Out-String) # Removed NinjaOne dependency + } + else { + # Ninja-Property-Set -Name $CustomField -Value ($ControlInstances.LaunchURL | Out-String) # Removed NinjaOne dependency + } + } + else { + Write-Host "ninjarmm-cli does not support PowerShell 1 & 2. Refer to https://ninjarmm.zendesk.com/hc/en-us/articles/4405408656013 ." + } +} +end { + + + +} diff --git a/Powershell Scripts/Get OS Install Date.ps1 b/Powershell Scripts/Get OS Install Date.ps1 index 68ad201..0239067 100644 --- a/Powershell Scripts/Get OS Install Date.ps1 +++ b/Powershell Scripts/Get OS Install Date.ps1 @@ -1,102 +1,102 @@ # Fetches the install date. Outputs to the activity feed and can store it in a custom field. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Fetches the install date. Outputs to the activity feed and can store it in a custom field. -.DESCRIPTION - Fetches the install date. Outputs to the activity feed and can store it in a custom field. - -.EXAMPLE - (No Parameters) - ## EXAMPLE OUTPUT WITHOUT PARAMS ## - Install Date: 08/18/2021 13:50:15 - -PARAMETER: -CustomField "InstallDate" - A custom field to save the install date to. -.EXAMPLE - -CustomField "InstallDate" - ## EXAMPLE OUTPUT WITH CustomField ## - Install Date: 08/18/2021 13:50:15 -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Renamed script and added Script Variable support -#> - -[CmdletBinding()] -param ( - [string]$CustomField -) - -begin { - $Epoch = [DateTime]'1/1/1970' - if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } - Write-Host "" -} -process { - $InstallDate = $( - try { - # Get Install Date from registry - Get-ChildItem -Path "HKLM:\System\Setup\Source*" -ErrorAction SilentlyContinue | ForEach-Object { - $InstallDate = Get-ItemPropertyValue -Path Registry::$_ -Name "InstallDate" -ErrorAction SilentlyContinue - [System.TimeZone]::CurrentTimeZone.ToLocalTime(($Epoch).AddSeconds($InstallDate)) - } - $InstallDateCu = Get-ItemPropertyValue -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name "InstallDate" -ErrorAction SilentlyContinue - [System.TimeZone]::CurrentTimeZone.ToLocalTime(($Epoch).AddSeconds($InstallDateCu)) - } - catch { - # Skip if errors - } - - try { - # Get Install date from system info - $SystemInfo = systeminfo.exe - # --- Output of system info --- - # Original Install Date: 9/3/2020, 8:54:48 AM - $($SystemInfo | Select-String "install date") -split 'Date:\s+' | Select-Object -Last 1 | Get-Date - } - catch { - # Skip if errors - } - - try { - # Get Install date from WMI - $(Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue).InstallDate - } - catch { - # Skip if errors - } - - try { - if ($PSVersionTable.PSVersion.Major -ge 5 -and $PSVersionTable.PSVersion.Minor -ge 1) { - $ComputerInfo = Get-ComputerInfo -Property WindowsInstallDateFromRegistry, OsInstallDate -ErrorAction SilentlyContinue - $ComputerInfo.WindowsInstallDateFromRegistry - $ComputerInfo.OsInstallDate - } - } - catch { - # Skip if errors - } - ) | Sort-Object | Select-Object -First 1 - - if ($InstallDate) { - if ($CustomField) { - Ninja-Property-Set -Name $CustomField -Value $InstallDate - } - Write-Host "Install Date: $InstallDate" - } - else { - if ($CustomField) { - Ninja-Property-Set -Name $CustomField -Value "Unknown" - } - Write-Host "Install Date: Unknown" - } -} -end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + Fetches the install date. Outputs to the activity feed and can store it in a custom field. +.DESCRIPTION + Fetches the install date. Outputs to the activity feed and can store it in a custom field. + +.EXAMPLE + (No Parameters) + ## EXAMPLE OUTPUT WITHOUT PARAMS ## + Install Date: 08/18/2021 13:50:15 + +PARAMETER: -CustomField "InstallDate" + A custom field to save the install date to. +.EXAMPLE + -CustomField "InstallDate" + ## EXAMPLE OUTPUT WITH CustomField ## + Install Date: 08/18/2021 13:50:15 +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Renamed script and added Script Variable support +#> + +[CmdletBinding()] +param ( + [string]$CustomField +) + +begin { + $Epoch = [DateTime]'1/1/1970' + if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } + Write-Host "" +} +process { + $InstallDate = $( + try { + # Get Install Date from registry + Get-ChildItem -Path "HKLM:\System\Setup\Source*" -ErrorAction SilentlyContinue | ForEach-Object { + $InstallDate = Get-ItemPropertyValue -Path Registry::$_ -Name "InstallDate" -ErrorAction SilentlyContinue + [System.TimeZone]::CurrentTimeZone.ToLocalTime(($Epoch).AddSeconds($InstallDate)) + } + $InstallDateCu = Get-ItemPropertyValue -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name "InstallDate" -ErrorAction SilentlyContinue + [System.TimeZone]::CurrentTimeZone.ToLocalTime(($Epoch).AddSeconds($InstallDateCu)) + } + catch { + # Skip if errors + } + + try { + # Get Install date from system info + $SystemInfo = systeminfo.exe + # --- Output of system info --- + # Original Install Date: 9/3/2020, 8:54:48 AM + $($SystemInfo | Select-String "install date") -split 'Date:\s+' | Select-Object -Last 1 | Get-Date + } + catch { + # Skip if errors + } + + try { + # Get Install date from WMI + $(Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue).InstallDate + } + catch { + # Skip if errors + } + + try { + if ($PSVersionTable.PSVersion.Major -ge 5 -and $PSVersionTable.PSVersion.Minor -ge 1) { + $ComputerInfo = Get-ComputerInfo -Property WindowsInstallDateFromRegistry, OsInstallDate -ErrorAction SilentlyContinue + $ComputerInfo.WindowsInstallDateFromRegistry + $ComputerInfo.OsInstallDate + } + } + catch { + # Skip if errors + } + ) | Sort-Object | Select-Object -First 1 + + if ($InstallDate) { + if ($CustomField) { + # Ninja-Property-Set -Name $CustomField -Value $InstallDate # Removed NinjaOne dependency + } + Write-Host "Install Date: $InstallDate" + } + else { + if ($CustomField) { + # Ninja-Property-Set -Name $CustomField -Value "Unknown" # Removed NinjaOne dependency + } + Write-Host "Install Date: Unknown" + } +} +end { + + + +} + diff --git a/Powershell Scripts/Get Server Roles.ps1 b/Powershell Scripts/Get Server Roles.ps1 index 40c2668..a139eff 100644 --- a/Powershell Scripts/Get Server Roles.ps1 +++ b/Powershell Scripts/Get Server Roles.ps1 @@ -1,95 +1,95 @@ # Retrieves the installed server roles. -#Requires -Version 4.0 - -<# -.SYNOPSIS - Retrieves the installed server roles. -.DESCRIPTION - Retrieves the installed server roles. - - For Exchange and SQL, this just detects if the services are installed. - -.EXAMPLE - (No Parameters) - ## EXAMPLE OUTPUT WITHOUT PARAMS ## - DisplayName FeatureType Installed PostConfigurationNeeded - ----------- ----------- --------- ----------------------- - Active Directory Domain Services Role True False - DNS Server Role True False - File and Storage Services Role True False - -PARAMETER: -CustomField "Roles" - Saves the results to a multi-line custom field. -.EXAMPLE - -CustomField "Roles" - ## EXAMPLE OUTPUT WITH CustomField ## - DisplayName FeatureType Installed PostConfigurationNeeded - ----------- ----------- --------- ----------------------- - Active Directory Domain Services Role True False - DNS Server Role True False - File and Storage Services Role True False - -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Server 2012 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [string] - $CustomField -) - -begin { - if ($env:customfield -notlike "null" -and $env:customfield) { - $CustomField = $env:customfield - } - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } -} -process { - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - $SQLServices = Get-Service | Where-Object { $_.DisplayName -like "SQL Server*" } - $ExchangeServices = Get-Service -Name MSExchangeServiceHost -ErrorAction SilentlyContinue - $InstalledFeatures = Get-WindowsFeature | Where-Object { $_.Installed -and $_.FeatureType -like "Role" } | Select-Object -Property DisplayName, FeatureType, Installed, PostConfigurationNeeded - $InstalledFeatures = if ($SQLServices) { - $InstalledFeatures - [PSCustomObject]@{ - DisplayName = "SQL Server" - FeatureType = "Role" - Installed = $true - PostConfigurationNeeded = $null - } - } - else { $InstalledFeatures } - $InstalledFeatures = if ($ExchangeServices) { - $InstalledFeatures - [PSCustomObject]@{ - DisplayName = "Exchange Server" - FeatureType = "Role" - Installed = $true - PostConfigurationNeeded = $null - } - } - else { $InstalledFeatures } - - $InstalledFeatures | Format-Table -AutoSize | Out-String | Write-Host - - if ($CustomField) { - Ninja-Property-Set -Name $CustomField -Value $($InstalledFeatures.DisplayName | Out-String) - } -} -end { - - - -} +#Requires -Version 4.0 + +<# +.SYNOPSIS + Retrieves the installed server roles. +.DESCRIPTION + Retrieves the installed server roles. + + For Exchange and SQL, this just detects if the services are installed. + +.EXAMPLE + (No Parameters) + ## EXAMPLE OUTPUT WITHOUT PARAMS ## + DisplayName FeatureType Installed PostConfigurationNeeded + ----------- ----------- --------- ----------------------- + Active Directory Domain Services Role True False + DNS Server Role True False + File and Storage Services Role True False + +PARAMETER: -CustomField "Roles" + Saves the results to a multi-line custom field. +.EXAMPLE + -CustomField "Roles" + ## EXAMPLE OUTPUT WITH CustomField ## + DisplayName FeatureType Installed PostConfigurationNeeded + ----------- ----------- --------- ----------------------- + Active Directory Domain Services Role True False + DNS Server Role True False + File and Storage Services Role True False + +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Server 2012 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [string] + $CustomField +) + +begin { + if ($env:customfield -notlike "null" -and $env:customfield) { + $CustomField = $env:customfield + } + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } +} +process { + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + $SQLServices = Get-Service | Where-Object { $_.DisplayName -like "SQL Server*" } + $ExchangeServices = Get-Service -Name MSExchangeServiceHost -ErrorAction SilentlyContinue + $InstalledFeatures = Get-WindowsFeature | Where-Object { $_.Installed -and $_.FeatureType -like "Role" } | Select-Object -Property DisplayName, FeatureType, Installed, PostConfigurationNeeded + $InstalledFeatures = if ($SQLServices) { + $InstalledFeatures + [PSCustomObject]@{ + DisplayName = "SQL Server" + FeatureType = "Role" + Installed = $true + PostConfigurationNeeded = $null + } + } + else { $InstalledFeatures } + $InstalledFeatures = if ($ExchangeServices) { + $InstalledFeatures + [PSCustomObject]@{ + DisplayName = "Exchange Server" + FeatureType = "Role" + Installed = $true + PostConfigurationNeeded = $null + } + } + else { $InstalledFeatures } + + $InstalledFeatures | Format-Table -AutoSize | Out-String | Write-Host + + if ($CustomField) { + # Ninja-Property-Set -Name $CustomField -Value $($InstalledFeatures.DisplayName | Out-String) # Removed NinjaOne dependency + } +} +end { + + + +} diff --git a/Powershell Scripts/Get iSCSI Connection Details.ps1 b/Powershell Scripts/Get iSCSI Connection Details.ps1 index a512431..6aa2e38 100644 --- a/Powershell Scripts/Get iSCSI Connection Details.ps1 +++ b/Powershell Scripts/Get iSCSI Connection Details.ps1 @@ -1,264 +1,264 @@ # Retrieves detailed information about the iSCSI initiator connections and sessions on the local machine. - -<# - -.SYNOPSIS - Retrieves detailed information about the iSCSI initiator connections and sessions on the local machine. - -.DESCRIPTION - Gathers details about the iSCSI initiator connections and sessions on the local machine. - It retrieves information such as connection identifiers, initiator and target addresses, port numbers, session identifiers, and various other session attributes. - This can also save these details to a specified custom field in a WYSIWYG format if provided. - -.EXAMPLE - Get-IscsiDetails - - This command gets the iSCSI initiator details. - -PARAMETER: -WYSIWYGCustomFieldName "wysiwygCustomFieldName" - The name of the custom field to save the iSCSI initiator details. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial release -#> -[CmdletBinding()] -param( - [String] - $WYSIWYGCustomFieldName -) -begin { - - if ($env:wysiwygCustomFieldName -notlike "null") { - $WYSIWYGCustomFieldName = $env:wysiwygCustomFieldName - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName, - [Parameter()] - [Switch]$Piped - ) - # Remove the non-breaking space character - if ($Type -eq "WYSIWYG") { - $Value = $Value -replace ' ', ' ' - } - - # Measure the number of characters in the provided value - $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # Throw an error if the value exceeds the character limit of 200,000 characters - if ($Piped -and $Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - if (!$Piped -and $Characters -ge 45000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") - } - - # Initialize a hashtable for additional documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define a list of valid field types - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - - # Warn the user if the provided type is not valid - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # Define types that require options to be retrieved - $NeedsOptions = "Dropdown" - - # If the property is being set in a document or field and the type needs options, retrieve them - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an error if there was an issue retrieving the property options - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Process the property value based on its type - switch ($Type) { - "Checkbox" { - # Convert the value to a boolean for Checkbox type - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Convert the value to a Unix timestamp for Date or Date Time type - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Convert the dropdown value to its corresponding GUID - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - # Throw an error if the value is not present in the dropdown options - if (!($Selection)) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # For other types, use the value as is - $NinjaValue = $Value - } - } - - # Set the property value in the document if a document name is provided - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - try { - # Otherwise, set the standard property value - if ($Piped) { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - } - catch { - Write-Host -Object "[Error] Failed to set custom field." - throw $_.Exception.Message - } - } - - # Throw an error if setting the property failed - if ($CustomField.Exception) { - throw $CustomField - } - } - $ShouldOutputResults = $false - $HasHadError = $false -} - -process { - try { - $iscsiConnection = Get-IscsiConnection -ErrorAction Stop - $iscsiSession = Get-IscsiSession -ErrorAction Stop - } - catch [System.Management.Automation.CommandNotFoundException] { - Write-Host "[Error] The Get-IscsiConnection or Get-IscsiSession cmdlet is not available on this system." - exit 1 - } - catch [System.Management.Automation.ActionPreferenceStopException] { - Write-Host "[Error] Failed to retrieve iSCSI initiator details." - exit 1 - } - catch { - if ($null -eq $iscsiConnection) { - Write-Host "[Info] No iSCSI connections found." - } - if ($null -eq $iscsiSession) { - Write-Host "[Info] No iSCSI sessions found." - } - exit - } - - # If a custom field name is provided, save the results to the custom field - if ($WYSIWYGCustomFieldName) { - # If there are no iSCSI connections or sessions, output the details to the Activity Feed - if ($iscsiSession.Count -eq 0) { - $ShouldOutputResults = $true - Write-Host "[Info] No data to save to custom field." - } - else { - # Create an HTML string to save to the custom field - - # iSCSI initiator details - $wysiwyghtml = "

iSCSI Connection

" - $wysiwyghtml += $iscsiConnection | Select-Object -Property @{l = 'Connection Identifier'; e = { $_.ConnectionIdentifier } }, - @{l = 'Initiator Address'; e = { $_.InitiatorAddress } }, - @{l = 'Initiator Port'; e = { $_.InitiatorPortNumber } }, - @{l = 'Target Address'; e = { $_.TargetAddress } }, - @{l = 'Target Port'; e = { $_.TargetPortNumber } } | ConvertTo-Html -Fragment - - # iSCSI session details - $wysiwyghtml += "

iSCSI Session

" - $wysiwyghtml += $iscsiSession | Select-Object -Property @{l = 'Authentication Type'; e = { $_.AuthenticationType } }, - @{l = 'Initiator Name'; e = { $_.InitiatorInstanceName } }, - @{l = 'Initiator Node Address'; e = { $_.InitiatorNodeAddress } }, - @{l = 'Initiator Portal Address'; e = { $_.InitiatorPortalAddress } }, - @{l = 'Initiator Side Identifier'; e = { $_.InitiatorSideIdentifier } }, - @{l = 'Is Connected'; e = { $_.IsConnected } }, - @{l = 'Is Data Digest'; e = { $_.IsDataDigest } }, - @{l = 'Is Persistent'; e = { $_.IsPersistent } }, - @{l = 'Number of Connections'; e = { $_.NumberOfConnections } }, - @{l = 'Session Identifier'; e = { $_.SessionIdentifier } }, - @{l = 'Target Node Address'; e = { $_.TargetNodeAddress } }, - @{l = 'Target Side Identifier'; e = { $_.TargetSideIdentifier } } | ConvertTo-Html -Fragment - - # Save the HTML string to the custom field - try { - Set-NinjaProperty -Name $WYSIWYGCustomFieldName -Value $wysiwyghtml -Type "WYSIWYG" -Piped - Write-Host "[Info] Results saved to custom field: $WYSIWYGCustomFieldName" - $ShouldOutputResults = $true - } - catch { - Write-Host "[Error] Failed to save results to custom field: $WYSIWYGCustomFieldName" - $ShouldOutputResults = $true - $HasHadError = $true - } - } - } - else { - $ShouldOutputResults = $true - } - - # Output the iSCSI initiator details to the Activity Feed - if ($ShouldOutputResults) { - # Output the iSCSI initiator details to the Activity Feed - Write-Host "---iSCSI Connection---" - $iscsiConnection | Select-Object -Property @{l = 'Connection ID'; e = { $_.ConnectionIdentifier } }, - @{l = 'Initiator Address'; e = { $_.InitiatorAddress } }, - @{l = 'Initiator Port'; e = { $_.InitiatorPortNumber } }, - @{l = 'Target Address'; e = { $_.TargetAddress } }, - @{l = 'Target Port'; e = { $_.TargetPortNumber } } | Format-List | Out-String -Width 4000 | Write-Host - - # Output the iSCSI session details to the Activity Feed - Write-Host "---iSCSI Session---" - $iscsiSession | Select-Object -Property @{l = 'Auth'; e = { $_.AuthenticationType } }, - @{l = 'Init Name'; e = { $_.InitiatorInstanceName } }, - @{l = 'Init Node Address'; e = { $_.InitiatorNodeAddress } }, - @{l = 'Init Portal Address'; e = { $_.InitiatorPortalAddress } }, - @{l = 'Init Side ID'; e = { $_.InitiatorSideIdentifier } }, - @{l = 'Connected'; e = { $_.IsConnected } }, - @{l = 'Data Digest'; e = { $_.IsDataDigest } }, - @{l = 'Persistent'; e = { $_.IsPersistent } }, - @{l = '# Connections'; e = { $_.NumberOfConnections } }, - @{l = 'SID'; e = { $_.SessionIdentifier } }, - @{l = 'Tgt Node Address'; e = { $_.TargetNodeAddress } }, - @{l = 'Tgt Side ID'; e = { $_.TargetSideIdentifier } } | Format-List | Out-String -Width 4000 | Write-Host - } - - if ($HasHadError) { - exit 1 - } -} - -end { - - - -} + +<# + +.SYNOPSIS + Retrieves detailed information about the iSCSI initiator connections and sessions on the local machine. + +.DESCRIPTION + Gathers details about the iSCSI initiator connections and sessions on the local machine. + It retrieves information such as connection identifiers, initiator and target addresses, port numbers, session identifiers, and various other session attributes. + This can also save these details to a specified custom field in a WYSIWYG format if provided. + +.EXAMPLE + Get-IscsiDetails + + This command gets the iSCSI initiator details. + +PARAMETER: -WYSIWYGCustomFieldName "wysiwygCustomFieldName" + The name of the custom field to save the iSCSI initiator details. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial release +#> +[CmdletBinding()] +param( + [String] + $WYSIWYGCustomFieldName +) +begin { + + if ($env:wysiwygCustomFieldName -notlike "null") { + $WYSIWYGCustomFieldName = $env:wysiwygCustomFieldName + } + + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [Switch]$Piped # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + # # Remove the non-breaking space character # Removed NinjaOne dependency + # if ($Type -eq "WYSIWYG") { # Removed NinjaOne dependency + # $Value = $Value -replace ' ', ' ' # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Measure the number of characters in the provided value # Removed NinjaOne dependency + # $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + + # # Throw an error if the value exceeds the character limit of 200,000 characters # Removed NinjaOne dependency + # if ($Piped -and $Characters -ge 200000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if (!$Piped -and $Characters -ge 45000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Initialize a hashtable for additional documentation parameters # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + + # # If a document name is provided, add it to the documentation parameters # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # Define a list of valid field types # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + + # # Warn the user if the provided type is not valid # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # Define types that require options to be retrieved # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + + # # If the property is being set in a document or field and the type needs options, retrieve them # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if there was an issue retrieving the property options # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # Process the property value based on its type # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # Convert the value to a boolean for Checkbox type # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Convert the value to a Unix timestamp for Date or Date Time type # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Convert the dropdown value to its corresponding GUID # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # # Throw an error if the value is not present in the dropdown options # Removed NinjaOne dependency + # if (!($Selection)) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # For other types, use the value as is # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Set the property value in the document if a document name is provided # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # try { # Removed NinjaOne dependency + # # Otherwise, set the standard property value # Removed NinjaOne dependency + # if ($Piped) { # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # catch { # Removed NinjaOne dependency + # Write-Host -Object "[Error] Failed to set custom field." # Removed NinjaOne dependency + # throw $_.Exception.Message # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if setting the property failed # Removed NinjaOne dependency + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + $ShouldOutputResults = $false + $HasHadError = $false +} + +process { + try { + $iscsiConnection = Get-IscsiConnection -ErrorAction Stop + $iscsiSession = Get-IscsiSession -ErrorAction Stop + } + catch [System.Management.Automation.CommandNotFoundException] { + Write-Host "[Error] The Get-IscsiConnection or Get-IscsiSession cmdlet is not available on this system." + exit 1 + } + catch [System.Management.Automation.ActionPreferenceStopException] { + Write-Host "[Error] Failed to retrieve iSCSI initiator details." + exit 1 + } + catch { + if ($null -eq $iscsiConnection) { + Write-Host "[Info] No iSCSI connections found." + } + if ($null -eq $iscsiSession) { + Write-Host "[Info] No iSCSI sessions found." + } + exit + } + + # If a custom field name is provided, save the results to the custom field + if ($WYSIWYGCustomFieldName) { + # If there are no iSCSI connections or sessions, output the details to the Activity Feed + if ($iscsiSession.Count -eq 0) { + $ShouldOutputResults = $true + Write-Host "[Info] No data to save to custom field." + } + else { + # Create an HTML string to save to the custom field + + # iSCSI initiator details + $wysiwyghtml = "

iSCSI Connection

" + $wysiwyghtml += $iscsiConnection | Select-Object -Property @{l = 'Connection Identifier'; e = { $_.ConnectionIdentifier } }, + @{l = 'Initiator Address'; e = { $_.InitiatorAddress } }, + @{l = 'Initiator Port'; e = { $_.InitiatorPortNumber } }, + @{l = 'Target Address'; e = { $_.TargetAddress } }, + @{l = 'Target Port'; e = { $_.TargetPortNumber } } | ConvertTo-Html -Fragment + + # iSCSI session details + $wysiwyghtml += "

iSCSI Session

" + $wysiwyghtml += $iscsiSession | Select-Object -Property @{l = 'Authentication Type'; e = { $_.AuthenticationType } }, + @{l = 'Initiator Name'; e = { $_.InitiatorInstanceName } }, + @{l = 'Initiator Node Address'; e = { $_.InitiatorNodeAddress } }, + @{l = 'Initiator Portal Address'; e = { $_.InitiatorPortalAddress } }, + @{l = 'Initiator Side Identifier'; e = { $_.InitiatorSideIdentifier } }, + @{l = 'Is Connected'; e = { $_.IsConnected } }, + @{l = 'Is Data Digest'; e = { $_.IsDataDigest } }, + @{l = 'Is Persistent'; e = { $_.IsPersistent } }, + @{l = 'Number of Connections'; e = { $_.NumberOfConnections } }, + @{l = 'Session Identifier'; e = { $_.SessionIdentifier } }, + @{l = 'Target Node Address'; e = { $_.TargetNodeAddress } }, + @{l = 'Target Side Identifier'; e = { $_.TargetSideIdentifier } } | ConvertTo-Html -Fragment + + # Save the HTML string to the custom field + try { + # Set-NinjaProperty -Name $WYSIWYGCustomFieldName -Value $wysiwyghtml -Type "WYSIWYG" -Piped # Removed NinjaOne dependency + Write-Host "[Info] Results saved to custom field: $WYSIWYGCustomFieldName" + $ShouldOutputResults = $true + } + catch { + Write-Host "[Error] Failed to save results to custom field: $WYSIWYGCustomFieldName" + $ShouldOutputResults = $true + $HasHadError = $true + } + } + } + else { + $ShouldOutputResults = $true + } + + # Output the iSCSI initiator details to the Activity Feed + if ($ShouldOutputResults) { + # Output the iSCSI initiator details to the Activity Feed + Write-Host "---iSCSI Connection---" + $iscsiConnection | Select-Object -Property @{l = 'Connection ID'; e = { $_.ConnectionIdentifier } }, + @{l = 'Initiator Address'; e = { $_.InitiatorAddress } }, + @{l = 'Initiator Port'; e = { $_.InitiatorPortNumber } }, + @{l = 'Target Address'; e = { $_.TargetAddress } }, + @{l = 'Target Port'; e = { $_.TargetPortNumber } } | Format-List | Out-String -Width 4000 | Write-Host + + # Output the iSCSI session details to the Activity Feed + Write-Host "---iSCSI Session---" + $iscsiSession | Select-Object -Property @{l = 'Auth'; e = { $_.AuthenticationType } }, + @{l = 'Init Name'; e = { $_.InitiatorInstanceName } }, + @{l = 'Init Node Address'; e = { $_.InitiatorNodeAddress } }, + @{l = 'Init Portal Address'; e = { $_.InitiatorPortalAddress } }, + @{l = 'Init Side ID'; e = { $_.InitiatorSideIdentifier } }, + @{l = 'Connected'; e = { $_.IsConnected } }, + @{l = 'Data Digest'; e = { $_.IsDataDigest } }, + @{l = 'Persistent'; e = { $_.IsPersistent } }, + @{l = '# Connections'; e = { $_.NumberOfConnections } }, + @{l = 'SID'; e = { $_.SessionIdentifier } }, + @{l = 'Tgt Node Address'; e = { $_.TargetNodeAddress } }, + @{l = 'Tgt Side ID'; e = { $_.TargetSideIdentifier } } | Format-List | Out-String -Width 4000 | Write-Host + } + + if ($HasHadError) { + exit 1 + } +} + +end { + + + +} diff --git a/Powershell Scripts/Hash Found Alert.ps1 b/Powershell Scripts/Hash Found Alert.ps1 index 29a9396..6a498fa 100644 --- a/Powershell Scripts/Hash Found Alert.ps1 +++ b/Powershell Scripts/Hash Found Alert.ps1 @@ -1,365 +1,365 @@ # Alerts if a file with the extension and specified hash is found in the given search directory or subdirectories. Warning: Hashing large files may impact performance. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Alerts if a file with the extension and specified hash is found in the given search directory or subdirectories. Warning: Hashing large files may impact performance. -.DESCRIPTION - Alerts if a file with the extension and specified hash is found in the given search directory or subdirectories. Warning: Hashing large files may impact performance. -.EXAMPLE - -Hash "F55A61A82F4F5943F86565E1FA2CCB4F" -SearchPath "C:" -FileType ".ico" -CustomField "multiline" - WARNING: Backslash missing from the search path. Changing it to C:\. - WARNING: File with MD5 hash of F55A61A82F4F5943F86565E1FA2CCB4F found! - - File Name Path - --------- ---- - zoo_ecosystem.ico C:\Users\Administrator\Desktop\Find-FileHash\Test Folder 1\zoo_ecosystem.ico - zoo_ecosystem.ico C:\Users\Administrator\Desktop\Find-FileHash\TestFolder1\Test Folder 1\zoo_ecosystem.ico - zoo_ecosystem.ico C:\Users\Administrator\Desktop\Find-FileHash\TestFolder2\Test Folder 1\zoo_ecosystem.ico - - Attempting to set Custom Field 'multiline'. - Successfully set Custom Field 'multiline'! - -PARAMETER: -Hash "REPLACEMEWITHAVALIDHASH" - Files with this hash should cause the alert to trigger. - -PARAMETER: -Algorithm "MD5" - Hashing algorithm used for the above hash. - -PARAMETER: -SearchPath "C:\ReplaceMeWithAValidSearchPath" - Specifies one or more starting directories for the search, separated by commas. The search will recursively include all subdirectories from these starting points. - -PARAMETER: -Timeout "15" - Once this timeout is reached, the script will stop searching for files that match your given hash. - -PARAMETER: -FileType ".exe" - Specifies the file extension to filter the search. Only files with this extension will be analyzed for a hash match. Example: '.exe' - -PARAMETER: -CustomField "NameOfMultiLineCustomField" - Specifies the name of an optional multiline custom field where results can be sent. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$Hash, - [Parameter()] - [String]$SearchPath = "C:\Windows", - [Parameter()] - [String]$FileType, - [Parameter()] - [Int]$Timeout = 15, - [Parameter()] - [String]$Algorithm = "MD5", - [Parameter()] - [String]$CustomField -) - -begin { - # Set parameters using dynamic script variables. - if ($env:hash -and $env:hash -notlike "null") { $Hash = $env:hash } - if ($env:hashType -and $env:hashType -notlike "null") { $Algorithm = $env:hashType } - if ($env:searchPath -and $env:searchPath -notlike "null") { $SearchPath = $env:searchPath } - if ($env:timeoutInMinutes -and $env:timeoutInMinutes -notlike "null") { $Timeout = $env:timeoutInMinutes } - if ($env:fileExtensionToSearchFor -and $env:fileExtensionToSearchFor -notlike "null") { $FileType = $env:fileExtensionToSearchFor } - if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } - - # If given a comma-separated list, split the paths. - $PathsToSearch = New-Object System.Collections.Generic.List[String] - if ($SearchPath -match ",") { - $SearchPath -split "," | ForEach-Object { $PathsToSearch.Add($_.Trim()) } - } - else { - $PathsToSearch.Add($SearchPath) - } - - $ReplacementPaths = New-Object System.Collections.Generic.List[Object] - $PathsToRemove = New-Object System.Collections.Generic.List[String] - - # If given a drive without the backslash add it in. - $PathsToSearch | ForEach-Object { - if ($_ -notmatch '^[A-Z]:\\$' -and $_ -match '^[A-Z]:$') { - $NewPath = "$_\" - $ReplacementPaths.Add( - [PSCustomObject]@{ - Index = $PathsToSearch.IndexOf("$_") - NewPath = $NewPath - } - ) - - Write-Warning "Backslash missing from the search path. Changing it to $NewPath." - } - } - - # Apply replacements - $ReplacementPaths | ForEach-Object { - $PathsToSearch[$_.index] = $_.NewPath - } - - # Check if the search path is valid. - $PathsToSearch | ForEach-Object { - if (-not (Test-Path $_)) { - Write-Host -Object "[Error] $_ does not exist!" - $PathsToRemove.Add($_) - $ExitCode = 1 - } - } - - $PathsToRemove | ForEach-Object { - $PathsToSearch.Remove($_) | Out-Null - } - - - # Error out if no valid paths to search. - if ($PathsToSearch.Count -eq 0) { - Write-Host "[Error] No valid paths to search!" - exit 1 - } - - # A file extension is required. - if (-not $FileType) { - Write-Host -Object "[Error] File Type is required!" - exit 1 - } - - # If we were given the extension without the . we'll add it back in - if ($FileType -notmatch '^\.') { - $FileType = ".$FileType" - Write-Warning -Message "Extension missing changing filetype to $FileType." - } - - # The timeout has to be between 1 and 120 - if ($Timeout -lt 1 -or $Timeout -gt 120) { - Write-Host "[Error] Invalid timeout given of $Timeout minutes. Please enter a value between 1 and 120." - exit 1 - } - - # PowerShell 5.1 supports more algorithms than this, however PowerShell 7 does not. - $ValidAlgorithms = "SHA1", "SHA256", "SHA384", "SHA512", "MD5" - if ($ValidAlgorithms -notcontains $Algorithm) { - Write-Host "[Error] Invalid Algorithm selected. Only SHA1, SHA256, SHA384, SHA512 and MD5 are supported." - exit 1 - } - - # Helper function to make it easier to set custom fields. - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 10000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") - } - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to be set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received it will have an exception property, the function will exit with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the Date-Time to be in Unix Epoch time so we'll convert it here. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - - # Test for local administrator rights - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - $ExitCode = 0 -} -process { - if (-not (Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # If we're given a file instead of a folder, we'll check if it matches anyways. - $PathsToSearch | ForEach-Object { - if (-not (Get-Item $_).PSIsContainer) { - Write-Warning "The search path you gave is actually a file not a folder. Checking the hash of the file..." - } - } - - $HashJobs = New-Object System.Collections.Generic.List[object] - $MatchingFiles = New-Object System.Collections.Generic.List[object] - $CustomFieldValue = New-Object System.Collections.Generic.List[string] - - # We'll use a PowerShell job so that we can timeout appropriately. - $PathsToSearch | ForEach-Object { - $HashJobs.Add( - ( - Start-Job -ScriptBlock { - param($SearchPath, $FileType, $Algorithm, $Hash) - $Files = Get-ChildItem -Path $SearchPath -Filter "*$FileType" -File -Recurse - $Files | ForEach-Object { - $CurrentHash = Get-FileHash -Path $_.FullName -Algorithm $Algorithm - if ($CurrentHash.Hash -match $Hash) { $_.FullName } - } - } -ArgumentList $_, $FileType, $Algorithm, $Hash - ) - ) - } - - $TimeoutInSeconds = $Timeout * 60 - $StartTime = Get-Date - - # Wait for all jobs to complete or timeout - foreach ($HashJob in $HashJobs) { - # Calculate the remaining time - $TimeElapsed = (Get-Date) - $StartTime - $RemainingTime = $TimeoutInSeconds - $TimeElapsed.TotalSeconds - - # If there is no remaining time, break the loop - if ($RemainingTime -le 0) { - break - } - - # Wait for the current job with the remaining time as the timeout - $HashJob | Wait-Job -Timeout $RemainingTime | Out-Null - } - - # If we failed to complete the job, we'll output a warning. - $IncompleteJobs = $HashJobs | Get-Job | Where-Object { $_.State -eq "Running" } - if ($IncompleteJobs) { - Write-Host "[Error] The timeout period of $Timeout minutes has been reached, but some files still require a hash check!" - $CustomFieldValue.Add("[Error] The timeout period of $Timeout minutes has been reached, but some files still require a hash check!") - $ExitCode = 1 - } - - # Receive the data from the job. - $HashJobs | Receive-Job -ErrorAction SilentlyContinue -ErrorVariable JobErrors | ForEach-Object { - $MatchingFiles.Add( - [PSCustomObject]@{ - "File Name" = $(Split-Path $_ -Leaf) - Path = $_ - } - ) - } - - # If we have any matching files, we'll output them here. - if ($MatchingFiles) { - Write-Warning -Message "File with $Algorithm hash of $Hash found!" - $MatchingFiles | Format-Table -AutoSize | Out-String | Write-Host - $MatchingFiles | Select-Object -ExpandProperty Path | ForEach-Object { $CustomFieldValue.Add($_) } - } - else { - Write-Host -Object "No files found with $Hash." - } - - # If we received any failures or errors, we'll output that here. - $FailedJobs = $HashJobs | Get-Job | Where-Object { $_.State -ne "Completed" -and $_.State -ne "Running" } - if ($FailedJobs -or $JobErrors) { - Write-Host "" - Write-Host "[Error] Failed to get the hash of certain files due to an error." - $CustomFieldValue.Add(" ") - $CustomFieldValue.Add("[Error] Failed to get the hash of certain files due to an error.") - if ($JobErrors) { - Write-Host "" - $JobErrors | ForEach-Object { Write-Host "[Error] $($_.Exception.Message)" } - $CustomFieldValue.Add(" ") - $JobErrors | ForEach-Object { $CustomFieldValue.Add("[Error] $($_.Exception.Message)") } - } - $ExitCode = 1 - } - - $HashJobs | Remove-Job -Force - - # If we're given a custom field, we'll attempt to save the results to it. - if ($CustomField) { - try { - Write-Host "Attempting to set Custom Field '$CustomField'." - Set-NinjaProperty -Name $CustomField -Value ($CustomFieldValue | Out-String) - Write-Host "Successfully set Custom Field '$CustomField'!" - } - catch { - if (-not $_.Exception.Message) { - Write-Host "[Error] $($_.Message)" - } - else { - Write-Host "[Error] $($_.Exception.Message)" - } - $ExitCode = 1 - } - - } - - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Alerts if a file with the extension and specified hash is found in the given search directory or subdirectories. Warning: Hashing large files may impact performance. +.DESCRIPTION + Alerts if a file with the extension and specified hash is found in the given search directory or subdirectories. Warning: Hashing large files may impact performance. +.EXAMPLE + -Hash "F55A61A82F4F5943F86565E1FA2CCB4F" -SearchPath "C:" -FileType ".ico" -CustomField "multiline" + WARNING: Backslash missing from the search path. Changing it to C:\. + WARNING: File with MD5 hash of F55A61A82F4F5943F86565E1FA2CCB4F found! + + File Name Path + --------- ---- + zoo_ecosystem.ico C:\Users\Administrator\Desktop\Find-FileHash\Test Folder 1\zoo_ecosystem.ico + zoo_ecosystem.ico C:\Users\Administrator\Desktop\Find-FileHash\TestFolder1\Test Folder 1\zoo_ecosystem.ico + zoo_ecosystem.ico C:\Users\Administrator\Desktop\Find-FileHash\TestFolder2\Test Folder 1\zoo_ecosystem.ico + + Attempting to set Custom Field 'multiline'. + Successfully set Custom Field 'multiline'! + +PARAMETER: -Hash "REPLACEMEWITHAVALIDHASH" + Files with this hash should cause the alert to trigger. + +PARAMETER: -Algorithm "MD5" + Hashing algorithm used for the above hash. + +PARAMETER: -SearchPath "C:\ReplaceMeWithAValidSearchPath" + Specifies one or more starting directories for the search, separated by commas. The search will recursively include all subdirectories from these starting points. + +PARAMETER: -Timeout "15" + Once this timeout is reached, the script will stop searching for files that match your given hash. + +PARAMETER: -FileType ".exe" + Specifies the file extension to filter the search. Only files with this extension will be analyzed for a hash match. Example: '.exe' + +PARAMETER: -CustomField "NameOfMultiLineCustomField" + Specifies the name of an optional multiline custom field where results can be sent. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$Hash, + [Parameter()] + [String]$SearchPath = "C:\Windows", + [Parameter()] + [String]$FileType, + [Parameter()] + [Int]$Timeout = 15, + [Parameter()] + [String]$Algorithm = "MD5", + [Parameter()] + [String]$CustomField +) + +begin { + # Set parameters using dynamic script variables. + if ($env:hash -and $env:hash -notlike "null") { $Hash = $env:hash } + if ($env:hashType -and $env:hashType -notlike "null") { $Algorithm = $env:hashType } + if ($env:searchPath -and $env:searchPath -notlike "null") { $SearchPath = $env:searchPath } + if ($env:timeoutInMinutes -and $env:timeoutInMinutes -notlike "null") { $Timeout = $env:timeoutInMinutes } + if ($env:fileExtensionToSearchFor -and $env:fileExtensionToSearchFor -notlike "null") { $FileType = $env:fileExtensionToSearchFor } + if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } + + # If given a comma-separated list, split the paths. + $PathsToSearch = New-Object System.Collections.Generic.List[String] + if ($SearchPath -match ",") { + $SearchPath -split "," | ForEach-Object { $PathsToSearch.Add($_.Trim()) } + } + else { + $PathsToSearch.Add($SearchPath) + } + + $ReplacementPaths = New-Object System.Collections.Generic.List[Object] + $PathsToRemove = New-Object System.Collections.Generic.List[String] + + # If given a drive without the backslash add it in. + $PathsToSearch | ForEach-Object { + if ($_ -notmatch '^[A-Z]:\\$' -and $_ -match '^[A-Z]:$') { + $NewPath = "$_\" + $ReplacementPaths.Add( + [PSCustomObject]@{ + Index = $PathsToSearch.IndexOf("$_") + NewPath = $NewPath + } + ) + + Write-Warning "Backslash missing from the search path. Changing it to $NewPath." + } + } + + # Apply replacements + $ReplacementPaths | ForEach-Object { + $PathsToSearch[$_.index] = $_.NewPath + } + + # Check if the search path is valid. + $PathsToSearch | ForEach-Object { + if (-not (Test-Path $_)) { + Write-Host -Object "[Error] $_ does not exist!" + $PathsToRemove.Add($_) + $ExitCode = 1 + } + } + + $PathsToRemove | ForEach-Object { + $PathsToSearch.Remove($_) | Out-Null + } + + + # Error out if no valid paths to search. + if ($PathsToSearch.Count -eq 0) { + Write-Host "[Error] No valid paths to search!" + exit 1 + } + + # A file extension is required. + if (-not $FileType) { + Write-Host -Object "[Error] File Type is required!" + exit 1 + } + + # If we were given the extension without the . we'll add it back in + if ($FileType -notmatch '^\.') { + $FileType = ".$FileType" + Write-Warning -Message "Extension missing changing filetype to $FileType." + } + + # The timeout has to be between 1 and 120 + if ($Timeout -lt 1 -or $Timeout -gt 120) { + Write-Host "[Error] Invalid timeout given of $Timeout minutes. Please enter a value between 1 and 120." + exit 1 + } + + # PowerShell 5.1 supports more algorithms than this, however PowerShell 7 does not. + $ValidAlgorithms = "SHA1", "SHA256", "SHA384", "SHA512", "MD5" + if ($ValidAlgorithms -notcontains $Algorithm) { + Write-Host "[Error] Invalid Algorithm selected. Only SHA1, SHA256, SHA384, SHA512 and MD5 are supported." + exit 1 + } + + # Helper function to make it easier to set custom fields. + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + # if ($Characters -ge 10000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If we're requested to set the field value for a Ninja document we'll specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # The field below requires additional information to be set # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If an error is received it will have an exception property, the function will exit with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the Date-Time to be in Unix Epoch time so we'll convert it here. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # if (-not $Selection) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # We'll need to set the field differently depending on if its a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # Test for local administrator rights + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + $ExitCode = 0 +} +process { + if (-not (Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # If we're given a file instead of a folder, we'll check if it matches anyways. + $PathsToSearch | ForEach-Object { + if (-not (Get-Item $_).PSIsContainer) { + Write-Warning "The search path you gave is actually a file not a folder. Checking the hash of the file..." + } + } + + $HashJobs = New-Object System.Collections.Generic.List[object] + $MatchingFiles = New-Object System.Collections.Generic.List[object] + $CustomFieldValue = New-Object System.Collections.Generic.List[string] + + # We'll use a PowerShell job so that we can timeout appropriately. + $PathsToSearch | ForEach-Object { + $HashJobs.Add( + ( + Start-Job -ScriptBlock { + param($SearchPath, $FileType, $Algorithm, $Hash) + $Files = Get-ChildItem -Path $SearchPath -Filter "*$FileType" -File -Recurse + $Files | ForEach-Object { + $CurrentHash = Get-FileHash -Path $_.FullName -Algorithm $Algorithm + if ($CurrentHash.Hash -match $Hash) { $_.FullName } + } + } -ArgumentList $_, $FileType, $Algorithm, $Hash + ) + ) + } + + $TimeoutInSeconds = $Timeout * 60 + $StartTime = Get-Date + + # Wait for all jobs to complete or timeout + foreach ($HashJob in $HashJobs) { + # Calculate the remaining time + $TimeElapsed = (Get-Date) - $StartTime + $RemainingTime = $TimeoutInSeconds - $TimeElapsed.TotalSeconds + + # If there is no remaining time, break the loop + if ($RemainingTime -le 0) { + break + } + + # Wait for the current job with the remaining time as the timeout + $HashJob | Wait-Job -Timeout $RemainingTime | Out-Null + } + + # If we failed to complete the job, we'll output a warning. + $IncompleteJobs = $HashJobs | Get-Job | Where-Object { $_.State -eq "Running" } + if ($IncompleteJobs) { + Write-Host "[Error] The timeout period of $Timeout minutes has been reached, but some files still require a hash check!" + $CustomFieldValue.Add("[Error] The timeout period of $Timeout minutes has been reached, but some files still require a hash check!") + $ExitCode = 1 + } + + # Receive the data from the job. + $HashJobs | Receive-Job -ErrorAction SilentlyContinue -ErrorVariable JobErrors | ForEach-Object { + $MatchingFiles.Add( + [PSCustomObject]@{ + "File Name" = $(Split-Path $_ -Leaf) + Path = $_ + } + ) + } + + # If we have any matching files, we'll output them here. + if ($MatchingFiles) { + Write-Warning -Message "File with $Algorithm hash of $Hash found!" + $MatchingFiles | Format-Table -AutoSize | Out-String | Write-Host + $MatchingFiles | Select-Object -ExpandProperty Path | ForEach-Object { $CustomFieldValue.Add($_) } + } + else { + Write-Host -Object "No files found with $Hash." + } + + # If we received any failures or errors, we'll output that here. + $FailedJobs = $HashJobs | Get-Job | Where-Object { $_.State -ne "Completed" -and $_.State -ne "Running" } + if ($FailedJobs -or $JobErrors) { + Write-Host "" + Write-Host "[Error] Failed to get the hash of certain files due to an error." + $CustomFieldValue.Add(" ") + $CustomFieldValue.Add("[Error] Failed to get the hash of certain files due to an error.") + if ($JobErrors) { + Write-Host "" + $JobErrors | ForEach-Object { Write-Host "[Error] $($_.Exception.Message)" } + $CustomFieldValue.Add(" ") + $JobErrors | ForEach-Object { $CustomFieldValue.Add("[Error] $($_.Exception.Message)") } + } + $ExitCode = 1 + } + + $HashJobs | Remove-Job -Force + + # If we're given a custom field, we'll attempt to save the results to it. + if ($CustomField) { + try { + Write-Host "Attempting to set Custom Field '$CustomField'." + # Set-NinjaProperty -Name $CustomField -Value ($CustomFieldValue | Out-String) # Removed NinjaOne dependency + Write-Host "Successfully set Custom Field '$CustomField'!" + } + catch { + if (-not $_.Exception.Message) { + Write-Host "[Error] $($_.Message)" + } + else { + Write-Host "[Error] $($_.Exception.Message)" + } + $ExitCode = 1 + } + + } + + exit $ExitCode +} +end { + + + +} diff --git a/Powershell Scripts/Hyper-V - Checkpoint Expiration Alert.ps1 b/Powershell Scripts/Hyper-V - Checkpoint Expiration Alert.ps1 index d0fbf4e..2a3282e 100644 --- a/Powershell Scripts/Hyper-V - Checkpoint Expiration Alert.ps1 +++ b/Powershell Scripts/Hyper-V - Checkpoint Expiration Alert.ps1 @@ -82,7 +82,9 @@ process { $Threshold = (Get-Date).AddDays(-$OlderThan) if ($FromCustomField) { - $Threshold = (Get-Date).AddDays( - (Ninja-Property-Get $FromCustomField)) + # NinjaOne integration removed - cannot retrieve from custom field + Write-Warning "FromCustomField specified but NinjaOne integration has been removed. Please use -OlderThan parameter instead." + # $Threshold = (Get-Date).AddDays( - (Ninja-Property-Get $FromCustomField)) } $CheckPoints = Get-VM | Get-VMSnapshot | Where-Object { $_.CreationTime -lt $Threshold } diff --git a/Powershell Scripts/Hyper-V - Get Host Server Name from Guest.ps1 b/Powershell Scripts/Hyper-V - Get Host Server Name from Guest.ps1 index 47abc40..e61ad48 100644 --- a/Powershell Scripts/Hyper-V - Get Host Server Name from Guest.ps1 +++ b/Powershell Scripts/Hyper-V - Get Host Server Name from Guest.ps1 @@ -1,260 +1,260 @@ # Reports on the hypervisor hostname of a guest VM. Must be ran on a Hyper-V guest VM. -#Requires -Version 3 - -<# -.SYNOPSIS - Reports on the hypervisor hostname of a guest VM. Must be ran on a Hyper-V guest VM. -.DESCRIPTION - Reports on the hypervisor hostname of a guest VM. Must be ran on a Hyper-V guest VM. - -.PARAMETER -TextCustomFieldName - Enter the text custom field name where the hypervisor hostname will be saved. - -.EXAMPLE - (No Parameters) - - [Info] WIN11-EDUCATION is hosted on: HYPERV-HOST-1 - -.EXAMPLE - -TextCustomFieldName "text" - - [Info] Attempting to set Ninja custom field 'text'... - [Info] Successfully set Ninja custom field 'text' to value 'HYPERV-HOST-1'. - - [Info] WIN11-EDUCATION is hosted on: HYPERV-HOST-1 - -.NOTES - Minimum OS Architecture Supported: Windows 8, Windows Server 2012 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [string]$TextCustomFieldName -) - -begin { - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - function Test-IsVM { - try { - # first test via model. Hyper-V and VMWare sets these properties automatically and they are read-only - if ($PSVersionTable.PSVersion.Major -lt 3) { - $model = (Get-WmiObject -Class Win32_ComputerSystem -Property Model -ErrorAction Stop).Model - } - else { - $model = (Get-CimInstance -ClassName Win32_ComputerSystem -Property Model -ErrorAction Stop).Model - } - - # Hyper-V uses "Virtual Machine" VMWare uses "VM" - if ($model -match "Virtual|VM"){ - return $true - } - else{ - # Proxmox can be identified via the manufacturer - if ($PSVersionTable.PSVersion.Major -lt 3) { - $manufacturer = (Get-WmiObject -Class Win32_BIOS -Property Manufacturer -ErrorAction Stop).Manufacturer - } - else { - $manufacturer = (Get-CimInstance -Class Win32_BIOS -Property Manufacturer -ErrorAction Stop).Manufacturer - } - - if ($manufacturer -match "Proxmox"){ - return $true - } - else{ - return $false - } - } - } - catch { - Write-Host -Object "[Error] Unable to validate whether or not this device is a VM." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - } - - if (-not (Test-IsVM)){ - Write-Host "[Error] Host is not a virtual machine." - exit 1 - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName, - [Parameter()] - [Switch]$Piped - ) - # Remove the non-breaking space character - if ($Type -eq "WYSIWYG") { - $Value = $Value -replace ' ', ' ' - } - - # Measure the number of characters in the provided value - $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # Throw an error if the value exceeds the character limit of 200,000 characters - if ($Piped -and $Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - if (!$Piped -and $Characters -ge 45000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") - } - - # Initialize a hashtable for additional documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define a list of valid field types - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - - # Warn the user if the provided type is not valid - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # Define types that require options to be retrieved - $NeedsOptions = "Dropdown" - - # If the property is being set in a document or field and the type needs options, retrieve them - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an error if there was an issue retrieving the property options - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Process the property value based on its type - switch ($Type) { - "Checkbox" { - # Convert the value to a boolean for Checkbox type - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Convert the value to a Unix timestamp for Date or Date Time type - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Convert the dropdown value to its corresponding GUID - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - # Throw an error if the value is not present in the dropdown options - if (!($Selection)) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # For other types, use the value as is - $NinjaValue = $Value - } - } - - # Set the property value in the document if a document name is provided - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - try { - # Otherwise, set the standard property value - if ($Piped) { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - } - catch { - Write-Host -Object "[Error] Failed to set custom field." - throw $_.Exception.Message - } - } - - # Throw an error if setting the property failed - if ($CustomField.Exception) { - throw $CustomField - } - } - - if ($env:TextCustomFieldName -and $env:TextCustomFieldName -notlike ''){ - $TextCustomFieldName = $env:TextCustomFieldName - } -} -process { - if (-not (Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - $ExitCode = 0 - - $regPath = "HKLM:\Software\Microsoft\Virtual Machine\Guest\Parameters" - - Write-Host "" - - # if regPath is not present, error out - if (-not (Test-Path $regPath)){ - Write-Host "[Error] Registry key cannot be found. This either means that $env:computername is not a Hyper-V guest, or the 'Data Exchange' integration is disabled in the VM settings." - exit 1 - } - - # if registry key exists, get value of property - $HyperVHost = (Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue).PhysicalHostName - - if ([string]::IsNullOrWhiteSpace($HyperVHost)){ - Write-Host "[Error] Registry key exists but the value is blank.`n" - exit 1 - } - else{ - Write-Host "[Info] $env:computername is hosted on: $HyperVHost" - } - - # write to custom field if value is supplied - if ($TextCustomFieldName){ - - # attempt custom field write - try { - Write-Host "[Info] Attempting to set Ninja custom field '$TextCustomFieldName'..." - Set-NinjaProperty -Name $TextCustomFieldName -Type "Text" -Value $HyperVHost -ErrorAction Stop - Write-Host "[Info] Successfully set Ninja custom field '$TextCustomFieldName' to value '$HyperVHost'.`n" - } - catch { - Write-Host "[Error] Error setting custom field '$TextCustomFieldName' to value '$HyperVHost'." - Write-Host "$($_.Exception.Message)" - Write-Host "" - $ExitCode = 1 - } - } - - exit $ExitCode -} -end { - - - -} +#Requires -Version 3 + +<# +.SYNOPSIS + Reports on the hypervisor hostname of a guest VM. Must be ran on a Hyper-V guest VM. +.DESCRIPTION + Reports on the hypervisor hostname of a guest VM. Must be ran on a Hyper-V guest VM. + +.PARAMETER -TextCustomFieldName + Enter the text custom field name where the hypervisor hostname will be saved. + +.EXAMPLE + (No Parameters) + + [Info] WIN11-EDUCATION is hosted on: HYPERV-HOST-1 + +.EXAMPLE + -TextCustomFieldName "text" + + [Info] Attempting to set Ninja custom field 'text'... + [Info] Successfully set Ninja custom field 'text' to value 'HYPERV-HOST-1'. + + [Info] WIN11-EDUCATION is hosted on: HYPERV-HOST-1 + +.NOTES + Minimum OS Architecture Supported: Windows 8, Windows Server 2012 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [string]$TextCustomFieldName +) + +begin { + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + function Test-IsVM { + try { + # first test via model. Hyper-V and VMWare sets these properties automatically and they are read-only + if ($PSVersionTable.PSVersion.Major -lt 3) { + $model = (Get-WmiObject -Class Win32_ComputerSystem -Property Model -ErrorAction Stop).Model + } + else { + $model = (Get-CimInstance -ClassName Win32_ComputerSystem -Property Model -ErrorAction Stop).Model + } + + # Hyper-V uses "Virtual Machine" VMWare uses "VM" + if ($model -match "Virtual|VM"){ + return $true + } + else{ + # Proxmox can be identified via the manufacturer + if ($PSVersionTable.PSVersion.Major -lt 3) { + $manufacturer = (Get-WmiObject -Class Win32_BIOS -Property Manufacturer -ErrorAction Stop).Manufacturer + } + else { + $manufacturer = (Get-CimInstance -Class Win32_BIOS -Property Manufacturer -ErrorAction Stop).Manufacturer + } + + if ($manufacturer -match "Proxmox"){ + return $true + } + else{ + return $false + } + } + } + catch { + Write-Host -Object "[Error] Unable to validate whether or not this device is a VM." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + } + + if (-not (Test-IsVM)){ + Write-Host "[Error] Host is not a virtual machine." + exit 1 + } + + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [Switch]$Piped # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + # # Remove the non-breaking space character # Removed NinjaOne dependency + # if ($Type -eq "WYSIWYG") { # Removed NinjaOne dependency + # $Value = $Value -replace ' ', ' ' # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Measure the number of characters in the provided value # Removed NinjaOne dependency + # $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + + # # Throw an error if the value exceeds the character limit of 200,000 characters # Removed NinjaOne dependency + # if ($Piped -and $Characters -ge 200000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if (!$Piped -and $Characters -ge 45000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Initialize a hashtable for additional documentation parameters # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + + # # If a document name is provided, add it to the documentation parameters # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # Define a list of valid field types # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + + # # Warn the user if the provided type is not valid # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # Define types that require options to be retrieved # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + + # # If the property is being set in a document or field and the type needs options, retrieve them # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if there was an issue retrieving the property options # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # Process the property value based on its type # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # Convert the value to a boolean for Checkbox type # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Convert the value to a Unix timestamp for Date or Date Time type # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Convert the dropdown value to its corresponding GUID # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # # Throw an error if the value is not present in the dropdown options # Removed NinjaOne dependency + # if (!($Selection)) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # For other types, use the value as is # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Set the property value in the document if a document name is provided # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # try { # Removed NinjaOne dependency + # # Otherwise, set the standard property value # Removed NinjaOne dependency + # if ($Piped) { # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # catch { # Removed NinjaOne dependency + # Write-Host -Object "[Error] Failed to set custom field." # Removed NinjaOne dependency + # throw $_.Exception.Message # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if setting the property failed # Removed NinjaOne dependency + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + if ($env:TextCustomFieldName -and $env:TextCustomFieldName -notlike ''){ + $TextCustomFieldName = $env:TextCustomFieldName + } +} +process { + if (-not (Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + $ExitCode = 0 + + $regPath = "HKLM:\Software\Microsoft\Virtual Machine\Guest\Parameters" + + Write-Host "" + + # if regPath is not present, error out + if (-not (Test-Path $regPath)){ + Write-Host "[Error] Registry key cannot be found. This either means that $env:computername is not a Hyper-V guest, or the 'Data Exchange' integration is disabled in the VM settings." + exit 1 + } + + # if registry key exists, get value of property + $HyperVHost = (Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue).PhysicalHostName + + if ([string]::IsNullOrWhiteSpace($HyperVHost)){ + Write-Host "[Error] Registry key exists but the value is blank.`n" + exit 1 + } + else{ + Write-Host "[Info] $env:computername is hosted on: $HyperVHost" + } + + # write to custom field if value is supplied + if ($TextCustomFieldName){ + + # attempt custom field write + try { + Write-Host "[Info] Attempting to set Ninja custom field '$TextCustomFieldName'..." + # Set-NinjaProperty -Name $TextCustomFieldName -Type "Text" -Value $HyperVHost -ErrorAction Stop # Removed NinjaOne dependency + Write-Host "[Info] Successfully set Ninja custom field '$TextCustomFieldName' to value '$HyperVHost'.`n" + } + catch { + Write-Host "[Error] Error setting custom field '$TextCustomFieldName' to value '$HyperVHost'." + Write-Host "$($_.Exception.Message)" + Write-Host "" + $ExitCode = 1 + } + } + + exit $ExitCode +} +end { + + + +} diff --git a/Powershell Scripts/Hyper-V - Replication Alert.ps1 b/Powershell Scripts/Hyper-V - Replication Alert.ps1 index 7605053..86ca068 100644 --- a/Powershell Scripts/Hyper-V - Replication Alert.ps1 +++ b/Powershell Scripts/Hyper-V - Replication Alert.ps1 @@ -1,289 +1,289 @@ # This will get information about the current status of Hyper-V Replication. If its abnormal itll check the last replication time to see if it should alert on it. -#Requires -Version 5.1 - -<# -.SYNOPSIS - This will get information about the current status of Hyper-V Replication. If its abnormal it'll check the last replication time to see if it should alert on it. -.DESCRIPTION - This will get information about the current status of Hyper-V Replication. If its abnormal it'll check the last replication time to see if it should alert on it. -.EXAMPLE - (No Parameters) - Replication is currently failing! - + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorExcep - tion - + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorExceptio - n,customscript_gen_2.ps1 - - - VMName PrimaryServer State Health LastReplicationTime - ------ ------------- ----- ------ ------------------- - WIN10-TEST SRV16-TEST.test.lan Error Critical 4/13/2023 8:20:11 AM - Win10-TEST2 SRV16-TEST.test.lan Error Critical 4/13/2023 8:20:11 AM - -PARAMETER: -FailedFor "30" - Time in minutes any given vm replication is allowed to be abnormal. - Ex. "20" will alert on a vm replication after its been in the abnormal state for 20 minutes. -.EXAMPLE - -FailedFor "20" - WARNING: Some of the vm's currently have replication paused! - - - VMName PrimaryServer State Health LastReplicationTime - ------ ------------- ----- ------ ------------------- - WIN10-TEST SRV16-TEST.test.lan Replicating Normal 4/13/2023 8:40:04 AM - Win10-TEST2 SRV16-TEST.test.lan Suspended Warning 4/13/2023 8:32:06 AM - -PARAMETER: -IncludePaused - Script will consider paused vm's abnormal if this parameter is used. -.EXAMPLE - -IncludePaused - Replication is currently failing! - + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorExcep - tion - + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorExceptio - n,customscript_gen_2.ps1 - - - VMName PrimaryServer State Health LastReplicationTime - ------ ------------- ----- ------ ------------------- - WIN10-TEST SRV16-TEST.test.lan Replicating Normal 4/13/2023 8:40:04 AM - Win10-TEST2 SRV16-TEST.test.lan Suspended Warning 4/13/2023 8:32:06 AM - -PARAMETER: -FromCustomField "ReplaceMeWithAnyIntegerCustomField" - Name of an integer custom field that contains your desired FailedFor threshold. - ex. "ReplicationAlertThreshold" where you have entered in your desired alert limit in the "ReplicationAlertThreshold" custom field rather than in a parameter. -.EXAMPLE - -FromCustomField "ReplaceMeWithAnyIntegerCustomField" - WARNING: Some of the vm's currently have replication paused! - - - VMName PrimaryServer State Health LastReplicationTime - ------ ------------- ----- ------ ------------------- - WIN10-TEST SRV16-TEST.test.lan Replicating Normal 4/13/2023 8:40:04 AM - Win10-TEST2 SRV16-TEST.test.lan Suspended Warning 4/13/2023 8:32:06 AM -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 10, Server 2016 - Release Notes: Updated Calculated Name -#> - -[CmdletBinding()] -param ( - [Parameter()] - [int]$FailedFor = "60", - [Parameter()] - [String]$FromCustomField, - [Parameter()] - [Switch]$IncludePaused = [System.Convert]::ToBoolean($env:includePausedReplications) -) -begin { - - if ($env:allowedToFailForXMinutes -and $env:allowedToFailForXMinutes -notlike "null") { $FailedFor = $env:allowedToFailForXMinutes } - if ($env:retrieveAllowedFailureTimeFromCustomField -and $env:retrieveAllowedFailureTimeFromCustomField -notlike "null" ) { $FromCustomField = $env:retrieveAllowedFailureTimeFromCustomField } - - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - function Test-IsSystem { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - return $id.Name -like "NT AUTHORITY*" -or $id.IsSystem - } - - if (!(Test-IsElevated) -and !(Test-IsSystem)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - # This function is to make it easier to parse Ninja Custom Fields. - function Get-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter()] - [String]$DocumentName - ) - - # If we're requested to get the field value from a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # These two types require more information to parse. - $NeedsOptions = "DropDown","MultiSelect" - - # Grabbing document values requires a slightly different command. - if ($DocumentName) { - # Secure fields are only readable when they're a device custom field - if ($Type -Like "Secure") { throw "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - Write-Host "Retrieving value from Ninja Document..." - $NinjaPropertyValue = Ninja-Property-Docs-Get -AttributeName $Name @DocumentationParams 2>&1 - - # Certain fields require more information to parse. - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - # We'll redirect error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1 - - # Certain fields require more information to parse. - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If we received some sort of error it should have an exception property and we'll exit the function with that error information. - if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # This switch will compare the type given with the quoted string. If it matches, it'll parse it further; otherwise, the default option will be selected. - switch ($Type) { - "Attachment" { - # Attachments come in a JSON format this will convert it into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Checkbox" { - # Checkbox's come in as a string representing an integer. We'll need to cast that string into an integer and then convert it to a more traditional boolean. - [System.Convert]::ToBoolean([int]$NinjaPropertyValue) - } - "Date or Date Time" { - # In Ninja Date and Date/Time fields are in Unix Epoch time in the UTC timezone the below should convert it into local time as a datetime object. - $UnixTimeStamp = $NinjaPropertyValue - $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp) - $TimeZone = [TimeZoneInfo]::Local - [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) - } - "Decimal" { - # In ninja decimals are strings that represent a decimal this will cast it into a double data type. - [double]$NinjaPropertyValue - } - "Device Dropdown" { - # Device Drop-Downs Fields come in a JSON format this will convert it into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Device MultiSelect" { - # Device Multi-Select Fields come in a JSON format this will convert it into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Dropdown" { - # Drop-Down custom fields come in as a comma-separated list of GUIDs; we'll compare these with all the options and return just the option values selected instead of a GUID. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name - } - "Integer" { - # Cast's the Ninja provided string into an integer. - if($NinjaPropertyValue){ - [int]$NinjaPropertyValue - }else{ - $NinjaPropertyValue - } - } - "MultiSelect" { - # Multi-Select custom fields come in as a comma-separated list of GUID's we'll compare these with all the options and return just the option values selected instead of a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = ($NinjaPropertyValue -split ',').trim() - - foreach ($Item in $Selection) { - $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name - } - } - "Organization Dropdown" { - # Turns the Ninja provided JSON into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization Location Dropdown" { - # Turns the Ninja provided JSON into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization Location MultiSelect" { - # Turns the Ninja provided JSON into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization MultiSelect" { - # Turns the Ninja provided JSON into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Time" { - # Time fields are given as a number of seconds starting from midnight. This will convert it into a datetime object. - $Seconds = $NinjaPropertyValue - $UTC = ([timespan]::fromseconds($Seconds)).ToString("hh\:mm\:ss") - $TimeZone = [TimeZoneInfo]::Local - $ConvertedTime = [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) - - Get-Date $ConvertedTime -DisplayHint Time - } - default { - # If no type was given or not one that matches the above types just output what we retrieved. - $NinjaPropertyValue - } - } - } -} -process { - if ($FromCustomField) { - try{ - $CustomFieldValue = Get-NinjaProperty -Name $FromCustomField -Type "Integer" - }catch{ - Write-Warning "$($_.ToString())" - } - - if($CustomFieldValue){ - $FailedFor = $CustomFieldValue - }else{ - Write-Warning "Custom Field $FromCustomField was empty?" - } - } - - if($FailedFor -gt 0){ - $FailedFor = $FailedFor * -1 - } - - $Threshold = (Get-Date).AddMinutes($FailedFor) - Write-Host "Checking vm's that have not replicated prior to $Threshold." - - $Failed = New-Object System.Collections.Generic.List[string] - $UnhealthyVMs = Get-VMReplication | Where-Object { $_.Health -notlike "Normal" -and $_.LastReplicationTime -lt $Threshold -and $_.State -notlike "Suspended" } - $PausedVMs = Get-VMReplication | Where-Object { $_.LastReplicationTime -lt $Threshold -and $_.State -like "Suspended" } - - if ($UnhealthyVMs) { - $Failed.Add($UnhealthyVMs) - } - - if ($PausedVMs) { - Write-Warning "Some of the vm's currently have replication paused!" - - if(-not $IncludePaused){ - Write-Warning "Please use 'Include Paused Replications' to include paused replications in the alert. Otherwise, they will be skipped." - } - } - - if ($PausedVMs -and $IncludePaused) { - $Failed.Add($PausedVMs) - } - - if ($Failed) { - Write-Error "Hyper-V Replication is currently failing!" - Get-VMReplication | Format-Table -Property VMName, PrimaryServer, State, Health, LastReplicationTime | Out-String | Write-Host - exit 1 - } - else { - Write-Host "No failing replications detected prior to $Threshold." - - Get-VMReplication | Format-Table -Property VMName, PrimaryServer, State, Health, LastReplicationTime | Out-String | Write-Host - exit 0 - } -}end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + This will get information about the current status of Hyper-V Replication. If its abnormal it'll check the last replication time to see if it should alert on it. +.DESCRIPTION + This will get information about the current status of Hyper-V Replication. If its abnormal it'll check the last replication time to see if it should alert on it. +.EXAMPLE + (No Parameters) + Replication is currently failing! + + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorExcep + tion + + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorExceptio + n,customscript_gen_2.ps1 + + + VMName PrimaryServer State Health LastReplicationTime + ------ ------------- ----- ------ ------------------- + WIN10-TEST SRV16-TEST.test.lan Error Critical 4/13/2023 8:20:11 AM + Win10-TEST2 SRV16-TEST.test.lan Error Critical 4/13/2023 8:20:11 AM + +PARAMETER: -FailedFor "30" + Time in minutes any given vm replication is allowed to be abnormal. + Ex. "20" will alert on a vm replication after its been in the abnormal state for 20 minutes. +.EXAMPLE + -FailedFor "20" + WARNING: Some of the vm's currently have replication paused! + + + VMName PrimaryServer State Health LastReplicationTime + ------ ------------- ----- ------ ------------------- + WIN10-TEST SRV16-TEST.test.lan Replicating Normal 4/13/2023 8:40:04 AM + Win10-TEST2 SRV16-TEST.test.lan Suspended Warning 4/13/2023 8:32:06 AM + +PARAMETER: -IncludePaused + Script will consider paused vm's abnormal if this parameter is used. +.EXAMPLE + -IncludePaused + Replication is currently failing! + + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorExcep + tion + + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorExceptio + n,customscript_gen_2.ps1 + + + VMName PrimaryServer State Health LastReplicationTime + ------ ------------- ----- ------ ------------------- + WIN10-TEST SRV16-TEST.test.lan Replicating Normal 4/13/2023 8:40:04 AM + Win10-TEST2 SRV16-TEST.test.lan Suspended Warning 4/13/2023 8:32:06 AM + +PARAMETER: -FromCustomField "ReplaceMeWithAnyIntegerCustomField" + Name of an integer custom field that contains your desired FailedFor threshold. + ex. "ReplicationAlertThreshold" where you have entered in your desired alert limit in the "ReplicationAlertThreshold" custom field rather than in a parameter. +.EXAMPLE + -FromCustomField "ReplaceMeWithAnyIntegerCustomField" + WARNING: Some of the vm's currently have replication paused! + + + VMName PrimaryServer State Health LastReplicationTime + ------ ------------- ----- ------ ------------------- + WIN10-TEST SRV16-TEST.test.lan Replicating Normal 4/13/2023 8:40:04 AM + Win10-TEST2 SRV16-TEST.test.lan Suspended Warning 4/13/2023 8:32:06 AM +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 10, Server 2016 + Release Notes: Updated Calculated Name +#> + +[CmdletBinding()] +param ( + [Parameter()] + [int]$FailedFor = "60", + [Parameter()] + [String]$FromCustomField, + [Parameter()] + [Switch]$IncludePaused = [System.Convert]::ToBoolean($env:includePausedReplications) +) +begin { + + if ($env:allowedToFailForXMinutes -and $env:allowedToFailForXMinutes -notlike "null") { $FailedFor = $env:allowedToFailForXMinutes } + if ($env:retrieveAllowedFailureTimeFromCustomField -and $env:retrieveAllowedFailureTimeFromCustomField -notlike "null" ) { $FromCustomField = $env:retrieveAllowedFailureTimeFromCustomField } + + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + function Test-IsSystem { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + return $id.Name -like "NT AUTHORITY*" -or $id.IsSystem + } + + if (!(Test-IsElevated) -and !(Test-IsSystem)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + # This function is to make it easier to parse Ninja Custom Fields. + function Get-NinjaProperty { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [String]$Name, + [Parameter()] + [String]$Type, + [Parameter()] + [String]$DocumentName + ) + + # If we're requested to get the field value from a Ninja document we'll specify it here. + $DocumentationParams = @{} + if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } + + # These two types require more information to parse. + $NeedsOptions = "DropDown","MultiSelect" + + # Grabbing document values requires a slightly different command. + if ($DocumentName) { + # Secure fields are only readable when they're a device custom field + if ($Type -Like "Secure") { throw "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } + + # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. + Write-Host "Retrieving value from Ninja Document..." + # $NinjaPropertyValue = Ninja-Property-Docs-Get -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + + # Certain fields require more information to parse. + if ($NeedsOptions -contains $Type) { + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + } + } + else { + # We'll redirect error output to the success stream to make it easier to error out if nothing was found or something else went wrong. + # $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1 # Removed NinjaOne dependency + + # Certain fields require more information to parse. + if ($NeedsOptions -contains $Type) { + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + } + } + + # If we received some sort of error it should have an exception property and we'll exit the function with that error information. + if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } + if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } + + # This switch will compare the type given with the quoted string. If it matches, it'll parse it further; otherwise, the default option will be selected. + switch ($Type) { + "Attachment" { + # Attachments come in a JSON format this will convert it into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Checkbox" { + # Checkbox's come in as a string representing an integer. We'll need to cast that string into an integer and then convert it to a more traditional boolean. + [System.Convert]::ToBoolean([int]$NinjaPropertyValue) + } + "Date or Date Time" { + # In Ninja Date and Date/Time fields are in Unix Epoch time in the UTC timezone the below should convert it into local time as a datetime object. + $UnixTimeStamp = $NinjaPropertyValue + $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp) + $TimeZone = [TimeZoneInfo]::Local + [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) + } + "Decimal" { + # In ninja decimals are strings that represent a decimal this will cast it into a double data type. + [double]$NinjaPropertyValue + } + "Device Dropdown" { + # Device Drop-Downs Fields come in a JSON format this will convert it into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Device MultiSelect" { + # Device Multi-Select Fields come in a JSON format this will convert it into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Dropdown" { + # Drop-Down custom fields come in as a comma-separated list of GUIDs; we'll compare these with all the options and return just the option values selected instead of a GUID. + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name + } + "Integer" { + # Cast's the Ninja provided string into an integer. + if($NinjaPropertyValue){ + [int]$NinjaPropertyValue + }else{ + $NinjaPropertyValue + } + } + "MultiSelect" { + # Multi-Select custom fields come in as a comma-separated list of GUID's we'll compare these with all the options and return just the option values selected instead of a guid. + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Selection = ($NinjaPropertyValue -split ',').trim() + + foreach ($Item in $Selection) { + $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name + } + } + "Organization Dropdown" { + # Turns the Ninja provided JSON into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization Location Dropdown" { + # Turns the Ninja provided JSON into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization Location MultiSelect" { + # Turns the Ninja provided JSON into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization MultiSelect" { + # Turns the Ninja provided JSON into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Time" { + # Time fields are given as a number of seconds starting from midnight. This will convert it into a datetime object. + $Seconds = $NinjaPropertyValue + $UTC = ([timespan]::fromseconds($Seconds)).ToString("hh\:mm\:ss") + $TimeZone = [TimeZoneInfo]::Local + $ConvertedTime = [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) + + Get-Date $ConvertedTime -DisplayHint Time + } + default { + # If no type was given or not one that matches the above types just output what we retrieved. + $NinjaPropertyValue + } + } + } +} +process { + if ($FromCustomField) { + try{ + $CustomFieldValue = Get-NinjaProperty -Name $FromCustomField -Type "Integer" + }catch{ + Write-Warning "$($_.ToString())" + } + + if($CustomFieldValue){ + $FailedFor = $CustomFieldValue + }else{ + Write-Warning "Custom Field $FromCustomField was empty?" + } + } + + if($FailedFor -gt 0){ + $FailedFor = $FailedFor * -1 + } + + $Threshold = (Get-Date).AddMinutes($FailedFor) + Write-Host "Checking vm's that have not replicated prior to $Threshold." + + $Failed = New-Object System.Collections.Generic.List[string] + $UnhealthyVMs = Get-VMReplication | Where-Object { $_.Health -notlike "Normal" -and $_.LastReplicationTime -lt $Threshold -and $_.State -notlike "Suspended" } + $PausedVMs = Get-VMReplication | Where-Object { $_.LastReplicationTime -lt $Threshold -and $_.State -like "Suspended" } + + if ($UnhealthyVMs) { + $Failed.Add($UnhealthyVMs) + } + + if ($PausedVMs) { + Write-Warning "Some of the vm's currently have replication paused!" + + if(-not $IncludePaused){ + Write-Warning "Please use 'Include Paused Replications' to include paused replications in the alert. Otherwise, they will be skipped." + } + } + + if ($PausedVMs -and $IncludePaused) { + $Failed.Add($PausedVMs) + } + + if ($Failed) { + Write-Error "Hyper-V Replication is currently failing!" + Get-VMReplication | Format-Table -Property VMName, PrimaryServer, State, Health, LastReplicationTime | Out-String | Write-Host + exit 1 + } + else { + Write-Host "No failing replications detected prior to $Threshold." + + Get-VMReplication | Format-Table -Property VMName, PrimaryServer, State, Health, LastReplicationTime | Out-String | Write-Host + exit 0 + } +}end { + + + +} + diff --git a/Powershell Scripts/Hyper-V - Shared Volume Disk Space Alert.ps1 b/Powershell Scripts/Hyper-V - Shared Volume Disk Space Alert.ps1 index ababc63..17a2d37 100644 --- a/Powershell Scripts/Hyper-V - Shared Volume Disk Space Alert.ps1 +++ b/Powershell Scripts/Hyper-V - Shared Volume Disk Space Alert.ps1 @@ -1,258 +1,258 @@ # Hyper-V Monitor shared volume disk free space. Must be ran as a Local or Domain Admin user. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Hyper-V Monitor shared volume disk free space. Must be ran as a Local or Domain Admin user. -.DESCRIPTION - Hyper-V Monitor shared volume disk free space. Must be ran as a Local or Domain Admin user. - -.EXAMPLE - (No Parameters) - ## EXAMPLE OUTPUT WITHOUT PARAMS ## -Name Path Size(GB) FreeSpace(GB) UsedSpace(GB) PercentFree ----- ---- -------- ------------- ------------- ----------- -Cluster Virtual Disk (vd1) C:\ClusterStorage\vd1 3,068.98 168.77 2900.21 9.99 - -PARAMETER: -MinimumPercentage 20 - Only errors when any shared volume disk is below the specified percentage. - Defaults to 10 percent. -.EXAMPLE - -MinimumPercentage 20 - ## EXAMPLE OUTPUT WITH MinimumPercentage ## -Name Path Size(GB) FreeSpace(GB) UsedSpace(GB) PercentFree ----- ---- -------- ------------- ------------- ----------- -Cluster Virtual Disk (vd1) C:\ClusterStorage\vd1 3,068.98 168.77 2900.21 9.99 - -PARAMETER: -MinimumFreeBytes 1073741824 - Only errors when any shared volume disk is below the specified percentage. - Defaults to 1GB or 1073741824 bytes. -.EXAMPLE - -MinimumFreeBytes 1073741824 - ## EXAMPLE OUTPUT WITH MinimumFreeBytes ## -Name Path Size(GB) FreeSpace(GB) UsedSpace(GB) PercentFree ----- ---- -------- ------------- ------------- ----------- -Cluster Virtual Disk (vd1) C:\ClusterStorage\vd1 3,068.98 168.77 2900.21 9.99 - -PARAMETER: -ExcludeDrivesByName MyDisk - Excludes drives that contains the text MyDisk in its name. -.EXAMPLE - -ExcludeDrivesByName 1073741824 - ## EXAMPLE OUTPUT WITH ExcludeDrivesByName ## -Name Path Size(GB) FreeSpace(GB) UsedSpace(GB) PercentFree ----- ---- -------- ------------- ------------- ----------- -Cluster Virtual Disk (vd1) C:\ClusterStorage\vd1 3,068.98 168.77 2900.21 9.99 - -PARAMETER: -ExcludeDrivesByPath C:\ClusterStorage\MyDisk - Excludes drives that contains the text MyDisk in its name. -.EXAMPLE - -ExcludeDrivesByPath C:\ClusterStorage\MyDisk -.EXAMPLE - -ExcludeDrivesByPath MyDisk - ## EXAMPLE OUTPUT WITH ExcludeDrivesByPath ## -Name Path Size(GB) FreeSpace(GB) UsedSpace(GB) PercentFree ----- ---- -------- ------------- ------------- ----------- -Cluster Virtual Disk (vd1) C:\ClusterStorage\vd1 3,068.98 168.77 2900.21 9.99 - -PARAMETER: -CustomFieldParam "ReplaceMeWithAnyMultilineCustomField" - Saves the results to a multi-line string custom field. -.EXAMPLE - -CustomFieldParam "ReplaceMeWithAnyMultilineCustomField" - ## EXAMPLE OUTPUT WITH CustomFieldParam ## -Name Path Size(GB) FreeSpace(GB) UsedSpace(GB) PercentFree ----- ---- -------- ------------- ------------- ----------- -Cluster Virtual Disk (vd1) C:\ClusterStorage\vd1 3,068.98 168.77 2900.21 9.99 -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [int]$MinimumPercentage = 10, - $MinimumFreeBytes = 1GB, - [String]$ExcludeDrivesByName, - [String]$ExcludeDrivesByPath, - [string]$CustomFieldParam -) - -begin { - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - function Test-IsSystem { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - return $id.Name -like "NT AUTHORITY*" -or $id.IsSystem - } - function Get-FriendlySize { - param($Bytes) - # Converts Bytes to the highest matching unit - $Sizes = 'Bytes,KB,MB,GB,TB,PB,EB,ZB' -split ',' - for ($i = 0; ($Bytes -ge 1kb) -and ($i -lt $Sizes.Count); $i++) { $Bytes /= 1kb } - $N = 2 - if ($i -eq 0) { $N = 0 } - if ($Bytes) { "$([System.Math]::Round($Bytes,$N)) $($Sizes[$i])" }else { "0 B" } - } - function Get-Size { - param ( - [string]$String, - [ValidateSet("PB", "TB", "GB", "MB", "KB", "B", "Bytes")][string]$DefaultSize = "GB" - ) - switch -wildcard ($String) { - '*PB' { [int64]$($String -replace '[^\d+]+') * 1PB; break } - '*TB' { [int64]$($String -replace '[^\d+]+') * 1TB; break } - '*GB' { [int64]$($String -replace '[^\d+]+') * 1GB; break } - '*MB' { [int64]$($String -replace '[^\d+]+') * 1MB; break } - '*KB' { [int64]$($String -replace '[^\d+]+') * 1KB; break } - '*B' { [int64]$($String -replace '[^\d+]+') * 1; break } - '*Bytes' { [int64]$($String -replace '[^\d+]+') * 1; break } - Default { Get-Size -String "$String $DefaultSize" } - } - } - function Invoke-FilterDisks { - [CmdletBinding()] - param( - [parameter(ValueFromPipeline = $true)] - $Disks - ) - process { - $Disks | ForEach-Object { - # Check if exclude by name is needed - if ($([string]::IsNullOrEmpty($ExcludeDrivesByName) -or [string]::IsNullOrWhiteSpace($ExcludeDrivesByName))) { - $_ - } - else { - if ( - $_.Name -like "*$ExcludeDrivesByName*" - ) { - # Output Nothing - } - else { - $_ - } - } - } | ForEach-Object { - # Check if exclude by name is needed - if ($([string]::IsNullOrEmpty($ExcludeDrivesByPath) -or [string]::IsNullOrWhiteSpace($ExcludeDrivesByPath))) { - $_ - } - else { - if ( - $_.Path -like "*$ExcludeDrivesByPath*" - ) { - # Output Nothing - } - else { - $_ - } - } - } - } - } - if ($env:MinimumPercentage) { - $MinimumPercentage = $env:MinimumPercentage - } - if ($env:minimumFreeSpace) { - $MinimumFreeBytes = Get-Size -String $env:minimumFreeSpace - } - if ($env:ExcludeDrivesByName) { - $ExcludeDrivesByName = $env:ExcludeDrivesByName - } - if ($env:ExcludeDrivesByPath) { - $ExcludeDrivesByPath = $env:ExcludeDrivesByPath - } - if ($env:CustomFieldParam) { - $CustomFieldParam = $env:CustomFieldParam - } -} -process { - if (Test-IsSystem) { - Write-Error -Message "Access Denied. Please run with a Domain Account or a Local Account that has permissions to access this node." - exit 1 - } - - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - Import-Module FailoverClusters -ErrorAction SilentlyContinue - - if (-not $(Get-Command -Name "Get-Cluster" -ErrorAction SilentlyContinue)) { - Write-Error "[Error] Must run this script on a server that is apart of a Cluster or can communicate with a Cluster." - exit 1 - } - - try { - Get-ClusterNode -ErrorAction Stop | Out-Null - } - catch { - Write-Error "[Error] Failed to get Cluster Nodes." - exit 1 - } - - # Get Cluster Shared Volume Info - $Volumes = foreach ( $csv in $(Get-ClusterSharedVolume) ) { - foreach ( $csvinfo in $($csv | Select-Object -Property Name -ExpandProperty SharedVolumeInfo) ) { - [PSCustomObject]@{ - Name = $csv.Name - Path = $csvinfo.FriendlyVolumeName - Size = $csvinfo.Partition.Size - FreeSpace = $csvinfo.Partition.FreeSpace - UsedSpace = $csvinfo.Partition.UsedSpace - PercentFree = $csvinfo.Partition.PercentFree - } - } - } - - # Prep Format-Table substitutions - $Size = @{ Label = "Size(GB)" ; Expression = { (Get-FriendlySize -Bytes $_.Size) } } - $FreeSpace = @{ Label = "FreeSpace(GB)" ; Expression = { (Get-FriendlySize -Bytes $_.FreeSpace) } } - $UsedSpace = @{ Label = "UsedSpace(GB)" ; Expression = { (Get-FriendlySize -Bytes $_.UsedSpace) } } - $PercentFree = @{ Label = "PercentFree" ; Expression = { ($_.PercentFree) } } - # Sort disks by FreeSpace - $Disks = $Volumes | Sort-Object FreeSpace - # Save results as a string - $DisksFormattedString = $Disks | Format-Table -AutoSize Name, Path, $Size, $FreeSpace, $UsedSpace, $PercentFree | Out-String - - # If using a custom field sent that to the specified custom field, should be a multi-line - if ($CustomFieldParam) { - Ninja-Property-Set -Name $CustomFieldParam -Value $DisksFormattedString - } - - # Loop through each disk - $DiskUnderPercentage = $Disks | Invoke-FilterDisks | Where-Object { $_.PercentFree -lt $MinimumPercentage } - $DiskUnderFreeBytes = $Disks | Invoke-FilterDisks | Where-Object { $_.FreeSpace -lt $MinimumFreeBytes } - - if ($DiskUnderPercentage -or $DiskUnderFreeBytes) { - if ($DiskUnderPercentage) { - Write-Host "[Issue] One or more Disks under $MinimumPercentage % free!" - } - if ($DiskUnderFreeBytes) { - Write-Host "[Issue] One or more Disks under $(Get-FriendlySize -Bytes $MinimumFreeBytes) free!" - } - $DisksFormattedString | Write-Host - exit 1 - } - - # List all shared volumes - if (-not $DiskUnderPercentage) { - Write-Host "[Info] One or more Disks over $MinimumPercentage % free." - } - if (-not $DiskUnderFreeBytes) { - Write-Host "[Info] One or more Disks over $(Get-FriendlySize -Bytes $MinimumFreeBytes) free." - } - $DisksFormattedString | Write-Host - exit 0 -} -end { - - - -} - - +#Requires -Version 5.1 + +<# +.SYNOPSIS + Hyper-V Monitor shared volume disk free space. Must be ran as a Local or Domain Admin user. +.DESCRIPTION + Hyper-V Monitor shared volume disk free space. Must be ran as a Local or Domain Admin user. + +.EXAMPLE + (No Parameters) + ## EXAMPLE OUTPUT WITHOUT PARAMS ## +Name Path Size(GB) FreeSpace(GB) UsedSpace(GB) PercentFree +---- ---- -------- ------------- ------------- ----------- +Cluster Virtual Disk (vd1) C:\ClusterStorage\vd1 3,068.98 168.77 2900.21 9.99 + +PARAMETER: -MinimumPercentage 20 + Only errors when any shared volume disk is below the specified percentage. + Defaults to 10 percent. +.EXAMPLE + -MinimumPercentage 20 + ## EXAMPLE OUTPUT WITH MinimumPercentage ## +Name Path Size(GB) FreeSpace(GB) UsedSpace(GB) PercentFree +---- ---- -------- ------------- ------------- ----------- +Cluster Virtual Disk (vd1) C:\ClusterStorage\vd1 3,068.98 168.77 2900.21 9.99 + +PARAMETER: -MinimumFreeBytes 1073741824 + Only errors when any shared volume disk is below the specified percentage. + Defaults to 1GB or 1073741824 bytes. +.EXAMPLE + -MinimumFreeBytes 1073741824 + ## EXAMPLE OUTPUT WITH MinimumFreeBytes ## +Name Path Size(GB) FreeSpace(GB) UsedSpace(GB) PercentFree +---- ---- -------- ------------- ------------- ----------- +Cluster Virtual Disk (vd1) C:\ClusterStorage\vd1 3,068.98 168.77 2900.21 9.99 + +PARAMETER: -ExcludeDrivesByName MyDisk + Excludes drives that contains the text MyDisk in its name. +.EXAMPLE + -ExcludeDrivesByName 1073741824 + ## EXAMPLE OUTPUT WITH ExcludeDrivesByName ## +Name Path Size(GB) FreeSpace(GB) UsedSpace(GB) PercentFree +---- ---- -------- ------------- ------------- ----------- +Cluster Virtual Disk (vd1) C:\ClusterStorage\vd1 3,068.98 168.77 2900.21 9.99 + +PARAMETER: -ExcludeDrivesByPath C:\ClusterStorage\MyDisk + Excludes drives that contains the text MyDisk in its name. +.EXAMPLE + -ExcludeDrivesByPath C:\ClusterStorage\MyDisk +.EXAMPLE + -ExcludeDrivesByPath MyDisk + ## EXAMPLE OUTPUT WITH ExcludeDrivesByPath ## +Name Path Size(GB) FreeSpace(GB) UsedSpace(GB) PercentFree +---- ---- -------- ------------- ------------- ----------- +Cluster Virtual Disk (vd1) C:\ClusterStorage\vd1 3,068.98 168.77 2900.21 9.99 + +PARAMETER: -CustomFieldParam "ReplaceMeWithAnyMultilineCustomField" + Saves the results to a multi-line string custom field. +.EXAMPLE + -CustomFieldParam "ReplaceMeWithAnyMultilineCustomField" + ## EXAMPLE OUTPUT WITH CustomFieldParam ## +Name Path Size(GB) FreeSpace(GB) UsedSpace(GB) PercentFree +---- ---- -------- ------------- ------------- ----------- +Cluster Virtual Disk (vd1) C:\ClusterStorage\vd1 3,068.98 168.77 2900.21 9.99 +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [int]$MinimumPercentage = 10, + $MinimumFreeBytes = 1GB, + [String]$ExcludeDrivesByName, + [String]$ExcludeDrivesByPath, + [string]$CustomFieldParam +) + +begin { + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + function Test-IsSystem { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + return $id.Name -like "NT AUTHORITY*" -or $id.IsSystem + } + function Get-FriendlySize { + param($Bytes) + # Converts Bytes to the highest matching unit + $Sizes = 'Bytes,KB,MB,GB,TB,PB,EB,ZB' -split ',' + for ($i = 0; ($Bytes -ge 1kb) -and ($i -lt $Sizes.Count); $i++) { $Bytes /= 1kb } + $N = 2 + if ($i -eq 0) { $N = 0 } + if ($Bytes) { "$([System.Math]::Round($Bytes,$N)) $($Sizes[$i])" }else { "0 B" } + } + function Get-Size { + param ( + [string]$String, + [ValidateSet("PB", "TB", "GB", "MB", "KB", "B", "Bytes")][string]$DefaultSize = "GB" + ) + switch -wildcard ($String) { + '*PB' { [int64]$($String -replace '[^\d+]+') * 1PB; break } + '*TB' { [int64]$($String -replace '[^\d+]+') * 1TB; break } + '*GB' { [int64]$($String -replace '[^\d+]+') * 1GB; break } + '*MB' { [int64]$($String -replace '[^\d+]+') * 1MB; break } + '*KB' { [int64]$($String -replace '[^\d+]+') * 1KB; break } + '*B' { [int64]$($String -replace '[^\d+]+') * 1; break } + '*Bytes' { [int64]$($String -replace '[^\d+]+') * 1; break } + Default { Get-Size -String "$String $DefaultSize" } + } + } + function Invoke-FilterDisks { + [CmdletBinding()] + param( + [parameter(ValueFromPipeline = $true)] + $Disks + ) + process { + $Disks | ForEach-Object { + # Check if exclude by name is needed + if ($([string]::IsNullOrEmpty($ExcludeDrivesByName) -or [string]::IsNullOrWhiteSpace($ExcludeDrivesByName))) { + $_ + } + else { + if ( + $_.Name -like "*$ExcludeDrivesByName*" + ) { + # Output Nothing + } + else { + $_ + } + } + } | ForEach-Object { + # Check if exclude by name is needed + if ($([string]::IsNullOrEmpty($ExcludeDrivesByPath) -or [string]::IsNullOrWhiteSpace($ExcludeDrivesByPath))) { + $_ + } + else { + if ( + $_.Path -like "*$ExcludeDrivesByPath*" + ) { + # Output Nothing + } + else { + $_ + } + } + } + } + } + if ($env:MinimumPercentage) { + $MinimumPercentage = $env:MinimumPercentage + } + if ($env:minimumFreeSpace) { + $MinimumFreeBytes = Get-Size -String $env:minimumFreeSpace + } + if ($env:ExcludeDrivesByName) { + $ExcludeDrivesByName = $env:ExcludeDrivesByName + } + if ($env:ExcludeDrivesByPath) { + $ExcludeDrivesByPath = $env:ExcludeDrivesByPath + } + if ($env:CustomFieldParam) { + $CustomFieldParam = $env:CustomFieldParam + } +} +process { + if (Test-IsSystem) { + Write-Error -Message "Access Denied. Please run with a Domain Account or a Local Account that has permissions to access this node." + exit 1 + } + + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + Import-Module FailoverClusters -ErrorAction SilentlyContinue + + if (-not $(Get-Command -Name "Get-Cluster" -ErrorAction SilentlyContinue)) { + Write-Error "[Error] Must run this script on a server that is apart of a Cluster or can communicate with a Cluster." + exit 1 + } + + try { + Get-ClusterNode -ErrorAction Stop | Out-Null + } + catch { + Write-Error "[Error] Failed to get Cluster Nodes." + exit 1 + } + + # Get Cluster Shared Volume Info + $Volumes = foreach ( $csv in $(Get-ClusterSharedVolume) ) { + foreach ( $csvinfo in $($csv | Select-Object -Property Name -ExpandProperty SharedVolumeInfo) ) { + [PSCustomObject]@{ + Name = $csv.Name + Path = $csvinfo.FriendlyVolumeName + Size = $csvinfo.Partition.Size + FreeSpace = $csvinfo.Partition.FreeSpace + UsedSpace = $csvinfo.Partition.UsedSpace + PercentFree = $csvinfo.Partition.PercentFree + } + } + } + + # Prep Format-Table substitutions + $Size = @{ Label = "Size(GB)" ; Expression = { (Get-FriendlySize -Bytes $_.Size) } } + $FreeSpace = @{ Label = "FreeSpace(GB)" ; Expression = { (Get-FriendlySize -Bytes $_.FreeSpace) } } + $UsedSpace = @{ Label = "UsedSpace(GB)" ; Expression = { (Get-FriendlySize -Bytes $_.UsedSpace) } } + $PercentFree = @{ Label = "PercentFree" ; Expression = { ($_.PercentFree) } } + # Sort disks by FreeSpace + $Disks = $Volumes | Sort-Object FreeSpace + # Save results as a string + $DisksFormattedString = $Disks | Format-Table -AutoSize Name, Path, $Size, $FreeSpace, $UsedSpace, $PercentFree | Out-String + + # If using a custom field sent that to the specified custom field, should be a multi-line + if ($CustomFieldParam) { + # Ninja-Property-Set -Name $CustomFieldParam -Value $DisksFormattedString # Removed NinjaOne dependency + } + + # Loop through each disk + $DiskUnderPercentage = $Disks | Invoke-FilterDisks | Where-Object { $_.PercentFree -lt $MinimumPercentage } + $DiskUnderFreeBytes = $Disks | Invoke-FilterDisks | Where-Object { $_.FreeSpace -lt $MinimumFreeBytes } + + if ($DiskUnderPercentage -or $DiskUnderFreeBytes) { + if ($DiskUnderPercentage) { + Write-Host "[Issue] One or more Disks under $MinimumPercentage % free!" + } + if ($DiskUnderFreeBytes) { + Write-Host "[Issue] One or more Disks under $(Get-FriendlySize -Bytes $MinimumFreeBytes) free!" + } + $DisksFormattedString | Write-Host + exit 1 + } + + # List all shared volumes + if (-not $DiskUnderPercentage) { + Write-Host "[Info] One or more Disks over $MinimumPercentage % free." + } + if (-not $DiskUnderFreeBytes) { + Write-Host "[Info] One or more Disks over $(Get-FriendlySize -Bytes $MinimumFreeBytes) free." + } + $DisksFormattedString | Write-Host + exit 0 +} +end { + + + +} + + diff --git a/Powershell Scripts/Install Certificate - Windows.ps1 b/Powershell Scripts/Install Certificate - Windows.ps1 index cc4438d..977bb35 100644 --- a/Powershell Scripts/Install Certificate - Windows.ps1 +++ b/Powershell Scripts/Install Certificate - Windows.ps1 @@ -1,744 +1,744 @@ # Installs a given certificate to the selected location. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Installs a given certificate to the selected location. - -.DESCRIPTION - Installs a given certificate to the selected location. - Running this script as SYSTEM will install the certificate to the LocalMachine certificate store. - Running this script as a user will install the certificate to the CurrentUser certificate store of that user. - - Note that this will install ALL certificates in a collection to the specified store. If you need to install - specific certificates to different stores, you will need to break apart the collection and - install each certificate individually. - -.PARAMETER CertificatePath - The file path or URL to the certificate file. - -.PARAMETER CertificateStore - The certificate store to install the certificate to. - Supported values: - "Personal" - "Trusted Root Certification Authorities" - "Third-Party Root Certification Authorities" - "Trusted Publisher" - "Intermediate Certification Authorities" - "Untrusted Certificates" - "Trusted People" - "Other People" - -.PARAMETER CertificatePassword - The password for the certificate. - Running from NinjaRMM, this can be stored in a custom field and retrieved using the "Certificate Password Custom Field Name" Script Variable. - -.PARAMETER OverwriteCertificateIfExisting - Overwrite the certificate if it already exists. - This removes any certificates that have a matching Thumbprint in the specified certificate store, - e.g. Cert was installed in Personal, but needs to be replaced. - -.EXAMPLE - -CertificatePath 'C:\certs\mycert.pfx' -CertificateStore 'Personal' -CertificatePassword 'password' - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial release -#> - -[CmdletBinding()] -param ( - [string]$CertificatePath, - [string]$CertificateStore, - [string]$CertificatePassword, - [switch]$OverwriteCertificateIfExisting -) - -begin { - - function Test-IsSystem { - # Get the current Windows identity of the user running the script - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - - # Check if the current identity's name matches "NT AUTHORITY*" - # or if the identity represents the SYSTEM account - return $id.Name -like "NT AUTHORITY*" -or $id.IsSystem - } - function Invoke-Download { - param( - [Parameter()] - [String]$URL, - [Parameter()] - [String]$Path, - [Parameter()] - [int]$Attempts = 3, - [Parameter()] - [Switch]$SkipSleep - ) - - # Display the URL being used for the download - Write-Host -Object "URL '$URL' was given." - Write-Host -Object "Downloading the file..." - - # Determine the supported TLS versions and set the appropriate security protocol - $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType') - if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12 - } - elseif ( $SupportedTLSversions -contains 'Tls12' ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 - } - else { - # Warn the user if TLS 1.2 and 1.3 are not supported, which may cause the download to fail - Write-Warning "TLS 1.2 and/or TLS 1.3 are not supported on this system. This download may fail!" - if ($PSVersionTable.PSVersion.Major -lt 3) { - Write-Warning "PowerShell 2 / .NET 2.0 doesn't support TLS 1.2." - } - } - - # Initialize the attempt counter - $i = 1 - While ($i -le $Attempts) { - # If SkipSleep is not set, wait for a random time between 3 and 15 seconds before each attempt - if (!($SkipSleep)) { - $SleepTime = Get-Random -Minimum 3 -Maximum 15 - Write-Host "Waiting for $SleepTime seconds." - Start-Sleep -Seconds $SleepTime - } - - # Provide a visual break between attempts - if ($i -ne 1) { Write-Host "" } - Write-Host "Download Attempt $i" - - # Temporarily disable progress reporting to speed up script performance - $PreviousProgressPreference = $ProgressPreference - $ProgressPreference = 'SilentlyContinue' - try { - if ($PSVersionTable.PSVersion.Major -lt 4) { - # For older versions of PowerShell, use WebClient to download the file - $WebClient = New-Object System.Net.WebClient - $WebClient.DownloadFile($URL, $Path) - } - else { - # For PowerShell 4.0 and above, use Invoke-WebRequest with specified arguments - $WebRequestArgs = @{ - Uri = $URL - OutFile = $Path - MaximumRedirection = 10 - UseBasicParsing = $true - } - - Invoke-WebRequest @WebRequestArgs - } - - # Verify if the file was successfully downloaded - $File = Test-Path -Path $Path -ErrorAction SilentlyContinue - } - catch { - # Handle any errors that occur during the download attempt - Write-Warning "An error has occurred while downloading!" - Write-Warning $_.Exception.Message - - # If the file partially downloaded, delete it to avoid corruption - if (Test-Path -Path $Path -ErrorAction SilentlyContinue) { - Remove-Item $Path -Force -Confirm:$false -ErrorAction SilentlyContinue - } - - $File = $False - } - - # Restore the original progress preference setting - $ProgressPreference = $PreviousProgressPreference - # If the file was successfully downloaded, exit the loop - if ($File) { - $i = $Attempts - } - else { - # Warn the user if the download attempt failed - Write-Warning "File failed to download." - Write-Host "" - } - - # Increment the attempt counter - $i++ - } - - # Final check: if the file still doesn't exist, report an error and exit - if (!(Test-Path $Path)) { - Write-Host -Object "[Error] Failed to download file." - Write-Host -Object "Please verify the URL of '$URL'." - exit 1 - } - else { - # If the download succeeded, return the path to the downloaded file - return $Path - } - } - - function Get-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter()] - [String]$DocumentName - ) - - # Initialize a hashtable for documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define types that require options to be retrieved - $NeedsOptions = "DropDown", "MultiSelect" - - # If a document name is provided, retrieve the property value from the document - if ($DocumentName) { - # Throw an error if the type is "Secure", as it's not a valid type in this context - if ($Type -Like "Secure") { throw [System.ArgumentOutOfRangeException]::New("$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality") } - - # Notify the user that the value is being retrieved from a Ninja document - Write-Host "Retrieving value from Ninja Document..." - $NinjaPropertyValue = Ninja-Property-Docs-Get -AttributeName $Name @DocumentationParams 2>&1 - - # If the property type requires options, retrieve them - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - # If no document name is provided, retrieve the property value directly - $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1 - - # If the property type requires options, retrieve them - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an exception if there was an error retrieving the property value or options - if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Throw an error if the retrieved property value is null or empty - if (!($NinjaPropertyValue)) { - throw "The Custom Field '$Name' is empty!" - } - - # Handle the property value based on its type - switch ($Type) { - "Attachment" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Checkbox" { - # Convert the value to a boolean - [System.Convert]::ToBoolean([int]$NinjaPropertyValue) - } - "Date or Date Time" { - # Convert a Unix timestamp to local date and time - $UnixTimeStamp = $NinjaPropertyValue - $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp) - $TimeZone = [TimeZoneInfo]::Local - [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) - } - "Decimal" { - # Convert the value to a double (floating-point number) - [double]$NinjaPropertyValue - } - "Device Dropdown" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Device MultiSelect" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Dropdown" { - # Convert options to a CSV format and match the GUID to retrieve the display name - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name - } - "Integer" { - # Convert the value to an integer - [int]$NinjaPropertyValue - } - "MultiSelect" { - # Convert options to a CSV format, then match and return selected items - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = ($NinjaPropertyValue -split ',').trim() - - foreach ($Item in $Selection) { - $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name - } - } - "Organization Dropdown" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization Location Dropdown" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization Location MultiSelect" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization MultiSelect" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Time" { - # Convert the value from seconds to a time format in the local timezone - $Seconds = $NinjaPropertyValue - $UTC = ([timespan]::fromseconds($Seconds)).ToString("hh\:mm\:ss") - $TimeZone = [TimeZoneInfo]::Local - $ConvertedTime = [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) - - Get-Date $ConvertedTime -DisplayHint Time - } - default { - # For any other types, return the raw value - $NinjaPropertyValue - } - } - } - - $CertPassword = "" - - # Get the certificate password from the parameter - if ($CertificatePassword) { - $CertPassword = $CertificatePassword - } - - if ($env:urlOrPathToCertificate) { - $CertificatePath = "$env:urlOrPathToCertificate".Trim() - } - - # Get the certificate password from the custom field - if ($env:certificatePasswordCustomFieldName) { - if (-not (Test-IsSystem)) { - # Running as a user from NinjaRMM. Normal users don't have access to read from customfields. - Write-Host "[Error] Must be ran as SYSTEM to install certificates with a password" - exit 1 - } - try { - $CertPassword = Get-NinjaProperty -Name "$env:certificatePasswordCustomFieldName".Trim() -Type "Text" - } - catch { - Write-Host "[Error] Failed to get the certificate password from the custom field: ($($env:certificatePasswordCustomFieldName))" - exit 1 - } - } - - if ($env:overwriteCertificateIfExisting -like "true") { - $OverwriteCertificateIfExisting = $true - } - - if ($env:certificateStore) { - $CertificateStore = $env:certificateStore - } - - # Select the certificate store based on the input - $CertStore = switch ($CertificateStore) { - "Personal" { [System.Security.Cryptography.X509Certificates.StoreName]::My } - "Trusted Root Certification Authorities" { [System.Security.Cryptography.X509Certificates.StoreName]::Root } - "Third-Party Root Certification Authorities" { [System.Security.Cryptography.X509Certificates.StoreName]::AuthRoot } - "Trusted Publisher" { [System.Security.Cryptography.X509Certificates.StoreName]::TrustedPublisher } - "Intermediate Certification Authorities" { [System.Security.Cryptography.X509Certificates.StoreName]::CertificateAuthority } - "Untrusted Certificates" { [System.Security.Cryptography.X509Certificates.StoreName]::Disallowed } - "Trusted People" { [System.Security.Cryptography.X509Certificates.StoreName]::TrustedPeople } - "Other People" { [System.Security.Cryptography.X509Certificates.StoreName]::AddressBook } - Default { - Write-Host "[Error] Invalid or unsupported certificate store: ($CertificateStore)" - Write-Host "[Info] Supported certificate stores:" - Write-Host " Personal," - Write-Host " Trusted Root Certification Authorities," - Write-Host " Third-Party Root Certification Authorities," - Write-Host " Trusted Publisher," - Write-Host " Intermediate Certification Authorities," - Write-Host " Untrusted Certificates," - Write-Host " Trusted People," - Write-Host " Other People" - exit 1 - } - } - - # Check if the certificate store exists based on the object type or path - if ($CertStore.GetType() -ne [System.Security.Cryptography.X509Certificates.StoreName] -and -not (Test-Path cert:\$CertStore -ErrorAction SilentlyContinue)) { - Write-Host "[Error] Certificate store ($CertificateStore) does not exist" - exit 1 - } -} -process { - # Generate a unique identifier for the certificate temporary file - $CertUID = New-Guid | Select-Object -ExpandProperty Guid - $CertPath = "$env:TEMP\cert$($CertUID)" - - # Determine the file type of the certificate - if ($CertificatePath -like "*.*") { - switch ($CertificatePath.Split(".")[-1]) { - "pfx" { $CertPath += ".pfx" } - "cer" { $CertPath += ".cer" } - "sst" { $CertPath += ".sst" } - "p7b" { $CertPath += ".p7b" } - "pem" { $CertPath += ".pem" } - Default { - Write-Host "[Error] Invalid certificate file type (pfx, cer, pem, sst, p7b supported): $CertificatePath" - exit 1 - } - } - } - else { - Write-Host "[Error] Certificate file missing extenstion (pfx, cer, pem, sst, p7b supported): $CertificatePath" - exit 1 - } - - if ($CertificatePath -like "http*") { - # Check if the $CertificatePath is a URL - if ($CertificatePath -notmatch "^http(s)?://") { - Write-Host "[Error] Invalid URL format: $CertificatePath" - exit 1 - } - - # Warn if http is used - if ($CertificatePath -match "^http://") { - Write-Host "[Warn] The certificate is being downloaded over an insecure connection. Ensure the certificate is from a trusted source." - } - - # Download the certificate if it is a URL - Invoke-Download -URL $CertificatePath -Path $CertPath - } - else { - # Copy the certificate to the temp directory - if (-not (Test-Path $CertificatePath)) { - Write-Host "[Error] Certificate path does not exist" - exit 1 - } - try { - Copy-Item -Path $(Resolve-Path -Path $CertificatePath) -Destination $CertPath -Force - } - catch { - Write-Host "[Error] Failed to copy certificate from path ($CertificatePath)" - exit 1 - } - } - - # Initialize the certificate object and load the certificate - $Certificate = if ($CertPassword) { - $FailedImport = $false - try { - # Get the certificate type - Write-Host "[Info] Getting certificate type" - $CertType = [System.Security.Cryptography.X509Certificates.X509Certificate2]::GetCertContentType($CertPath) - Write-Host "[Info] Certificate type: $CertType" - - # Set the flags based on context - $Flags = if (Test-IsSystem) { - [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet -bor - [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet - } - else { - [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::UserKeySet -bor - [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet - } - - # Load the certificate based on the type - switch ($CertType) { - "Unknown" { - Write-Host "[Error] Certificate file is empty or invalid: $CertPath" - exit 1 - } - { "$_" -in @("Cert", "SerializedCert") } { - Write-Host "[Info] Loading$(if($CertType){$CertType}) Certificate" - [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($CertPath) - Write-Host "[Info] Loaded$(if($CertType){$CertType}) Certificate" - } - { "$_" -in @("Pfx", "Pkcs12", "SerializedStore", "Pkcs7", "Authenticode") } { - Write-Host "[Info] Loading $CertType Certificate" - $Cc = [System.Security.Cryptography.X509Certificates.X509Certificate2Collection]::new() - $Cc.Import($CertPath) - Write-Host "[Info] Loaded $CertType Certificate" - $Cc - } - default { - throw "Invalid certificate type: $CertType" - } - } - } - catch { - $FailedImport = $true - } - if ($FailedImport) { - try { - # Try to load the certificate with out flags - [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($CertPath, $CertPassword) - } - catch { - Write-Host "[Error] Failed to load certificate from path ($CertificatePath)" - if ($_.Exception.Message -like "") { - Write-Host "[Error] Certificate file is empty or invalid: $CertificatePath" - } - else { - switch -Regex ($_.Exception.Message) { - "Cannot find the original signer" { - Write-Host "[Error] Cannot find the original signer" - } - "Invalid certificate type" { - Write-Host "[Error] Invalid certificate type: $CertType" - } - default { - Write-Host "[Error] $($_)" - } - } - } - Remove-Item -Path $CertPath -Force -ErrorAction SilentlyContinue - exit 1 - } - } - } - else { - $FailedImport = $false - try { - # Get the certificate type - Write-Host "[Info] Getting certificate type" - $CertType = [System.Security.Cryptography.X509Certificates.X509Certificate2]::GetCertContentType($CertPath) - Write-Host "[Info] Certificate type: $CertType" - - # Load the certificate based on the type - switch ($CertType) { - "Unknown" { - Write-Host "[Error] Certificate file is empty or invalid: $CertPath" - exit 1 - } - { "$_" -in @("Cert", "SerializedCert") } { - Write-Host "[Info] Loading$(if($CertType -like "SerializedCert"){" $CertType"}) Certificate" - [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($CertPath) - Write-Host "[Info] Loaded$(if($CertType -like "SerializedCert"){" $CertType"}) Certificate" - } - { "$_" -in @("Pfx", "Pkcs12", "SerializedStore", "Pkcs7", "Authenticode") } { - Write-Host "[Info] Loading $CertType Certificate" - $Cc = [System.Security.Cryptography.X509Certificates.X509Certificate2Collection]::new() - $Cc.Import($CertPath) - Write-Host "[Info] Loaded $CertType Certificate" - $Cc - } - default { - throw "Invalid certificate type: $CertType" - } - } - } - catch { - Write-Host "[Error] Failed to load certificate from path ($CertificatePath)" - if ($_.Exception.Message -like "") { - Write-Host "[Error] Certificate file is empty or invalid: $CertificatePath" - } - else { - switch -Regex ($_.Exception.Message) { - "Cannot find the original signer" { - Write-Host "[Error] Cannot find the original signer" - } - "Invalid certificate type" { - Write-Host "[Error] Invalid certificate type: $CertType" - } - default { - Write-Host "[Error] $($_)" - } - } - } - Remove-Item -Path $CertPath -Force -ErrorAction SilentlyContinue - exit 1 - } - } - - # Create a new X509Store object - try { - $Store = if (Test-IsSystem) { - # X509Store: https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.x509store?view=netframework-4.8.1 - [System.Security.Cryptography.X509Certificates.X509Store]::new($CertStore, [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine) - } - else { - [System.Security.Cryptography.X509Certificates.X509Store]::new($CertStore, [System.Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser) - } - } - catch { - Write-Host "[Error] Failed to create certificate store object" - Write-Host "[Error] $($_.Exception.Message)" - exit 1 - } - - # Open the certificate store - try { - $Store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::MaxAllowed) - Write-Host "[Info] Certificate store ($CertificateStore) opened with Read/Write access" - } - catch { - Write-Host "[Error] Failed to open certificate store with read and write access" - Write-Host "[Error] $($_.Exception.Message)" - exit 1 - } - - # Check if the certificate is already installed - if ($Certificate -and ( - # Check if the certificate is a collection - $Certificate -is [System.Security.Cryptography.X509Certificates.X509Certificate2Collection] -or - $Certificate -is [System.Object[]] - ) - ) { - # Check if any certificates in the collection are already installed - $Certificate | Where-Object { - $Store.Certificates.Thumbprint -contains $_.Thumbprint - } | ForEach-Object { - $cert = $_ - if ($OverwriteCertificateIfExisting) { - # Remove existing certificate with the same thumbprint - Write-Output "[Info] Overwriting existing certificate" - $RemoveErrors = [System.Collections.Generic.List[String]]::new() - $Store.Certificates | Where-Object { $_.Thumbprint -eq $cert.Thumbprint } | ForEach-Object { - try { - Write-Host "[Info] Removing existing certificate: $($_.FriendlyName)(Thumbprint: $($_.Thumbprint))" - $Store.Remove($_) - Write-Host "[Info] Certificate removed" - } - catch { - Write-Host "[Error] Failed to remove existing certificate" - Write-Host "[Error] $($_.Exception.Message)" - $RemoveErrors.Add( - [PSCustomObject]@{ - Error = $_.Exception.Message - Certificate = $_.FriendlyName - thumbprint = $_.Thumbprint - } - ) - } - } - - # Check if any errors occurred while removing certificates - if ($RemoveErrors.Count -gt 0) { - # Display the errors and exit - Write-Host "[Error] Failed to remove the following certificates:" - $RemoveErrors | ForEach-Object { - Write-Host "[Error] Certificate: $($_.Certificate)(Thumbprint: $($_.thumbprint))" - } - exit 1 - } - } - } - } - elseif ($Certificate -and $Certificate -is [System.Security.Cryptography.X509Certificates.X509Certificate2]) { - # Check if the certificate is already installed - if ($Store.Certificates.Thumbprint -contains $Certificate.Thumbprint) { - if ($OverwriteCertificateIfExisting) { - # Remove existing certificate with the same thumbprint - Write-Output "[Info] Overwriting existing certificate" - $RemoveErrors = [System.Collections.Generic.List[String]]::new() - $Store.Certificates | Where-Object { $_.Thumbprint -eq $Certificate.Thumbprint } | ForEach-Object { - try { - Write-Host "[Info] Removing existing certificate: $($_.FriendlyName)(Thumbprint: $($_.Thumbprint))" - $Store.Remove($_) - Write-Host "[Info] Certificate removed" - } - catch { - Write-Host "[Error] Failed to remove existing certificate" - Write-Host "[Error] $($_.Exception.Message)" - $RemoveErrors.Add( - [PSCustomObject]@{ - Error = $_.Exception.Message - Certificate = $_.FriendlyName - thumbprint = $_.Thumbprint - } - ) - } - } - - # Check if any errors occurred while removing certificates - if ($RemoveErrors.Count -gt 0) { - # Display the errors and exit - Write-Host "[Error] Failed to remove the following certificates:" - $RemoveErrors | ForEach-Object { - Write-Host "[Error] Certificate: $($_.Certificate)(Thumbprint: $($_.thumbprint))" - } - exit 1 - } - } - else { - Write-Output "[Info] Certificate already installed" - exit 0 - } - } - } - else { - Write-Host "[Error] Check: Invalid certificate object type (X509Certificate2 or X509Certificate2Collection expected)" - if ($Certificate) { - Write-Host "[Error] Certificate type: $($Certificate.GetType())" - Write-Host "[Error] Certificate:" - $($Certificate) | Out-String | Write-Host - } - else { - Write-Host "[Error] Certificate object is null" - } - exit 1 - } - - # Install the certificate to the specified store - if ($Certificate -is [System.Security.Cryptography.X509Certificates.X509Certificate2Collection] -or $Certificate -is [System.Object[]]) { - # Install each certificate in the collection - try { - if ($Certificate -is [System.Object[]]) { - $Certificate | ForEach-Object { - $Store.Add($_) - Write-Host "[Info] Certificate added to store: $($_.FriendlyName)(Thumbprint: $($_.Thumbprint))" - } - } - else { - $Store.AddRange($Certificate) - $Certificate | ForEach-Object { - Write-Host "[Info] Certificate added to store: $($_.FriendlyName)(Thumbprint: $($_.Thumbprint))" - } - } - } - catch { - Write-Host "[Error] Failed to add certificates to store" - Write-Host "[Error] $($_.Exception.Message)" - exit 1 - } - } - elseif ($Certificate -is [System.Security.Cryptography.X509Certificates.X509Certificate2]) { - # Install the certificate - try { - $Store.Add($Certificate) - Write-Host "[Info] Certificate added to store: $($Certificate.FriendlyName)(Thumbprint: $($Certificate.Thumbprint))" - } - catch { - Write-Host "[Error] Failed to add certificate to store" - Write-Host "[Error] $($_.Exception.Message)" - exit 1 - } - } - else { - Write-Host "[Error] Add: Invalid certificate object type (X509Certificate2 or X509Certificate2Collection expected)" - exit 1 - } - - # Close the certificate store - try { - $Store.Close() - Write-Host "[Info] Certificate store($CertificateStore) closed" - } - catch { - Write-Host "[Warn] Failed to close certificate store" - Write-Host "[Error] $($_.Exception.Message)" - } - - Write-Output "[Info] Certificate installed successfully" - - exit 0 -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Installs a given certificate to the selected location. + +.DESCRIPTION + Installs a given certificate to the selected location. + Running this script as SYSTEM will install the certificate to the LocalMachine certificate store. + Running this script as a user will install the certificate to the CurrentUser certificate store of that user. + + Note that this will install ALL certificates in a collection to the specified store. If you need to install + specific certificates to different stores, you will need to break apart the collection and + install each certificate individually. + +.PARAMETER CertificatePath + The file path or URL to the certificate file. + +.PARAMETER CertificateStore + The certificate store to install the certificate to. + Supported values: + "Personal" + "Trusted Root Certification Authorities" + "Third-Party Root Certification Authorities" + "Trusted Publisher" + "Intermediate Certification Authorities" + "Untrusted Certificates" + "Trusted People" + "Other People" + +.PARAMETER CertificatePassword + The password for the certificate. + Running from NinjaRMM, this can be stored in a custom field and retrieved using the "Certificate Password Custom Field Name" Script Variable. + +.PARAMETER OverwriteCertificateIfExisting + Overwrite the certificate if it already exists. + This removes any certificates that have a matching Thumbprint in the specified certificate store, + e.g. Cert was installed in Personal, but needs to be replaced. + +.EXAMPLE + -CertificatePath 'C:\certs\mycert.pfx' -CertificateStore 'Personal' -CertificatePassword 'password' + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial release +#> + +[CmdletBinding()] +param ( + [string]$CertificatePath, + [string]$CertificateStore, + [string]$CertificatePassword, + [switch]$OverwriteCertificateIfExisting +) + +begin { + + function Test-IsSystem { + # Get the current Windows identity of the user running the script + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + + # Check if the current identity's name matches "NT AUTHORITY*" + # or if the identity represents the SYSTEM account + return $id.Name -like "NT AUTHORITY*" -or $id.IsSystem + } + function Invoke-Download { + param( + [Parameter()] + [String]$URL, + [Parameter()] + [String]$Path, + [Parameter()] + [int]$Attempts = 3, + [Parameter()] + [Switch]$SkipSleep + ) + + # Display the URL being used for the download + Write-Host -Object "URL '$URL' was given." + Write-Host -Object "Downloading the file..." + + # Determine the supported TLS versions and set the appropriate security protocol + $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType') + if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12 + } + elseif ( $SupportedTLSversions -contains 'Tls12' ) { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 + } + else { + # Warn the user if TLS 1.2 and 1.3 are not supported, which may cause the download to fail + Write-Warning "TLS 1.2 and/or TLS 1.3 are not supported on this system. This download may fail!" + if ($PSVersionTable.PSVersion.Major -lt 3) { + Write-Warning "PowerShell 2 / .NET 2.0 doesn't support TLS 1.2." + } + } + + # Initialize the attempt counter + $i = 1 + While ($i -le $Attempts) { + # If SkipSleep is not set, wait for a random time between 3 and 15 seconds before each attempt + if (!($SkipSleep)) { + $SleepTime = Get-Random -Minimum 3 -Maximum 15 + Write-Host "Waiting for $SleepTime seconds." + Start-Sleep -Seconds $SleepTime + } + + # Provide a visual break between attempts + if ($i -ne 1) { Write-Host "" } + Write-Host "Download Attempt $i" + + # Temporarily disable progress reporting to speed up script performance + $PreviousProgressPreference = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + try { + if ($PSVersionTable.PSVersion.Major -lt 4) { + # For older versions of PowerShell, use WebClient to download the file + $WebClient = New-Object System.Net.WebClient + $WebClient.DownloadFile($URL, $Path) + } + else { + # For PowerShell 4.0 and above, use Invoke-WebRequest with specified arguments + $WebRequestArgs = @{ + Uri = $URL + OutFile = $Path + MaximumRedirection = 10 + UseBasicParsing = $true + } + + Invoke-WebRequest @WebRequestArgs + } + + # Verify if the file was successfully downloaded + $File = Test-Path -Path $Path -ErrorAction SilentlyContinue + } + catch { + # Handle any errors that occur during the download attempt + Write-Warning "An error has occurred while downloading!" + Write-Warning $_.Exception.Message + + # If the file partially downloaded, delete it to avoid corruption + if (Test-Path -Path $Path -ErrorAction SilentlyContinue) { + Remove-Item $Path -Force -Confirm:$false -ErrorAction SilentlyContinue + } + + $File = $False + } + + # Restore the original progress preference setting + $ProgressPreference = $PreviousProgressPreference + # If the file was successfully downloaded, exit the loop + if ($File) { + $i = $Attempts + } + else { + # Warn the user if the download attempt failed + Write-Warning "File failed to download." + Write-Host "" + } + + # Increment the attempt counter + $i++ + } + + # Final check: if the file still doesn't exist, report an error and exit + if (!(Test-Path $Path)) { + Write-Host -Object "[Error] Failed to download file." + Write-Host -Object "Please verify the URL of '$URL'." + exit 1 + } + else { + # If the download succeeded, return the path to the downloaded file + return $Path + } + } + + function Get-NinjaProperty { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [String]$Name, + [Parameter()] + [String]$Type, + [Parameter()] + [String]$DocumentName + ) + + # Initialize a hashtable for documentation parameters + $DocumentationParams = @{} + + # If a document name is provided, add it to the documentation parameters + if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } + + # Define types that require options to be retrieved + $NeedsOptions = "DropDown", "MultiSelect" + + # If a document name is provided, retrieve the property value from the document + if ($DocumentName) { + # Throw an error if the type is "Secure", as it's not a valid type in this context + if ($Type -Like "Secure") { throw [System.ArgumentOutOfRangeException]::New("$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality") } + + # Notify the user that the value is being retrieved from a Ninja document + Write-Host "Retrieving value from Ninja Document..." + # $NinjaPropertyValue = Ninja-Property-Docs-Get -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + + # If the property type requires options, retrieve them + if ($NeedsOptions -contains $Type) { + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + } + } + else { + # If no document name is provided, retrieve the property value directly + # $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1 # Removed NinjaOne dependency + + # If the property type requires options, retrieve them + if ($NeedsOptions -contains $Type) { + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + } + } + + # Throw an exception if there was an error retrieving the property value or options + if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } + if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } + + # Throw an error if the retrieved property value is null or empty + if (!($NinjaPropertyValue)) { + throw "The Custom Field '$Name' is empty!" + } + + # Handle the property value based on its type + switch ($Type) { + "Attachment" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Checkbox" { + # Convert the value to a boolean + [System.Convert]::ToBoolean([int]$NinjaPropertyValue) + } + "Date or Date Time" { + # Convert a Unix timestamp to local date and time + $UnixTimeStamp = $NinjaPropertyValue + $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp) + $TimeZone = [TimeZoneInfo]::Local + [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) + } + "Decimal" { + # Convert the value to a double (floating-point number) + [double]$NinjaPropertyValue + } + "Device Dropdown" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Device MultiSelect" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Dropdown" { + # Convert options to a CSV format and match the GUID to retrieve the display name + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name + } + "Integer" { + # Convert the value to an integer + [int]$NinjaPropertyValue + } + "MultiSelect" { + # Convert options to a CSV format, then match and return selected items + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Selection = ($NinjaPropertyValue -split ',').trim() + + foreach ($Item in $Selection) { + $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name + } + } + "Organization Dropdown" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization Location Dropdown" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization Location MultiSelect" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization MultiSelect" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Time" { + # Convert the value from seconds to a time format in the local timezone + $Seconds = $NinjaPropertyValue + $UTC = ([timespan]::fromseconds($Seconds)).ToString("hh\:mm\:ss") + $TimeZone = [TimeZoneInfo]::Local + $ConvertedTime = [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) + + Get-Date $ConvertedTime -DisplayHint Time + } + default { + # For any other types, return the raw value + $NinjaPropertyValue + } + } + } + + $CertPassword = "" + + # Get the certificate password from the parameter + if ($CertificatePassword) { + $CertPassword = $CertificatePassword + } + + if ($env:urlOrPathToCertificate) { + $CertificatePath = "$env:urlOrPathToCertificate".Trim() + } + + # Get the certificate password from the custom field + if ($env:certificatePasswordCustomFieldName) { + if (-not (Test-IsSystem)) { + # Running as a user from NinjaRMM. Normal users don't have access to read from customfields. + Write-Host "[Error] Must be ran as SYSTEM to install certificates with a password" + exit 1 + } + try { + $CertPassword = Get-NinjaProperty -Name "$env:certificatePasswordCustomFieldName".Trim() -Type "Text" + } + catch { + Write-Host "[Error] Failed to get the certificate password from the custom field: ($($env:certificatePasswordCustomFieldName))" + exit 1 + } + } + + if ($env:overwriteCertificateIfExisting -like "true") { + $OverwriteCertificateIfExisting = $true + } + + if ($env:certificateStore) { + $CertificateStore = $env:certificateStore + } + + # Select the certificate store based on the input + $CertStore = switch ($CertificateStore) { + "Personal" { [System.Security.Cryptography.X509Certificates.StoreName]::My } + "Trusted Root Certification Authorities" { [System.Security.Cryptography.X509Certificates.StoreName]::Root } + "Third-Party Root Certification Authorities" { [System.Security.Cryptography.X509Certificates.StoreName]::AuthRoot } + "Trusted Publisher" { [System.Security.Cryptography.X509Certificates.StoreName]::TrustedPublisher } + "Intermediate Certification Authorities" { [System.Security.Cryptography.X509Certificates.StoreName]::CertificateAuthority } + "Untrusted Certificates" { [System.Security.Cryptography.X509Certificates.StoreName]::Disallowed } + "Trusted People" { [System.Security.Cryptography.X509Certificates.StoreName]::TrustedPeople } + "Other People" { [System.Security.Cryptography.X509Certificates.StoreName]::AddressBook } + Default { + Write-Host "[Error] Invalid or unsupported certificate store: ($CertificateStore)" + Write-Host "[Info] Supported certificate stores:" + Write-Host " Personal," + Write-Host " Trusted Root Certification Authorities," + Write-Host " Third-Party Root Certification Authorities," + Write-Host " Trusted Publisher," + Write-Host " Intermediate Certification Authorities," + Write-Host " Untrusted Certificates," + Write-Host " Trusted People," + Write-Host " Other People" + exit 1 + } + } + + # Check if the certificate store exists based on the object type or path + if ($CertStore.GetType() -ne [System.Security.Cryptography.X509Certificates.StoreName] -and -not (Test-Path cert:\$CertStore -ErrorAction SilentlyContinue)) { + Write-Host "[Error] Certificate store ($CertificateStore) does not exist" + exit 1 + } +} +process { + # Generate a unique identifier for the certificate temporary file + $CertUID = New-Guid | Select-Object -ExpandProperty Guid + $CertPath = "$env:TEMP\cert$($CertUID)" + + # Determine the file type of the certificate + if ($CertificatePath -like "*.*") { + switch ($CertificatePath.Split(".")[-1]) { + "pfx" { $CertPath += ".pfx" } + "cer" { $CertPath += ".cer" } + "sst" { $CertPath += ".sst" } + "p7b" { $CertPath += ".p7b" } + "pem" { $CertPath += ".pem" } + Default { + Write-Host "[Error] Invalid certificate file type (pfx, cer, pem, sst, p7b supported): $CertificatePath" + exit 1 + } + } + } + else { + Write-Host "[Error] Certificate file missing extenstion (pfx, cer, pem, sst, p7b supported): $CertificatePath" + exit 1 + } + + if ($CertificatePath -like "http*") { + # Check if the $CertificatePath is a URL + if ($CertificatePath -notmatch "^http(s)?://") { + Write-Host "[Error] Invalid URL format: $CertificatePath" + exit 1 + } + + # Warn if http is used + if ($CertificatePath -match "^http://") { + Write-Host "[Warn] The certificate is being downloaded over an insecure connection. Ensure the certificate is from a trusted source." + } + + # Download the certificate if it is a URL + Invoke-Download -URL $CertificatePath -Path $CertPath + } + else { + # Copy the certificate to the temp directory + if (-not (Test-Path $CertificatePath)) { + Write-Host "[Error] Certificate path does not exist" + exit 1 + } + try { + Copy-Item -Path $(Resolve-Path -Path $CertificatePath) -Destination $CertPath -Force + } + catch { + Write-Host "[Error] Failed to copy certificate from path ($CertificatePath)" + exit 1 + } + } + + # Initialize the certificate object and load the certificate + $Certificate = if ($CertPassword) { + $FailedImport = $false + try { + # Get the certificate type + Write-Host "[Info] Getting certificate type" + $CertType = [System.Security.Cryptography.X509Certificates.X509Certificate2]::GetCertContentType($CertPath) + Write-Host "[Info] Certificate type: $CertType" + + # Set the flags based on context + $Flags = if (Test-IsSystem) { + [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet -bor + [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet + } + else { + [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::UserKeySet -bor + [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet + } + + # Load the certificate based on the type + switch ($CertType) { + "Unknown" { + Write-Host "[Error] Certificate file is empty or invalid: $CertPath" + exit 1 + } + { "$_" -in @("Cert", "SerializedCert") } { + Write-Host "[Info] Loading$(if($CertType){$CertType}) Certificate" + [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($CertPath) + Write-Host "[Info] Loaded$(if($CertType){$CertType}) Certificate" + } + { "$_" -in @("Pfx", "Pkcs12", "SerializedStore", "Pkcs7", "Authenticode") } { + Write-Host "[Info] Loading $CertType Certificate" + $Cc = [System.Security.Cryptography.X509Certificates.X509Certificate2Collection]::new() + $Cc.Import($CertPath) + Write-Host "[Info] Loaded $CertType Certificate" + $Cc + } + default { + throw "Invalid certificate type: $CertType" + } + } + } + catch { + $FailedImport = $true + } + if ($FailedImport) { + try { + # Try to load the certificate with out flags + [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($CertPath, $CertPassword) + } + catch { + Write-Host "[Error] Failed to load certificate from path ($CertificatePath)" + if ($_.Exception.Message -like "") { + Write-Host "[Error] Certificate file is empty or invalid: $CertificatePath" + } + else { + switch -Regex ($_.Exception.Message) { + "Cannot find the original signer" { + Write-Host "[Error] Cannot find the original signer" + } + "Invalid certificate type" { + Write-Host "[Error] Invalid certificate type: $CertType" + } + default { + Write-Host "[Error] $($_)" + } + } + } + Remove-Item -Path $CertPath -Force -ErrorAction SilentlyContinue + exit 1 + } + } + } + else { + $FailedImport = $false + try { + # Get the certificate type + Write-Host "[Info] Getting certificate type" + $CertType = [System.Security.Cryptography.X509Certificates.X509Certificate2]::GetCertContentType($CertPath) + Write-Host "[Info] Certificate type: $CertType" + + # Load the certificate based on the type + switch ($CertType) { + "Unknown" { + Write-Host "[Error] Certificate file is empty or invalid: $CertPath" + exit 1 + } + { "$_" -in @("Cert", "SerializedCert") } { + Write-Host "[Info] Loading$(if($CertType -like "SerializedCert"){" $CertType"}) Certificate" + [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($CertPath) + Write-Host "[Info] Loaded$(if($CertType -like "SerializedCert"){" $CertType"}) Certificate" + } + { "$_" -in @("Pfx", "Pkcs12", "SerializedStore", "Pkcs7", "Authenticode") } { + Write-Host "[Info] Loading $CertType Certificate" + $Cc = [System.Security.Cryptography.X509Certificates.X509Certificate2Collection]::new() + $Cc.Import($CertPath) + Write-Host "[Info] Loaded $CertType Certificate" + $Cc + } + default { + throw "Invalid certificate type: $CertType" + } + } + } + catch { + Write-Host "[Error] Failed to load certificate from path ($CertificatePath)" + if ($_.Exception.Message -like "") { + Write-Host "[Error] Certificate file is empty or invalid: $CertificatePath" + } + else { + switch -Regex ($_.Exception.Message) { + "Cannot find the original signer" { + Write-Host "[Error] Cannot find the original signer" + } + "Invalid certificate type" { + Write-Host "[Error] Invalid certificate type: $CertType" + } + default { + Write-Host "[Error] $($_)" + } + } + } + Remove-Item -Path $CertPath -Force -ErrorAction SilentlyContinue + exit 1 + } + } + + # Create a new X509Store object + try { + $Store = if (Test-IsSystem) { + # X509Store: https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.x509store?view=netframework-4.8.1 + [System.Security.Cryptography.X509Certificates.X509Store]::new($CertStore, [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine) + } + else { + [System.Security.Cryptography.X509Certificates.X509Store]::new($CertStore, [System.Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser) + } + } + catch { + Write-Host "[Error] Failed to create certificate store object" + Write-Host "[Error] $($_.Exception.Message)" + exit 1 + } + + # Open the certificate store + try { + $Store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::MaxAllowed) + Write-Host "[Info] Certificate store ($CertificateStore) opened with Read/Write access" + } + catch { + Write-Host "[Error] Failed to open certificate store with read and write access" + Write-Host "[Error] $($_.Exception.Message)" + exit 1 + } + + # Check if the certificate is already installed + if ($Certificate -and ( + # Check if the certificate is a collection + $Certificate -is [System.Security.Cryptography.X509Certificates.X509Certificate2Collection] -or + $Certificate -is [System.Object[]] + ) + ) { + # Check if any certificates in the collection are already installed + $Certificate | Where-Object { + $Store.Certificates.Thumbprint -contains $_.Thumbprint + } | ForEach-Object { + $cert = $_ + if ($OverwriteCertificateIfExisting) { + # Remove existing certificate with the same thumbprint + Write-Output "[Info] Overwriting existing certificate" + $RemoveErrors = [System.Collections.Generic.List[String]]::new() + $Store.Certificates | Where-Object { $_.Thumbprint -eq $cert.Thumbprint } | ForEach-Object { + try { + Write-Host "[Info] Removing existing certificate: $($_.FriendlyName)(Thumbprint: $($_.Thumbprint))" + $Store.Remove($_) + Write-Host "[Info] Certificate removed" + } + catch { + Write-Host "[Error] Failed to remove existing certificate" + Write-Host "[Error] $($_.Exception.Message)" + $RemoveErrors.Add( + [PSCustomObject]@{ + Error = $_.Exception.Message + Certificate = $_.FriendlyName + thumbprint = $_.Thumbprint + } + ) + } + } + + # Check if any errors occurred while removing certificates + if ($RemoveErrors.Count -gt 0) { + # Display the errors and exit + Write-Host "[Error] Failed to remove the following certificates:" + $RemoveErrors | ForEach-Object { + Write-Host "[Error] Certificate: $($_.Certificate)(Thumbprint: $($_.thumbprint))" + } + exit 1 + } + } + } + } + elseif ($Certificate -and $Certificate -is [System.Security.Cryptography.X509Certificates.X509Certificate2]) { + # Check if the certificate is already installed + if ($Store.Certificates.Thumbprint -contains $Certificate.Thumbprint) { + if ($OverwriteCertificateIfExisting) { + # Remove existing certificate with the same thumbprint + Write-Output "[Info] Overwriting existing certificate" + $RemoveErrors = [System.Collections.Generic.List[String]]::new() + $Store.Certificates | Where-Object { $_.Thumbprint -eq $Certificate.Thumbprint } | ForEach-Object { + try { + Write-Host "[Info] Removing existing certificate: $($_.FriendlyName)(Thumbprint: $($_.Thumbprint))" + $Store.Remove($_) + Write-Host "[Info] Certificate removed" + } + catch { + Write-Host "[Error] Failed to remove existing certificate" + Write-Host "[Error] $($_.Exception.Message)" + $RemoveErrors.Add( + [PSCustomObject]@{ + Error = $_.Exception.Message + Certificate = $_.FriendlyName + thumbprint = $_.Thumbprint + } + ) + } + } + + # Check if any errors occurred while removing certificates + if ($RemoveErrors.Count -gt 0) { + # Display the errors and exit + Write-Host "[Error] Failed to remove the following certificates:" + $RemoveErrors | ForEach-Object { + Write-Host "[Error] Certificate: $($_.Certificate)(Thumbprint: $($_.thumbprint))" + } + exit 1 + } + } + else { + Write-Output "[Info] Certificate already installed" + exit 0 + } + } + } + else { + Write-Host "[Error] Check: Invalid certificate object type (X509Certificate2 or X509Certificate2Collection expected)" + if ($Certificate) { + Write-Host "[Error] Certificate type: $($Certificate.GetType())" + Write-Host "[Error] Certificate:" + $($Certificate) | Out-String | Write-Host + } + else { + Write-Host "[Error] Certificate object is null" + } + exit 1 + } + + # Install the certificate to the specified store + if ($Certificate -is [System.Security.Cryptography.X509Certificates.X509Certificate2Collection] -or $Certificate -is [System.Object[]]) { + # Install each certificate in the collection + try { + if ($Certificate -is [System.Object[]]) { + $Certificate | ForEach-Object { + $Store.Add($_) + Write-Host "[Info] Certificate added to store: $($_.FriendlyName)(Thumbprint: $($_.Thumbprint))" + } + } + else { + $Store.AddRange($Certificate) + $Certificate | ForEach-Object { + Write-Host "[Info] Certificate added to store: $($_.FriendlyName)(Thumbprint: $($_.Thumbprint))" + } + } + } + catch { + Write-Host "[Error] Failed to add certificates to store" + Write-Host "[Error] $($_.Exception.Message)" + exit 1 + } + } + elseif ($Certificate -is [System.Security.Cryptography.X509Certificates.X509Certificate2]) { + # Install the certificate + try { + $Store.Add($Certificate) + Write-Host "[Info] Certificate added to store: $($Certificate.FriendlyName)(Thumbprint: $($Certificate.Thumbprint))" + } + catch { + Write-Host "[Error] Failed to add certificate to store" + Write-Host "[Error] $($_.Exception.Message)" + exit 1 + } + } + else { + Write-Host "[Error] Add: Invalid certificate object type (X509Certificate2 or X509Certificate2Collection expected)" + exit 1 + } + + # Close the certificate store + try { + $Store.Close() + Write-Host "[Info] Certificate store($CertificateStore) closed" + } + catch { + Write-Host "[Warn] Failed to close certificate store" + Write-Host "[Error] $($_.Exception.Message)" + } + + Write-Output "[Info] Certificate installed successfully" + + exit 0 +} +end { + + + +} diff --git a/Powershell Scripts/Installed Remote Access Tools Report.ps1 b/Powershell Scripts/Installed Remote Access Tools Report.ps1 index 580a866..c602ad1 100644 --- a/Powershell Scripts/Installed Remote Access Tools Report.ps1 +++ b/Powershell Scripts/Installed Remote Access Tools Report.ps1 @@ -1,367 +1,369 @@ -# This script will look for remote access tools installed on the system. It can be given a list of tools to ignore as well as grab the exclusion list from a designated custom field. - -DISCLAIMER: This script is provided as a best effort for detecting remote access software installed on an agent, but it is not guaranteed to be 100% accurate. +# This script will look for remote access tools installed on the system. It can be given a list of tools to ignore as well as grab the exclusion list from a designated custom field. + +DISCLAIMER: This script is provided as a best effort for detecting remote access software installed on an agent, but it is not guaranteed to be 100% accurate. Some remote access software may not be detected, or false positives may be reported. Use this script at your own risk and verify its results with other methods where possible. -#Requires -Version 5.1 - -<# -.SYNOPSIS - This script will look for remote access tools installed on the system. It can be given a list of tools to ignore as well as grab the exclusion list from a designated custom field. - - DISCLAIMER: This script is provided as a best effort for detecting remote access software installed on an agent, but it is not guaranteed to be 100% accurate. - Some remote access software may not be detected, or false positives may be reported. Use this script at your own risk and verify its results with other methods where possible. -.DESCRIPTION - This script will look for remote access tools installed on the system. Below is the full list of tools. Please note you can give it a list of tools to ignore and you can have - it grab the list from a custom field of your choosing. - - DISCLAIMER: This script is provided as a best effort for detecting remote access software installed on an agent, but it is not guaranteed to be 100% accurate. - Some remote access software may not be detected, or false positives may be reported. Use this script at your own risk and verify its results with other methods where possible. - - Remote Tools: AeroAdmin, Ammyy Admin, AnyDesk, BeyondTrust, Chrome Remote Desktop, Connectwise Control, DWService, GoToMyPC, LiteManager, LogMeIn, ManageEngine, - NoMachine, Parsec, Remote Utilities, RemotePC, Splashtop, Supremo, TeamViewer, TightVNC, UltraVNC, VNC Connect (RealVNC), Zoho Assist - RMM's: Atera, Automate, Datto RMM, Kaseya, N-Able N-Central, N-Able N-Sight, Syncro - -.EXAMPLE - (No Parameters) - Name CurrentlyRunning HasRunningService UninstallString - ---- ---------------- ----------------- --------------- - Connectwise Control Yes Yes MsiExec /X{examplestring} - Chrome Remote Desktop Yes Yes MsiExec /X{examplestring} - -PARAMETER: -ExcludeTools "Chrome Remote Desktop,Connectwise Control" - A comma separated list of tools you'd like to exclude from alerting on. -.EXAMPLE - -ExcludeTools "Chrome Remote Desktop,Connectwise Control" - We couldn't find any active remote access tools! - -PARAMETER: -ExclusionsFromCustomField "ReplaceMeWithAnyTextCustomField" - The name of a custom field that contains a comma separated list of tools to exclude from alerting. E.g. "ApprovedRemoteTools" -.EXAMPLE - -ExclusionsFromCustomField "ReplaceMeWithAnyTextCustomField" - We couldn't find any active remote access tools! - -PARAMETER: -ExportCSV "ReplaceMeWithAnyMultiLineCustomField" - The name of a multiline custom field to export to in csv format. ex. "RemoteTools" -.EXAMPLE - -ExportCSV "ReplaceMeWithAnyMultiLineCustomField" - Name CurrentlyRunning HasRunningService UninstallString - ---- ---------------- ----------------- --------------- - Connectwise Control Yes Yes MsiExec /X{examplestring} - Chrome Remote Desktop Yes Yes MsiExec /X{examplestring} - -PARAMETER: -ExportJSON "ReplaceMeWithAnyMultiLineCustomField" - The name of a multiline custom field to export to in JSON format. E.g. "RemoteTools" -.EXAMPLE - -ExportJSON "ReplaceMeWithAnyMultiLineCustomField" - Name CurrentlyRunning HasRunningService UninstallString - ---- ---------------- ----------------- --------------- - Connectwise Control Yes Yes MsiExec /X{examplestring} - Chrome Remote Desktop Yes Yes MsiExec /X{examplestring} - -PARAMETER: -ShowNotFound - Show the tools the script did not find as well. -.EXAMPLE - -ShowNotFound - Name CurrentlyRunning HasRunningService UninstallString - ---- ---------------- ----------------- --------------- - AeroAdmin No No - Ammyy Admin No No - BeyondTrust No No - Connectwise Control Yes Yes MsiExec /X{examplestring} - Chrome Remote Desktop Yes Yes MsiExec /X{examplestring} - -.OUTPUTS - None -.NOTES - General notes: CustomFields must be multiline for export. Regular text is fine for ExclusionsFromCustomField - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Renamed script and added Script Variable support, added error for when -ExportJSON and -ExportCSV are used together -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$ExcludeTools, - [Parameter()] - [String]$ExclusionsFromCustomField, - [Parameter()] - [String]$ExportCSV, - [Parameter()] - [String]$ExportJSON, - [Parameter()] - [Switch]$ShowNotFound = [System.Convert]::ToBoolean($env:includeToolsThatWereNotFound) -) - -begin { - #DISCLAIMER: This script is provided as a best effort for detecting remote access software installed on an agent, but it is not guaranteed to be 100% accurate. - #Some remote access software may not be detected, or false positives may be reported. Use this script at your own risk and verify its results with other methods where possible. - - if ($env:toolsToIgnore -and $env:toolsToIgnore -notlike "null") { $ExcludeTools = $env:toolsToIgnore } - if ($env:retrieveIgnoreListFromCustomField -and $env:retrieveIgnoreListFromCustomField -notlike "null") { $ExclusionsFromCustomField = $env:retrieveIgnoreListFromCustomField } - if ($env:exportCsvResultsToThisCustomField -and $env:exportCsvResultsToThisCustomField -notlike "null") { $ExportCSV = $env:exportCsvResultsToThisCustomField } - if ($env:exportJsonResultsToThisCustomField -and $env:exportJsonResultsToThisCustomField -notlike "null") { $ExportJSON = $env:exportJsonResultsToThisCustomField } - - if ($ExportCSV -and $ExportJSON) { Write-Error "You can only export in either JSON or CSV format. Not both."; exit 1 } - - # Check's the two Uninstall registry keys to see if the app is installed. Needs the name as it would appear in Control Panel. - function Find-UninstallKey { - [CmdletBinding()] - param ( - [Parameter(ValueFromPipeline)] - [String]$DisplayName, - [Parameter()] - [Switch]$UninstallString - ) - process { - $UninstallList = New-Object System.Collections.Generic.List[Object] - - $Result = Get-ChildItem HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Get-ItemProperty | - Where-Object { $_.DisplayName -like "*$DisplayName*" } - - if ($Result) { $UninstallList.Add($Result) } - - $Result = Get-ChildItem HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Get-ItemProperty | - Where-Object { $_.DisplayName -like "*$DisplayName*" } - - if ($Result) { $UninstallList.Add($Result) } - - # Programs don't always have an uninstall string listed here so to account for that I made this optional. - if ($UninstallString) { - # 64 Bit - $UninstallList | Select-Object -ExpandProperty UninstallString -ErrorAction Ignore - } - else { - $UninstallList - } - } - } - - # This will see if the process is currently active. Some people may want to react sooner to these alerts if its currently running vs not. - function Find-Process { - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline)] - [String]$Name - ) - process { - Get-Process | Where-Object { $_.ProcessName -like "*$Name*" } | Select-Object -ExpandProperty Name - } - } - - # This will search C:\ProgramFiles and C:\ProgramFiles(x86) for the executable these tools use to run. - function Find-Executable { - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline)] - [String]$Path, - [Parameter()] - [Switch]$Special - ) - process { - if (!$Special) { - if (Test-Path "$env:ProgramFiles\$Path") { - "$env:ProgramFiles\$Path" - } - - if (Test-Path "${Env:ProgramFiles(x86)}\$Path") { - "${Env:ProgramFiles(x86)}\$Path" - } - - if (Test-Path "$env:ProgramData\$Path") { - "$env:ProgramData\$Path" - } - } - else { - if (Test-Path $Path) { - $Path - } - } - } - } - - # Brought Get-CimInstance outside the function for better performance. - - $ServiceList = Get-CimInstance win32_service - function Find-Service { - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline)] - [String]$Name - ) - process { - # Get-Service will display an error everytime it has an issue reading a service. Ignoring them as they're not relevant. - $ServiceList | Where-Object { $_.State -notlike "Disabled" -and $_.State -notlike "Stopped" } | - Where-Object { $_.PathName -Like "*$Name.exe*" } - } - } - - function Export-CustomField { - [CmdletBinding()] - param( - [Parameter()] - [String]$Name, - [Parameter()] - [ValidateSet("csv", "json")] - [String]$Format, - [Parameter()] - [PSCustomObject]$Object - ) - if ($Format -eq "csv") { - $csv = $Object | ConvertTo-Csv -NoTypeInformation | Out-String - Ninja-Property-Set $Name $csv - } - else { - $json = $Object | ConvertTo-Json | Out-String - Ninja-Property-Set $Name $json - } - } - - # This define's what tools we're looking for and how the script can find them. Some don't actually install anywhere (portable app) others do. - # Some change their installation path everytime so not particularly worth it to find it that way. - # Others store themselves in a super weird directory. Many don't list exactly where there .exe file is stored and suggest you exclude the whole folder from the av. - $RemoteToolList = @( - [PSCustomObject]@{Name = "AeroAdmin"; ProcessName = "AeroAdmin" } - [PSCustomObject]@{Name = "Ammyy Admin"; ProcessName = "AA_v3" } - [PSCustomObject]@{Name = "AnyDesk"; DisplayName = "AnyDesk"; ProcessName = "AnyDesk"; ExecutablePath = "AnyDesk\AnyDesk.exe" } - [PSCustomObject]@{Name = "BeyondTrust"; DisplayName = "Remote Support Jump Client", "Jumpoint"; ProcessName = "bomgar-jpt" } - [PSCustomObject]@{Name = "Chrome Remote Desktop"; DisplayName = "Chrome Remote Desktop Host"; ProcessName = "remoting_host"; ExecutablePath = "Google\Chrome Remote Desktop\112.0.5615.26\remoting_host.exe" } - [PSCustomObject]@{Name = "Connectwise Control"; DisplayName = "ScreenConnect Client"; ProcessName = "ScreenConnect.ClientService" } - [PSCustomObject]@{Name = "DWService"; DisplayName = "DWAgent"; ProcessName = "dwagent", "dwagsvc"; ExecutablePath = "DWAgent\runtime\dwagent.exe" } - [PSCustomObject]@{Name = "GoToMyPC"; DisplayName = "GoToMyPC"; ProcessName = "g2comm", "g2pre", "g2svc", "g2tray"; ExecutablePath = "GoToMyPC\g2comm.exe", "GoToMyPC\g2pre.exe", "GoToMyPC\g2svc.exe", "GoToMyPC\g2tray.exe" } - [PSCustomObject]@{Name = "LiteManager"; DisplayName = "LiteManager Pro - Server"; ProcessName = "ROMServer", "ROMFUSClient"; ExecutablePath = "LiteManager Pro - Server\ROMFUSClient.exe", "LiteManager Pro - Server\ROMServer.exe" } - [PSCustomObject]@{Name = "LogMeIn"; DisplayName = "LogMeIn"; ProcessName = "LogMeIn"; ExecutablePath = "LogMeIn\x64\LogMeIn.exe", "LogMeIn\x64\LogMeInSystray.exe" } - [PSCustomObject]@{Name = "ManageEngine"; DisplayName = "ManageEngine Remote Access Plus - Server", "ManageEngine UEMS - Agent"; ProcessName = "dcagenttrayicon", "UEMS", "dcagentservice"; ExecutablePath = "UEMS_Agent\bin\dcagenttrayicon.exe", "UEMS_CentralServer\bin\UEMS.exe", "UEMS_Agent\bin\dcagentservice.exe" } - [PSCustomObject]@{Name = "NoMachine"; DisplayName = "NoMachine"; ProcessName = "nxd", "nxnode.bin", "nxserver.bin", "nxservice64"; ExecutablePath = "NoMachine\bin\nxd.exe", "NoMachine\bin\nxnode.bin", "NoMachine\bin\nxserver.bin", "NoMachine\bin\nxservice64.exe" } - [PSCustomObject]@{Name = "Parsec"; DisplayName = "Parsec"; ProcessName = "parsecd", "pservice"; ExecutablePath = "Parsec\parsecd.exe", "Parsec\pservice.exe" } - [PSCustomObject]@{Name = "Remote Utilities"; DisplayName = "Remote Utilities - Host"; ProcessName = "rutserv", "rfusclient"; ExecutablePath = "Remote Utilities - Host\rfusclient.exe" } - [PSCustomObject]@{Name = "RemotePC"; DisplayName = "RemotePC"; ProcessName = "RemotePCHostUI", "RPCPerformanceService"; ExecutablePath = "RemotePC Host\RemotePCHostUI.exe", "RemotePC Host\RemotePCPerformance\RPCPerformanceService.exe" } - [PSCustomObject]@{Name = "Splashtop"; DisplayName = "Splashtop Streamer"; ProcessName = "SRAgent", "SRAppPB", "SRFeature", "SRManager", "SRService"; ExecutablePath = "Splashtop\Splashtop Remote\Server\SRService.exe" } - [PSCustomObject]@{Name = "Supremo"; ProcessName = "Supremo", "SupremoHelper", "SupremoService"; ExecutablePath = "Supremo\SupremoService.exe" } - [PSCustomObject]@{Name = "TeamViewer"; DisplayName = "TeamViewer"; ProcessName = "TeamViewer", "TeamViewer_Service", "tv_w32", "tv_x64"; ExecutablePath = "TeamViewer\TeamViewer.exe", "TeamViewer\TeamViewer_Service.exe", "TeamViewer\tv_w32.exe", "TeamViewer\tv_x64.exe" } - [PSCustomObject]@{Name = "TightVNC"; DisplayName = "TightVNC"; ProcessName = "tvnserver"; ExecutablePath = "TightVNC\tvnserver.exe" } - [PSCustomObject]@{Name = "UltraVNC"; DisplayName = "UltraVNC"; ProcessName = "winvnc"; ExecutablePath = "uvnc bvba\UltraVNC\WinVNC.exe" } - [PSCustomObject]@{Name = "VNC Connect (RealVNC)"; DisplayName = "VNC Server"; ProcessName = "vncserver"; ExecutablePath = "RealVNC\VNC Server\vncserver.exe" } - [PSCustomObject]@{Name = "Zoho Assist"; DisplayName = "Zoho Assist Unattended Agent"; ProcessName = "ZohoURS", "ZohoURSService"; ExecutablePath = "ZohoMeeting\UnAttended\ZohoMeeting\ZohoURS.exe", "ZohoMeeting\UnAttended\ZohoMeeting\ZohoURSService.exe" } - [PSCustomObject]@{Name = "Atera"; DisplayName = "AteraAgent"; ProcessName = "AteraAgent"; ExecutablePath = "ATERA Networks\AteraAgent\AteraAgent.exe" } - [PSCustomObject]@{Name = "Automate"; DisplayName = "Connectwise Automate"; ProcessName = "LTService", "LabTechService"; SpecialExecutablePath = "C:\Windows\LTSvc\LTSvc.exe" } - [PSCustomObject]@{Name = "Datto RMM"; DisplayName = "Datto RMM"; ProcessName = "AEMAgent"; ExecutablePath = "CentraStage\AEMAgent\AEMAgent.exe", "CentraStage\gui.exe" } - [PSCustomObject]@{Name = "Kaseya"; DisplayName = "Kaseya Agent"; ProcessName = "AgentMon", "KaseyaRemoteControlHost", "Kasaya.AgentEndpoint"; ExecutablePath = "Kaseya\AgentMon\AgentMon.exe" } - [PSCustomObject]@{Name = "N-Able N-Central"; DisplayName = "Windows Agent"; ProcessName = "winagent"; ExecutablePath = "N-able Technologies\Windows Agent\winagent.exe" } - [PSCustomObject]@{Name = "N-Able N-Sight"; DisplayName = "Advanced Monitoring Agent"; ProcessName = "winagent"; ExecutablePath = "Advanced Monitoring Agent\winagent.exe", "Advanced Monitoring Agent GP\winagent.exe" } - [PSCustomObject]@{Name = "Syncro"; DisplayName = "Syncro", "Kabuto"; ProcessName = "Syncro.App.Runner", "Kabuto.App.Runner", "Syncro.Service.Runner", "Kabuto.Service.Runner", "SyncroLive.Agent.Runner", "Kabuto.Agent.Runner", "SyncroLive.Agent.Service", "Syncro.Access.Service", "Syncro.Access.App"; ExecutablePath = "RepairTech\Syncro\Syncro.Service.Runner.exe", "RepairTech\Syncro\Syncro.App.Runner.exe" } - ) -} -process { - - # Lets see what tools we don't want to alert on. - $ExcludedTools = New-Object System.Collections.Generic.List[String] - - if ($ExcludeTools) { - $ExcludeTools -split ',' | ForEach-Object { $ExcludedTools.Add($_.Trim()) } - } - - # For this kind of alert it might be worth it to create a whole custom field of ignorables. - if ($ExclusionsFromCustomField) { - (Ninja-Property-Get $ExclusionsFromCustomField) -split ',' | ForEach-Object { $ExcludedTools.Add($_.Trim()) } - } - - if ($ExportCSV) { - $Format = "csv" - - if ($ExportCSV) { - $ExportResults = $ExportCSV - } - } - elseif ($ExportJSON) { - $Format = "json" - - if ($ExportJSON) { - $ExportResults = $ExportJSON - } - } - - # This take's our list and begins searching by the 4 method's in the begin block. - $RemoteAccessTools = $RemoteToolList | ForEach-Object { - - $UninstallKey = if ($_.DisplayName) { - $_.DisplayName | Find-UninstallKey - } - - $UninstallInfo = if ($_.DisplayName) { - $_.DisplayName | Find-UninstallKey -UninstallString - } - - $RunningStatus = if ($_.ProcessName) { - $_.ProcessName | Find-Process - } - - $ServiceStatus = if ($_.ProcessName) { - $_.ProcessName | Find-Service - } - - $InstallPath = if ($_.ExecutablePath) { - $_.ExecutablePath | Find-Executable - } - elseif ($_.SpecialExecutablePath) { - $_.SpecialExecutablePath | Find-Executable -Special - } - - if ($UninstallKey -or $RunningStatus -or $InstallPath -or $ServiceStatus) { - $Installed = "Yes" - } - else { - $Installed = "No" - } - - [PSCustomObject]@{ - Name = $_.Name - Installed = $Installed - CurrentlyRunning = if ($RunningStatus) { "Yes" }else { "No" } - HasRunningService = if ($ServiceStatus) { "Yes" }else { "No" } - UninstallString = $UninstallInfo - ExePath = $InstallPath - } | Where-Object { $ExcludedTools -notcontains $_.Name } - } - - $ActiveRemoteAccessTools = $RemoteAccessTools | Where-Object { $_.Installed -eq "Yes" } - - # If we found anything in the three check's we're gonna indicate it's installed but we may also want to save our results to a custom field. - # We also may want to output more than "We couldn't find any active remote access tools!" in the event we find nothing. - if ($ShowNotFound) { - - $RemoteAccessTools | Format-Table -Property Name, Installed, CurrentlyRunning, HasRunningService, UninstallString -AutoSize -Wrap | Out-String | Write-Host - - if ($ExportResults) { - Export-CustomField -Name $ExportResults -Format $Format -Object ($RemoteAccessTools | Select-Object Name, Installed, CurrentlyRunning, HasRunningService) - } - - } - else { - if ($ActiveRemoteAccessTools) { - - $ActiveRemoteAccessTools | Format-Table -Property Name, CurrentlyRunning, HasRunningService, UninstallString -AutoSize -Wrap | Out-String | Write-Host - - if ($ExportResults) { - Export-CustomField -Name $ExportResults -Format $Format -Object ($ActiveRemoteAccessTools | Select-Object Name, CurrentlyRunning, HasRunningService) - } - - } - else { - Write-Host "We couldn't find any active remote access tools!" - } - } - - if ($ActiveRemoteAccessTools) { - # We're going to set a failure status code in the event that we find something. - exit 1 - } - else { - exit 0 - } -} -end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + This script will look for remote access tools installed on the system. It can be given a list of tools to ignore as well as grab the exclusion list from a designated custom field. + + DISCLAIMER: This script is provided as a best effort for detecting remote access software installed on an agent, but it is not guaranteed to be 100% accurate. + Some remote access software may not be detected, or false positives may be reported. Use this script at your own risk and verify its results with other methods where possible. +.DESCRIPTION + This script will look for remote access tools installed on the system. Below is the full list of tools. Please note you can give it a list of tools to ignore and you can have + it grab the list from a custom field of your choosing. + + DISCLAIMER: This script is provided as a best effort for detecting remote access software installed on an agent, but it is not guaranteed to be 100% accurate. + Some remote access software may not be detected, or false positives may be reported. Use this script at your own risk and verify its results with other methods where possible. + + Remote Tools: AeroAdmin, Ammyy Admin, AnyDesk, BeyondTrust, Chrome Remote Desktop, Connectwise Control, DWService, GoToMyPC, LiteManager, LogMeIn, ManageEngine, + NoMachine, Parsec, Remote Utilities, RemotePC, Splashtop, Supremo, TeamViewer, TightVNC, UltraVNC, VNC Connect (RealVNC), Zoho Assist + RMM's: Atera, Automate, Datto RMM, Kaseya, N-Able N-Central, N-Able N-Sight, Syncro + +.EXAMPLE + (No Parameters) + Name CurrentlyRunning HasRunningService UninstallString + ---- ---------------- ----------------- --------------- + Connectwise Control Yes Yes MsiExec /X{examplestring} + Chrome Remote Desktop Yes Yes MsiExec /X{examplestring} + +PARAMETER: -ExcludeTools "Chrome Remote Desktop,Connectwise Control" + A comma separated list of tools you'd like to exclude from alerting on. +.EXAMPLE + -ExcludeTools "Chrome Remote Desktop,Connectwise Control" + We couldn't find any active remote access tools! + +PARAMETER: -ExclusionsFromCustomField "ReplaceMeWithAnyTextCustomField" + The name of a custom field that contains a comma separated list of tools to exclude from alerting. E.g. "ApprovedRemoteTools" +.EXAMPLE + -ExclusionsFromCustomField "ReplaceMeWithAnyTextCustomField" + We couldn't find any active remote access tools! + +PARAMETER: -ExportCSV "ReplaceMeWithAnyMultiLineCustomField" + The name of a multiline custom field to export to in csv format. ex. "RemoteTools" +.EXAMPLE + -ExportCSV "ReplaceMeWithAnyMultiLineCustomField" + Name CurrentlyRunning HasRunningService UninstallString + ---- ---------------- ----------------- --------------- + Connectwise Control Yes Yes MsiExec /X{examplestring} + Chrome Remote Desktop Yes Yes MsiExec /X{examplestring} + +PARAMETER: -ExportJSON "ReplaceMeWithAnyMultiLineCustomField" + The name of a multiline custom field to export to in JSON format. E.g. "RemoteTools" +.EXAMPLE + -ExportJSON "ReplaceMeWithAnyMultiLineCustomField" + Name CurrentlyRunning HasRunningService UninstallString + ---- ---------------- ----------------- --------------- + Connectwise Control Yes Yes MsiExec /X{examplestring} + Chrome Remote Desktop Yes Yes MsiExec /X{examplestring} + +PARAMETER: -ShowNotFound + Show the tools the script did not find as well. +.EXAMPLE + -ShowNotFound + Name CurrentlyRunning HasRunningService UninstallString + ---- ---------------- ----------------- --------------- + AeroAdmin No No + Ammyy Admin No No + BeyondTrust No No + Connectwise Control Yes Yes MsiExec /X{examplestring} + Chrome Remote Desktop Yes Yes MsiExec /X{examplestring} + +.OUTPUTS + None +.NOTES + General notes: CustomFields must be multiline for export. Regular text is fine for ExclusionsFromCustomField + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Renamed script and added Script Variable support, added error for when -ExportJSON and -ExportCSV are used together +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$ExcludeTools, + [Parameter()] + [String]$ExclusionsFromCustomField, + [Parameter()] + [String]$ExportCSV, + [Parameter()] + [String]$ExportJSON, + [Parameter()] + [Switch]$ShowNotFound = [System.Convert]::ToBoolean($env:includeToolsThatWereNotFound) +) + +begin { + #DISCLAIMER: This script is provided as a best effort for detecting remote access software installed on an agent, but it is not guaranteed to be 100% accurate. + #Some remote access software may not be detected, or false positives may be reported. Use this script at your own risk and verify its results with other methods where possible. + + if ($env:toolsToIgnore -and $env:toolsToIgnore -notlike "null") { $ExcludeTools = $env:toolsToIgnore } + if ($env:retrieveIgnoreListFromCustomField -and $env:retrieveIgnoreListFromCustomField -notlike "null") { $ExclusionsFromCustomField = $env:retrieveIgnoreListFromCustomField } + if ($env:exportCsvResultsToThisCustomField -and $env:exportCsvResultsToThisCustomField -notlike "null") { $ExportCSV = $env:exportCsvResultsToThisCustomField } + if ($env:exportJsonResultsToThisCustomField -and $env:exportJsonResultsToThisCustomField -notlike "null") { $ExportJSON = $env:exportJsonResultsToThisCustomField } + + if ($ExportCSV -and $ExportJSON) { Write-Error "You can only export in either JSON or CSV format. Not both."; exit 1 } + + # Check's the two Uninstall registry keys to see if the app is installed. Needs the name as it would appear in Control Panel. + function Find-UninstallKey { + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline)] + [String]$DisplayName, + [Parameter()] + [Switch]$UninstallString + ) + process { + $UninstallList = New-Object System.Collections.Generic.List[Object] + + $Result = Get-ChildItem HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Get-ItemProperty | + Where-Object { $_.DisplayName -like "*$DisplayName*" } + + if ($Result) { $UninstallList.Add($Result) } + + $Result = Get-ChildItem HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Get-ItemProperty | + Where-Object { $_.DisplayName -like "*$DisplayName*" } + + if ($Result) { $UninstallList.Add($Result) } + + # Programs don't always have an uninstall string listed here so to account for that I made this optional. + if ($UninstallString) { + # 64 Bit + $UninstallList | Select-Object -ExpandProperty UninstallString -ErrorAction Ignore + } + else { + $UninstallList + } + } + } + + # This will see if the process is currently active. Some people may want to react sooner to these alerts if its currently running vs not. + function Find-Process { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline)] + [String]$Name + ) + process { + Get-Process | Where-Object { $_.ProcessName -like "*$Name*" } | Select-Object -ExpandProperty Name + } + } + + # This will search C:\ProgramFiles and C:\ProgramFiles(x86) for the executable these tools use to run. + function Find-Executable { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline)] + [String]$Path, + [Parameter()] + [Switch]$Special + ) + process { + if (!$Special) { + if (Test-Path "$env:ProgramFiles\$Path") { + "$env:ProgramFiles\$Path" + } + + if (Test-Path "${Env:ProgramFiles(x86)}\$Path") { + "${Env:ProgramFiles(x86)}\$Path" + } + + if (Test-Path "$env:ProgramData\$Path") { + "$env:ProgramData\$Path" + } + } + else { + if (Test-Path $Path) { + $Path + } + } + } + } + + # Brought Get-CimInstance outside the function for better performance. + + $ServiceList = Get-CimInstance win32_service + function Find-Service { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline)] + [String]$Name + ) + process { + # Get-Service will display an error everytime it has an issue reading a service. Ignoring them as they're not relevant. + $ServiceList | Where-Object { $_.State -notlike "Disabled" -and $_.State -notlike "Stopped" } | + Where-Object { $_.PathName -Like "*$Name.exe*" } + } + } + + function Export-CustomField { + [CmdletBinding()] + param( + [Parameter()] + [String]$Name, + [Parameter()] + [ValidateSet("csv", "json")] + [String]$Format, + [Parameter()] + [PSCustomObject]$Object + ) + if ($Format -eq "csv") { + $csv = $Object | ConvertTo-Csv -NoTypeInformation | Out-String + # Ninja-Property-Set $Name $csv # Removed NinjaOne dependency + } + else { + $json = $Object | ConvertTo-Json | Out-String + # Ninja-Property-Set $Name $json # Removed NinjaOne dependency + } + } + + # This define's what tools we're looking for and how the script can find them. Some don't actually install anywhere (portable app) others do. + # Some change their installation path everytime so not particularly worth it to find it that way. + # Others store themselves in a super weird directory. Many don't list exactly where there .exe file is stored and suggest you exclude the whole folder from the av. + $RemoteToolList = @( + [PSCustomObject]@{Name = "AeroAdmin"; ProcessName = "AeroAdmin" } + [PSCustomObject]@{Name = "Ammyy Admin"; ProcessName = "AA_v3" } + [PSCustomObject]@{Name = "AnyDesk"; DisplayName = "AnyDesk"; ProcessName = "AnyDesk"; ExecutablePath = "AnyDesk\AnyDesk.exe" } + [PSCustomObject]@{Name = "BeyondTrust"; DisplayName = "Remote Support Jump Client", "Jumpoint"; ProcessName = "bomgar-jpt" } + [PSCustomObject]@{Name = "Chrome Remote Desktop"; DisplayName = "Chrome Remote Desktop Host"; ProcessName = "remoting_host"; ExecutablePath = "Google\Chrome Remote Desktop\112.0.5615.26\remoting_host.exe" } + [PSCustomObject]@{Name = "Connectwise Control"; DisplayName = "ScreenConnect Client"; ProcessName = "ScreenConnect.ClientService" } + [PSCustomObject]@{Name = "DWService"; DisplayName = "DWAgent"; ProcessName = "dwagent", "dwagsvc"; ExecutablePath = "DWAgent\runtime\dwagent.exe" } + [PSCustomObject]@{Name = "GoToMyPC"; DisplayName = "GoToMyPC"; ProcessName = "g2comm", "g2pre", "g2svc", "g2tray"; ExecutablePath = "GoToMyPC\g2comm.exe", "GoToMyPC\g2pre.exe", "GoToMyPC\g2svc.exe", "GoToMyPC\g2tray.exe" } + [PSCustomObject]@{Name = "LiteManager"; DisplayName = "LiteManager Pro - Server"; ProcessName = "ROMServer", "ROMFUSClient"; ExecutablePath = "LiteManager Pro - Server\ROMFUSClient.exe", "LiteManager Pro - Server\ROMServer.exe" } + [PSCustomObject]@{Name = "LogMeIn"; DisplayName = "LogMeIn"; ProcessName = "LogMeIn"; ExecutablePath = "LogMeIn\x64\LogMeIn.exe", "LogMeIn\x64\LogMeInSystray.exe" } + [PSCustomObject]@{Name = "ManageEngine"; DisplayName = "ManageEngine Remote Access Plus - Server", "ManageEngine UEMS - Agent"; ProcessName = "dcagenttrayicon", "UEMS", "dcagentservice"; ExecutablePath = "UEMS_Agent\bin\dcagenttrayicon.exe", "UEMS_CentralServer\bin\UEMS.exe", "UEMS_Agent\bin\dcagentservice.exe" } + [PSCustomObject]@{Name = "NoMachine"; DisplayName = "NoMachine"; ProcessName = "nxd", "nxnode.bin", "nxserver.bin", "nxservice64"; ExecutablePath = "NoMachine\bin\nxd.exe", "NoMachine\bin\nxnode.bin", "NoMachine\bin\nxserver.bin", "NoMachine\bin\nxservice64.exe" } + [PSCustomObject]@{Name = "Parsec"; DisplayName = "Parsec"; ProcessName = "parsecd", "pservice"; ExecutablePath = "Parsec\parsecd.exe", "Parsec\pservice.exe" } + [PSCustomObject]@{Name = "Remote Utilities"; DisplayName = "Remote Utilities - Host"; ProcessName = "rutserv", "rfusclient"; ExecutablePath = "Remote Utilities - Host\rfusclient.exe" } + [PSCustomObject]@{Name = "RemotePC"; DisplayName = "RemotePC"; ProcessName = "RemotePCHostUI", "RPCPerformanceService"; ExecutablePath = "RemotePC Host\RemotePCHostUI.exe", "RemotePC Host\RemotePCPerformance\RPCPerformanceService.exe" } + [PSCustomObject]@{Name = "Splashtop"; DisplayName = "Splashtop Streamer"; ProcessName = "SRAgent", "SRAppPB", "SRFeature", "SRManager", "SRService"; ExecutablePath = "Splashtop\Splashtop Remote\Server\SRService.exe" } + [PSCustomObject]@{Name = "Supremo"; ProcessName = "Supremo", "SupremoHelper", "SupremoService"; ExecutablePath = "Supremo\SupremoService.exe" } + [PSCustomObject]@{Name = "TeamViewer"; DisplayName = "TeamViewer"; ProcessName = "TeamViewer", "TeamViewer_Service", "tv_w32", "tv_x64"; ExecutablePath = "TeamViewer\TeamViewer.exe", "TeamViewer\TeamViewer_Service.exe", "TeamViewer\tv_w32.exe", "TeamViewer\tv_x64.exe" } + [PSCustomObject]@{Name = "TightVNC"; DisplayName = "TightVNC"; ProcessName = "tvnserver"; ExecutablePath = "TightVNC\tvnserver.exe" } + [PSCustomObject]@{Name = "UltraVNC"; DisplayName = "UltraVNC"; ProcessName = "winvnc"; ExecutablePath = "uvnc bvba\UltraVNC\WinVNC.exe" } + [PSCustomObject]@{Name = "VNC Connect (RealVNC)"; DisplayName = "VNC Server"; ProcessName = "vncserver"; ExecutablePath = "RealVNC\VNC Server\vncserver.exe" } + [PSCustomObject]@{Name = "Zoho Assist"; DisplayName = "Zoho Assist Unattended Agent"; ProcessName = "ZohoURS", "ZohoURSService"; ExecutablePath = "ZohoMeeting\UnAttended\ZohoMeeting\ZohoURS.exe", "ZohoMeeting\UnAttended\ZohoMeeting\ZohoURSService.exe" } + [PSCustomObject]@{Name = "Atera"; DisplayName = "AteraAgent"; ProcessName = "AteraAgent"; ExecutablePath = "ATERA Networks\AteraAgent\AteraAgent.exe" } + [PSCustomObject]@{Name = "Automate"; DisplayName = "Connectwise Automate"; ProcessName = "LTService", "LabTechService"; SpecialExecutablePath = "C:\Windows\LTSvc\LTSvc.exe" } + [PSCustomObject]@{Name = "Datto RMM"; DisplayName = "Datto RMM"; ProcessName = "AEMAgent"; ExecutablePath = "CentraStage\AEMAgent\AEMAgent.exe", "CentraStage\gui.exe" } + [PSCustomObject]@{Name = "Kaseya"; DisplayName = "Kaseya Agent"; ProcessName = "AgentMon", "KaseyaRemoteControlHost", "Kasaya.AgentEndpoint"; ExecutablePath = "Kaseya\AgentMon\AgentMon.exe" } + [PSCustomObject]@{Name = "N-Able N-Central"; DisplayName = "Windows Agent"; ProcessName = "winagent"; ExecutablePath = "N-able Technologies\Windows Agent\winagent.exe" } + [PSCustomObject]@{Name = "N-Able N-Sight"; DisplayName = "Advanced Monitoring Agent"; ProcessName = "winagent"; ExecutablePath = "Advanced Monitoring Agent\winagent.exe", "Advanced Monitoring Agent GP\winagent.exe" } + [PSCustomObject]@{Name = "Syncro"; DisplayName = "Syncro", "Kabuto"; ProcessName = "Syncro.App.Runner", "Kabuto.App.Runner", "Syncro.Service.Runner", "Kabuto.Service.Runner", "SyncroLive.Agent.Runner", "Kabuto.Agent.Runner", "SyncroLive.Agent.Service", "Syncro.Access.Service", "Syncro.Access.App"; ExecutablePath = "RepairTech\Syncro\Syncro.Service.Runner.exe", "RepairTech\Syncro\Syncro.App.Runner.exe" } + ) +} +process { + + # Lets see what tools we don't want to alert on. + $ExcludedTools = New-Object System.Collections.Generic.List[String] + + if ($ExcludeTools) { + $ExcludeTools -split ',' | ForEach-Object { $ExcludedTools.Add($_.Trim()) } + } + + # For this kind of alert it might be worth it to create a whole custom field of ignorables. + if ($ExclusionsFromCustomField) { + # NinjaOne integration removed - cannot retrieve from custom field + Write-Warning "ExclusionsFromCustomField specified but NinjaOne integration has been removed. Please use -ExcludeTools parameter instead." + # (Ninja-Property-Get $ExclusionsFromCustomField) -split ',' | ForEach-Object { $ExcludedTools.Add($_.Trim()) } + } + + if ($ExportCSV) { + $Format = "csv" + + if ($ExportCSV) { + $ExportResults = $ExportCSV + } + } + elseif ($ExportJSON) { + $Format = "json" + + if ($ExportJSON) { + $ExportResults = $ExportJSON + } + } + + # This take's our list and begins searching by the 4 method's in the begin block. + $RemoteAccessTools = $RemoteToolList | ForEach-Object { + + $UninstallKey = if ($_.DisplayName) { + $_.DisplayName | Find-UninstallKey + } + + $UninstallInfo = if ($_.DisplayName) { + $_.DisplayName | Find-UninstallKey -UninstallString + } + + $RunningStatus = if ($_.ProcessName) { + $_.ProcessName | Find-Process + } + + $ServiceStatus = if ($_.ProcessName) { + $_.ProcessName | Find-Service + } + + $InstallPath = if ($_.ExecutablePath) { + $_.ExecutablePath | Find-Executable + } + elseif ($_.SpecialExecutablePath) { + $_.SpecialExecutablePath | Find-Executable -Special + } + + if ($UninstallKey -or $RunningStatus -or $InstallPath -or $ServiceStatus) { + $Installed = "Yes" + } + else { + $Installed = "No" + } + + [PSCustomObject]@{ + Name = $_.Name + Installed = $Installed + CurrentlyRunning = if ($RunningStatus) { "Yes" }else { "No" } + HasRunningService = if ($ServiceStatus) { "Yes" }else { "No" } + UninstallString = $UninstallInfo + ExePath = $InstallPath + } | Where-Object { $ExcludedTools -notcontains $_.Name } + } + + $ActiveRemoteAccessTools = $RemoteAccessTools | Where-Object { $_.Installed -eq "Yes" } + + # If we found anything in the three check's we're gonna indicate it's installed but we may also want to save our results to a custom field. + # We also may want to output more than "We couldn't find any active remote access tools!" in the event we find nothing. + if ($ShowNotFound) { + + $RemoteAccessTools | Format-Table -Property Name, Installed, CurrentlyRunning, HasRunningService, UninstallString -AutoSize -Wrap | Out-String | Write-Host + + if ($ExportResults) { + Export-CustomField -Name $ExportResults -Format $Format -Object ($RemoteAccessTools | Select-Object Name, Installed, CurrentlyRunning, HasRunningService) + } + + } + else { + if ($ActiveRemoteAccessTools) { + + $ActiveRemoteAccessTools | Format-Table -Property Name, CurrentlyRunning, HasRunningService, UninstallString -AutoSize -Wrap | Out-String | Write-Host + + if ($ExportResults) { + Export-CustomField -Name $ExportResults -Format $Format -Object ($ActiveRemoteAccessTools | Select-Object Name, CurrentlyRunning, HasRunningService) + } + + } + else { + Write-Host "We couldn't find any active remote access tools!" + } + } + + if ($ActiveRemoteAccessTools) { + # We're going to set a failure status code in the event that we find something. + exit 1 + } + else { + exit 0 + } +} +end { + + + +} + diff --git a/Powershell Scripts/Last Reboot Reason.ps1 b/Powershell Scripts/Last Reboot Reason.ps1 index 3687396..5e8b658 100644 --- a/Powershell Scripts/Last Reboot Reason.ps1 +++ b/Powershell Scripts/Last Reboot Reason.ps1 @@ -1,406 +1,406 @@ # Retrieve the previous 14 reboot reasons and optionally save them to a WYSIWYG custom field, or save only the latest reboot reason to a text custom field. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Retrieve the previous 14 reboot reasons and optionally save them to a WYSIWYG custom field, or save only the latest reboot reason to a text custom field. -.DESCRIPTION - Retrieve the previous 14 reboot reasons and optionally save them to a WYSIWYG custom field, or save only the latest reboot reason to a text custom field. -.EXAMPLE - (No Parameters) - - Checking the event logs for possible reboot reasons. - [Warning] Only the previous 5 reboot reasons were found. - Translating the SIDs provided to usernames. - - ### Past Reboots ### - FormattedDate : 12/18/2024 11:20 AM - Id : 6008 - User : N/A - Message : The previous system shutdown at 11:20:06 AM on 12/18/2024 - was unexpected. - - FormattedDate : 12/18/2024 11:01 AM - Id : 1074 - User : NT AUTHORITY\SYSTEM - Message : The process C:\Windows\servicing\TrustedInstaller.exe - (SRV16-TEST) has initiated the restart of computer SRV16-TEST - on behalf of user NT AUTHORITY\SYSTEM for the following - reason: Operating System: Upgrade (Planned) - Reason Code: 0x80020003 - Shutdown Type: restart - Comment: - - FormattedDate : 12/18/2024 10:57 AM - Id : 6008 - User : N/A - Message : The previous system shutdown at 10:56:15 AM on 12/18/2024 - was unexpected. - - FormattedDate : 12/16/2024 5:25 PM - Id : 1074 - User : SRV16-TEST\Administrator - Message : The process C:\Windows\system32\wbem\wmiprvse.exe (SRV16-TEST) - has initiated the shutdown of computer SRV16-TEST on behalf of - user SRV16-TEST\Administrator for the following reason: No - title for this reason could be found - Reason Code: 0x80070015 - Shutdown Type: shutdown - Comment: - - FormattedDate : 12/16/2024 5:22 PM - Id : 1074 - User : NT AUTHORITY\SYSTEM - Message : The process C:\Windows\system32\winlogon.exe (MINWINPC) has - initiated the restart of computer WIN-2686BKBDV33 on behalf of - user NT AUTHORITY\SYSTEM for the following reason: Operating - System: Upgrade (Planned) - Reason Code: 0x80020003 - Shutdown Type: restart - Comment: - -PARAMETER: -TextCustomField "ExampleInput" - Optionally save the latest reboot reason to a text custom field of your choosing. - -PARAMETER: -WysiwygCustomField "ReplaceMeWithAnyMultilineCustomField" - Optionally save the previous 14 reboot reasons to a WYSIWYG custom field of your choosing. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$TextCustomField, - [Parameter()] - [String]$WysiwygCustomField -) - -begin { - # If script form variables are used, replace the command line parameters with their value. - if ($env:lastRebootReasonTextCustomField -and $env:lastRebootReasonTextCustomField -notlike "null") { $TextCustomField = $env:lastRebootReasonTextCustomField } - if ($env:last14RebootReasonsWysiwygCustomField -and $env:last14RebootReasonsWysiwygCustomField -notlike "null") { $WysiwygCustomField = $env:last14RebootReasonsWysiwygCustomField } - - # Check if a text custom field value was provided. - if($TextCustomField){ - # Remove any leading or trailing whitespace. - $TextCustomField = $TextCustomField.Trim() - - # If, after trimming, the text custom field is empty, print an error and exit. - if(!$TextCustomField){ - Write-Host -Object "[Error] Please enter a valid text custom field." - exit 1 - } - } - - # Check if a WYSIWYG custom field value was provided. - if($WysiwygCustomField){ - # Remove any leading or trailing whitespace. - $WysiwygCustomField = $WysiwygCustomField.Trim() - - # If, after trimming, the WYSIWYG custom field is empty, print an error and exit. - if(!$WysiwygCustomField){ - Write-Host -Object "[Error] Please enter a valid WYSIWYG custom field." - exit 1 - } - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName, - [Parameter()] - [Switch]$Piped - ) - # Remove the non-breaking space character - if ($Type -eq "WYSIWYG") { - $Value = $Value -replace ' ', ' ' - } - - # Measure the number of characters in the provided value - $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # Throw an error if the value exceeds the character limit of 200,000 characters - if ($Piped -and $Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - if (!$Piped -and $Characters -ge 45000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") - } - - # Initialize a hashtable for additional documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define a list of valid field types - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - - # Warn the user if the provided type is not valid - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # Define types that require options to be retrieved - $NeedsOptions = "Dropdown" - - # If the property is being set in a document or field and the type needs options, retrieve them - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an error if there was an issue retrieving the property options - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Process the property value based on its type - switch ($Type) { - "Checkbox" { - # Convert the value to a boolean for Checkbox type - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Convert the value to a Unix timestamp for Date or Date Time type - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Convert the dropdown value to its corresponding GUID - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - # Throw an error if the value is not present in the dropdown options - if (!($Selection)) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # For other types, use the value as is - $NinjaValue = $Value - } - } - - # Set the property value in the document if a document name is provided - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - try { - # Otherwise, set the standard property value - if ($Piped) { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - } - catch { - Write-Host -Object "[Error] Failed to set custom field." - throw $_.Exception.Message - } - } - - # Throw an error if setting the property failed - if ($CustomField.Exception) { - throw $CustomField - } - } - - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # Check if the current user is elevated (running as Administrator). - if (!(Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Inform the user that the script is checking the event logs for possible reboot reasons. - Write-Host -Object "Checking the event logs for possible reboot reasons." - - # Create an XML query to filter for certain event IDs (6008 and 1074) within the System event log. - [xml]$EventViewerXML = " - - - - -" - - try { - # Retrieve up to 14 recent matching events from the System log using the XML filter. - # Stop on errors so exceptions can be caught. - $MatchingEvents = Get-WinEvent -FilterXml $EventViewerXML -MaxEvents 14 -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to search event log." - exit 1 - } - - # Count how many events were retrieved. - $MatchingEventCount = $MatchingEvents | Measure-Object | Select-Object -ExpandProperty Count - - # If we found some events, but fewer than 14, warn the user that we have a limited number of reboot reasons. - if ($MatchingEventCount -gt 0 -and $MatchingEventCount -lt 14) { - Write-Host -Object "[Warning] Only the previous $MatchingEventCount reboot reasons were found." - } - - # If no events were found, print an error and set the exit code to 1. - if ($MatchingEventCount -lt 1) { - Write-Host -Object "[Error] No reboot reasons were found." - $ExitCode = 1 - } - - # Inform the user that SIDs are being translated to usernames. - Write-Host -Object "Translating the SIDs provided to usernames." - - # Retrieve user profile information from the registry for SID-to-username mapping. - try { - $AllUserProfiles = Get-ItemProperty "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - Write-Host -Object "[Error] Failed to find any user profiles." - $ExitCode = 1 - } - - # Format the retrieved events into a custom object with a friendly date format, ID, user, and message. - $FormattedResults = $MatchingEvents | Select-Object -Property "TimeCreated", "Id", "UserId", "Message" | ForEach-Object { - $Username = $null - - if ($_.UserId) { - try { - # Temporarily stop on errors to ensure exceptions are caught for SID translation. - $ErrorActionPreference = "Stop" - $SID = New-Object System.Security.Principal.SecurityIdentifier($_.UserId) - - # Attempt to translate the SID to an NT account (username). - $Username = $SID.Translate([System.Security.Principal.NTAccount]) | Select-Object -ExpandProperty Value -ErrorAction SilentlyContinue - } - catch { - # If direct SID translation fails, look up the profile in the registry. - $Sid = $_.UserId - $ProfileKey = $AllUserProfiles | Where-Object { $_.PSChildName -eq $Sid } | Select-Object @{Name = "Username"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf -ErrorAction SilentlyContinue)" } } -ErrorAction SilentlyContinue - Write-Host -Object "[Error] Failed to gather complete profile information for the SID '$($_.UserId)'." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - else { - # If there's no UserId, set the username to "N/A". - $Username = "N/A" - } - - # If no username was found directly, but we have a ProfileKey, use that as an approximation. - if (!$Username -and $ProfileKey) { - Write-Host -Object "Approximating the username for the SID '$Sid'." - $Username = $ProfileKey - } - elseif (!$Username) { - # If still no username is found, fall back to the SID itself. - $Username = $SID - } - - # Create a custom object with formatted data. - [PSCustomObject]@{ - TimeCreated = $_.TimeCreated - FormattedDate = "$($_.TimeCreated.ToShortDateString()) $($_.TimeCreated.ToShortTimeString())" - Id = $_.Id - User = $Username - Message = $_.Message - } - - # Restore the error action preference to continue. - $ErrorActionPreference = "Continue" - } - - # If either text or WYSIWYG custom fields were provided, print a blank line for readability. - if ($TextCustomField -or $WysiwygCustomField) { - Write-Host -Object "" - } - - # If a text custom field is specified, set it to display the most recent event (truncated if too long). - if ($TextCustomField) { - # Get the most recent event (first in the list). - $MostRecentEvent = $FormattedResults | Select-Object -First 1 - - # If the message is longer than 100 characters, truncate it and add ellipsis. - if (($MostRecentEvent.Message -replace '\r?\n', ' ').Length -gt 100) { - $MostRecentEvent = $MostRecentEvent | Select-Object -Property FormattedDate, Id, User, @{Name = "Message"; Expression = { "$($_.Message.Substring(0,97))..." } } - } - - # Construct the value to set in the text custom field. - $TextCustomFieldValue = "$($MostRecentEvent.FormattedDate) | EventID: $($MostRecentEvent.Id) | Username: $($MostRecentEvent.User) | Reason: $($MostRecentEvent.Message -replace '\r?\n', ' ')" - - # Attempt to set the specified text custom field. - try { - Write-Host "Attempting to set the Custom Field '$TextCustomField'." - Set-NinjaProperty -Name $TextCustomField -Value $TextCustomFieldValue - Write-Host "Successfully set the Custom Field '$TextCustomField'!" - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - - # If a WYSIWYG custom field is specified, construct an HTML table of the results and set the field. - if ($WysiwygCustomField) { - # Convert the formatted results to an HTML fragment. - $HTMLTable = $FormattedResults | Select-Object -Property FormattedDate, Id, User, Message | ConvertTo-Html -Fragment - - # Bold the table headers and adjust column widths. - $HTMLTable = $HTMLTable -replace '
', '' -replace 'FormattedDate', "Date" -replace 'Id', "Event Id" - $HTMLTable = $HTMLTable -replace 'User', "Username" -replace 'Message', "Reason" - - # Attempt to set the WYSIWYG custom field with the constructed HTML table. - try { - Write-Host "Attempting to set the Custom Field '$WysiwygCustomField'." - Set-NinjaProperty -Name $WysiwygCustomField -Value $HTMLTable - Write-Host "Successfully set the Custom Field '$WysiwygCustomField'!" - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - - # Print a heading for past reboots and then display the formatted results as a list for reference. - Write-Host -Object "`n### Past Reboots ###" - ($FormattedResults | Format-List -Property FormattedDate, Id, User, UserId, Message | Out-String).Trim() | Write-Host - - # Exit with the previously set exit code (defaulting to 0 if not set). - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Retrieve the previous 14 reboot reasons and optionally save them to a WYSIWYG custom field, or save only the latest reboot reason to a text custom field. +.DESCRIPTION + Retrieve the previous 14 reboot reasons and optionally save them to a WYSIWYG custom field, or save only the latest reboot reason to a text custom field. +.EXAMPLE + (No Parameters) + + Checking the event logs for possible reboot reasons. + [Warning] Only the previous 5 reboot reasons were found. + Translating the SIDs provided to usernames. + + ### Past Reboots ### + FormattedDate : 12/18/2024 11:20 AM + Id : 6008 + User : N/A + Message : The previous system shutdown at 11:20:06 AM on 12/18/2024 + was unexpected. + + FormattedDate : 12/18/2024 11:01 AM + Id : 1074 + User : NT AUTHORITY\SYSTEM + Message : The process C:\Windows\servicing\TrustedInstaller.exe + (SRV16-TEST) has initiated the restart of computer SRV16-TEST + on behalf of user NT AUTHORITY\SYSTEM for the following + reason: Operating System: Upgrade (Planned) + Reason Code: 0x80020003 + Shutdown Type: restart + Comment: + + FormattedDate : 12/18/2024 10:57 AM + Id : 6008 + User : N/A + Message : The previous system shutdown at 10:56:15 AM on 12/18/2024 + was unexpected. + + FormattedDate : 12/16/2024 5:25 PM + Id : 1074 + User : SRV16-TEST\Administrator + Message : The process C:\Windows\system32\wbem\wmiprvse.exe (SRV16-TEST) + has initiated the shutdown of computer SRV16-TEST on behalf of + user SRV16-TEST\Administrator for the following reason: No + title for this reason could be found + Reason Code: 0x80070015 + Shutdown Type: shutdown + Comment: + + FormattedDate : 12/16/2024 5:22 PM + Id : 1074 + User : NT AUTHORITY\SYSTEM + Message : The process C:\Windows\system32\winlogon.exe (MINWINPC) has + initiated the restart of computer WIN-2686BKBDV33 on behalf of + user NT AUTHORITY\SYSTEM for the following reason: Operating + System: Upgrade (Planned) + Reason Code: 0x80020003 + Shutdown Type: restart + Comment: + +PARAMETER: -TextCustomField "ExampleInput" + Optionally save the latest reboot reason to a text custom field of your choosing. + +PARAMETER: -WysiwygCustomField "ReplaceMeWithAnyMultilineCustomField" + Optionally save the previous 14 reboot reasons to a WYSIWYG custom field of your choosing. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$TextCustomField, + [Parameter()] + [String]$WysiwygCustomField +) + +begin { + # If script form variables are used, replace the command line parameters with their value. + if ($env:lastRebootReasonTextCustomField -and $env:lastRebootReasonTextCustomField -notlike "null") { $TextCustomField = $env:lastRebootReasonTextCustomField } + if ($env:last14RebootReasonsWysiwygCustomField -and $env:last14RebootReasonsWysiwygCustomField -notlike "null") { $WysiwygCustomField = $env:last14RebootReasonsWysiwygCustomField } + + # Check if a text custom field value was provided. + if($TextCustomField){ + # Remove any leading or trailing whitespace. + $TextCustomField = $TextCustomField.Trim() + + # If, after trimming, the text custom field is empty, print an error and exit. + if(!$TextCustomField){ + Write-Host -Object "[Error] Please enter a valid text custom field." + exit 1 + } + } + + # Check if a WYSIWYG custom field value was provided. + if($WysiwygCustomField){ + # Remove any leading or trailing whitespace. + $WysiwygCustomField = $WysiwygCustomField.Trim() + + # If, after trimming, the WYSIWYG custom field is empty, print an error and exit. + if(!$WysiwygCustomField){ + Write-Host -Object "[Error] Please enter a valid WYSIWYG custom field." + exit 1 + } + } + + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [Switch]$Piped # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + # # Remove the non-breaking space character # Removed NinjaOne dependency + # if ($Type -eq "WYSIWYG") { # Removed NinjaOne dependency + # $Value = $Value -replace ' ', ' ' # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Measure the number of characters in the provided value # Removed NinjaOne dependency + # $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + + # # Throw an error if the value exceeds the character limit of 200,000 characters # Removed NinjaOne dependency + # if ($Piped -and $Characters -ge 200000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if (!$Piped -and $Characters -ge 45000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Initialize a hashtable for additional documentation parameters # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + + # # If a document name is provided, add it to the documentation parameters # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # Define a list of valid field types # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + + # # Warn the user if the provided type is not valid # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # Define types that require options to be retrieved # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + + # # If the property is being set in a document or field and the type needs options, retrieve them # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if there was an issue retrieving the property options # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # Process the property value based on its type # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # Convert the value to a boolean for Checkbox type # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Convert the value to a Unix timestamp for Date or Date Time type # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Convert the dropdown value to its corresponding GUID # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # # Throw an error if the value is not present in the dropdown options # Removed NinjaOne dependency + # if (!($Selection)) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # For other types, use the value as is # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Set the property value in the document if a document name is provided # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # try { # Removed NinjaOne dependency + # # Otherwise, set the standard property value # Removed NinjaOne dependency + # if ($Piped) { # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # catch { # Removed NinjaOne dependency + # Write-Host -Object "[Error] Failed to set custom field." # Removed NinjaOne dependency + # throw $_.Exception.Message # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if setting the property failed # Removed NinjaOne dependency + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # Check if the current user is elevated (running as Administrator). + if (!(Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Inform the user that the script is checking the event logs for possible reboot reasons. + Write-Host -Object "Checking the event logs for possible reboot reasons." + + # Create an XML query to filter for certain event IDs (6008 and 1074) within the System event log. + [xml]$EventViewerXML = " + + + + +" + + try { + # Retrieve up to 14 recent matching events from the System log using the XML filter. + # Stop on errors so exceptions can be caught. + $MatchingEvents = Get-WinEvent -FilterXml $EventViewerXML -MaxEvents 14 -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to search event log." + exit 1 + } + + # Count how many events were retrieved. + $MatchingEventCount = $MatchingEvents | Measure-Object | Select-Object -ExpandProperty Count + + # If we found some events, but fewer than 14, warn the user that we have a limited number of reboot reasons. + if ($MatchingEventCount -gt 0 -and $MatchingEventCount -lt 14) { + Write-Host -Object "[Warning] Only the previous $MatchingEventCount reboot reasons were found." + } + + # If no events were found, print an error and set the exit code to 1. + if ($MatchingEventCount -lt 1) { + Write-Host -Object "[Error] No reboot reasons were found." + $ExitCode = 1 + } + + # Inform the user that SIDs are being translated to usernames. + Write-Host -Object "Translating the SIDs provided to usernames." + + # Retrieve user profile information from the registry for SID-to-username mapping. + try { + $AllUserProfiles = Get-ItemProperty "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + Write-Host -Object "[Error] Failed to find any user profiles." + $ExitCode = 1 + } + + # Format the retrieved events into a custom object with a friendly date format, ID, user, and message. + $FormattedResults = $MatchingEvents | Select-Object -Property "TimeCreated", "Id", "UserId", "Message" | ForEach-Object { + $Username = $null + + if ($_.UserId) { + try { + # Temporarily stop on errors to ensure exceptions are caught for SID translation. + $ErrorActionPreference = "Stop" + $SID = New-Object System.Security.Principal.SecurityIdentifier($_.UserId) + + # Attempt to translate the SID to an NT account (username). + $Username = $SID.Translate([System.Security.Principal.NTAccount]) | Select-Object -ExpandProperty Value -ErrorAction SilentlyContinue + } + catch { + # If direct SID translation fails, look up the profile in the registry. + $Sid = $_.UserId + $ProfileKey = $AllUserProfiles | Where-Object { $_.PSChildName -eq $Sid } | Select-Object @{Name = "Username"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf -ErrorAction SilentlyContinue)" } } -ErrorAction SilentlyContinue + Write-Host -Object "[Error] Failed to gather complete profile information for the SID '$($_.UserId)'." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + } + else { + # If there's no UserId, set the username to "N/A". + $Username = "N/A" + } + + # If no username was found directly, but we have a ProfileKey, use that as an approximation. + if (!$Username -and $ProfileKey) { + Write-Host -Object "Approximating the username for the SID '$Sid'." + $Username = $ProfileKey + } + elseif (!$Username) { + # If still no username is found, fall back to the SID itself. + $Username = $SID + } + + # Create a custom object with formatted data. + [PSCustomObject]@{ + TimeCreated = $_.TimeCreated + FormattedDate = "$($_.TimeCreated.ToShortDateString()) $($_.TimeCreated.ToShortTimeString())" + Id = $_.Id + User = $Username + Message = $_.Message + } + + # Restore the error action preference to continue. + $ErrorActionPreference = "Continue" + } + + # If either text or WYSIWYG custom fields were provided, print a blank line for readability. + if ($TextCustomField -or $WysiwygCustomField) { + Write-Host -Object "" + } + + # If a text custom field is specified, set it to display the most recent event (truncated if too long). + if ($TextCustomField) { + # Get the most recent event (first in the list). + $MostRecentEvent = $FormattedResults | Select-Object -First 1 + + # If the message is longer than 100 characters, truncate it and add ellipsis. + if (($MostRecentEvent.Message -replace '\r?\n', ' ').Length -gt 100) { + $MostRecentEvent = $MostRecentEvent | Select-Object -Property FormattedDate, Id, User, @{Name = "Message"; Expression = { "$($_.Message.Substring(0,97))..." } } + } + + # Construct the value to set in the text custom field. + $TextCustomFieldValue = "$($MostRecentEvent.FormattedDate) | EventID: $($MostRecentEvent.Id) | Username: $($MostRecentEvent.User) | Reason: $($MostRecentEvent.Message -replace '\r?\n', ' ')" + + # Attempt to set the specified text custom field. + try { + Write-Host "Attempting to set the Custom Field '$TextCustomField'." + # Set-NinjaProperty -Name $TextCustomField -Value $TextCustomFieldValue # Removed NinjaOne dependency + Write-Host "Successfully set the Custom Field '$TextCustomField'!" + } + catch { + Write-Host "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + } + + # If a WYSIWYG custom field is specified, construct an HTML table of the results and set the field. + if ($WysiwygCustomField) { + # Convert the formatted results to an HTML fragment. + $HTMLTable = $FormattedResults | Select-Object -Property FormattedDate, Id, User, Message | ConvertTo-Html -Fragment + + # Bold the table headers and adjust column widths. + $HTMLTable = $HTMLTable -replace '', '' -replace 'FormattedDate', "Date" -replace 'Id', "Event Id" + $HTMLTable = $HTMLTable -replace 'User', "Username" -replace 'Message', "Reason" + + # Attempt to set the WYSIWYG custom field with the constructed HTML table. + try { + Write-Host "Attempting to set the Custom Field '$WysiwygCustomField'." + # Set-NinjaProperty -Name $WysiwygCustomField -Value $HTMLTable # Removed NinjaOne dependency + Write-Host "Successfully set the Custom Field '$WysiwygCustomField'!" + } + catch { + Write-Host "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + } + + # Print a heading for past reboots and then display the formatted results as a list for reference. + Write-Host -Object "`n### Past Reboots ###" + ($FormattedResults | Format-List -Property FormattedDate, Id, User, UserId, Message | Out-String).Trim() | Write-Host + + # Exit with the previously set exit code (defaulting to 0 if not set). + exit $ExitCode +} +end { + + + +} diff --git a/Powershell Scripts/List Browser Extensions.ps1 b/Powershell Scripts/List Browser Extensions.ps1 index 39192e1..d2d2935 100644 --- a/Powershell Scripts/List Browser Extensions.ps1 +++ b/Powershell Scripts/List Browser Extensions.ps1 @@ -1,568 +1,568 @@ # Reports on all installed browser extensions for Chrome, Firefox and Edge. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Reports on all installed browser extensions for Chrome, Firefox and Edge. -.DESCRIPTION - Reports on all installed browser extensions for Chrome, Firefox and Edge. -.EXAMPLE - (No Parameters) - - A Google Chrome installation was detected. Searching Chrome for browser extensions... - A Microsoft Edge installation was detected. Searching Microsoft Edge for browser extensions... - A Firefox installation was detected. Searching Firefox for browser extensions... - Attempting to set Custom Field 'Multiline'. - WARNING: 10,000 Character Limit has been reached! Trimming output until the character limit is satisfied... - Successfully set Custom Field 'Multiline'! - Attempting to set Custom Field 'WYSIWYG'. - Successfully set Custom Field 'WYSIWYG'! - Browser extensions were detected. - - - Browser : Chrome - User : cheart - Name : askBelynda | Sustainable Shopping - Extension ID : pcmbjnfbjkeieekkahdfgchcbjfhhgdi - Description : Sustainable shopping made simple with askBelynda. Choose ethical products o(...) - - Browser : Chrome - User : cheart - Name : Beni - Your secondhand shopping assistant - Extension ID : efdgbhncnligcbloejoaemnfhjihkccj - Description : The easiest way to shop secondhand. Beni finds the best resale alternative(...) - - Browser : Chrome - User : cheart - Name : Bonjourr · Minimalist Startpage - Extension ID : dlnejlppicbjfcfcedcflplfjajinajd - Description : Improve your web browsing experience with Bonjourr, a beautiful, customizab(...) - - Browser : Chrome - User : cheart - Name : Boxel 3D - Extension ID : mjjgmlmpeaikcaajghilhnioimmaibon - Description : Boxel 3D is the 3rd release of your favorite box jumping game made by the d(...) - - ... - -PARAMETER: -MultilineCustomField "ReplaceMeWithNameOfAMultilineCustomField" - Specify the name of a multiline custom field to optionally store the search results in. Leave blank to not set a multiline field. - -PARAMETER: -WysiwygCustomField "ReplaceMeWithAnyWYSIWYGCustomField" - Specify the name of a WYSIWYG custom field to optionally store the search results in. Leave blank to not set a WYSIWYG field. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$MultilineCustomField, - [Parameter()] - [String]$WysiwygCustomField -) - -begin { - # Replace parameters with the dynamic script variables. - if ($env:multilineCustomFieldName -and $env:multilineCustomFieldName -notlike "null") { $MultilineCustomField = $env:multilineCustomFieldName } - if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $WysiwygCustomField = $env:wysiwygCustomFieldName } - - # Check if $MultilineCustomField and $WysiwygCustomField are both not null and have the same value - if ($MultilineCustomField -and $WysiwygCustomField -and $MultilineCustomField -eq $WysiwygCustomField) { - Write-Host "[Error] Custom Fields of different types cannot have the same name." - Write-Host "https://ninjarmm.zendesk.com/hc/en-us/articles/360060920631-Custom-Fields-Configuration-Device-Role-Fields" - exit 1 - } - - # Function to get user registry hives based on the type of account - function Get-UserHives { - param ( - [Parameter()] - [ValidateSet('AzureAD', 'DomainAndLocal', 'All')] - [String]$Type = "All", - [Parameter()] - [String[]]$ExcludedUsers, - [Parameter()] - [switch]$IncludeDefault - ) - - # Patterns for user SID depending on account type - $Patterns = switch ($Type) { - "AzureAD" { "S-1-12-1-(\d+-?){4}$" } - "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" } - "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" } - } - - # Fetch user profiles whose SIDs match the defined patterns and prepare objects with their details - $UserProfiles = Foreach ($Pattern in $Patterns) { - Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | - Where-Object { $_.PSChildName -match $Pattern } | - Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, - @{Name = "UserName"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } }, - @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, - @{Name = "Path"; Expression = { $_.ProfileImagePath } } - } - - # Handle inclusion of the default user profile if requested - switch ($IncludeDefault) { - $True { - $DefaultProfile = "" | Select-Object UserName, SID, UserHive, Path - $DefaultProfile.UserName = "Default" - $DefaultProfile.SID = "DefaultProfile" - $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT" - $DefaultProfile.Path = "C:\Users\Default" - - $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.UserName } - } - } - - # Return user profiles, excluding any specified users - $UserProfiles | Where-Object { $ExcludedUsers -notcontains $_.UserName } - } - - # Function to check if the current PowerShell session is running with elevated permissions - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded; the value is greater than or equal to 200,000 characters.") - } - - # If requested to set the field value for a Ninja document, we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is specified, it is assumed that the input does not need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to be set. - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # Redirect error output to the success stream to make it easier to handle errors if nothing is found or if something else goes wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received with an exception property, the function will exit with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below types require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # Although it's highly likely we were given a value like "True" or a boolean datatype, it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - - # Function to find installation keys based on the display name, optionally returning uninstall strings - function Find-InstallKey { - [CmdletBinding()] - param ( - [Parameter(ValueFromPipeline = $True)] - [String]$DisplayName, - [Parameter()] - [Switch]$UninstallString, - [Parameter()] - [String]$UserBaseKey - ) - process { - # Initialize an empty list to hold installation objects - $InstallList = New-Object System.Collections.Generic.List[Object] - - # If no user base key is specified, search in the default system-wide uninstall paths - if (!$UserBaseKey) { - # Search for programs in 32-bit and 64-bit locations. Then add them to the list if they match the display name - $Result = Get-ChildItem -Path "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } - if ($Result) { $InstallList.Add($Result) } - - $Result = Get-ChildItem -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } - if ($Result) { $InstallList.Add($Result) } - } - else { - # If a user base key is specified, search in the user-specified 64-bit and 32-bit paths. - $Result = Get-ChildItem -Path "$UserBaseKey\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } - if ($Result) { $InstallList.Add($Result) } - - $Result = Get-ChildItem -Path "$UserBaseKey\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } - if ($Result) { $InstallList.Add($Result) } - } - - # If the UninstallString switch is specified, return only the uninstall strings; otherwise, return the full installation objects. - if ($UninstallString) { - $InstallList | Select-Object -ExpandProperty UninstallString -ErrorAction SilentlyContinue - } - else { - $InstallList - } - } - } - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # Check if the script is running with elevated permissions (administrator rights) - if (!(Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Search for Chrome installations on the system and enable chrome extension search if found. - Find-InstallKey -DisplayName "Chrome" | ForEach-Object { - $ChromeInstallations = $True - } - - # Search for Firefox installations on the system and enable firefox extension search if found. - Find-InstallKey -DisplayName "Firefox" | ForEach-Object { - $FireFoxInstallations = $True - } - - # Search for Edge installations on the system and flag if found and enable edge extension search if found. - Find-InstallKey -DisplayName "Edge" | ForEach-Object { - $EdgeInstallations = $True - } - - # Retrieve all user profiles from the system - $UserProfiles = Get-UserHives -Type "All" - # Loop through each profile on the machine - Foreach ($UserProfile in $UserProfiles) { - # Load User ntuser.dat if it's not already loaded - If (($ProfileWasLoaded = Test-Path Registry::HKEY_USERS\$($UserProfile.SID)) -eq $false) { - Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe LOAD HKU\$($UserProfile.SID) `"$($UserProfile.UserHive)`"" -Wait -WindowStyle Hidden - } - - # Repeat search for installations of browsers but in the user's registry context - Find-InstallKey -UserBaseKey "Registry::HKEY_USERS\$($UserProfile.SID)" -DisplayName "Chrome" | ForEach-Object { - $ChromeInstallations = $True - } - Find-InstallKey -UserBaseKey "Registry::HKEY_USERS\$($UserProfile.SID)" -DisplayName "Firefox" | ForEach-Object { - $FireFoxInstallations = $True - } - Find-InstallKey -UserBaseKey "Registry::HKEY_USERS\$($UserProfile.SID)" -DisplayName "Edge" | ForEach-Object { - $EdgeInstallations = $True - } - - # Unload NTuser.dat - If ($ProfileWasLoaded -eq $false) { - [gc]::Collect() - Start-Sleep 1 - Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe UNLOAD HKU\$($UserProfile.SID)" -Wait -WindowStyle Hidden | Out-Null - } - } - - # Initialize a list to store details of detected browser extensions - $BrowserExtensions = New-Object System.Collections.Generic.List[object] - - # If Chrome was found, search for Chrome extensions in each user's profile - if ($ChromeInstallations) { - Write-Host -Object "A Google Chrome installation was detected. Searching Chrome for browser extensions..." - $UserProfiles | ForEach-Object { - if (!(Test-Path -Path "$($_.Path)\AppData\Local\Google\Chrome\User Data" -ErrorAction SilentlyContinue)) { - return - } - - if(Test-Path -Path "$($_.Path)\AppData\Local\Google\Chrome\User Data\Local State" -ErrorAction SilentlyContinue){ - $AllProfiles = Get-Content -Path "$($_.Path)\AppData\Local\Google\Chrome\User Data\Local State" | ConvertFrom-JSON - } - - $PreferenceFiles = Get-ChildItem "$($_.Path)\AppData\Local\Google\Chrome\User Data\*\Preferences" -Exclude "System Profile" | Select-Object -ExpandProperty Fullname - - foreach ($PreferenceFile in $PreferenceFiles) { - - $GooglePreferences = Get-Content -Path $PreferenceFile | ConvertFrom-Json - if($AllProfiles){ - $ProfileLocation = $PreferenceFile | Get-Item | Select-Object -ExpandProperty Directory | Split-Path -Leaf - $ProfileName = $AllProfiles.profile.info_cache | Select-Object -ExpandProperty $ProfileLocation | Select-Object -ExpandProperty Name - }else{ - $ProfileName = $GooglePreferences.profile.name - } - - foreach ($Extension in $GooglePreferences.extensions.settings.PSObject.Properties) { - $BrowserExtensions.Add( - [PSCustomObject]@{ - Browser = "Chrome" - User = $_.UserName - Profile = $ProfileName - Name = $Extension.Value.manifest.name - "Extension ID" = $Extension.name - Description = $Extension.Value.manifest.description - } - ) - } - } - } - } - - # If Edge was found, search for Edge extensions in each user's profile - if ($EdgeInstallations) { - Write-Host -Object "A Microsoft Edge installation was detected. Searching Microsoft Edge for browser extensions..." - $UserProfiles | ForEach-Object { - if (!(Test-Path -Path "$($_.Path)\AppData\Local\Microsoft\Edge\User Data" -ErrorAction SilentlyContinue)) { - return - } - - if(Test-Path -Path "$($_.Path)\AppData\Local\Microsoft\Edge\User Data\Local State" -ErrorAction SilentlyContinue){ - $AllProfiles = Get-Content -Path "$($_.Path)\AppData\Local\Microsoft\Edge\User Data\Local State" | ConvertFrom-JSON - } - - $PreferenceFiles = Get-ChildItem "$($_.Path)\AppData\Local\Microsoft\Edge\User Data\*\Preferences" -Exclude "System Profile" | Select-Object -ExpandProperty Fullname - - foreach ($PreferenceFile in $PreferenceFiles) { - - $EdgePreferences = Get-Content -Path $PreferenceFile | ConvertFrom-Json - if($AllProfiles){ - $ProfileLocation = $PreferenceFile | Get-Item | Select-Object -ExpandProperty Directory | Split-Path -Leaf - $ProfileName = $AllProfiles.profile.info_cache | Select-Object -ExpandProperty $ProfileLocation | Select-Object -ExpandProperty Name - }else{ - $ProfileName = $EdgePreferences.profile.name - } - - foreach ($Extension in $EdgePreferences.extensions.settings.PSObject.Properties) { - if ($Extension.Value.active_bit -like "False" ) { continue } - if (!$Extension.Value.manifest.name) { continue } - $BrowserExtensions.Add( - [PSCustomObject]@{ - Browser = "Edge" - User = $_.UserName - Profile = $ProfileName - Name = $Extension.Value.manifest.name - "Extension ID" = $Extension.name - Description = $Extension.Value.manifest.description - } - ) - } - } - } - } - - # If Firefox was found, search for Firefox extensions in each user's profile - if ($FireFoxInstallations) { - Write-Host -Object "A Firefox installation was detected. Searching Firefox for browser extensions..." - $UserProfiles | ForEach-Object { - if (!(Test-Path -Path "$($_.Path)\AppData\Roaming\Mozilla\Firefox\Profiles" -ErrorAction SilentlyContinue)) { - return - } - - $FirefoxProfileFolders = Get-ChildItem -Path "$($_.Path)\AppData\Roaming\Mozilla\Firefox\Profiles" -Directory | Where-Object { $_.Name -match "\.default-release$" } | Select-Object -ExpandProperty Fullname - - foreach ( $FirefoxProfile in $FirefoxProfileFolders ) { - - if (!(Test-Path -Path "$FirefoxProfile\extensions.json")) { - continue - } - - $Extensions = Get-Content -Path "$FirefoxProfile\extensions.json" | ConvertFrom-Json - - foreach ($Extension in $Extensions.addons) { - $BrowserExtensions.Add( - [PSCustomObject]@{ - Browser = "Firefox" - User = $_.UserName - Profile = "N/A" - Name = $Extension.defaultlocale.name - "Extension ID" = $Extension.id - Description = $Extension.defaultlocale.description - } - ) - } - } - } - } - - # Check if there are any browser extensions to process - if ($BrowserExtensions.Count -gt 0) { - # Format the BrowserExtensions list to include a shortened description if the description is too long. - $BrowserExtensions = $BrowserExtensions | Select-Object Browser, User, Profile, Name, "Extension ID", @{ - Name = "Description" - Expression = { - $Characters = $_.Description | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -gt 75) { - "$(($_.Description).SubString(0,75))(...)" - } - else { - $_.Description - } - } - } - } - - # Check if extensions were found and if we were requested to set a multiline custom field - if ($BrowserExtensions.Count -gt 0 -and $MultilineCustomField) { - try { - Write-Host "Attempting to set Custom Field '$MultilineCustomField'." - $CustomFieldValue = New-Object System.Collections.Generic.List[string] - - # Sort and format the list of extensions for output - $CustomFieldList = $BrowserExtensions | Sort-Object Browser, User, Profile, Name | Select-Object Browser, User, Profile, Name, "Extension ID", Description - $CustomFieldValue.Add(($CustomFieldList | Format-List | Out-String)) - - # Measure the total character count of the formatted string - $Characters = $CustomFieldValue | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 9500) { - Write-Warning "10,000 Character Limit has been reached! Trimming output until the character limit is satisfied..." - - # If it doesn't comply with the limits we'll need to recreate it with some adjustments. - $i = 0 - do { - # Recreate the custom field output starting with a warning that we truncated the output. - $CustomFieldValue = New-Object System.Collections.Generic.List[string] - $CustomFieldValue.Add("This info has been truncated to accommodate the 10,000 character limit.") - - # Flip the array so that the last entry is on top. - [array]::Reverse($CustomFieldList) - - # Remove the next item. - $CustomFieldList[$i] = $null - $i++ - - # We'll flip the array back to right side up. - [array]::Reverse($CustomFieldList) - - # Add it back to the output. - $CustomFieldValue.Add(($CustomFieldList | Format-List | Out-String)) - - # Check that we now comply with the character limit. If not restart the do loop. - $Characters = $CustomFieldValue | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters - }while ($Characters -ge 9500) - } - - Set-NinjaProperty -Name $MultilineCustomField -Value $CustomFieldValue - Write-Host "Successfully set Custom Field '$MultilineCustomField'!" - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - - # Check if extensions were found and if we were requested to set a WYSIWYG custom field. - if ($BrowserExtensions.Count -gt 0 -and $WysiwygCustomField) { - try { - Write-Host "Attempting to set Custom Field '$WysiwygCustomField'." - - # Prepare the custom field output. - $CustomFieldValue = New-Object System.Collections.Generic.List[string] - - # Convert the matching events into an html report. - $htmlTable = $BrowserExtensions | Sort-Object Browser, User, Profile, Name | Select-Object Browser, User, Profile, Name, "Extension ID", Description | ConvertTo-Html -Fragment - - # Add the newly created html into the custom field output. - $CustomFieldValue.Add($htmlTable) - - # Check that the output complies with the hard character limits. - $Characters = $CustomFieldValue | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 199500) { - Write-Warning "200,000 Character Limit has been reached! Trimming output until the character limit is satisfied..." - - # If it doesn't comply with the limits we'll need to recreate it with some adjustments. - $i = 0 - do { - # Recreate the custom field output starting with a warning that we truncated the output. - $CustomFieldValue = New-Object System.Collections.Generic.List[string] - $CustomFieldValue.Add("

This info has been truncated to accommodate the 200,000 character limit.

") - - # Flip the array so that the last entry is on top. - [array]::Reverse($htmlTable) - # If the next entry is a row we'll delete it. - if ($htmlTable[$i] -match '
' -or $htmlTable[$i] -match '
' -or $htmlTable[$i] -match '
", "" -replace "
", "
" - - # Set the custom field using the generated HTML - Set-NinjaProperty -Name $WysiwygCustomField -Value $CustomFieldValue - Write-Host "Successfully set Custom Field '$WysiwygCustomField'!" - } - catch { - # If setting the custom field fails, display an error and exit - Write-Host "[Error] $($_.Exception.Message)" - exit 1 - } - } - - # Exit with the appropriate exit code - exit $ExitCode -} -end { - - - -} +#Requires -Version 4 + +<# +.SYNOPSIS + List all local accounts on the machine and optionally save the results to a WYSIWYG custom field. +.DESCRIPTION + List all local accounts on the machine and optionally save the results to a WYSIWYG custom field. +.EXAMPLE + (No Parameters) + Retrieving list of local users. + ExitCode: 1 + Parsing username list into machine-readable format. + Retrieving additional information on individual user accounts. + + Username FullName Enabled PasswordLastSet LastLogon + -------- -------- ------- --------------- --------- + helpdesk True 9/13/2024 9:01:22 AM 9/13/2024 9:20:25 AM + +PARAMETER: -IncludeDisabledUsers + Include disabled user accounts in the results. + +PARAMETER: -WysiwygCustomField "ReplaceMeWithAnyWysiwygCustomField" + Optionally specify the name of a WYSIWYG custom field to store the results in. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2012 R2 + Release Notes: Added WYSIWYG support, switched to using only "net user", made more verbose, and improved error handling. +#> + +[CmdletBinding()] +param ( + [Parameter()] + [Switch]$IncludeDisabledUsers = [System.Convert]::ToBoolean($env:includeDisabledUsers), + [Parameter()] + [String]$WysiwygCustomField +) + +begin { + # If script form variables are used, replace the command line parameters with their value. + if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $WysiwygCustomField = $env:wysiwygCustomFieldName } + + function Test-IsDomainController { + # Determine the method to retrieve the operating system information based on PowerShell version + try { + $OS = if ($PSVersionTable.PSVersion.Major -lt 5) { + Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop + } + else { + Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop + } + } + catch { + Write-Host -Object "[Error] Unable to validate whether or not this device is a domain controller." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Check if the ProductType is "2", which indicates that the system is a domain controller + if ($OS.ProductType -eq "2") { + return $true + } + } + + function Test-IsDomainJoined { + # Check the PowerShell version to determine the appropriate cmdlet to use + try { + if ($PSVersionTable.PSVersion.Major -lt 5) { + return $(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain + } + else { + return $(Get-CimInstance -Class Win32_ComputerSystem).PartOfDomain + } + } + catch { + Write-Host -Object "[Error] Unable to validate whether or not this device is a part of a domain." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + } + + function Test-IsEntraJoined { + # Check if the operating system version is Windows 10 or higher + if ([environment]::OSVersion.Version.Major -ge 10) { + # Run the dsregcmd.exe tool to check Entra join status and look for "AzureAdJoined : YES" + $dsreg = dsregcmd.exe /status | Select-String "AzureAdJoined : YES" + } + + # If the search found the "AzureAdJoined : YES" string, return True, otherwise return False + if ($dsreg) { return $True }else { return $False } + } + + # If running on a domain controller, display an error message and exit + if (Test-IsDomainController) { + Write-Host -Object "[Error] This script is not compatible with domain controllers." + exit 1 + } + + # If running on a domain joined machine, warn that only local accounts will be displayed. + if (Test-IsDomainJoined) { + Write-Warning -Message "This script will only display local accounts. It will not display Active Directory accounts." + } + + # If running on an Entra joined machine, warn that only local accounts will be displayed. + if (Test-IsEntraJoined) { + Write-Warning -Message "This script will only display local accounts. It will not display Microsoft Entra accounts." + } + + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # # Measure the number of characters in the provided value # Removed NinjaOne dependency + # $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + + # # Throw an error if the value exceeds the character limit of 200,000 characters # Removed NinjaOne dependency + # if ($Characters -ge 200000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Initialize a hashtable for additional documentation parameters # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + + # # If a document name is provided, add it to the documentation parameters # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # Define a list of valid field types # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + + # # Warn the user if the provided type is not valid # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # Define types that require options to be retrieved # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + + # # If the property is being set in a document or field and the type needs options, retrieve them # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if there was an issue retrieving the property options # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # Process the property value based on its type # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # Convert the value to a boolean for Checkbox type # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Convert the value to a Unix timestamp for Date or Date Time type # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Convert the dropdown value to its corresponding GUID # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # # Throw an error if the value is not present in the dropdown options # Removed NinjaOne dependency + # if (!($Selection)) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # For other types, use the value as is # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Set the property value in the document if a document name is provided # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # # Otherwise, set the standard property value # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if setting the property failed # Removed NinjaOne dependency + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # Check if the script is running with elevated (administrator) privileges. If not elevated, display an error and exit with code 1. + if (!(Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Define paths for standard output and error logs, with random names to avoid conflicts + $StandardOutLog = "$env:TEMP\$(Get-Random)_stdout.log" + $StandardErrLog = "$env:TEMP\$(Get-Random)_stderr.log" + + # Define the arguments for the "net user" command to list all users + $NetUserArguments = @( + "user" + ) + + # Configure the process start parameters for the "net.exe" command + $ProcessArguments = @{ + FilePath = "$env:SystemRoot\System32\net.exe" + ArgumentList = $NetUserArguments + RedirectStandardOutput = $StandardOutLog + RedirectStandardError = $StandardErrLog + PassThru = $True + NoNewWindow = $True + Wait = $True + } + + # Inform the user that the script is retrieving the list of local users + Write-Host -Object "Retrieving list of local users." + + # Try to start the "net.exe" process and catch any errors + try { + $NetUserProcess = Start-Process @ProcessArguments -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] Failed to start net.exe" + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Output the exit code of the net.exe process + Write-Host -Object "ExitCode: $($NetUserProcess.ExitCode)" + + # Check if the exit code indicates success (0 or 1) + if ($NetUserProcess.ExitCode -ne 0 -and $NetUserProcess.ExitCode -ne 1) { + Write-Warning "Exit code of $($NetUserProcess.ExitCode) does not indicate success." + } + + # Check if the standard error log exists, indicating an error occurred + if (Test-Path -Path $StandardErrLog -ErrorAction SilentlyContinue) { + + # Attempt to read the error log + try { + $ErrorLog = Get-Content -Path $StandardErrLog -ErrorAction Stop + } + catch { + # If reading the log fails, display an error and exit + Write-Host -Object "[Error] Failed to open error log at '$StandardErrLog'." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Remove the error log file after reading + try { + Remove-Item -Path $StandardErrLog -ErrorAction Stop + } + catch { + # If removing the log file fails, display an error + Write-Host -Object "[Error] Failed to remove standard error log at '$StandardErrLog'." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + } + + # If there is any content in the error log, display it and exit + if ($ErrorLog) { + Write-Host -Object "[Error] An error has occurred." + $ErrorLog | ForEach-Object { + Write-Host -Object "[Error] $_" + } + exit 1 + } + + # Check if the standard output log exists, which contains the user list + if (!(Test-Path -Path $StandardOutLog -ErrorAction SilentlyContinue)) { + Write-Host -Object "[Error] No net user output detected." + exit 1 + } + + # Try to read the standard output log for user data + try { + $NetUserOutput = Get-Content -Path $StandardOutLog -ErrorAction Stop + } + catch { + # If reading the log fails, display an error and exit + Write-Host -Object "[Error] Failed to open output log at '$StandardOutLog'." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Try to remove the standard output log after reading + try { + Remove-Item -Path $StandardOutLog -ErrorAction Stop + } + catch { + # If removing the log file fails, display an error + Write-Host -Object "[Error] Failed to remove standard output log at '$StandardOutLog'." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + + # Inform the user that the username list is being parsed + Write-Host -Object "Parsing username list into machine-readable format." + + # Skip the first 4 lines of the output and filter out any empty lines or the completion message + $NetUserOutput = $NetUserOutput | Select-Object -Skip 4 | Where-Object { $_ -and $_ -notmatch 'command completed' } + + # Split the usernames by 4 or more spaces and trim whitespace + $Usernames = $NetUserOutput -split '\s{4,}' | ForEach-Object { $_.Trim() } | Where-Object { $_ } + + # Create a list to hold user account information + $LocalUserAccounts = New-Object System.Collections.Generic.List[object] + + # Inform the user that additional information is being retrieved for each account + Write-Host -Object "Retrieving additional information on individual user accounts." + + # For each username in the list, retrieve more details + $Usernames | ForEach-Object { + $Username = if ($_ -notmatch '"') { "`"$_`"" }else { $_ } + $StandardOutLog = "$env:TEMP\$(Get-Random)_stdout.log" + $StandardErrLog = "$env:TEMP\$(Get-Random)_stderr.log" + + # Define the arguments for the "net user" command for a specific user + $NetUserArguments = @( + "user" + $Username + ) + + # Configure the process start parameters for the "net.exe" command + $ProcessArguments = @{ + FilePath = "$env:SystemRoot\System32\net.exe" + ArgumentList = $NetUserArguments + RedirectStandardOutput = $StandardOutLog + RedirectStandardError = $StandardErrLog + PassThru = $True + NoNewWindow = $True + Wait = $True + } + + # Try to start the "net.exe" process for the current user + try { + $NetUserProcess = Start-Process @ProcessArguments -ErrorAction Stop + } + catch { + # If the process fails, display an error and return to the next iteration + Write-Host -Object "[Error] Failed to start net.exe for user '$_'" + Write-Host -Object "[Error] $($_.Exception.Message)" + return + } + + # Check if the exit code of the net.exe process indicates failure + if ($NetUserProcess.ExitCode -ne 0) { + Write-Warning "Exit code of $($NetUserProcess.ExitCode) does not indicate success." + } + + # Check if the standard error log exists, indicating an error occurred + if (Test-Path -Path $StandardErrLog -ErrorAction SilentlyContinue) { + try { + $ErrorLog = Get-Content -Path $StandardErrLog -ErrorAction Stop + } + catch { + # If reading the log fails, display an error and set the exit code to 1 + Write-Host -Object "[Error] Failed to open error log at '$StandardErrLog'." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + + # Remove the error log file after reading + try { + Remove-Item -Path $StandardErrLog -ErrorAction Stop + } + catch { + # If removing the log file fails, display an error and set the exit code to 1 + Write-Host -Object "[Error] Failed to remove standard error log at '$StandardErrLog'." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + } + + # If there is any content in the error log, display it and set the exit code to 1 + if ($ErrorLog) { + Write-Host -Object "[Error] An error has occurred." + $ErrorLog | ForEach-Object { + Write-Host -Object "[Error] $_" + } + $ExitCode = 1 + } + + # Check if the standard output log exists, which contains the user details + if (!(Test-Path -Path $StandardOutLog -ErrorAction SilentlyContinue)) { + Write-Host -Object "[Error] No net user output detected for '$_'." + return + } + + # Try to read the standard output log for user details + try { + $NetUserOutput = Get-Content -Path $StandardOutLog -ErrorAction Stop + } + catch { + # If reading the log fails, display an error and return + Write-Host -Object "[Error] Failed to open output log at '$StandardOutLog'." + Write-Host -Object "[Error] $($_.Exception.Message)" + return + } + + # Try to remove the standard output log after reading + try { + Remove-Item -Path $StandardOutLog -ErrorAction Stop + } + catch { + # If removing the log file fails, display an error and set the exit code to 1 + Write-Host -Object "[Error] Failed to remove standard output log at '$StandardOutLog'." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + + # Extract relevant information from the output log for each user account + $LastSet = "$(($NetUserOutput | Select-String 'Password last set') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + $Expired = "$(($NetUserOutput | Select-String 'Password expires') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + $Changeable = "$(($NetUserOutput | Select-String 'Password changeable') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + $LastLogon = "$(($NetUserOutput | Select-String 'Last logon') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + + # Try to add user account details to the list of local user accounts + try { + $ErrorActionPreference = "Stop" + $LocalUserAccounts.Add( + [PSCustomObject]@{ + Username = $_ + FullName = "$(($NetUserOutput | Select-String 'Full Name') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + Comment = "$(($NetUserOutput | Select-String 'Comment') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + Enabled = if ("$(($NetUserOutput | Select-String 'Account active') -split '\s{4,}' | Select-Object -Skip 1)".Trim() -like "Yes") { $true }else { $false } + AccountExpires = "$(($NetUserOutput | Select-String 'Account expires') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + PasswordLastSet = if ($LastSet) { Get-Date -Date $LastSet }else { $null } + PasswordExpires = if ($Expired -notmatch "Never" -and $Expired -notlike "") { Get-Date -Date $Expired }else { $Expired } + PasswordChangeable = if ($Changeable) { Get-Date -Date $Changeable }else { $null } + PasswordRequired = if ("$(($NetUserOutput | Select-String 'Password required') -split '\s{4,}' | Select-Object -Skip 1)".Trim() -like "Yes") { $true }else { $false } + UserMayChangePassword = if ("$(($NetUserOutput | Select-String 'User may change password') -split '\s{4,}' | Select-Object -Skip 1)".Trim() -like "Yes") { $true }else { $false } + WorkstationsAllowed = "$(($NetUserOutput | Select-String 'Workstations allowed') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + LogonScript = "$(($NetUserOutput | Select-String 'Logon script') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + UserProfile = "$(($NetUserOutput | Select-String 'User profile') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + LastLogon = if ($LastLogon -notmatch "Never" -and $LastLogon -notlike "") { Get-Date -Date $LastLogon }else { $LastLogon } + LogonHoursAllowed = "$(($NetUserOutput | Select-String 'Logon hours allowed') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + } + ) + $ErrorActionPreference = "Continue" + } + catch { + # If adding the account details fails, display an error and set the exit code to 1 + Write-Host -Object "[Error] Failed to parse account '$_'" + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + + # Silently continue and add a basic object with just the username to the list + $ErrorActionPreference = "SilentlyContinue" + $LocalUserAccounts.Add( + [PSCustomObject]@{ + Username = $_ + } + ) + $ErrorActionPreference = "Continue" + } + + } + + # Filter out disabled users if $IncludeDisabledUsers is not specified + if (!$IncludeDisabledUsers) { + $LocalUserAccounts = $LocalUserAccounts | Where-Object { $_.Enabled } + } + + # Display the final list of users in a table format + Write-Host -Object "" + ($LocalUserAccounts | Sort-Object -Property Username | Format-Table -Property Username, FullName, Enabled, PasswordLastSet, LastLogon -AutoSize | Out-String).Trim() | Write-Host + Write-Host -Object "" + + # If a custom field is specified, display the HTML formatted output + if ($WysiwygCustomField) { + Write-Host "" + Write-Host "Note: Custom field '$WysiwygCustomField' was specified but NinjaOne integration has been removed." + Write-Host "HTML formatted output:" + + # Generate HTML content from the user accounts and format the output + $CustomFieldValue = $LocalUserAccounts | Sort-Object -Property Username | Select-Object -Property Username, @{ Name = "Full Name" ; Expression = { $_.FullName } }, Enabled, @{ Name = "Password Last Set" ; Expression = { $_.PasswordLastSet } }, @{ Name = "Last Logon" ; Expression = { $_.LastLogon } } | ConvertTo-Html -Fragment + $CustomFieldValue = $CustomFieldValue -replace "", "" + $CustomFieldValue = $CustomFieldValue -replace "
Local Users
", "" -replace "
", "
" + + Write-Host $CustomFieldValue + } + + # Exit with the appropriate exit code + exit $ExitCode +} +end { + + + +} diff --git a/Powershell Scripts/Locked Out User Report.ps1 b/Powershell Scripts/Locked Out User Report.ps1 index bdbe729..7b108fa 100644 --- a/Powershell Scripts/Locked Out User Report.ps1 +++ b/Powershell Scripts/Locked Out User Report.ps1 @@ -1,147 +1,147 @@ -# This script will see if any accounts on a local machine or on a domain controller are locked out. -You can optionally export this information into a custom field. - +# This script will see if any accounts on a local machine or on a domain controller are locked out. +You can optionally export this information into a custom field. + Does NOT check Azure AD Accounts. - -<# -.SYNOPSIS - This script will see if any accounts on a local machine or on a domain controller are locked out. - You can optionally export this information into a custom field. - - Does NOT check Azure AD Accounts. -.DESCRIPTION - This script will see if any accounts on a local machine or on a domain controller are locked out. - You can optionally export this information into a custom field. - - Does NOT check Azure AD Accounts. - -.EXAMPLE - (No Parameters but ran on a DC) - SamAccountName LastLogonDate PasswordExpired Enabled - -------------- ------------- --------------- ------- - user 4/20/2023 1:09:23 PM False True - -.EXAMPLE - (No Parameters but ran on a Workstation) - Name Domain LocalAccount Disabled - ---- ------ ------------ -------- - user TEST False False - -PARAMETER: -ExportTXT "ReplaceMeWithAnyMultiLineCustomField" - Name of a multi-line customfield you'd like to export the results to. -.EXAMPLE - -ExportTXT "ReplaceMeWithAnyMultiLineCustomField" - Name Domain LocalAccount Disabled - ---- ------ ------------ -------- - user TEST False False - -PARAMETER: -ExportCSV "ReplaceMeWithAnyMultiLineCustomField" - Name of a multi-line customfield you'd like to export the results to. -.EXAMPLE - -ExportCSV "ReplaceMeWithAnyMultiLineCustomField" - Name Domain LocalAccount Disabled - ---- ------ ------------ -------- - user TEST False False - -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 7, Windows Server 2008 - Release Notes: Renamed script, added Script Variable support, added support for showing results of only 1 or more specific users. -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$Users, - [Parameter()] - [String]$ExportCSV, - [Parameter()] - [String]$ExportTXT -) - -begin { - if ($env:usersToCheck -and $env:usersToCheck -notlike "null") { $Users = $env:usersToCheck } - if ($env:exportCsvResultsToThisCustomField -and $env:exportCsvResultsToThisCustomField -notlike "null") { $ExportCSV = $env:exportCsvResultsToThisCustomField } - if ($env:exportTextResultsToThisCustomField -and $env:exportTextResultsToThisCustomField -notlike "null") { $ExportTXT = $env:exportTextResultsToThisCustomField } - - if ($Users) { - $UsersToCheck = $Users.split(',') | ForEach-Object { $_.Trim() } - Write-Warning "Only the following users will be checked: $UsersToCheck" - } - function Test-IsDomainController { - if ($PSVersionTable.PSVersion.Major -ge 5) { - $OS = Get-CimInstance -ClassName Win32_OperatingSystem - } - else { - $OS = Get-WmiObject -Class Win32_OperatingSystem - } - - if ($OS.ProductType -eq "2") { - return $True - } - } - - function Test-IsAzureJoined { - $dsreg = dsregcmd.exe /status | Select-String "AzureAdJoined : YES" - if ($dsreg) { - return $True - } - } - - if ([System.Environment]::OSVersion.Version.Major -ge 10) { - if (Test-IsAzureJoined) { Write-Warning "This device is Azure AD Joined, this script currently cannot detect if Azure AD Users are locked out!" } - } -} -process { - - # For Domain Controllers find the locked out account using Search-ADAccount - if (Test-IsDomainController) { - Import-Module ActiveDirectory - $LockedOutUsers = Search-ADAccount -LockedOut | Select-Object SamAccountName, LastLogonDate, PasswordExpired, Enabled - } - else { - $LockedOutUsers = if ($PSVersionTable.PSVersion.Major -ge 5) { - Get-CimInstance -ClassName Win32_Useraccount | Where-Object { $_.Lockout -eq $True } | Select-Object Name, Domain, LocalAccount, Disabled - } - else { - Get-WmiObject -Class Win32_Useraccount | Where-Object { $_.Lockout -eq $True } | Select-Object Name, Domain, LocalAccount, Disabled - } - } - - if ($Users) { - $LockedOutUsers = $LockedOutUsers | Where-Object { $UsersToCheck -contains $_.Name -or $UsersToCheck -contains $_.SamAccountName } - } - - if ($LockedOutUsers) { - # Output any locked out users into the activity log - Write-Warning "Locked out users were found!" - $LockedOutUsers | Format-Table | Out-String | Write-Host - - # Export the list in CSV format into a custom field - if ($ExportCSV) { - Ninja-Property-Set $ExportCSV ($LockedOutUsers | ConvertTo-Csv -NoTypeInformation) - } - - # Export the usernames into a custom field - if ($ExportTXT) { - if ($LockedOutUsers.Name) { - Ninja-Property-Set $ExportTXT ($LockedOutUsers.Name | Out-String) - } - - if ($LockedOutUsers.SamAccountName) { - Ninja-Property-Set $ExportTXT ($LockedOutUsers.SamAccountName | Out-String) - } - } - Exit 1 - } - - Write-Host "No locked out users detected. Please note this does NOT check Azure AD Accounts." - Exit 0 -} -end { - - - -} - + +<# +.SYNOPSIS + This script will see if any accounts on a local machine or on a domain controller are locked out. + You can optionally export this information into a custom field. + + Does NOT check Azure AD Accounts. +.DESCRIPTION + This script will see if any accounts on a local machine or on a domain controller are locked out. + You can optionally export this information into a custom field. + + Does NOT check Azure AD Accounts. + +.EXAMPLE + (No Parameters but ran on a DC) + SamAccountName LastLogonDate PasswordExpired Enabled + -------------- ------------- --------------- ------- + user 4/20/2023 1:09:23 PM False True + +.EXAMPLE + (No Parameters but ran on a Workstation) + Name Domain LocalAccount Disabled + ---- ------ ------------ -------- + user TEST False False + +PARAMETER: -ExportTXT "ReplaceMeWithAnyMultiLineCustomField" + Name of a multi-line customfield you'd like to export the results to. +.EXAMPLE + -ExportTXT "ReplaceMeWithAnyMultiLineCustomField" + Name Domain LocalAccount Disabled + ---- ------ ------------ -------- + user TEST False False + +PARAMETER: -ExportCSV "ReplaceMeWithAnyMultiLineCustomField" + Name of a multi-line customfield you'd like to export the results to. +.EXAMPLE + -ExportCSV "ReplaceMeWithAnyMultiLineCustomField" + Name Domain LocalAccount Disabled + ---- ------ ------------ -------- + user TEST False False + +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 7, Windows Server 2008 + Release Notes: Renamed script, added Script Variable support, added support for showing results of only 1 or more specific users. +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$Users, + [Parameter()] + [String]$ExportCSV, + [Parameter()] + [String]$ExportTXT +) + +begin { + if ($env:usersToCheck -and $env:usersToCheck -notlike "null") { $Users = $env:usersToCheck } + if ($env:exportCsvResultsToThisCustomField -and $env:exportCsvResultsToThisCustomField -notlike "null") { $ExportCSV = $env:exportCsvResultsToThisCustomField } + if ($env:exportTextResultsToThisCustomField -and $env:exportTextResultsToThisCustomField -notlike "null") { $ExportTXT = $env:exportTextResultsToThisCustomField } + + if ($Users) { + $UsersToCheck = $Users.split(',') | ForEach-Object { $_.Trim() } + Write-Warning "Only the following users will be checked: $UsersToCheck" + } + function Test-IsDomainController { + if ($PSVersionTable.PSVersion.Major -ge 5) { + $OS = Get-CimInstance -ClassName Win32_OperatingSystem + } + else { + $OS = Get-WmiObject -Class Win32_OperatingSystem + } + + if ($OS.ProductType -eq "2") { + return $True + } + } + + function Test-IsAzureJoined { + $dsreg = dsregcmd.exe /status | Select-String "AzureAdJoined : YES" + if ($dsreg) { + return $True + } + } + + if ([System.Environment]::OSVersion.Version.Major -ge 10) { + if (Test-IsAzureJoined) { Write-Warning "This device is Azure AD Joined, this script currently cannot detect if Azure AD Users are locked out!" } + } +} +process { + + # For Domain Controllers find the locked out account using Search-ADAccount + if (Test-IsDomainController) { + Import-Module ActiveDirectory + $LockedOutUsers = Search-ADAccount -LockedOut | Select-Object SamAccountName, LastLogonDate, PasswordExpired, Enabled + } + else { + $LockedOutUsers = if ($PSVersionTable.PSVersion.Major -ge 5) { + Get-CimInstance -ClassName Win32_Useraccount | Where-Object { $_.Lockout -eq $True } | Select-Object Name, Domain, LocalAccount, Disabled + } + else { + Get-WmiObject -Class Win32_Useraccount | Where-Object { $_.Lockout -eq $True } | Select-Object Name, Domain, LocalAccount, Disabled + } + } + + if ($Users) { + $LockedOutUsers = $LockedOutUsers | Where-Object { $UsersToCheck -contains $_.Name -or $UsersToCheck -contains $_.SamAccountName } + } + + if ($LockedOutUsers) { + # Output any locked out users into the activity log + Write-Warning "Locked out users were found!" + $LockedOutUsers | Format-Table | Out-String | Write-Host + + # Export the list in CSV format into a custom field + if ($ExportCSV) { + # Ninja-Property-Set $ExportCSV ($LockedOutUsers | ConvertTo-Csv -NoTypeInformation) # Removed NinjaOne dependency + } + + # Export the usernames into a custom field + if ($ExportTXT) { + if ($LockedOutUsers.Name) { + # Ninja-Property-Set $ExportTXT ($LockedOutUsers.Name | Out-String) # Removed NinjaOne dependency + } + + if ($LockedOutUsers.SamAccountName) { + # Ninja-Property-Set $ExportTXT ($LockedOutUsers.SamAccountName | Out-String) # Removed NinjaOne dependency + } + } + Exit 1 + } + + Write-Host "No locked out users detected. Please note this does NOT check Azure AD Accounts." + Exit 0 +} +end { + + + +} + diff --git a/Powershell Scripts/Microsoft Entra Audit.ps1 b/Powershell Scripts/Microsoft Entra Audit.ps1 index abe7fb2..a29b1a2 100644 --- a/Powershell Scripts/Microsoft Entra Audit.ps1 +++ b/Powershell Scripts/Microsoft Entra Audit.ps1 @@ -1,110 +1,110 @@ # Retrieves details regarding the devices Microsoft Entra/Azure AD connection status. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Retrieves details regarding the device's Microsoft Entra/Azure AD connection status. -.DESCRIPTION - Retrieves details regarding the device's Microsoft Entra/Azure AD connection status. -.EXAMPLE - (No Parameters) - - Join Type: Microsoft Entra Joined - - Tenant Name Tenant ID Device Name Device ID - ----------- --------- ----------- --------- - NinjaOne 0e0adb39-f83f-4576-9102-db1b902ca108 KYLE-WIN11-TEST 59fd69ed-4893-41df-9f7d-211d5a0a8986 - -PARAMETER: -DeviceStateCustomFieldName "ReplaceMe" - Name of a custom field to store the device state (Microsoft Entra Joined, Domain Joined etc...) in. E.g., deviceState - -PARAMETER: -TenantInfoCustomFieldName "ReplaceMe" - Name of a custom field to store tenant info in. E.g., azureInfo. - -.OUTPUTS - None -.NOTES - Minimum Supported OS: Windows 10+ - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$DeviceStateCustomFieldName, - [Parameter()] - [String]$TenantInfoCustomFieldName -) - -begin { - # Retrieve custom field name from dynamic script form - if ($env:joinTypeCustomFieldName -and $env:joinTypeCustomFieldName -notlike "null") { $DeviceStateCustomFieldName = $env:joinTypeCustomFieldName } - if ($env:tenantInfoCustomFieldName -and $env:tenantInfoCustomFieldName -notlike "null") { $TenantInfoCustomFieldName = $env:tenantInfoCustomFieldName } - - # Turn dsregcmd.exe /status into a much more parseable PowerShell object - function Get-DSRegCMD { - $DSReg = dsregcmd.exe /status | Where-Object { $_ -match " : " } - - $properties = @{} - - $DSReg | ForEach-Object { - $split = ($_ -split '\s:\s').trim() - $properties[$split[0]] = $split[1] - } - - [PSCustomObject]$properties - } -} -process { - # Retrieve current Azure AD information - $AzureInfo = Get-DSRegCMD - - $JoinType = if ($AzureInfo.AzureAdJoined -eq "YES" -and $AzureInfo.DomainJoined -eq "NO" -and $AzureInfo.EnterpriseJoined -eq "NO") { - "Microsoft Entra Joined" - } - elseif ($AzureInfo.AzureAdJoined -eq "NO" -and $AzureInfo.DomainJoined -eq "YES" -and $AzureInfo.EnterpriseJoined -eq "NO") { - "Domain Joined" - } - elseif ($AzureInfo.AzureAdJoined -eq "YES" -and $AzureInfo.DomainJoined -eq "YES" -and $AzureInfo.EnterpriseJoined -eq "NO") { - "Microsoft Entra Hybrid Joined" - } - elseif ($AzureInfo.AzureAdJoined -eq "NO" -and $AzureInfo.DomainJoined -eq "YES" -and $AzureInfo.EnterpriseJoined -eq "YES") { - "On-Premises DRS Joined" - } - else { - "None" - } - - # Retrieve the most relevant information - $TenantInfo = [PSCustomObject]@{ - "Tenant Name" = $AzureInfo.TenantName - "Tenant ID" = $AzureInfo.TenantId - "Device Name" = $AzureInfo."Device Name" - "Device ID" = $AzureInfo.DeviceId - } - - # Report results into the activity log - Write-Host "Join Type: $JoinType" - $TenantInfo | Format-Table | Out-String | Write-Host - - # Store results into a custom field - if($DeviceStateCustomFieldName -eq $TenantInfoCustomFieldName -and $DeviceStateCustomFieldName){ - $TenantInfo | Add-Member -MemberType NoteProperty -Name 'Join Type' -Value $JoinType - - Ninja-Property-Set -Name $DeviceStateCustomFieldName -Value ($TenantInfo | Format-List -Property "Tenant Name","Tenant ID","Join Type","Device Name","Device ID" | Out-String) - exit 0 - } - - if ($DeviceStateCustomFieldName) { - Ninja-Property-Set -Name $DeviceStateCustomFieldName -Value ($JoinType) - } - - if ($TenantInfoCustomFieldName) { - Ninja-Property-Set -Name $TenantInfoCustomFieldName -Value ($TenantInfo | Format-List | Out-String) - } -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Retrieves details regarding the device's Microsoft Entra/Azure AD connection status. +.DESCRIPTION + Retrieves details regarding the device's Microsoft Entra/Azure AD connection status. +.EXAMPLE + (No Parameters) + + Join Type: Microsoft Entra Joined + + Tenant Name Tenant ID Device Name Device ID + ----------- --------- ----------- --------- + NinjaOne 0e0adb39-f83f-4576-9102-db1b902ca108 KYLE-WIN11-TEST 59fd69ed-4893-41df-9f7d-211d5a0a8986 + +PARAMETER: -DeviceStateCustomFieldName "ReplaceMe" + Name of a custom field to store the device state (Microsoft Entra Joined, Domain Joined etc...) in. E.g., deviceState + +PARAMETER: -TenantInfoCustomFieldName "ReplaceMe" + Name of a custom field to store tenant info in. E.g., azureInfo. + +.OUTPUTS + None +.NOTES + Minimum Supported OS: Windows 10+ + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$DeviceStateCustomFieldName, + [Parameter()] + [String]$TenantInfoCustomFieldName +) + +begin { + # Retrieve custom field name from dynamic script form + if ($env:joinTypeCustomFieldName -and $env:joinTypeCustomFieldName -notlike "null") { $DeviceStateCustomFieldName = $env:joinTypeCustomFieldName } + if ($env:tenantInfoCustomFieldName -and $env:tenantInfoCustomFieldName -notlike "null") { $TenantInfoCustomFieldName = $env:tenantInfoCustomFieldName } + + # Turn dsregcmd.exe /status into a much more parseable PowerShell object + function Get-DSRegCMD { + $DSReg = dsregcmd.exe /status | Where-Object { $_ -match " : " } + + $properties = @{} + + $DSReg | ForEach-Object { + $split = ($_ -split '\s:\s').trim() + $properties[$split[0]] = $split[1] + } + + [PSCustomObject]$properties + } +} +process { + # Retrieve current Azure AD information + $AzureInfo = Get-DSRegCMD + + $JoinType = if ($AzureInfo.AzureAdJoined -eq "YES" -and $AzureInfo.DomainJoined -eq "NO" -and $AzureInfo.EnterpriseJoined -eq "NO") { + "Microsoft Entra Joined" + } + elseif ($AzureInfo.AzureAdJoined -eq "NO" -and $AzureInfo.DomainJoined -eq "YES" -and $AzureInfo.EnterpriseJoined -eq "NO") { + "Domain Joined" + } + elseif ($AzureInfo.AzureAdJoined -eq "YES" -and $AzureInfo.DomainJoined -eq "YES" -and $AzureInfo.EnterpriseJoined -eq "NO") { + "Microsoft Entra Hybrid Joined" + } + elseif ($AzureInfo.AzureAdJoined -eq "NO" -and $AzureInfo.DomainJoined -eq "YES" -and $AzureInfo.EnterpriseJoined -eq "YES") { + "On-Premises DRS Joined" + } + else { + "None" + } + + # Retrieve the most relevant information + $TenantInfo = [PSCustomObject]@{ + "Tenant Name" = $AzureInfo.TenantName + "Tenant ID" = $AzureInfo.TenantId + "Device Name" = $AzureInfo."Device Name" + "Device ID" = $AzureInfo.DeviceId + } + + # Report results into the activity log + Write-Host "Join Type: $JoinType" + $TenantInfo | Format-Table | Out-String | Write-Host + + # Store results into a custom field + if($DeviceStateCustomFieldName -eq $TenantInfoCustomFieldName -and $DeviceStateCustomFieldName){ + $TenantInfo | Add-Member -MemberType NoteProperty -Name 'Join Type' -Value $JoinType + + # Ninja-Property-Set -Name $DeviceStateCustomFieldName -Value ($TenantInfo | Format-List -Property "Tenant Name","Tenant ID","Join Type","Device Name","Device ID" | Out-String) # Removed NinjaOne dependency + exit 0 + } + + if ($DeviceStateCustomFieldName) { + # Ninja-Property-Set -Name $DeviceStateCustomFieldName -Value ($JoinType) # Removed NinjaOne dependency + } + + if ($TenantInfoCustomFieldName) { + # Ninja-Property-Set -Name $TenantInfoCustomFieldName -Value ($TenantInfo | Format-List | Out-String) # Removed NinjaOne dependency + } +} +end { + + + +} diff --git a/Powershell Scripts/Network Connection Profile Check.ps1 b/Powershell Scripts/Network Connection Profile Check.ps1 index 8fd96d4..08f411d 100644 --- a/Powershell Scripts/Network Connection Profile Check.ps1 +++ b/Powershell Scripts/Network Connection Profile Check.ps1 @@ -1,237 +1,237 @@ # Checks the current network connections to see what profile they are currently using and optionally save the results to a custom field. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Checks the current network connections to see what profile they are currently using and optionally save the results to a custom field. -.DESCRIPTION - Checks the current network connections to see what profile they are currently using and optionally save the results to a custom field. -.EXAMPLE - (No Parameters) - - Retrieving network adapters. - Gathering additional information. - - NetworkAdapter MacAddress Type Profile - -------------- ---------- ---- ------- - LabNet - Win10 0 00-17-FB-00-00-02 Wired Public - -PARAMETER: -CustomField "ReplaceMeWithYourDesiredCustomField" - Optionally specify the name of a custom field to save the results to. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$CustomField -) - -begin { - # If script form variables are used, replace the command the command line parameters with their value. - if ($env:networkProfileCustomFieldName -and $env:networkProfileCustomFieldName -notlike "null") { $CustomField = $env:networkProfileCustomFieldName } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - # Measure the number of characters in the provided value - $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # Throw an error if the value exceeds the character limit of 200,000 characters - if ($Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - # Initialize a hashtable for additional documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define a list of valid field types - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - - # Warn the user if the provided type is not valid - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # Define types that require options to be retrieved - $NeedsOptions = "Dropdown" - - # If the property is being set in a document or field and the type needs options, retrieve them - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an error if there was an issue retrieving the property options - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Process the property value based on its type - switch ($Type) { - "Checkbox" { - # Convert the value to a boolean for Checkbox type - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Convert the value to a Unix timestamp for Date or Date Time type - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Convert the dropdown value to its corresponding GUID - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - # Throw an error if the value is not present in the dropdown options - if (!($Selection)) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # For other types, use the value as is - $NinjaValue = $Value - } - } - - # Set the property value in the document if a document name is provided - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - # Otherwise, set the standard property value - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - - # Throw an error if setting the property failed - if ($CustomField.Exception) { - throw $CustomField - } - } - - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # Check if the script is running with elevated (Administrator) privileges. - if ($CustomField -and !(Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Administrator privileges are required to set Custom Fields." - exit 1 - } - - # Initialize a list to store network information. - $NetworkInfo = New-Object System.Collections.Generic.List[object] - - # Inform the user that network adapters are being retrieved. - Write-Host -Object "Retrieving network adapters." - - try { - # Attempt to retrieve the network connection profiles and adapters. - $NetworkProfiles = Get-NetConnectionProfile -ErrorAction Stop - $NetworkAdapters = Get-NetAdapter -ErrorAction Stop - } - catch { - # Catch any errors during network profile/adapter retrieval and output error messages. - Write-Host -Object "[Error] Failed to retrieve network adapters." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Inform the user that additional information is being gathered. - Write-Host -Object "Gathering additional information." - - # Loop through each network profile. - foreach ($NetworkProfile in $NetworkProfiles) { - # Find the network adapter associated with the current network profile using the InterfaceIndex. - $NetAdapter = $NetworkAdapters | Where-Object { $_.ifIndex -eq $NetworkProfile.InterfaceIndex } - - # Determine the adapter type (Wired, Wi-Fi, or Other) based on the MediaType. - switch -Wildcard ($NetAdapter.MediaType) { - "802.3" { $AdapterType = "Wired" } - "*802.11" { $AdapterType = "Wi-Fi" } - default { $AdapterType = "Other" } - } - - # Add the network adapter information as a custom object to the $NetworkInfo list. - $NetworkInfo.Add( - [PSCustomObject]@{ - "NetworkAdapter" = $NetAdapter.Name - "MacAddress" = $NetAdapter.MacAddress - "Type" = $AdapterType - "Profile" = $NetworkProfile.NetworkCategory - } - ) - } - - # Check if the $NetworkInfo list is empty or contains fewer than one entry. - if (!$NetworkInfo -or ($NetworkInfo | Measure-Object | Select-Object -ExpandProperty Count) -lt 1) { - Write-Host -Object "[Error] No network interfaces found." - exit 1 - } - - # Format and output the network information in a table. - Write-Host -Object "" - ($NetworkInfo | Format-Table | Out-String).Trim() | Write-Host - Write-Host -Object "" - - # If a custom field is provided, iterate through the network information and append the adapter details to $CustomFieldValue. - if ($CustomField) { - $NetworkInfo | ForEach-Object { - if ($CustomFieldValue) { - # Append the network adapter name and profile to the existing $CustomFieldValue. - $CustomFieldValue = "$CustomFieldValue | $($_.NetworkAdapter): $($_.Profile)" - } - else { - # Set $CustomFieldValue if it hasn't been initialized yet. - $CustomFieldValue = "$($_.NetworkAdapter): $($_.Profile)" - } - } - - try { - # Try to set the custom field value using the Set-NinjaProperty function. - Write-Host "Attempting to set Custom Field '$CustomField'." - Set-NinjaProperty -Name $CustomField -Value $CustomFieldValue - Write-Host "Successfully set custom field '$CustomField'!" - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - exit 1 - } - } - - # Exit the script with the provided $ExitCode variable. - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Checks the current network connections to see what profile they are currently using and optionally save the results to a custom field. +.DESCRIPTION + Checks the current network connections to see what profile they are currently using and optionally save the results to a custom field. +.EXAMPLE + (No Parameters) + + Retrieving network adapters. + Gathering additional information. + + NetworkAdapter MacAddress Type Profile + -------------- ---------- ---- ------- + LabNet - Win10 0 00-17-FB-00-00-02 Wired Public + +PARAMETER: -CustomField "ReplaceMeWithYourDesiredCustomField" + Optionally specify the name of a custom field to save the results to. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$CustomField +) + +begin { + # If script form variables are used, replace the command the command line parameters with their value. + if ($env:networkProfileCustomFieldName -and $env:networkProfileCustomFieldName -notlike "null") { $CustomField = $env:networkProfileCustomFieldName } + + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # # Measure the number of characters in the provided value # Removed NinjaOne dependency + # $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + + # # Throw an error if the value exceeds the character limit of 200,000 characters # Removed NinjaOne dependency + # if ($Characters -ge 200000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Initialize a hashtable for additional documentation parameters # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + + # # If a document name is provided, add it to the documentation parameters # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # Define a list of valid field types # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + + # # Warn the user if the provided type is not valid # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # Define types that require options to be retrieved # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + + # # If the property is being set in a document or field and the type needs options, retrieve them # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if there was an issue retrieving the property options # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # Process the property value based on its type # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # Convert the value to a boolean for Checkbox type # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Convert the value to a Unix timestamp for Date or Date Time type # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Convert the dropdown value to its corresponding GUID # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # # Throw an error if the value is not present in the dropdown options # Removed NinjaOne dependency + # if (!($Selection)) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # For other types, use the value as is # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Set the property value in the document if a document name is provided # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # # Otherwise, set the standard property value # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if setting the property failed # Removed NinjaOne dependency + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # Check if the script is running with elevated (Administrator) privileges. + if ($CustomField -and !(Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Administrator privileges are required to set Custom Fields." + exit 1 + } + + # Initialize a list to store network information. + $NetworkInfo = New-Object System.Collections.Generic.List[object] + + # Inform the user that network adapters are being retrieved. + Write-Host -Object "Retrieving network adapters." + + try { + # Attempt to retrieve the network connection profiles and adapters. + $NetworkProfiles = Get-NetConnectionProfile -ErrorAction Stop + $NetworkAdapters = Get-NetAdapter -ErrorAction Stop + } + catch { + # Catch any errors during network profile/adapter retrieval and output error messages. + Write-Host -Object "[Error] Failed to retrieve network adapters." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Inform the user that additional information is being gathered. + Write-Host -Object "Gathering additional information." + + # Loop through each network profile. + foreach ($NetworkProfile in $NetworkProfiles) { + # Find the network adapter associated with the current network profile using the InterfaceIndex. + $NetAdapter = $NetworkAdapters | Where-Object { $_.ifIndex -eq $NetworkProfile.InterfaceIndex } + + # Determine the adapter type (Wired, Wi-Fi, or Other) based on the MediaType. + switch -Wildcard ($NetAdapter.MediaType) { + "802.3" { $AdapterType = "Wired" } + "*802.11" { $AdapterType = "Wi-Fi" } + default { $AdapterType = "Other" } + } + + # Add the network adapter information as a custom object to the $NetworkInfo list. + $NetworkInfo.Add( + [PSCustomObject]@{ + "NetworkAdapter" = $NetAdapter.Name + "MacAddress" = $NetAdapter.MacAddress + "Type" = $AdapterType + "Profile" = $NetworkProfile.NetworkCategory + } + ) + } + + # Check if the $NetworkInfo list is empty or contains fewer than one entry. + if (!$NetworkInfo -or ($NetworkInfo | Measure-Object | Select-Object -ExpandProperty Count) -lt 1) { + Write-Host -Object "[Error] No network interfaces found." + exit 1 + } + + # Format and output the network information in a table. + Write-Host -Object "" + ($NetworkInfo | Format-Table | Out-String).Trim() | Write-Host + Write-Host -Object "" + + # If a custom field is provided, iterate through the network information and append the adapter details to $CustomFieldValue. + if ($CustomField) { + $NetworkInfo | ForEach-Object { + if ($CustomFieldValue) { + # Append the network adapter name and profile to the existing $CustomFieldValue. + $CustomFieldValue = "$CustomFieldValue | $($_.NetworkAdapter): $($_.Profile)" + } + else { + # Set $CustomFieldValue if it hasn't been initialized yet. + $CustomFieldValue = "$($_.NetworkAdapter): $($_.Profile)" + } + } + + try { + # # Try to set the custom field value using the Set-NinjaProperty function. # Removed NinjaOne dependency + Write-Host "Attempting to set Custom Field '$CustomField'." + # Set-NinjaProperty -Name $CustomField -Value $CustomFieldValue # Removed NinjaOne dependency + Write-Host "Successfully set custom field '$CustomField'!" + } + catch { + Write-Host "[Error] $($_.Exception.Message)" + exit 1 + } + } + + # Exit the script with the provided $ExitCode variable. + exit $ExitCode +} +end { + + + +} diff --git a/Powershell Scripts/Orphaned User Profile Report.ps1 b/Powershell Scripts/Orphaned User Profile Report.ps1 index d4d8369..a16c6fb 100644 --- a/Powershell Scripts/Orphaned User Profile Report.ps1 +++ b/Powershell Scripts/Orphaned User Profile Report.ps1 @@ -1,408 +1,408 @@ # Looks for user profile folders that do not have an associated user account. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Looks for user profile folders that do not have an associated user account. -.DESCRIPTION - Looks for user profile folders that do not have an associated user account. -.EXAMPLE - (No Parameters) - - [Alert] Orphaned profiles found! - - Username Size Path SID - -------- ---- ---- --- - UNKNOWN 554.97 MB C:\Users\tuser1 S-1-5-21-1255570301-2732419320-3119746845-1104 - UNKNOWN 86.74 MB C:\Users\tuser22 S-1-5-21-3797121902-2219393589-2867574441-1001 - - -PARAMETER: -CustomField "ReplaceMeWithTheNameOfaWYSIWYGcustomField" - Specify the name of an optional WYSIWYG Custom Field to save the results in. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String[]]$DirectoriesToIgnore = ("All Users", "Default", "Default User", "Public"), - [Parameter()] - [String]$CustomField -) - -begin { - # Set parameters using dynamic script variables. - if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { - $CustomField = $env:wysiwygCustomFieldName - } - - # Function to convert a size in bytes to a more human-readable format - function Get-FriendlySize { - param($Bytes) - # Array of size units - $Sizes = 'Bytes,KB,MB,GB,TB,PB,EB,ZB' -split ',' - # Loop to find the appropriate size unit - for ($i = 0; ($Bytes -ge 1kb) -and ($i -lt $Sizes.Count); $i++) { $Bytes /= 1kb } - # Number of decimal places to show - $decimalPlaces = 2 - # If the size is in bytes, no decimal places are needed - if ($i -eq 0) { $decimalPlaces = 0 } - # Return the rounded size with the appropriate unit - if ($Bytes) { "$([System.Math]::Round($Bytes,$decimalPlaces)) $($Sizes[$i])" }else { "0 B" } - } - - # Function to retrieve user profile hives from the registry - function Get-UserHives { - param ( - [Parameter()] - [ValidateSet('AzureAD', 'DomainAndLocal', 'All')] - [String]$Type = "All", - [Parameter()] - [String[]]$ExcludedUsers, - [Parameter()] - [switch]$IncludeDefault - ) - - # Define patterns to match user SIDs based on the Type parameter - $Patterns = switch ($Type) { - "AzureAD" { "S-1-12-1-(\d+-?){4}$" } - "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" } - "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" } - } - - # Retrieve user profiles from the registry - $UserProfiles = Foreach ($Pattern in $Patterns) { - Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | - Where-Object { $_.PSChildName -match $Pattern } | - Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, - @{Name = "Username"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } }, - @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, - @{Name = "Path"; Expression = { $_.ProfileImagePath } }, - @{Name = "Size"; Expression = { $(Get-ChildItem -Path $_.ProfileImagePath -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Sum Length -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Sum -ErrorAction SilentlyContinue) } } - } - - # Include the default profile if specified - switch ($IncludeDefault) { - $True { - $DefaultProfile = "" | Select-Object Username, SID, UserHive, Path - $DefaultProfile.Username = "Default" - $DefaultProfile.SID = "DefaultProfile" - $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT" - $DefaultProfile.Path = "C:\Users\Default" - - $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.Username } - } - } - - # Iterate over the user profiles to resolve user names from SIDs - $UserProfiles | ForEach-Object { - $SidObject = $Null - $NewUsername = $Null - - # Convert SID to user name - if ($_.SID) { - try { - $SidObject = New-Object System.Security.Principal.SecurityIdentifier($_.SID) - $NewUsername = $SidObject.Translate([System.Security.Principal.NTAccount]) - } - catch { - $NewUsername = $Null - } - } - - # Assign the resolved user name or "UNKNOWN" if resolution fails - if ($NewUsername.Value) { - $_.Username = $NewUsername.Value - } - else { - $_.Username = "UNKNOWN" - } - } - - $UserProfiles | Where-Object { $ExcludedUsers -notcontains $_.Username } - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - # If requested to set the field value for a Ninja document, specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is specified, assume that the input does not need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to set. - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # Redirect error output to the success stream to handle errors more easily if nothing is found or something else goes wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received with an exception property, exit the function with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The types below require values not typically given to be set. The code below will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # Although it's highly likely we were given a value like "True" or a boolean data type, it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, match the given value with a GUID. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli expects the GUID of the option we're trying to select, so match the value we were given with a GUID. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # Set the field differently depending on whether it's a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - - # Function to check if the computer is joined to a domain - function Test-IsDomainJoined { - if ($PSVersionTable.PSVersion.Major -lt 5) { - return $(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain - } - else { - return $(Get-CimInstance -Class Win32_ComputerSystem).PartOfDomain - } - } - - # Function to check if the computer is a domain controller - function Test-IsDomainController { - $OS = if ($PSVersionTable.PSVersion.Major -lt 5) { - Get-WmiObject -Class Win32_OperatingSystem - } - else { - Get-CimInstance -ClassName Win32_OperatingSystem - } - - if ($OS.ProductType -eq "2") { - return $true - } - } - - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - function Test-IsDomainReachable { - try { - $searcher = [adsisearcher]"(&(objectCategory=computer)(name=$env:ComputerName))" - $searcher.FindOne() - } - catch { - Write-Host -Object "[Error] Failed to connect to the domain!" - Write-Host -Object "[Error] $($_.Exception.Message)" - $False - } - } - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # Check if the script is running with elevated privileges - if (!(Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Check if the computer is domain joined, can connect to the domain controller, and is not itself a domain controller - if ((Test-IsDomainJoined) -and !(Test-IsDomainReachable) -and !(Test-IsDomainController)) { - Write-Host -Object "[Error] Unable to connect to the domain controller! The domain must be reachable to confirm which profiles are orphaned." - exit 1 - } - - # Initialize a list to hold user profiles - $UserProfiles = New-Object System.Collections.Generic.List[Object] - # Retrieve user hives and add them to the list of user profiles - try { - $UserHives = Get-UserHives -Type "All" - } - catch { - Write-Host -Object "[Error] Failed to retrieve the user profile information from the registry!" - Write-Host -Object "$($_.Exception.Message)" - exit 1 - } - - $UserHives | ForEach-Object { - if ((Test-IsDomainJoined) -and $_.Username -notmatch [Regex]::Escape("$env:ComputerName") -and $_.Username -notmatch "AzureAD" -and $_.Username -ne "UNKNOWN") { - try { - $ADSIsearch = [adsisearcher]"(objectSid=$($_.SID))" - if (!($ADSIsearch.FindOne())) { - $_.Username = "UNKNOWN" - } - } - catch { - Write-Host -Object "[Error] Failed to connect to the domain to verify the account with the sid $($_.SID) is active." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - } - - try { - $UserProfiles.Add( - [PSCustomObject]@{ - SID = $_.SID - Username = $_.Username - UserHive = $_.UserHive - Path = $_.Path - Size = $_.Size - FriendlySize = (Get-FriendlySize -Bytes $_.Size) - } - ) - } - catch { - Write-Host -Object "[Error] Failed to add the profile with the SID $($_.SID) to the profile list!" - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - } - - # Attempt to retrieve the user profiles directory from the registry (typically C:\Users) - try { - $ProfilesDirectory = (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -ErrorAction Stop).ProfilesDirectory - } - catch { - Write-Host -Object "[Error] Unable to find the user profiles directory!" - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Attempt to get information about directories in the profiles directory that have not already been identified/found - try { - $ProfilesDirectory | Get-ChildItem -Directory -Force -ErrorAction Stop | Where-Object { $DirectoriesToIgnore -notcontains $_.Name -and $UserProfiles.Path -notcontains $_.FullName } | ForEach-Object { - $FriendlySize = $Null - $Size = $Null - $Hive = $Null - - # Check if the NTuser.dat file exists in the profile directory - if (Test-Path -Path "$($_.FullName)\NTuser.dat" -ErrorAction SilentlyContinue) { - $Hive = "$($_.FullName)\NTuser.dat" - } - else { - $Hive = "UNKNOWN" - } - - # Calculate the size of the profile directory - $Size = $(Get-ChildItem -Path $_.FullName -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Sum Length -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Sum -ErrorAction SilentlyContinue) - if ($Size) { - $FriendlySize = Get-FriendlySize -Bytes $Size - } - - # Add the profile information to the list of user profiles - $UserProfiles.Add( - [PSCustomObject]@{ - SID = "UNKNOWN" - Username = "UNKNOWN" - UserHive = $Hive - Path = $_.FullName - Size = $Size - FriendlySize = $FriendlySize - } - ) - } - } - catch { - Write-Host -Object "[Error] Unable to get all of the user profile information for $($_.FullName)!" - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Filter the list to find orphaned profiles (those with "UNKNOWN" as a username) - $OrphanedProfiles = $UserProfiles | Where-Object { $_.Username -eq "UNKNOWN" } - - # If a custom field is specified, generate an HTML report and set the custom field - if ($CustomField) { - $HTMLreport = $UserProfiles | Sort-Object Size -Descending | Select-Object Username, @{Label = "Size"; Expression = { $_.FriendlySize } }, Path, SID | ConvertTo-Html -Fragment - - # Highlight orphaned profiles in the HTML report - $HTMLreport = $HTMLreport | ForEach-Object { - $_ -replace '', '' - } - - try { - Write-Host "Attempting to set Custom Field '$CustomField'." - Set-NinjaProperty -Name $CustomField -Value $HTMLreport - Write-Host "Successfully set Custom Field '$CustomField'!" - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - - Write-Host "" - } - - # Display a message about orphaned profiles - if ($OrphanedProfiles) { - Write-Host -Object "[Alert] Orphaned profiles found!" - $OrphanedProfiles | Sort-Object Size -Descending | Format-Table Username, @{Label = "Size"; Expression = { $_.FriendlySize } }, Path, SID | Out-String | Write-Host - } - else { - Write-Host -Object "No orphaned profiles found!" - } - - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Looks for user profile folders that do not have an associated user account. +.DESCRIPTION + Looks for user profile folders that do not have an associated user account. +.EXAMPLE + (No Parameters) + + [Alert] Orphaned profiles found! + + Username Size Path SID + -------- ---- ---- --- + UNKNOWN 554.97 MB C:\Users\tuser1 S-1-5-21-1255570301-2732419320-3119746845-1104 + UNKNOWN 86.74 MB C:\Users\tuser22 S-1-5-21-3797121902-2219393589-2867574441-1001 + + +PARAMETER: -CustomField "ReplaceMeWithTheNameOfaWYSIWYGcustomField" + Specify the name of an optional WYSIWYG Custom Field to save the results in. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String[]]$DirectoriesToIgnore = ("All Users", "Default", "Default User", "Public"), + [Parameter()] + [String]$CustomField +) + +begin { + # Set parameters using dynamic script variables. + if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { + $CustomField = $env:wysiwygCustomFieldName + } + + # Function to convert a size in bytes to a more human-readable format + function Get-FriendlySize { + param($Bytes) + # Array of size units + $Sizes = 'Bytes,KB,MB,GB,TB,PB,EB,ZB' -split ',' + # Loop to find the appropriate size unit + for ($i = 0; ($Bytes -ge 1kb) -and ($i -lt $Sizes.Count); $i++) { $Bytes /= 1kb } + # Number of decimal places to show + $decimalPlaces = 2 + # If the size is in bytes, no decimal places are needed + if ($i -eq 0) { $decimalPlaces = 0 } + # Return the rounded size with the appropriate unit + if ($Bytes) { "$([System.Math]::Round($Bytes,$decimalPlaces)) $($Sizes[$i])" }else { "0 B" } + } + + # Function to retrieve user profile hives from the registry + function Get-UserHives { + param ( + [Parameter()] + [ValidateSet('AzureAD', 'DomainAndLocal', 'All')] + [String]$Type = "All", + [Parameter()] + [String[]]$ExcludedUsers, + [Parameter()] + [switch]$IncludeDefault + ) + + # Define patterns to match user SIDs based on the Type parameter + $Patterns = switch ($Type) { + "AzureAD" { "S-1-12-1-(\d+-?){4}$" } + "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" } + "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" } + } + + # Retrieve user profiles from the registry + $UserProfiles = Foreach ($Pattern in $Patterns) { + Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | + Where-Object { $_.PSChildName -match $Pattern } | + Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, + @{Name = "Username"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } }, + @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, + @{Name = "Path"; Expression = { $_.ProfileImagePath } }, + @{Name = "Size"; Expression = { $(Get-ChildItem -Path $_.ProfileImagePath -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Sum Length -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Sum -ErrorAction SilentlyContinue) } } + } + + # Include the default profile if specified + switch ($IncludeDefault) { + $True { + $DefaultProfile = "" | Select-Object Username, SID, UserHive, Path + $DefaultProfile.Username = "Default" + $DefaultProfile.SID = "DefaultProfile" + $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT" + $DefaultProfile.Path = "C:\Users\Default" + + $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.Username } + } + } + + # Iterate over the user profiles to resolve user names from SIDs + $UserProfiles | ForEach-Object { + $SidObject = $Null + $NewUsername = $Null + + # Convert SID to user name + if ($_.SID) { + try { + $SidObject = New-Object System.Security.Principal.SecurityIdentifier($_.SID) + $NewUsername = $SidObject.Translate([System.Security.Principal.NTAccount]) + } + catch { + $NewUsername = $Null + } + } + + # Assign the resolved user name or "UNKNOWN" if resolution fails + if ($NewUsername.Value) { + $_.Username = $NewUsername.Value + } + else { + $_.Username = "UNKNOWN" + } + } + + $UserProfiles | Where-Object { $ExcludedUsers -notcontains $_.Username } + } + + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + # if ($Characters -ge 200000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If requested to set the field value for a Ninja document, specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # This is a list of valid fields that can be set. If no type is specified, assume that the input does not need to be changed. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # The field below requires additional information to set. # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # Redirect error output to the success stream to handle errors more easily if nothing is found or something else goes wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If an error is received with an exception property, exit the function with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # The types below require values not typically given to be set. The code below will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # Although it's highly likely we were given a value like "True" or a boolean data type, it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, match the given value with a GUID. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the GUID of the option we're trying to select, so match the value we were given with a GUID. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # if (-not $Selection) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Set the field differently depending on whether it's a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # Function to check if the computer is joined to a domain + function Test-IsDomainJoined { + if ($PSVersionTable.PSVersion.Major -lt 5) { + return $(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain + } + else { + return $(Get-CimInstance -Class Win32_ComputerSystem).PartOfDomain + } + } + + # Function to check if the computer is a domain controller + function Test-IsDomainController { + $OS = if ($PSVersionTable.PSVersion.Major -lt 5) { + Get-WmiObject -Class Win32_OperatingSystem + } + else { + Get-CimInstance -ClassName Win32_OperatingSystem + } + + if ($OS.ProductType -eq "2") { + return $true + } + } + + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + function Test-IsDomainReachable { + try { + $searcher = [adsisearcher]"(&(objectCategory=computer)(name=$env:ComputerName))" + $searcher.FindOne() + } + catch { + Write-Host -Object "[Error] Failed to connect to the domain!" + Write-Host -Object "[Error] $($_.Exception.Message)" + $False + } + } + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # Check if the script is running with elevated privileges + if (!(Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Check if the computer is domain joined, can connect to the domain controller, and is not itself a domain controller + if ((Test-IsDomainJoined) -and !(Test-IsDomainReachable) -and !(Test-IsDomainController)) { + Write-Host -Object "[Error] Unable to connect to the domain controller! The domain must be reachable to confirm which profiles are orphaned." + exit 1 + } + + # Initialize a list to hold user profiles + $UserProfiles = New-Object System.Collections.Generic.List[Object] + # Retrieve user hives and add them to the list of user profiles + try { + $UserHives = Get-UserHives -Type "All" + } + catch { + Write-Host -Object "[Error] Failed to retrieve the user profile information from the registry!" + Write-Host -Object "$($_.Exception.Message)" + exit 1 + } + + $UserHives | ForEach-Object { + if ((Test-IsDomainJoined) -and $_.Username -notmatch [Regex]::Escape("$env:ComputerName") -and $_.Username -notmatch "AzureAD" -and $_.Username -ne "UNKNOWN") { + try { + $ADSIsearch = [adsisearcher]"(objectSid=$($_.SID))" + if (!($ADSIsearch.FindOne())) { + $_.Username = "UNKNOWN" + } + } + catch { + Write-Host -Object "[Error] Failed to connect to the domain to verify the account with the sid $($_.SID) is active." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + } + + try { + $UserProfiles.Add( + [PSCustomObject]@{ + SID = $_.SID + Username = $_.Username + UserHive = $_.UserHive + Path = $_.Path + Size = $_.Size + FriendlySize = (Get-FriendlySize -Bytes $_.Size) + } + ) + } + catch { + Write-Host -Object "[Error] Failed to add the profile with the SID $($_.SID) to the profile list!" + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + } + + # Attempt to retrieve the user profiles directory from the registry (typically C:\Users) + try { + $ProfilesDirectory = (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -ErrorAction Stop).ProfilesDirectory + } + catch { + Write-Host -Object "[Error] Unable to find the user profiles directory!" + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Attempt to get information about directories in the profiles directory that have not already been identified/found + try { + $ProfilesDirectory | Get-ChildItem -Directory -Force -ErrorAction Stop | Where-Object { $DirectoriesToIgnore -notcontains $_.Name -and $UserProfiles.Path -notcontains $_.FullName } | ForEach-Object { + $FriendlySize = $Null + $Size = $Null + $Hive = $Null + + # Check if the NTuser.dat file exists in the profile directory + if (Test-Path -Path "$($_.FullName)\NTuser.dat" -ErrorAction SilentlyContinue) { + $Hive = "$($_.FullName)\NTuser.dat" + } + else { + $Hive = "UNKNOWN" + } + + # Calculate the size of the profile directory + $Size = $(Get-ChildItem -Path $_.FullName -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Sum Length -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Sum -ErrorAction SilentlyContinue) + if ($Size) { + $FriendlySize = Get-FriendlySize -Bytes $Size + } + + # Add the profile information to the list of user profiles + $UserProfiles.Add( + [PSCustomObject]@{ + SID = "UNKNOWN" + Username = "UNKNOWN" + UserHive = $Hive + Path = $_.FullName + Size = $Size + FriendlySize = $FriendlySize + } + ) + } + } + catch { + Write-Host -Object "[Error] Unable to get all of the user profile information for $($_.FullName)!" + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Filter the list to find orphaned profiles (those with "UNKNOWN" as a username) + $OrphanedProfiles = $UserProfiles | Where-Object { $_.Username -eq "UNKNOWN" } + + # If a custom field is specified, generate an HTML report and set the custom field + if ($CustomField) { + $HTMLreport = $UserProfiles | Sort-Object Size -Descending | Select-Object Username, @{Label = "Size"; Expression = { $_.FriendlySize } }, Path, SID | ConvertTo-Html -Fragment + + # Highlight orphaned profiles in the HTML report + $HTMLreport = $HTMLreport | ForEach-Object { + $_ -replace '', '' + } + + try { + Write-Host "Attempting to set Custom Field '$CustomField'." + # Set-NinjaProperty -Name $CustomField -Value $HTMLreport # Removed NinjaOne dependency + Write-Host "Successfully set Custom Field '$CustomField'!" + } + catch { + Write-Host "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + + Write-Host "" + } + + # Display a message about orphaned profiles + if ($OrphanedProfiles) { + Write-Host -Object "[Alert] Orphaned profiles found!" + $OrphanedProfiles | Sort-Object Size -Descending | Format-Table Username, @{Label = "Size"; Expression = { $_.FriendlySize } }, Path, SID | Out-String | Write-Host + } + else { + Write-Host -Object "No orphaned profiles found!" + } + + exit $ExitCode +} +end { + + + +} diff --git a/Powershell Scripts/PKFail Vulnerability Check.ps1 b/Powershell Scripts/PKFail Vulnerability Check.ps1 index 64996a6..e39c15d 100644 --- a/Powershell Scripts/PKFail Vulnerability Check.ps1 +++ b/Powershell Scripts/PKFail Vulnerability Check.ps1 @@ -1,194 +1,194 @@ # Checks for the PKFail vulnerability. - -<# -.SYNOPSIS - Checks for the PKFail vulnerability. -.DESCRIPTION - Checks the PK(Platform Key) variable for 'DO NOT TRUST' or 'DO NOT SHIP'. - Can save a result of 'Trusted' or 'Not Trusted' to a custom field. - -.EXAMPLE - (No Parameters) - - Secure Boot is Trusted - -.EXAMPLE - (No Parameters) - - Secure Boot is Not Trusted - -PARAMETER: -CustomField "ReplaceMeWithAnyMultilineCustomField" - Name of a custom field to save the PKFail status to with either Trusted or Not Trusted. -.EXAMPLE - -CustomField "SecureBootPK" - - Attempting to set Custom Field 'SecureBootPK'. - Successfully set Custom Field 'SecureBootPK'! - Secure Boot is Trusted - -.NOTES - Minimum OS Supported: Windows 10, Windows Server 2016 - -.LINK - https://github.com/binarly-io/Vulnerability-REsearch/blob/main/PKfail/BRLY-2024-005.md - https://www.intel.com/content/www/us/en/security-center/announcement/intel-security-announcement-2024-07-25-001.html - https://www.supermicro.com/en/support/security_PKFAIL_Jul_2024 -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$CustomField -) - -begin { - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - # If requested to set the field value for a Ninja document, specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is specified, assume that the input does not need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to set. - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # Redirect error output to the success stream to handle errors more easily if nothing is found or something else goes wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received with an exception property, exit the function with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The types below require values not typically given to be set. The code below will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # Although it's highly likely we were given a value like "True" or a boolean data type, it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, match the given value with a GUID. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli expects the GUID of the option we're trying to select, so match the value we were given with a GUID. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # Set the field differently depending on whether it's a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - - if ($env:customFieldName -and $env:customFieldName -notlike "null") { - $CustomField = $env:customFieldName - } - - # Check if Secure Boot is supported on the system - try { - Get-SecureBootUEFI -Name PK -ErrorAction Stop | Out-Null - } - catch { - if ($_.Exception.Message -like "*Cmdlet not supported on this platform*") { - Write-Host "[Error] System does not support Secure Boot or is a BIOS (Non-UEFI) system." - exit 1 - } - elseif ($_.Exception.Message -like "*Variable is currently undefined*") { - Write-Host "[Error] PK variable is not defined." - exit 1 - } - } - - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - if (-not (Test-IsElevated)) { - Write-Host "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } -} -process { - - # Secure Boot PK cert status - try { - if ([System.Text.Encoding]::ASCII.GetString((Get-SecureBootUEFI -Name PK).bytes) -match "DO NOT TRUST|DO NOT SHIP") { - $SecureBootPK = "Not Trusted" - Write-Host "[Alert] Secure Boot is $SecureBootPK" - } - else { - $SecureBootPK = "Trusted" - Write-Host "[Info] Secure Boot is $SecureBootPK" - } - } - catch { - Write-Host "[Error] Get-SecureBootUEFI return error: $($_.Exception.Message)" - exit 1 - } - - if ($CustomField) { - try { - Write-Host "[Info] Attempting to set Custom Field '$CustomField'." - Set-NinjaProperty -Name $CustomField -Value $SecureBootPK - Write-Host "[Info] Successfully set Custom Field '$CustomField'!" - } - catch { - Write-Host "[Warn] Failed to set Custom Field '$CustomField'." - Write-Host "[Warn] $($_.Exception.Message)" - } - } -} -end { - - - -} + +<# +.SYNOPSIS + Checks for the PKFail vulnerability. +.DESCRIPTION + Checks the PK(Platform Key) variable for 'DO NOT TRUST' or 'DO NOT SHIP'. + Can save a result of 'Trusted' or 'Not Trusted' to a custom field. + +.EXAMPLE + (No Parameters) + + Secure Boot is Trusted + +.EXAMPLE + (No Parameters) + + Secure Boot is Not Trusted + +PARAMETER: -CustomField "ReplaceMeWithAnyMultilineCustomField" + Name of a custom field to save the PKFail status to with either Trusted or Not Trusted. +.EXAMPLE + -CustomField "SecureBootPK" + + Attempting to set Custom Field 'SecureBootPK'. + Successfully set Custom Field 'SecureBootPK'! + Secure Boot is Trusted + +.NOTES + Minimum OS Supported: Windows 10, Windows Server 2016 + +.LINK + https://github.com/binarly-io/Vulnerability-REsearch/blob/main/PKfail/BRLY-2024-005.md + https://www.intel.com/content/www/us/en/security-center/announcement/intel-security-announcement-2024-07-25-001.html + https://www.supermicro.com/en/support/security_PKFAIL_Jul_2024 +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$CustomField +) + +begin { + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + # if ($Characters -ge 200000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If requested to set the field value for a Ninja document, specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # This is a list of valid fields that can be set. If no type is specified, assume that the input does not need to be changed. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # The field below requires additional information to set. # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # Redirect error output to the success stream to handle errors more easily if nothing is found or something else goes wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If an error is received with an exception property, exit the function with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # The types below require values not typically given to be set. The code below will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # Although it's highly likely we were given a value like "True" or a boolean data type, it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, match the given value with a GUID. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the GUID of the option we're trying to select, so match the value we were given with a GUID. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # if (-not $Selection) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Set the field differently depending on whether it's a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + if ($env:customFieldName -and $env:customFieldName -notlike "null") { + $CustomField = $env:customFieldName + } + + # Check if Secure Boot is supported on the system + try { + Get-SecureBootUEFI -Name PK -ErrorAction Stop | Out-Null + } + catch { + if ($_.Exception.Message -like "*Cmdlet not supported on this platform*") { + Write-Host "[Error] System does not support Secure Boot or is a BIOS (Non-UEFI) system." + exit 1 + } + elseif ($_.Exception.Message -like "*Variable is currently undefined*") { + Write-Host "[Error] PK variable is not defined." + exit 1 + } + } + + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if (-not (Test-IsElevated)) { + Write-Host "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } +} +process { + + # Secure Boot PK cert status + try { + if ([System.Text.Encoding]::ASCII.GetString((Get-SecureBootUEFI -Name PK).bytes) -match "DO NOT TRUST|DO NOT SHIP") { + $SecureBootPK = "Not Trusted" + Write-Host "[Alert] Secure Boot is $SecureBootPK" + } + else { + $SecureBootPK = "Trusted" + Write-Host "[Info] Secure Boot is $SecureBootPK" + } + } + catch { + Write-Host "[Error] Get-SecureBootUEFI return error: $($_.Exception.Message)" + exit 1 + } + + if ($CustomField) { + try { + Write-Host "[Info] Attempting to set Custom Field '$CustomField'." + # Set-NinjaProperty -Name $CustomField -Value $SecureBootPK # Removed NinjaOne dependency + Write-Host "[Info] Successfully set Custom Field '$CustomField'!" + } + catch { + Write-Host "[Warn] Failed to set Custom Field '$CustomField'." + Write-Host "[Warn] $($_.Exception.Message)" + } + } +} +end { + + + +} diff --git a/Powershell Scripts/Password Expiration Alert_Report.ps1 b/Powershell Scripts/Password Expiration Alert_Report.ps1 index f8e60f3..5e2a3a7 100644 --- a/Powershell Scripts/Password Expiration Alert_Report.ps1 +++ b/Powershell Scripts/Password Expiration Alert_Report.ps1 @@ -1,796 +1,796 @@ # Generates a report of users whose passwords are nearing expiration, including both Active Directory domain and local accounts. Outputs an alert if any expiring accounts are found. -#Requires -Version 5 - -<# -.SYNOPSIS - Generates a report of users whose passwords are nearing expiration, including both Active Directory domain and local accounts. Outputs an alert if any expiring accounts are found. -.DESCRIPTION - Generates a report of users whose passwords are nearing expiration, including both Active Directory domain and local accounts. Outputs an alert if any expiring accounts are found. -.LINK - https://ninjarmm.zendesk.com/hc/en-us/articles/206864826-Policies-Condition-Configuration -.EXAMPLE - -DaysUntilExpiration 999999999999 - On a Workstation - - Not a Domain Controller. Checking the domain users on this machine... - Not a Domain Controller. Checking all local users on this machine... - [Alert] Users with passwords expiring in 999999999999 day(s) were found! - - Username User Principal Name E-mail Address Password Expiration Date - -------- ------------------- -------------- ------------------------ - cheart cheart@test.lan cheart@example.com 11/4/2024 6:01:44 PM - s user s user@test.lan 11/5/2024 9:06:30 AM - s us'er' s us'er'@test.lan 11/5/2024 9:35:08 AM - s us'er s us'er@test.lan 11/5/2024 9:44:57 AM - suse)r9 suse)r9@test.lan 11/5/2024 9:39:38 AM -.EXAMPLE - -DaysUntilExpiration 14 - On a Workstation - - Not a Domain Controller. Checking the domain users on this machine... - Not a Domain Controller. Checking all local users on this machine... - No users with expiring passwords found! - -.EXAMPLE - -DaysUntilExpiration 999999999999 - On a Domain Controller - - This is a domain controller. Checking all users... - [Alert] Users with passwords expiring in 999999999999 day(s) were found! - - Username User Principal Name E-mail Address Password Expiration Date - -------- ------------------- -------------- ------------------------ - tuser1 tuser1@test.lan tuser1@example.com 11/4/2024 8:13:59 AM - tuser2 tuser2@test.lan tuser2@example.com 11/4/2024 8:13:59 AM - tuser3 tuser3@test.lan tuser3@example.com 11/4/2024 8:13:59 AM - tuser4 tuser4@test.lan tuser4@example.com 11/4/2024 8:13:59 AM - tuser5 tuser5@test.lan tuser5@example.com 11/4/2024 8:13:59 AM - tuser6 tuser6@test.lan tuser6@example.com 11/4/2024 8:13:59 AM - tuser7 tuser7@test.lan tuser7@example.com 11/4/2024 8:13:59 AM - tuser8 tuser8@test.lan tuser8@example.com 11/4/2024 8:14:00 AM - tuser9 tuser9@test.lan tuser9@example.com 11/5/2024 10:39:20 AM - cheart cheart@test.lan cheart@example.com 11/4/2024 5:01:44 PM - s user s user@test.lan 11/5/2024 8:06:29 AM - s us'er' s us'er'@test.lan 11/5/2024 8:35:08 AM - s user' s user'@test.lan 11/5/2024 8:35:37 AM - s us'er s us'er@test.lan 11/5/2024 8:44:57 AM - suse)r9 suse)r9@test.lan 11/5/2024 8:39:38 AM - -.EXAMPLE - -DaysUntilExpiration 14 - On a Domain Controller - - This is a domain controller. Checking all users... - No users with expiring passwords found! - -PARAMETER: -DaysUntilExpiration "ReplaceWithAnyNumber" - Users whose passwords expire within the specified number of days will be included in the report and trigger an alert. - -PARAMETER: -CurrentUsers - Only users that are currently logged in will be included in the report and trigger an alert. - -PARAMETER: -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" - Optionally specify the name of a multiline custom field to export the results to. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Removed the use of exit codes as alert triggers, added alert text, added input validation, simplified some sections of code, updated functions, and switched to only alerting/reporting on accounts that have signed in on the device (unless running on a domain controller). Fixed an issue with identifying domain accounts that are set to change on the next sign-in when not running on a domain controller. Fixed an issue with accounts that have a ')' or '(' character or too many consecutive spaces. Fixed an issue with domain accounts that are set to never expire. - -#> - -[CmdletBinding()] -param ( - [Parameter()] - $DaysUntilExpiration = "14", - [Parameter()] - [Switch]$CurrentUsers = [System.Convert]::ToBoolean($env:loggedInUsersOnly), - [Parameter()] - [String]$CustomFieldName -) - -begin { - # If script form variables are used, replace the command line parameters with their value. - if ($env:daysUntilPasswordExpiration -and $env:daysUntilPasswordExpiration -notlike "null") { $DaysUntilExpiration = $env:daysUntilPasswordExpiration } - if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomFieldName = $env:customFieldName } - - # Check if $DaysUntilExpiration is not provided (null or empty). - # If it is missing, output an error message and exit the script. - if (!$DaysUntilExpiration) { - Write-Host -Object "[Error] You must provide a valid expiration cutoff." - exit 1 - } - - # Check if $DaysUntilExpiration contains any non-numeric characters (using regex). - # If it contains anything other than digits, output an error message and exit. - if ($DaysUntilExpiration -match '[^0-9]') { - Write-Host -Object "[Error] An invalid expiration cutoff of '$DaysUntilExpiration' was provided. Please provide a positive whole number that is greater than 0." - exit 1 - } - - # Attempt to cast $DaysUntilExpiration to a long integer. - # If an exception occurs during the conversion, output an error message and exit. - try { - $ErrorActionPreference = "Stop" - $DaysUntilExpiration = [long]$DaysUntilExpiration - $ErrorActionPreference = "Continue" - } - catch { - Write-Host -Object "[Error] An invalid expiration cutoff of '$DaysUntilExpiration' was provided. Please provide a positive whole number that is greater than 0." - Write-Host -Object "[Error] $($_.Exception.Message)." - exit 1 - } - - # Check if $DaysUntilExpiration is less than 1 (i.e., not a positive whole number). - # If it's invalid, output an error message and exit the script. - if ($DaysUntilExpiration -lt 1) { - Write-Host -Object "[Error] An invalid expiration cutoff of '$DaysUntilExpiration' was provided. Please provide a positive whole number that is greater than 0." - exit 1 - } - - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - function Test-IsDomainController { - # Determine the method to retrieve the operating system information based on PowerShell version - try { - $OS = if ($PSVersionTable.PSVersion.Major -lt 3) { - Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop - } - else { - Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop - } - } - catch { - Write-Host -Object "[Error] Unable to validate whether or not this device is a domain controller." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Check if the ProductType is "2", which indicates that the system is a domain controller - if ($OS.ProductType -eq "2") { - return $true - } - } - - function Test-IsEntraJoined { - # Check if the operating system version is Windows 10 or higher - if ([environment]::OSVersion.Version.Major -ge 10) { - # Run the dsregcmd.exe tool to check Entra join status and look for "AzureAdJoined : YES" - $dsreg = dsregcmd.exe /status | Select-String "AzureAdJoined : YES" - } - - # If the search found the "AzureAdJoined : YES" string, return True, otherwise return False - if ($dsreg) { return $True }else { return $False } - } - - function Test-IsDomainJoined { - # Check the PowerShell version to determine the appropriate cmdlet to use - try { - if ($PSVersionTable.PSVersion.Major -lt 3) { - return $(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain - } - else { - return $(Get-CimInstance -Class Win32_ComputerSystem).PartOfDomain - } - } - catch { - Write-Host -Object "[Error] Unable to validate whether or not this device is a part of a domain." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - } - - function Get-QUser { - $quser = quser.exe - $quser -replace '\s{2,}', ',' -replace '>' | ConvertFrom-Csv - } - - function Test-IsDomainReachable { - try { - $searcher = [adsisearcher]"(&(objectCategory=computer)(name=$env:ComputerName))" - $searcher.FindOne() - } - catch { - Write-Host -Object "[Error] Failed to connect to the domain!" - Write-Host -Object "[Error] $($_.Exception.Message)" - $False - } - } - - function Get-UserHives { - param ( - [Parameter()] - [ValidateSet('AzureAD', 'DomainAndLocal', 'All')] - [String]$Type = "All", - [Parameter()] - [String[]]$ExcludedUsers, - [Parameter()] - [switch]$IncludeDefault - ) - - # Define the SID patterns to match based on the selected user type - $Patterns = switch ($Type) { - "AzureAD" { "S-1-12-1-(\d+-?){4}$" } - "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" } - "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" } - } - - # Retrieve user profile information based on the defined patterns - try { - $UserProfiles = Foreach ($Pattern in $Patterns) { - Get-ItemProperty "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" -ErrorAction Stop | - Where-Object { $_.PSChildName -match $Pattern } | - Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, - @{Name = "Username"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } }, - @{Name = "Domain"; Expression = { if ($_.PSChildName -match "S-1-12-1-(\d+-?){4}$") { "AzureAD" }else { $Null } } }, - @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, - @{Name = "Path"; Expression = { $_.ProfileImagePath } } - } - } - catch { - Write-Host -Object "[Error] Failed to scan registry keys at 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList'." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # If the IncludeDefault switch is set, add the Default profile to the results - switch ($IncludeDefault) { - $True { - $DefaultProfile = "" | Select-Object Username, SID, UserHive, Path - $DefaultProfile.Username = "Default" - $DefaultProfile.Domain = $env:COMPUTERNAME - $DefaultProfile.SID = "DefaultProfile" - $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT" - $DefaultProfile.Path = "C:\Users\Default" - - # Exclude users specified in the ExcludedUsers list - $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.Username } - } - } - - try { - if ($PSVersionTable.PSVersion.Major -lt 3) { - $AllAccounts = Get-WmiObject -Class "win32_UserAccount" -ErrorAction Stop - } - else { - $AllAccounts = Get-CimInstance -ClassName "win32_UserAccount" -ErrorAction Stop - } - } - catch { - Write-Host -Object "[Error] Failed to gather complete profile information." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - $CompleteUserProfiles = $UserProfiles | ForEach-Object { - $SID = $_.SID - $Win32Object = $AllAccounts | Where-Object { $_.SID -like $SID } - - if ($Win32Object) { - $Win32Object | Add-Member -NotePropertyName UserHive -NotePropertyValue $_.UserHive - $Win32Object - } - else { - [PSCustomObject]@{ - Name = $_.Username - Domain = $_.Domain - SID = $_.SID - UserHive = $_.UserHive - Path = $_.Path - } - } - } - - # Return the list of user profiles, excluding any specified in the ExcludedUsers list - $CompleteUserProfiles | Where-Object { $ExcludedUsers -notcontains $_.Name } - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName, - [Parameter()] - [Switch]$Piped - ) - # Remove the non-breaking space character - if ($Type -eq "WYSIWYG") { - $Value = $Value -replace ' ', ' ' - } - - # Measure the number of characters in the provided value - $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # Throw an error if the value exceeds the character limit of 200,000 characters - if ($Piped -and $Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - if (!$Piped -and $Characters -ge 45000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") - } - - # Initialize a hashtable for additional documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define a list of valid field types - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - - # Warn the user if the provided type is not valid - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # Define types that require options to be retrieved - $NeedsOptions = "Dropdown" - - # If the property is being set in a document or field and the type needs options, retrieve them - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an error if there was an issue retrieving the property options - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Process the property value based on its type - switch ($Type) { - "Checkbox" { - # Convert the value to a boolean for Checkbox type - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Convert the value to a Unix timestamp for Date or Date Time type - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Convert the dropdown value to its corresponding GUID - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - # Throw an error if the value is not present in the dropdown options - if (!($Selection)) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # For other types, use the value as is - $NinjaValue = $Value - } - } - - # Set the property value in the document if a document name is provided - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - try { - # Otherwise, set the standard property value - if ($Piped) { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - } - catch { - Write-Host -Object "[Error] Failed to set custom field." - throw $_.Exception.Message - } - } - - # Throw an error if setting the property failed - if ($CustomField.Exception) { - throw $CustomField - } - } - - - # Attempt to retrieve the current datetime using Get-Date. - try { - $Today = Get-Date -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] Failed to retrieve current date." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # Check if the script is being run with elevated (Administrator) privileges. - # If not, output an error message and exit the script. - if (!(Test-IsElevated)) { - Write-Host "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Check if the machine is joined to Microsoft Entra. - # If true, output a warning that the script cannot check Entra accounts but will check local accounts. - if (Test-IsEntraJoined) { - Write-Warning -Message "This script is unable to check Microsoft Entra accounts, however, the script will check the local accounts on the machine." - } - - # If $CurrentUsers is defined, retrieve the active users on the machine using Get-Quser. - if ($CurrentUsers) { - $ActiveUsers = Get-Quser - } - - # Initialize an empty list to store users that will be reported. - $UsersToReport = New-Object System.Collections.Generic.List[object] - - # Check if the machine is a domain controller. If it is, proceed with checking all domain users. - if (Test-IsDomainController) { - Write-Host "This is a domain controller. Checking all users..." - try { - # Set error handling to stop on errors, and retrieve all Active Directory users with expiring passwords. - $ErrorActionPreference = "Stop" - $AllActiveDirectoryUsers = Get-ADUser -Filter { Enabled -eq $True -and PasswordNeverExpires -eq $False } -Properties SamAccountName, UserPrincipalName, mail, pwdLastSet, msDS-UserPasswordExpiryTimeComputed - - # Select and format the relevant user properties for reporting. - $AllActiveDirectoryUsers = $AllActiveDirectoryUsers | Select-Object @{ Name = "Username"; Expression = { $_.SamAccountName } }, - @{ Name = "User Principal Name"; Expression = { $_.UserPrincipalName } }, - @{ Name = "E-mail Address"; Expression = { $_.mail } }, - @{ Name = "Password Expiration Date"; Expression = { - if ($_.pwdLastSet -eq 0) { - "Must change at next logon" - } - else { - [datetime]::FromFileTime($_."msDS-UserPasswordExpiryTimeComputed") - } - } - } - - # Reset error handling to continue after retrieving the users. - $ErrorActionPreference = "Continue" - } - catch { - # If retrieving users fails, output an error message and exit the script. - Write-Host -Object "[Error] Failed to retrieve expiring Active Directory user accounts." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Filter the users to find those with passwords expiring within the specified timeframe or on the next logon. - try { - $ExpiringUsers = $AllActiveDirectoryUsers | Where-Object { $_."Password Expiration Date" -and $_."Password Expiration Date" -ne "Must change at next logon" -and ((New-TimeSpan $Today $_."Password Expiration Date" -ErrorAction Stop).Days -lt $DaysUntilExpiration -or $DaysUntilExpiration -eq 0) } - $ExpiredUsers = $AllActiveDirectoryUsers | Where-Object { $_."Password Expiration Date" -eq "Must change at next logon" } - } - catch { - Write-Host -Object "[Error] Failed to compute the password expiration timespan." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # If $ActiveUsers is defined, further filter the users to include only those who are currently logged in. - if ($ActiveUsers) { - $ExpiringUsers = $ExpiringUsers | Where-Object { $ActiveUsers.Username -contains $_.Username } - $ExpiredUsers = $ExpiredUsers | Where-Object { $ActiveUsers.Username -contains $_.Username } - } - - # Add expiring users and expired users to the report list. - if ($ExpiringUsers) { $ExpiringUsers | ForEach-Object { $UsersToReport.Add($_) } } - if ($ExpiredUsers) { $ExpiredUsers | ForEach-Object { $UsersToReport.Add($_) } } - } - - # Check if the machine is domain-joined but not a domain controller. - # If true, it will check domain users logged into this machine. - if ((Test-IsDomainJoined) -and !(Test-IsDomainController)) { - Write-Host "Not a Domain Controller. Checking the domain users on this machine..." - - # If the domain controller is unreachable, display an error that not all domain users may be included. - if (!(Test-IsDomainReachable)) { - Write-Host -Object "[Error] A secure connection to the domain controller could not be established. Some domain users may be missing from the results." - $ExitCode = 1 - } - - # Retrieve previously logged-in domain accounts that have expiring passwords. - $PreviouslyLoggedInDomainAccounts = Get-UserHives | Where-Object { -not $_.Disabled -and $_.Domain -eq $env:USERDOMAIN -and $_.PasswordExpires } - $UsersWithExpiration = New-Object System.Collections.Generic.List[Object] - - # Loop through the previously logged-in domain accounts. - $PreviouslyLoggedInDomainAccounts | ForEach-Object { - # Check if the domain is reachable. If not, display an error and skip further processing. - if (!(Test-IsDomainReachable -ErrorAction SilentlyContinue)) { - Write-Host -Object "[Error] Unable to check '$($_.Name)' while the computer is disconnected from the domain!" - $ExitCode = 1 - return - } - - # Define paths for standard output and error logs, with random names to avoid conflicts - $StandardOutLog = "$env:TEMP\$(Get-Random)_stdout.log" - $StandardErrLog = "$env:TEMP\$(Get-Random)_stderr.log" - - # Prepare arguments for the "net user" command to get domain user info. - $NetUserArguments = @( - "user" - "`"$($_.Name)`"" - "/domain" - ) - - # Configure the process start parameters for the "net.exe" command - $ProcessArguments = @{ - FilePath = "$env:SystemRoot\System32\net.exe" - ArgumentList = $NetUserArguments - RedirectStandardOutput = $StandardOutLog - RedirectStandardError = $StandardErrLog - PassThru = $True - NoNewWindow = $True - Wait = $True - } - - # Try to start the "net.exe" process and catch any errors - try { - $NetUserProcess = Start-Process @ProcessArguments -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] Failed to start net.exe to find the password expiration date for '$($_.Name)'" - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - return - } - - # Check if the exit code indicates success (0) - if ($NetUserProcess.ExitCode -ne 0) { - Write-Warning "Net user exit code of $($NetUserProcess.ExitCode) does not indicate success." - } - - # Check if the standard error log exists, indicating an error occurred - if (Test-Path -Path $StandardErrLog -ErrorAction SilentlyContinue) { - - # Attempt to read the error log - try { - $ErrorLog = Get-Content -Path $StandardErrLog -ErrorAction Stop - } - catch { - # If reading the log fails, display an error and exit - Write-Host -Object "[Error] Failed to open error log at '$StandardErrLog'." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - return - } - - # Remove the error log file after reading - try { - Remove-Item -Path $StandardErrLog -ErrorAction Stop - } - catch { - # If removing the log file fails, display an error - Write-Host -Object "[Error] Failed to remove standard error log at '$StandardErrLog'." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - - # If there is any content in the error log, display it and exit - if ($ErrorLog) { - Write-Host -Object "[Error] An error has occurred." - $ErrorLog | ForEach-Object { - Write-Host -Object "[Error] $_" - } - $ExitCode = 1 - return - } - - # Check if the standard output log exists, which contains the user list - if (!(Test-Path -Path $StandardOutLog -ErrorAction SilentlyContinue)) { - Write-Host -Object "[Error] No net user output detected." - $ExitCode = 1 - return - } - - # Try to read the standard output log for user data - try { - $NetUserOutput = Get-Content -Path $StandardOutLog -ErrorAction Stop - } - catch { - # If reading the log fails, display an error and exit - Write-Host -Object "[Error] Failed to open output log at '$StandardOutLog'." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - return - } - - # Try to remove the standard output log after reading - try { - Remove-Item -Path $StandardOutLog -ErrorAction Stop - } - catch { - # If removing the log file fails, display an error - Write-Host -Object "[Error] Failed to remove standard output log at '$StandardOutLog'." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - - # Extract user information from the net user output. - try { - $LastSet = "$(($NetUserOutput | Select-String 'Password last set') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - $Expired = "$(($NetUserOutput | Select-String 'Password expires') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - $Changeable = "$(($NetUserOutput | Select-String 'Password changeable') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - $LastLogon = "$(($NetUserOutput | Select-String 'Last logon') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - - $UsersWithExpiration.Add( - [PSCustomObject]@{ - Username = $($_.Name) - FullName = "$(($NetUserOutput | Select-String 'Full Name') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - Comment = "$(($NetUserOutput | Select-String 'Comment') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - Enabled = if ("$(($NetUserOutput | Select-String 'Account active') -split '\s{4,}' | Select-Object -Skip 1)".Trim() -like "Yes") { $true }else { $false } - AccountExpires = "$(($NetUserOutput | Select-String 'Account expires') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - PasswordLastSet = if ($LastSet) { Get-Date -Date $LastSet }else { $null } - PasswordExpires = if ($Expired -notmatch "Never" -and $Expired -notlike "") { Get-Date -Date $Expired }else { $Expired } - PasswordChangeable = if ($Changeable) { Get-Date -Date $Changeable }else { $null } - PasswordRequired = if ("$(($NetUserOutput | Select-String 'Password required') -split '\s{4,}' | Select-Object -Skip 1)".Trim() -like "Yes") { $true }else { $false } - UserMayChangePassword = if ("$(($NetUserOutput | Select-String 'User may change password') -split '\s{4,}' | Select-Object -Skip 1)".Trim() -like "Yes") { $true }else { $false } - WorkstationsAllowed = "$(($NetUserOutput | Select-String 'Workstations allowed') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - LogonScript = "$(($NetUserOutput | Select-String 'Logon script') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - UserProfile = "$(($NetUserOutput | Select-String 'User profile') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - LastLogon = if ($LastLogon -notmatch "Never" -and $LastLogon -notlike "") { Get-Date -Date $LastLogon }else { $LastLogon } - LogonHoursAllowed = "$(($NetUserOutput | Select-String 'Logon hours allowed') -split '\s{4,}' | Select-Object -Skip 1)".Trim() - } - ) - } - catch { - Write-Host -Object "[Error] Failed to format PowerShell object." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - return - } - } - - # If active users are defined, filter the users to include only those currently logged in. - if ($ActiveUsers) { - $UsersWithExpiration = $UsersWithExpiration | Where-Object { $ActiveUsers.Username -contains $_.Username } - } - - # Process each user with expiring passwords and retrieve additional information. - $UsersWithExpiration | ForEach-Object { - $Username = $_.Username - try { - $ErrorActionPreference = "Stop" - $searcher = [adsisearcher]"" - $searcher.Filter = "samaccountname=$Username" - - # Construct the expiring user object. - $ExpiringUser = [PSCustomObject]@{ - Username = $Username - "User Principal Name" = $searcher.FindOne().Properties.userprincipalname | Select-Object -First 1 - "E-mail Address" = $searcher.FindOne().Properties.mail | Select-Object -First 1 - "Password Expiration Date" = if ($searcher.FindOne().Properties.pwdlastset -like 0) { "Must change at next logon" }else { $_.PasswordExpires } - } - $ErrorActionPreference = "Continue" - } - catch { - Write-Host -Object "[Error] Failed to retrieve the User Principal Name and email address for '$Username'." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - $ErrorActionPreference = "Continue" - return - } - - try { - # Filter expiring users based on password expiration dates. - $ErrorActionPreference = "Stop" - $ExpiringUser = $ExpiringUser | Where-Object { $_."Password Expiration Date" -notmatch "Never" } - $ExpiringUser = $ExpiringUser | Where-Object { ($_."Password Expiration Date" -eq "Must change at next logon") -or ((New-TimeSpan $Today $_."Password Expiration Date" -ErrorAction Stop).Days -lt $DaysUntilExpiration -or $DaysUntilExpiration -eq 0) } - $ErrorActionPreference = "Continue" - } - catch { - Write-Host -Object "[Error] Failed to compute password expiration timespan for '$Username'." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - $ErrorActionPreference = "Continue" - return - } - - # Add the expiring user to the report if found. - if ($ExpiringUser) { - $UsersToReport.Add($ExpiringUser) - } - } - } - - # Check if the machine is not a domain controller. If not, proceed to check local user accounts. - if (!(Test-IsDomainController)) { - Write-Host "Not a Domain Controller. Checking all local users on this machine..." - - try { - # Retrieve local users whose accounts are enabled and have expiring passwords. - $LocalUsers = Get-LocalUser -ErrorAction Stop | Where-Object { $_.Enabled -and $_.PasswordExpires -and ((New-TimeSpan $Today $_.PasswordExpires -ErrorAction Stop).Days -lt $DaysUntilExpiration -or $DaysUntilExpiration -eq 0) } - - # Retrieve local users whose passwords have never been set (expired users). - $ExpiredUsers = Get-LocalUser -ErrorAction Stop | Where-Object { $_.Enabled -and -not $_.PasswordLastSet } - } - catch { - # If any errors occur during the retrieval of local users or computing the password expiration timespan, output an error message. - Write-Host -Object "[Error] Failed to retrieve local users and compute the password expiration timespan." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - - # If there are active users logged in, filter the users to include only those currently logged in. - if ($ActiveUsers) { - $LocalUsers = $LocalUsers | Where-Object { $ActiveUsers.Username -contains $_.Name } - $ExpiredUsers = $ExpiredUsers | Where-Object { $ActiveUsers.Username -contains $_.Name } - } - - # Add local users with expiring passwords to the report. - $LocalUsers | ForEach-Object { - $ExpiringUser = [PSCustomObject]@{ - Username = $_.Name - "Password Expiration Date" = $_.PasswordExpires - } - - $UsersToReport.Add($ExpiringUser) - } - - # Add local users whose passwords are set to change at next logon to the report. - $ExpiredUsers | ForEach-Object { - $ExpiringUser = [PSCustomObject]@{ - Username = $_.Name - "Password Expiration Date" = "Must change at next logon" - } - - $UsersToReport.Add($ExpiringUser) - } - } - - # If users with expiring passwords are found, display an alert and format the report for output. - if ($UsersToReport) { - Write-Host "[Alert] Users with passwords expiring in $DaysUntilExpiration day(s) were found!" - - # Format the report to display the users with expiring passwords. - $Report = $UsersToReport | Format-Table | Out-String - - # Prepare the custom field value based on the formatted user data. - $CustomFieldValue = ($UsersToReport | Format-List | Out-String).Trim() - } - else { - # If no users with expiring passwords are found, display a message. - $Report = "No users with expiring passwords found!" - # Set the custom field value to match the report. - $CustomFieldValue = $Report - } - - # Output the report to the console. - Write-Host $Report - - # If a custom field name is provided, attempt to set the custom field with the report value. - if ($CustomFieldName) { - try { - Write-Host "Attempting to set Custom Field '$CustomFieldName'." - Set-NinjaProperty -Name $CustomFieldName -Value $CustomFieldValue - Write-Host "Successfully set Custom Field '$CustomFieldName'!" - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - exit 1 - } - } - - exit $ExitCode -} -end { - - - -} - +#Requires -Version 5 + +<# +.SYNOPSIS + Generates a report of users whose passwords are nearing expiration, including both Active Directory domain and local accounts. Outputs an alert if any expiring accounts are found. +.DESCRIPTION + Generates a report of users whose passwords are nearing expiration, including both Active Directory domain and local accounts. Outputs an alert if any expiring accounts are found. +.LINK + https://ninjarmm.zendesk.com/hc/en-us/articles/206864826-Policies-Condition-Configuration +.EXAMPLE + -DaysUntilExpiration 999999999999 - On a Workstation + + Not a Domain Controller. Checking the domain users on this machine... + Not a Domain Controller. Checking all local users on this machine... + [Alert] Users with passwords expiring in 999999999999 day(s) were found! + + Username User Principal Name E-mail Address Password Expiration Date + -------- ------------------- -------------- ------------------------ + cheart cheart@test.lan cheart@example.com 11/4/2024 6:01:44 PM + s user s user@test.lan 11/5/2024 9:06:30 AM + s us'er' s us'er'@test.lan 11/5/2024 9:35:08 AM + s us'er s us'er@test.lan 11/5/2024 9:44:57 AM + suse)r9 suse)r9@test.lan 11/5/2024 9:39:38 AM +.EXAMPLE + -DaysUntilExpiration 14 - On a Workstation + + Not a Domain Controller. Checking the domain users on this machine... + Not a Domain Controller. Checking all local users on this machine... + No users with expiring passwords found! + +.EXAMPLE + -DaysUntilExpiration 999999999999 - On a Domain Controller + + This is a domain controller. Checking all users... + [Alert] Users with passwords expiring in 999999999999 day(s) were found! + + Username User Principal Name E-mail Address Password Expiration Date + -------- ------------------- -------------- ------------------------ + tuser1 tuser1@test.lan tuser1@example.com 11/4/2024 8:13:59 AM + tuser2 tuser2@test.lan tuser2@example.com 11/4/2024 8:13:59 AM + tuser3 tuser3@test.lan tuser3@example.com 11/4/2024 8:13:59 AM + tuser4 tuser4@test.lan tuser4@example.com 11/4/2024 8:13:59 AM + tuser5 tuser5@test.lan tuser5@example.com 11/4/2024 8:13:59 AM + tuser6 tuser6@test.lan tuser6@example.com 11/4/2024 8:13:59 AM + tuser7 tuser7@test.lan tuser7@example.com 11/4/2024 8:13:59 AM + tuser8 tuser8@test.lan tuser8@example.com 11/4/2024 8:14:00 AM + tuser9 tuser9@test.lan tuser9@example.com 11/5/2024 10:39:20 AM + cheart cheart@test.lan cheart@example.com 11/4/2024 5:01:44 PM + s user s user@test.lan 11/5/2024 8:06:29 AM + s us'er' s us'er'@test.lan 11/5/2024 8:35:08 AM + s user' s user'@test.lan 11/5/2024 8:35:37 AM + s us'er s us'er@test.lan 11/5/2024 8:44:57 AM + suse)r9 suse)r9@test.lan 11/5/2024 8:39:38 AM + +.EXAMPLE + -DaysUntilExpiration 14 - On a Domain Controller + + This is a domain controller. Checking all users... + No users with expiring passwords found! + +PARAMETER: -DaysUntilExpiration "ReplaceWithAnyNumber" + Users whose passwords expire within the specified number of days will be included in the report and trigger an alert. + +PARAMETER: -CurrentUsers + Only users that are currently logged in will be included in the report and trigger an alert. + +PARAMETER: -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" + Optionally specify the name of a multiline custom field to export the results to. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Removed the use of exit codes as alert triggers, added alert text, added input validation, simplified some sections of code, updated functions, and switched to only alerting/reporting on accounts that have signed in on the device (unless running on a domain controller). Fixed an issue with identifying domain accounts that are set to change on the next sign-in when not running on a domain controller. Fixed an issue with accounts that have a ')' or '(' character or too many consecutive spaces. Fixed an issue with domain accounts that are set to never expire. + +#> + +[CmdletBinding()] +param ( + [Parameter()] + $DaysUntilExpiration = "14", + [Parameter()] + [Switch]$CurrentUsers = [System.Convert]::ToBoolean($env:loggedInUsersOnly), + [Parameter()] + [String]$CustomFieldName +) + +begin { + # If script form variables are used, replace the command line parameters with their value. + if ($env:daysUntilPasswordExpiration -and $env:daysUntilPasswordExpiration -notlike "null") { $DaysUntilExpiration = $env:daysUntilPasswordExpiration } + if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomFieldName = $env:customFieldName } + + # Check if $DaysUntilExpiration is not provided (null or empty). + # If it is missing, output an error message and exit the script. + if (!$DaysUntilExpiration) { + Write-Host -Object "[Error] You must provide a valid expiration cutoff." + exit 1 + } + + # Check if $DaysUntilExpiration contains any non-numeric characters (using regex). + # If it contains anything other than digits, output an error message and exit. + if ($DaysUntilExpiration -match '[^0-9]') { + Write-Host -Object "[Error] An invalid expiration cutoff of '$DaysUntilExpiration' was provided. Please provide a positive whole number that is greater than 0." + exit 1 + } + + # Attempt to cast $DaysUntilExpiration to a long integer. + # If an exception occurs during the conversion, output an error message and exit. + try { + $ErrorActionPreference = "Stop" + $DaysUntilExpiration = [long]$DaysUntilExpiration + $ErrorActionPreference = "Continue" + } + catch { + Write-Host -Object "[Error] An invalid expiration cutoff of '$DaysUntilExpiration' was provided. Please provide a positive whole number that is greater than 0." + Write-Host -Object "[Error] $($_.Exception.Message)." + exit 1 + } + + # Check if $DaysUntilExpiration is less than 1 (i.e., not a positive whole number). + # If it's invalid, output an error message and exit the script. + if ($DaysUntilExpiration -lt 1) { + Write-Host -Object "[Error] An invalid expiration cutoff of '$DaysUntilExpiration' was provided. Please provide a positive whole number that is greater than 0." + exit 1 + } + + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + function Test-IsDomainController { + # Determine the method to retrieve the operating system information based on PowerShell version + try { + $OS = if ($PSVersionTable.PSVersion.Major -lt 3) { + Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop + } + else { + Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop + } + } + catch { + Write-Host -Object "[Error] Unable to validate whether or not this device is a domain controller." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Check if the ProductType is "2", which indicates that the system is a domain controller + if ($OS.ProductType -eq "2") { + return $true + } + } + + function Test-IsEntraJoined { + # Check if the operating system version is Windows 10 or higher + if ([environment]::OSVersion.Version.Major -ge 10) { + # Run the dsregcmd.exe tool to check Entra join status and look for "AzureAdJoined : YES" + $dsreg = dsregcmd.exe /status | Select-String "AzureAdJoined : YES" + } + + # If the search found the "AzureAdJoined : YES" string, return True, otherwise return False + if ($dsreg) { return $True }else { return $False } + } + + function Test-IsDomainJoined { + # Check the PowerShell version to determine the appropriate cmdlet to use + try { + if ($PSVersionTable.PSVersion.Major -lt 3) { + return $(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain + } + else { + return $(Get-CimInstance -Class Win32_ComputerSystem).PartOfDomain + } + } + catch { + Write-Host -Object "[Error] Unable to validate whether or not this device is a part of a domain." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + } + + function Get-QUser { + $quser = quser.exe + $quser -replace '\s{2,}', ',' -replace '>' | ConvertFrom-Csv + } + + function Test-IsDomainReachable { + try { + $searcher = [adsisearcher]"(&(objectCategory=computer)(name=$env:ComputerName))" + $searcher.FindOne() + } + catch { + Write-Host -Object "[Error] Failed to connect to the domain!" + Write-Host -Object "[Error] $($_.Exception.Message)" + $False + } + } + + function Get-UserHives { + param ( + [Parameter()] + [ValidateSet('AzureAD', 'DomainAndLocal', 'All')] + [String]$Type = "All", + [Parameter()] + [String[]]$ExcludedUsers, + [Parameter()] + [switch]$IncludeDefault + ) + + # Define the SID patterns to match based on the selected user type + $Patterns = switch ($Type) { + "AzureAD" { "S-1-12-1-(\d+-?){4}$" } + "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" } + "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" } + } + + # Retrieve user profile information based on the defined patterns + try { + $UserProfiles = Foreach ($Pattern in $Patterns) { + Get-ItemProperty "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" -ErrorAction Stop | + Where-Object { $_.PSChildName -match $Pattern } | + Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, + @{Name = "Username"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } }, + @{Name = "Domain"; Expression = { if ($_.PSChildName -match "S-1-12-1-(\d+-?){4}$") { "AzureAD" }else { $Null } } }, + @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, + @{Name = "Path"; Expression = { $_.ProfileImagePath } } + } + } + catch { + Write-Host -Object "[Error] Failed to scan registry keys at 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList'." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # If the IncludeDefault switch is set, add the Default profile to the results + switch ($IncludeDefault) { + $True { + $DefaultProfile = "" | Select-Object Username, SID, UserHive, Path + $DefaultProfile.Username = "Default" + $DefaultProfile.Domain = $env:COMPUTERNAME + $DefaultProfile.SID = "DefaultProfile" + $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT" + $DefaultProfile.Path = "C:\Users\Default" + + # Exclude users specified in the ExcludedUsers list + $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.Username } + } + } + + try { + if ($PSVersionTable.PSVersion.Major -lt 3) { + $AllAccounts = Get-WmiObject -Class "win32_UserAccount" -ErrorAction Stop + } + else { + $AllAccounts = Get-CimInstance -ClassName "win32_UserAccount" -ErrorAction Stop + } + } + catch { + Write-Host -Object "[Error] Failed to gather complete profile information." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + $CompleteUserProfiles = $UserProfiles | ForEach-Object { + $SID = $_.SID + $Win32Object = $AllAccounts | Where-Object { $_.SID -like $SID } + + if ($Win32Object) { + $Win32Object | Add-Member -NotePropertyName UserHive -NotePropertyValue $_.UserHive + $Win32Object + } + else { + [PSCustomObject]@{ + Name = $_.Username + Domain = $_.Domain + SID = $_.SID + UserHive = $_.UserHive + Path = $_.Path + } + } + } + + # Return the list of user profiles, excluding any specified in the ExcludedUsers list + $CompleteUserProfiles | Where-Object { $ExcludedUsers -notcontains $_.Name } + } + + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [Switch]$Piped # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + # # Remove the non-breaking space character # Removed NinjaOne dependency + # if ($Type -eq "WYSIWYG") { # Removed NinjaOne dependency + # $Value = $Value -replace ' ', ' ' # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Measure the number of characters in the provided value # Removed NinjaOne dependency + # $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + + # # Throw an error if the value exceeds the character limit of 200,000 characters # Removed NinjaOne dependency + # if ($Piped -and $Characters -ge 200000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if (!$Piped -and $Characters -ge 45000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Initialize a hashtable for additional documentation parameters # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + + # # If a document name is provided, add it to the documentation parameters # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # Define a list of valid field types # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + + # # Warn the user if the provided type is not valid # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # Define types that require options to be retrieved # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + + # # If the property is being set in a document or field and the type needs options, retrieve them # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if there was an issue retrieving the property options # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # Process the property value based on its type # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # Convert the value to a boolean for Checkbox type # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Convert the value to a Unix timestamp for Date or Date Time type # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Convert the dropdown value to its corresponding GUID # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # # Throw an error if the value is not present in the dropdown options # Removed NinjaOne dependency + # if (!($Selection)) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # For other types, use the value as is # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Set the property value in the document if a document name is provided # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # try { # Removed NinjaOne dependency + # # Otherwise, set the standard property value # Removed NinjaOne dependency + # if ($Piped) { # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # catch { # Removed NinjaOne dependency + # Write-Host -Object "[Error] Failed to set custom field." # Removed NinjaOne dependency + # throw $_.Exception.Message # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if setting the property failed # Removed NinjaOne dependency + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + + # Attempt to retrieve the current datetime using Get-Date. + try { + $Today = Get-Date -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] Failed to retrieve current date." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # Check if the script is being run with elevated (Administrator) privileges. + # If not, output an error message and exit the script. + if (!(Test-IsElevated)) { + Write-Host "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Check if the machine is joined to Microsoft Entra. + # If true, output a warning that the script cannot check Entra accounts but will check local accounts. + if (Test-IsEntraJoined) { + Write-Warning -Message "This script is unable to check Microsoft Entra accounts, however, the script will check the local accounts on the machine." + } + + # If $CurrentUsers is defined, retrieve the active users on the machine using Get-Quser. + if ($CurrentUsers) { + $ActiveUsers = Get-Quser + } + + # Initialize an empty list to store users that will be reported. + $UsersToReport = New-Object System.Collections.Generic.List[object] + + # Check if the machine is a domain controller. If it is, proceed with checking all domain users. + if (Test-IsDomainController) { + Write-Host "This is a domain controller. Checking all users..." + try { + # Set error handling to stop on errors, and retrieve all Active Directory users with expiring passwords. + $ErrorActionPreference = "Stop" + $AllActiveDirectoryUsers = Get-ADUser -Filter { Enabled -eq $True -and PasswordNeverExpires -eq $False } -Properties SamAccountName, UserPrincipalName, mail, pwdLastSet, msDS-UserPasswordExpiryTimeComputed + + # Select and format the relevant user properties for reporting. + $AllActiveDirectoryUsers = $AllActiveDirectoryUsers | Select-Object @{ Name = "Username"; Expression = { $_.SamAccountName } }, + @{ Name = "User Principal Name"; Expression = { $_.UserPrincipalName } }, + @{ Name = "E-mail Address"; Expression = { $_.mail } }, + @{ Name = "Password Expiration Date"; Expression = { + if ($_.pwdLastSet -eq 0) { + "Must change at next logon" + } + else { + [datetime]::FromFileTime($_."msDS-UserPasswordExpiryTimeComputed") + } + } + } + + # Reset error handling to continue after retrieving the users. + $ErrorActionPreference = "Continue" + } + catch { + # If retrieving users fails, output an error message and exit the script. + Write-Host -Object "[Error] Failed to retrieve expiring Active Directory user accounts." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Filter the users to find those with passwords expiring within the specified timeframe or on the next logon. + try { + $ExpiringUsers = $AllActiveDirectoryUsers | Where-Object { $_."Password Expiration Date" -and $_."Password Expiration Date" -ne "Must change at next logon" -and ((New-TimeSpan $Today $_."Password Expiration Date" -ErrorAction Stop).Days -lt $DaysUntilExpiration -or $DaysUntilExpiration -eq 0) } + $ExpiredUsers = $AllActiveDirectoryUsers | Where-Object { $_."Password Expiration Date" -eq "Must change at next logon" } + } + catch { + Write-Host -Object "[Error] Failed to compute the password expiration timespan." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # If $ActiveUsers is defined, further filter the users to include only those who are currently logged in. + if ($ActiveUsers) { + $ExpiringUsers = $ExpiringUsers | Where-Object { $ActiveUsers.Username -contains $_.Username } + $ExpiredUsers = $ExpiredUsers | Where-Object { $ActiveUsers.Username -contains $_.Username } + } + + # Add expiring users and expired users to the report list. + if ($ExpiringUsers) { $ExpiringUsers | ForEach-Object { $UsersToReport.Add($_) } } + if ($ExpiredUsers) { $ExpiredUsers | ForEach-Object { $UsersToReport.Add($_) } } + } + + # Check if the machine is domain-joined but not a domain controller. + # If true, it will check domain users logged into this machine. + if ((Test-IsDomainJoined) -and !(Test-IsDomainController)) { + Write-Host "Not a Domain Controller. Checking the domain users on this machine..." + + # If the domain controller is unreachable, display an error that not all domain users may be included. + if (!(Test-IsDomainReachable)) { + Write-Host -Object "[Error] A secure connection to the domain controller could not be established. Some domain users may be missing from the results." + $ExitCode = 1 + } + + # Retrieve previously logged-in domain accounts that have expiring passwords. + $PreviouslyLoggedInDomainAccounts = Get-UserHives | Where-Object { -not $_.Disabled -and $_.Domain -eq $env:USERDOMAIN -and $_.PasswordExpires } + $UsersWithExpiration = New-Object System.Collections.Generic.List[Object] + + # Loop through the previously logged-in domain accounts. + $PreviouslyLoggedInDomainAccounts | ForEach-Object { + # Check if the domain is reachable. If not, display an error and skip further processing. + if (!(Test-IsDomainReachable -ErrorAction SilentlyContinue)) { + Write-Host -Object "[Error] Unable to check '$($_.Name)' while the computer is disconnected from the domain!" + $ExitCode = 1 + return + } + + # Define paths for standard output and error logs, with random names to avoid conflicts + $StandardOutLog = "$env:TEMP\$(Get-Random)_stdout.log" + $StandardErrLog = "$env:TEMP\$(Get-Random)_stderr.log" + + # Prepare arguments for the "net user" command to get domain user info. + $NetUserArguments = @( + "user" + "`"$($_.Name)`"" + "/domain" + ) + + # Configure the process start parameters for the "net.exe" command + $ProcessArguments = @{ + FilePath = "$env:SystemRoot\System32\net.exe" + ArgumentList = $NetUserArguments + RedirectStandardOutput = $StandardOutLog + RedirectStandardError = $StandardErrLog + PassThru = $True + NoNewWindow = $True + Wait = $True + } + + # Try to start the "net.exe" process and catch any errors + try { + $NetUserProcess = Start-Process @ProcessArguments -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] Failed to start net.exe to find the password expiration date for '$($_.Name)'" + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + return + } + + # Check if the exit code indicates success (0) + if ($NetUserProcess.ExitCode -ne 0) { + Write-Warning "Net user exit code of $($NetUserProcess.ExitCode) does not indicate success." + } + + # Check if the standard error log exists, indicating an error occurred + if (Test-Path -Path $StandardErrLog -ErrorAction SilentlyContinue) { + + # Attempt to read the error log + try { + $ErrorLog = Get-Content -Path $StandardErrLog -ErrorAction Stop + } + catch { + # If reading the log fails, display an error and exit + Write-Host -Object "[Error] Failed to open error log at '$StandardErrLog'." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + return + } + + # Remove the error log file after reading + try { + Remove-Item -Path $StandardErrLog -ErrorAction Stop + } + catch { + # If removing the log file fails, display an error + Write-Host -Object "[Error] Failed to remove standard error log at '$StandardErrLog'." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + } + + # If there is any content in the error log, display it and exit + if ($ErrorLog) { + Write-Host -Object "[Error] An error has occurred." + $ErrorLog | ForEach-Object { + Write-Host -Object "[Error] $_" + } + $ExitCode = 1 + return + } + + # Check if the standard output log exists, which contains the user list + if (!(Test-Path -Path $StandardOutLog -ErrorAction SilentlyContinue)) { + Write-Host -Object "[Error] No net user output detected." + $ExitCode = 1 + return + } + + # Try to read the standard output log for user data + try { + $NetUserOutput = Get-Content -Path $StandardOutLog -ErrorAction Stop + } + catch { + # If reading the log fails, display an error and exit + Write-Host -Object "[Error] Failed to open output log at '$StandardOutLog'." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + return + } + + # Try to remove the standard output log after reading + try { + Remove-Item -Path $StandardOutLog -ErrorAction Stop + } + catch { + # If removing the log file fails, display an error + Write-Host -Object "[Error] Failed to remove standard output log at '$StandardOutLog'." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + + # Extract user information from the net user output. + try { + $LastSet = "$(($NetUserOutput | Select-String 'Password last set') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + $Expired = "$(($NetUserOutput | Select-String 'Password expires') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + $Changeable = "$(($NetUserOutput | Select-String 'Password changeable') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + $LastLogon = "$(($NetUserOutput | Select-String 'Last logon') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + + $UsersWithExpiration.Add( + [PSCustomObject]@{ + Username = $($_.Name) + FullName = "$(($NetUserOutput | Select-String 'Full Name') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + Comment = "$(($NetUserOutput | Select-String 'Comment') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + Enabled = if ("$(($NetUserOutput | Select-String 'Account active') -split '\s{4,}' | Select-Object -Skip 1)".Trim() -like "Yes") { $true }else { $false } + AccountExpires = "$(($NetUserOutput | Select-String 'Account expires') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + PasswordLastSet = if ($LastSet) { Get-Date -Date $LastSet }else { $null } + PasswordExpires = if ($Expired -notmatch "Never" -and $Expired -notlike "") { Get-Date -Date $Expired }else { $Expired } + PasswordChangeable = if ($Changeable) { Get-Date -Date $Changeable }else { $null } + PasswordRequired = if ("$(($NetUserOutput | Select-String 'Password required') -split '\s{4,}' | Select-Object -Skip 1)".Trim() -like "Yes") { $true }else { $false } + UserMayChangePassword = if ("$(($NetUserOutput | Select-String 'User may change password') -split '\s{4,}' | Select-Object -Skip 1)".Trim() -like "Yes") { $true }else { $false } + WorkstationsAllowed = "$(($NetUserOutput | Select-String 'Workstations allowed') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + LogonScript = "$(($NetUserOutput | Select-String 'Logon script') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + UserProfile = "$(($NetUserOutput | Select-String 'User profile') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + LastLogon = if ($LastLogon -notmatch "Never" -and $LastLogon -notlike "") { Get-Date -Date $LastLogon }else { $LastLogon } + LogonHoursAllowed = "$(($NetUserOutput | Select-String 'Logon hours allowed') -split '\s{4,}' | Select-Object -Skip 1)".Trim() + } + ) + } + catch { + Write-Host -Object "[Error] Failed to format PowerShell object." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + return + } + } + + # If active users are defined, filter the users to include only those currently logged in. + if ($ActiveUsers) { + $UsersWithExpiration = $UsersWithExpiration | Where-Object { $ActiveUsers.Username -contains $_.Username } + } + + # Process each user with expiring passwords and retrieve additional information. + $UsersWithExpiration | ForEach-Object { + $Username = $_.Username + try { + $ErrorActionPreference = "Stop" + $searcher = [adsisearcher]"" + $searcher.Filter = "samaccountname=$Username" + + # Construct the expiring user object. + $ExpiringUser = [PSCustomObject]@{ + Username = $Username + "User Principal Name" = $searcher.FindOne().Properties.userprincipalname | Select-Object -First 1 + "E-mail Address" = $searcher.FindOne().Properties.mail | Select-Object -First 1 + "Password Expiration Date" = if ($searcher.FindOne().Properties.pwdlastset -like 0) { "Must change at next logon" }else { $_.PasswordExpires } + } + $ErrorActionPreference = "Continue" + } + catch { + Write-Host -Object "[Error] Failed to retrieve the User Principal Name and email address for '$Username'." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + $ErrorActionPreference = "Continue" + return + } + + try { + # Filter expiring users based on password expiration dates. + $ErrorActionPreference = "Stop" + $ExpiringUser = $ExpiringUser | Where-Object { $_."Password Expiration Date" -notmatch "Never" } + $ExpiringUser = $ExpiringUser | Where-Object { ($_."Password Expiration Date" -eq "Must change at next logon") -or ((New-TimeSpan $Today $_."Password Expiration Date" -ErrorAction Stop).Days -lt $DaysUntilExpiration -or $DaysUntilExpiration -eq 0) } + $ErrorActionPreference = "Continue" + } + catch { + Write-Host -Object "[Error] Failed to compute password expiration timespan for '$Username'." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + $ErrorActionPreference = "Continue" + return + } + + # Add the expiring user to the report if found. + if ($ExpiringUser) { + $UsersToReport.Add($ExpiringUser) + } + } + } + + # Check if the machine is not a domain controller. If not, proceed to check local user accounts. + if (!(Test-IsDomainController)) { + Write-Host "Not a Domain Controller. Checking all local users on this machine..." + + try { + # Retrieve local users whose accounts are enabled and have expiring passwords. + $LocalUsers = Get-LocalUser -ErrorAction Stop | Where-Object { $_.Enabled -and $_.PasswordExpires -and ((New-TimeSpan $Today $_.PasswordExpires -ErrorAction Stop).Days -lt $DaysUntilExpiration -or $DaysUntilExpiration -eq 0) } + + # Retrieve local users whose passwords have never been set (expired users). + $ExpiredUsers = Get-LocalUser -ErrorAction Stop | Where-Object { $_.Enabled -and -not $_.PasswordLastSet } + } + catch { + # If any errors occur during the retrieval of local users or computing the password expiration timespan, output an error message. + Write-Host -Object "[Error] Failed to retrieve local users and compute the password expiration timespan." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + + # If there are active users logged in, filter the users to include only those currently logged in. + if ($ActiveUsers) { + $LocalUsers = $LocalUsers | Where-Object { $ActiveUsers.Username -contains $_.Name } + $ExpiredUsers = $ExpiredUsers | Where-Object { $ActiveUsers.Username -contains $_.Name } + } + + # Add local users with expiring passwords to the report. + $LocalUsers | ForEach-Object { + $ExpiringUser = [PSCustomObject]@{ + Username = $_.Name + "Password Expiration Date" = $_.PasswordExpires + } + + $UsersToReport.Add($ExpiringUser) + } + + # Add local users whose passwords are set to change at next logon to the report. + $ExpiredUsers | ForEach-Object { + $ExpiringUser = [PSCustomObject]@{ + Username = $_.Name + "Password Expiration Date" = "Must change at next logon" + } + + $UsersToReport.Add($ExpiringUser) + } + } + + # If users with expiring passwords are found, display an alert and format the report for output. + if ($UsersToReport) { + Write-Host "[Alert] Users with passwords expiring in $DaysUntilExpiration day(s) were found!" + + # Format the report to display the users with expiring passwords. + $Report = $UsersToReport | Format-Table | Out-String + + # Prepare the custom field value based on the formatted user data. + $CustomFieldValue = ($UsersToReport | Format-List | Out-String).Trim() + } + else { + # If no users with expiring passwords are found, display a message. + $Report = "No users with expiring passwords found!" + # Set the custom field value to match the report. + $CustomFieldValue = $Report + } + + # Output the report to the console. + Write-Host $Report + + # If a custom field name is provided, attempt to set the custom field with the report value. + if ($CustomFieldName) { + try { + Write-Host "Attempting to set Custom Field '$CustomFieldName'." + # Set-NinjaProperty -Name $CustomFieldName -Value $CustomFieldValue # Removed NinjaOne dependency + Write-Host "Successfully set Custom Field '$CustomFieldName'!" + } + catch { + Write-Host "[Error] $($_.Exception.Message)" + exit 1 + } + } + + exit $ExitCode +} +end { + + + +} + diff --git a/Powershell Scripts/Remote Desktop - Check Status and Port.ps1 b/Powershell Scripts/Remote Desktop - Check Status and Port.ps1 index a3fedbf..57dae6a 100644 --- a/Powershell Scripts/Remote Desktop - Check Status and Port.ps1 +++ b/Powershell Scripts/Remote Desktop - Check Status and Port.ps1 @@ -1,171 +1,171 @@ # Reports the status of Remote Desktop and the port it is listening on. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Reports the status of Remote Desktop and the port it is listening on. -.DESCRIPTION - Reports the status of Remote Desktop and the port it is listening on. - With the option to save the results to a custom field. - -.EXAMPLE - (No Parameters) - ## EXAMPLE OUTPUT WITHOUT PARAMS ## - [Info] Enabled | Port: 3389 - -PARAMETER: -RdpStatusCustomFieldName "RDPStatus" - Name of a custom field to save the results to. -.EXAMPLE - -RdpStatusCustomFieldName "RDPStatus" - ## EXAMPLE OUTPUT WITH RdpStatusCustomFieldName ## - [Info] Enabled | Port: 3389 - [Info] Attempting to set Custom Field 'RDPStatus'. - [Info] Successfully set Custom Field 'RDPStatus'! - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$RdpStatusCustomFieldName -) - -begin { - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - # Measure the number of characters in the provided value - $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # Throw an error if the value exceeds the character limit of 200,000 characters - if ($Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - # Initialize a hashtable for additional documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define a list of valid field types - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - - # Warn the user if the provided type is not valid - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # Define types that require options to be retrieved - $NeedsOptions = "Dropdown" - - # If the property is being set in a document or field and the type needs options, retrieve them - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an error if there was an issue retrieving the property options - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Process the property value based on its type - switch ($Type) { - "Checkbox" { - # Convert the value to a boolean for Checkbox type - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Convert the value to a Unix timestamp for Date or Date Time type - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Convert the dropdown value to its corresponding GUID - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - # Throw an error if the value is not present in the dropdown options - if (!($Selection)) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # For other types, use the value as is - $NinjaValue = $Value - } - } - - # Set the property value in the document if a document name is provided - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - # Otherwise, set the standard property value - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - - # Throw an error if setting the property failed - if ($CustomField.Exception) { - throw $CustomField - } - } -} -process { - - if ($env:rdpStatusCustomFieldName -and $env:rdpStatusCustomFieldName -notlike "null") { $RdpStatusCustomFieldName = $env:rdpStatusCustomFieldName } - - # Terminal Server registry path - $RdpPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' - # Deny RDP Connections - $DenyRdpConnections = Get-ItemProperty -Path $RdpPath -Name 'fDenyTSConnections' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty fDenyTSConnections -ErrorAction SilentlyContinue - # RDP Port - $RdpPort = Get-ItemProperty -Path "$RdpPath\WinStations\RDP-Tcp" -Name PortNumber -ErrorAction SilentlyContinue | Select-Object -ExpandProperty PortNumber -ErrorAction SilentlyContinue - - # 1 or $null = Disabled (Default) - # 0 = Enabled - $RdpEnabled = if ($DenyRdpConnections -eq 0) { "Enabled" }else { "Disabled" } - # 3389 or $null = 3389 (Default) - $RdpPort = if ($null -eq $RdpPort) { "3389" }else { "$RdpPort" } - - $Report = "$RdpEnabled | Port: $RdpPort" - - Write-Host "[Info] $Report" - - if ($RdpStatusCustomFieldName) { - try { - Write-Host "[Info] Attempting to set Custom Field '$RdpStatusCustomFieldName'." - Set-NinjaProperty -Name $RdpStatusCustomFieldName -Value $Report - Write-Host "[Info] Successfully set Custom Field '$RdpStatusCustomFieldName'!" - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - exit 1 - } - } - -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Reports the status of Remote Desktop and the port it is listening on. +.DESCRIPTION + Reports the status of Remote Desktop and the port it is listening on. + With the option to save the results to a custom field. + +.EXAMPLE + (No Parameters) + ## EXAMPLE OUTPUT WITHOUT PARAMS ## + [Info] Enabled | Port: 3389 + +PARAMETER: -RdpStatusCustomFieldName "RDPStatus" + Name of a custom field to save the results to. +.EXAMPLE + -RdpStatusCustomFieldName "RDPStatus" + ## EXAMPLE OUTPUT WITH RdpStatusCustomFieldName ## + [Info] Enabled | Port: 3389 + [Info] Attempting to set Custom Field 'RDPStatus'. + [Info] Successfully set Custom Field 'RDPStatus'! + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$RdpStatusCustomFieldName +) + +begin { + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # # Measure the number of characters in the provided value # Removed NinjaOne dependency + # $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + + # # Throw an error if the value exceeds the character limit of 200,000 characters # Removed NinjaOne dependency + # if ($Characters -ge 200000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Initialize a hashtable for additional documentation parameters # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + + # # If a document name is provided, add it to the documentation parameters # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # Define a list of valid field types # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + + # # Warn the user if the provided type is not valid # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # Define types that require options to be retrieved # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + + # # If the property is being set in a document or field and the type needs options, retrieve them # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if there was an issue retrieving the property options # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # Process the property value based on its type # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # Convert the value to a boolean for Checkbox type # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Convert the value to a Unix timestamp for Date or Date Time type # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Convert the dropdown value to its corresponding GUID # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # # Throw an error if the value is not present in the dropdown options # Removed NinjaOne dependency + # if (!($Selection)) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # For other types, use the value as is # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Set the property value in the document if a document name is provided # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # # Otherwise, set the standard property value # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if setting the property failed # Removed NinjaOne dependency + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency +} +process { + + if ($env:rdpStatusCustomFieldName -and $env:rdpStatusCustomFieldName -notlike "null") { $RdpStatusCustomFieldName = $env:rdpStatusCustomFieldName } + + # Terminal Server registry path + $RdpPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' + # Deny RDP Connections + $DenyRdpConnections = Get-ItemProperty -Path $RdpPath -Name 'fDenyTSConnections' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty fDenyTSConnections -ErrorAction SilentlyContinue + # RDP Port + $RdpPort = Get-ItemProperty -Path "$RdpPath\WinStations\RDP-Tcp" -Name PortNumber -ErrorAction SilentlyContinue | Select-Object -ExpandProperty PortNumber -ErrorAction SilentlyContinue + + # 1 or $null = Disabled (Default) + # 0 = Enabled + $RdpEnabled = if ($DenyRdpConnections -eq 0) { "Enabled" }else { "Disabled" } + # 3389 or $null = 3389 (Default) + $RdpPort = if ($null -eq $RdpPort) { "3389" }else { "$RdpPort" } + + $Report = "$RdpEnabled | Port: $RdpPort" + + Write-Host "[Info] $Report" + + if ($RdpStatusCustomFieldName) { + try { + Write-Host "[Info] Attempting to set Custom Field '$RdpStatusCustomFieldName'." + # Set-NinjaProperty -Name $RdpStatusCustomFieldName -Value $Report # Removed NinjaOne dependency + Write-Host "[Info] Successfully set Custom Field '$RdpStatusCustomFieldName'!" + } + catch { + Write-Host "[Error] $($_.Exception.Message)" + exit 1 + } + } + +} +end { + + + +} diff --git a/Powershell Scripts/Remove Microsoft Bloatware.ps1 b/Powershell Scripts/Remove Microsoft Bloatware.ps1 index f5a09a0..07aca44 100644 --- a/Powershell Scripts/Remove Microsoft Bloatware.ps1 +++ b/Powershell Scripts/Remove Microsoft Bloatware.ps1 @@ -1,228 +1,228 @@ # Removes common bloatware that is often pre-installed on a PC. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Removes common bloatware that is often pre-installed on a PC. -.DESCRIPTION - Removes common bloatware that is often pre-installed on a PC. -.EXAMPLE - -AppsToRemove "Amazon.com.Amazon, AmazonVideo.PrimeVideo, Clipchamp.Clipchamp, Disney.37853FC22B2CE, DropboxInc.Dropbox, Facebook.Facebook, Facebook.InstagramBeta, king.com.BubbleWitch3Saga, king.com.CandyCrushSaga, king.com.CandyCrushSodaSaga, 5A894077.McAfeeSecurity, 4DF9E0F8.Netflix, SpotifyAB.SpotifyMusic, BytedancePte.Ltd.TikTok, 5319275A.WhatsAppDesktop" - - [Warn] Amazon.com.Amazon is not installed! - Attempting to remove AmazonVideo.PrimeVideo... - Successfully removed AmazonVideo.PrimeVideo. - Attempting to remove Clipchamp.Clipchamp... - Successfully removed Clipchamp.Clipchamp. - Attempting to remove Disney.37853FC22B2CE... - Successfully removed Disney.37853FC22B2CE. - Attempting to remove DropboxInc.Dropbox... - Successfully removed DropboxInc.Dropbox. - Attempting to remove FACEBOOK.FACEBOOK... - Successfully removed FACEBOOK.FACEBOOK. - Attempting to remove Facebook.InstagramBeta... - Successfully removed Facebook.InstagramBeta. - [Warn] king.com.BubbleWitch3Saga is not installed! - [Warn] king.com.CandyCrushSaga is not installed! - [Warn] king.com.CandyCrushSodaSaga is not installed! - Attempting to remove 5A894077.McAfeeSecurity... - Successfully removed 5A894077.McAfeeSecurity. - Attempting to remove 4DF9E0F8.Netflix... - Successfully removed 4DF9E0F8.Netflix. - Attempting to remove SpotifyAB.SpotifyMusic... - Successfully removed SpotifyAB.SpotifyMusic. - Attempting to remove BytedancePte.Ltd.TikTok... - Successfully removed BytedancePte.Ltd.TikTok. - Attempting to remove 5319275A.WhatsAppDesktop... - Successfully removed 5319275A.WhatsAppDesktop. - -PARAMETER: -AppsToRemove "AmazonVideo.PrimeVideo" - A comma-separated list of Appx package names you would like to remove. - -PARAMETER: -OverrideWithCustomField "ReplaceMeWithAmultilineCustomFieldName" - Name of a multiline custom field to retrieve the 'Apps To Remove' list. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$AppsToRemove = "Amazon.com.Amazon, AmazonVideo.PrimeVideo, Clipchamp.Clipchamp, Disney.37853FC22B2CE, DropboxInc.Dropbox, Facebook.Facebook, Facebook.InstagramBeta, king.com.BubbleWitch3Saga, king.com.CandyCrushSaga, king.com.CandyCrushSodaSaga, 5A894077.McAfeeSecurity, 4DF9E0F8.Netflix, SpotifyAB.SpotifyMusic, BytedancePte.Ltd.TikTok, 5319275A.WhatsAppDesktop", - [Parameter()] - [String]$OverrideWithCustomField -) - -begin { - # Replace parameters with dynamic script variables. - if ($env:appsToRemove -and $env:appsToRemove -notlike "null") { $AppsToRemove = $env:appsToRemove } - if ($env:overrideWithCustomFieldName -and $env:overrideWithCustomFieldName -notlike "null") { $OverrideWithCustomField = $env:overrideWithCustomFieldName } - - $AppList = New-Object System.Collections.Generic.List[string] - - function Get-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - [String]$Name - ) - - # We'll redirect error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1 - - # If we received some sort of error it should have an exception property and we'll exit the function with that error information. - if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } - - if (-not $NinjaPropertyValue) { - throw [System.NullReferenceException]::New("The Custom Field '$Name' is empty!") - } - - $NinjaPropertyValue - } - - if ($OverrideWithCustomField) { - Write-Host "Attempting to retrieve uninstall list from '$OverrideWithCustomField'." - try { - $AppsToRemove = Get-NinjaProperty -Name $OverrideWithCustomField -ErrorAction Stop - } - catch { - # If we ran into some sort of error we'll output it here. - Write-Host "[Error] $($_.Exception.Message)" - exit 1 - } - } - - # Check if apps to remove are specified; otherwise, list all Appx packages and exit - if (!$AppsToRemove) { - Write-Host "[Error] Nothing given to remove? Please specify one of the below packages." - Get-AppxPackage -AllUsers | Select-Object Name | Sort-Object Name | Out-String | Write-Host - exit 1 - } - - # Regex to detect invalid characters in Appx package names - $InvalidCharacters = "[#!@&$)(<>?|:;\/{}^%`"']+" - - # Process each app name after splitting the input string - if ($AppsToRemove -match ",") { - $AppsToRemove -split ',' | ForEach-Object { - $App = $_.Trim() - if ($App -match '^[-.]' -or $App -match '\.\.|--' -or $App -match '[-.]$' -or $App -match "\s" -or $App -match $InvalidCharacters) { - Write-Host "[Error] Invalid character in '$App'. Appx package names cannot contain '#!@&$)(<>?|:;\/{}^%`"'', start with '.-', contain a space, or have consecutive '.' or '-' characters." - $ExitCode = 1 - return - } - - if ($App.Length -ge 50) { - Write-Host "[Error] Appx package name of '$App' is invalid Appx package names must be less than 50 characters." - $ExitCode = 1 - return - } - - $AppList.Add($App) - } - } - else { - $AppsToRemove = $AppsToRemove.Trim() - if ($AppsToRemove -match '^[-.]' -or $AppsToRemove -match '\.\.|--' -or $AppsToRemove -match '[-.]$' -or $AppsToRemove -match "\s" -or $AppsToRemove -match $InvalidCharacters) { - Write-Host "[Error] Invalid character in '$AppsToRemove'. AppxPackage names cannot contain '#!@&$)(<>?|:;\/{}^%`"'', start with '.-', contain a space, or have consecutive '.' or '-' characters." - Get-AppxPackage -AllUsers | Select-Object Name | Sort-Object Name | Out-String | Write-Host - exit 1 - } - - if ($AppsToRemove.Length -ge 50) { - Write-Host "[Error] Appx package name of '$AppsToRemove' is invalid Appx package names must be less than 50 characters." - Get-AppxPackage -AllUsers | Select-Object Name | Sort-Object Name | Out-String | Write-Host - exit 1 - } - - $AppList.Add($AppsToRemove) - } - - # Exit if no valid apps to remove - if ($AppList.Count -eq 0) { - Write-Host "[Error] No valid apps to remove!" - Get-AppxPackage -AllUsers | Select-Object Name | Sort-Object Name | Out-String | Write-Host - exit 1 - } - - # Function to check if the script is running with Administrator privileges - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # Check for Administrator privileges before attempting to remove any packages - if (!(Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Attempt to remove each specified app - foreach ($App in $AppList) { - $AppxPackage = Get-AppxPackage -AllUsers | Where-Object { $_.Name -Like "*$App*" } | Sort-Object Name -Unique - $ProvisionedPackage = Get-AppxProvisionedPackage -Online | Where-Object { $_.DisplayName -like "*$App*" } | Sort-Object DisplayName -Unique - - # Warn if the app is not installed - if (!$AppxPackage -and !$ProvisionedPackage) { - Write-Host "`n[Warn] $App is not installed!" - continue - } - - # Output an error if too many apps were selected for uninstall - if ($AppxPackage.Count -gt 1) { - Write-Host "[Error] Too many Apps were found with the name '$App'. Please re-run with a more specific name." - Write-Host ($AppxPackage | Select-Object Name | Sort-Object Name | Out-String) - $ExitCode = 1 - continue - } - if ($ProvisionedPackage.Count -gt 1) { - Write-Host "[Error] Too many Apps were found with the name '$App'. Please re-run with a more specific name." - Write-Host ($ProvisionedPackage | Select-Object DisplayName | Sort-Object DisplayName | Out-String) - ExitCode = 1 - continue - } - - # Output an error if two different packages got selected. - if ($ProvisionedPackage -and $AppxPackage -and $AppxPackage.Name -ne $ProvisionedPackage.DisplayName) { - Write-Host "[Error] Too many Apps were found with the name '$App'. Please re-run with a more specific name." - Write-Host ($ProvisionedPackage | Select-Object DisplayName | Sort-Object DisplayName | Out-String) - ExitCode = 1 - continue - } - - try { - # Remove the provisioning package first. - if ($ProvisionedPackage) { - Write-Host "`nAttempting to remove provisioning package $($ProvisionedPackage.DisplayName)..." - Get-AppxProvisionedPackage -Online | Where-Object { $_.DisplayName -like "*$App*" } | Remove-AppxProvisionedPackage -Online -AllUsers | Out-Null - Write-Host "Successfully removed provisioning package $($ProvisionedPackage.DisplayName)." - } - - # Remove the installed instances. - if ($AppxPackage) { - Write-Host "`nAttempting to remove $($AppxPackage.Name)..." - Get-AppxPackage -AllUsers | Where-Object { $_.Name -Like "*$App*" } | Remove-AppxPackage -AllUsers - Write-Host "Successfully removed $($AppxPackage.Name)." - } - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Removes common bloatware that is often pre-installed on a PC. +.DESCRIPTION + Removes common bloatware that is often pre-installed on a PC. +.EXAMPLE + -AppsToRemove "Amazon.com.Amazon, AmazonVideo.PrimeVideo, Clipchamp.Clipchamp, Disney.37853FC22B2CE, DropboxInc.Dropbox, Facebook.Facebook, Facebook.InstagramBeta, king.com.BubbleWitch3Saga, king.com.CandyCrushSaga, king.com.CandyCrushSodaSaga, 5A894077.McAfeeSecurity, 4DF9E0F8.Netflix, SpotifyAB.SpotifyMusic, BytedancePte.Ltd.TikTok, 5319275A.WhatsAppDesktop" + + [Warn] Amazon.com.Amazon is not installed! + Attempting to remove AmazonVideo.PrimeVideo... + Successfully removed AmazonVideo.PrimeVideo. + Attempting to remove Clipchamp.Clipchamp... + Successfully removed Clipchamp.Clipchamp. + Attempting to remove Disney.37853FC22B2CE... + Successfully removed Disney.37853FC22B2CE. + Attempting to remove DropboxInc.Dropbox... + Successfully removed DropboxInc.Dropbox. + Attempting to remove FACEBOOK.FACEBOOK... + Successfully removed FACEBOOK.FACEBOOK. + Attempting to remove Facebook.InstagramBeta... + Successfully removed Facebook.InstagramBeta. + [Warn] king.com.BubbleWitch3Saga is not installed! + [Warn] king.com.CandyCrushSaga is not installed! + [Warn] king.com.CandyCrushSodaSaga is not installed! + Attempting to remove 5A894077.McAfeeSecurity... + Successfully removed 5A894077.McAfeeSecurity. + Attempting to remove 4DF9E0F8.Netflix... + Successfully removed 4DF9E0F8.Netflix. + Attempting to remove SpotifyAB.SpotifyMusic... + Successfully removed SpotifyAB.SpotifyMusic. + Attempting to remove BytedancePte.Ltd.TikTok... + Successfully removed BytedancePte.Ltd.TikTok. + Attempting to remove 5319275A.WhatsAppDesktop... + Successfully removed 5319275A.WhatsAppDesktop. + +PARAMETER: -AppsToRemove "AmazonVideo.PrimeVideo" + A comma-separated list of Appx package names you would like to remove. + +PARAMETER: -OverrideWithCustomField "ReplaceMeWithAmultilineCustomFieldName" + Name of a multiline custom field to retrieve the 'Apps To Remove' list. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$AppsToRemove = "Amazon.com.Amazon, AmazonVideo.PrimeVideo, Clipchamp.Clipchamp, Disney.37853FC22B2CE, DropboxInc.Dropbox, Facebook.Facebook, Facebook.InstagramBeta, king.com.BubbleWitch3Saga, king.com.CandyCrushSaga, king.com.CandyCrushSodaSaga, 5A894077.McAfeeSecurity, 4DF9E0F8.Netflix, SpotifyAB.SpotifyMusic, BytedancePte.Ltd.TikTok, 5319275A.WhatsAppDesktop", + [Parameter()] + [String]$OverrideWithCustomField +) + +begin { + # Replace parameters with dynamic script variables. + if ($env:appsToRemove -and $env:appsToRemove -notlike "null") { $AppsToRemove = $env:appsToRemove } + if ($env:overrideWithCustomFieldName -and $env:overrideWithCustomFieldName -notlike "null") { $OverrideWithCustomField = $env:overrideWithCustomFieldName } + + $AppList = New-Object System.Collections.Generic.List[string] + + function Get-NinjaProperty { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [String]$Name + ) + + # We'll redirect error output to the success stream to make it easier to error out if nothing was found or something else went wrong. + # $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1 # Removed NinjaOne dependency + + # If we received some sort of error it should have an exception property and we'll exit the function with that error information. + if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } + + if (-not $NinjaPropertyValue) { + throw [System.NullReferenceException]::New("The Custom Field '$Name' is empty!") + } + + $NinjaPropertyValue + } + + if ($OverrideWithCustomField) { + Write-Host "Attempting to retrieve uninstall list from '$OverrideWithCustomField'." + try { + $AppsToRemove = Get-NinjaProperty -Name $OverrideWithCustomField -ErrorAction Stop + } + catch { + # If we ran into some sort of error we'll output it here. + Write-Host "[Error] $($_.Exception.Message)" + exit 1 + } + } + + # Check if apps to remove are specified; otherwise, list all Appx packages and exit + if (!$AppsToRemove) { + Write-Host "[Error] Nothing given to remove? Please specify one of the below packages." + Get-AppxPackage -AllUsers | Select-Object Name | Sort-Object Name | Out-String | Write-Host + exit 1 + } + + # Regex to detect invalid characters in Appx package names + $InvalidCharacters = "[#!@&$)(<>?|:;\/{}^%`"']+" + + # Process each app name after splitting the input string + if ($AppsToRemove -match ",") { + $AppsToRemove -split ',' | ForEach-Object { + $App = $_.Trim() + if ($App -match '^[-.]' -or $App -match '\.\.|--' -or $App -match '[-.]$' -or $App -match "\s" -or $App -match $InvalidCharacters) { + Write-Host "[Error] Invalid character in '$App'. Appx package names cannot contain '#!@&$)(<>?|:;\/{}^%`"'', start with '.-', contain a space, or have consecutive '.' or '-' characters." + $ExitCode = 1 + return + } + + if ($App.Length -ge 50) { + Write-Host "[Error] Appx package name of '$App' is invalid Appx package names must be less than 50 characters." + $ExitCode = 1 + return + } + + $AppList.Add($App) + } + } + else { + $AppsToRemove = $AppsToRemove.Trim() + if ($AppsToRemove -match '^[-.]' -or $AppsToRemove -match '\.\.|--' -or $AppsToRemove -match '[-.]$' -or $AppsToRemove -match "\s" -or $AppsToRemove -match $InvalidCharacters) { + Write-Host "[Error] Invalid character in '$AppsToRemove'. AppxPackage names cannot contain '#!@&$)(<>?|:;\/{}^%`"'', start with '.-', contain a space, or have consecutive '.' or '-' characters." + Get-AppxPackage -AllUsers | Select-Object Name | Sort-Object Name | Out-String | Write-Host + exit 1 + } + + if ($AppsToRemove.Length -ge 50) { + Write-Host "[Error] Appx package name of '$AppsToRemove' is invalid Appx package names must be less than 50 characters." + Get-AppxPackage -AllUsers | Select-Object Name | Sort-Object Name | Out-String | Write-Host + exit 1 + } + + $AppList.Add($AppsToRemove) + } + + # Exit if no valid apps to remove + if ($AppList.Count -eq 0) { + Write-Host "[Error] No valid apps to remove!" + Get-AppxPackage -AllUsers | Select-Object Name | Sort-Object Name | Out-String | Write-Host + exit 1 + } + + # Function to check if the script is running with Administrator privileges + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # Check for Administrator privileges before attempting to remove any packages + if (!(Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Attempt to remove each specified app + foreach ($App in $AppList) { + $AppxPackage = Get-AppxPackage -AllUsers | Where-Object { $_.Name -Like "*$App*" } | Sort-Object Name -Unique + $ProvisionedPackage = Get-AppxProvisionedPackage -Online | Where-Object { $_.DisplayName -like "*$App*" } | Sort-Object DisplayName -Unique + + # Warn if the app is not installed + if (!$AppxPackage -and !$ProvisionedPackage) { + Write-Host "`n[Warn] $App is not installed!" + continue + } + + # Output an error if too many apps were selected for uninstall + if ($AppxPackage.Count -gt 1) { + Write-Host "[Error] Too many Apps were found with the name '$App'. Please re-run with a more specific name." + Write-Host ($AppxPackage | Select-Object Name | Sort-Object Name | Out-String) + $ExitCode = 1 + continue + } + if ($ProvisionedPackage.Count -gt 1) { + Write-Host "[Error] Too many Apps were found with the name '$App'. Please re-run with a more specific name." + Write-Host ($ProvisionedPackage | Select-Object DisplayName | Sort-Object DisplayName | Out-String) + ExitCode = 1 + continue + } + + # Output an error if two different packages got selected. + if ($ProvisionedPackage -and $AppxPackage -and $AppxPackage.Name -ne $ProvisionedPackage.DisplayName) { + Write-Host "[Error] Too many Apps were found with the name '$App'. Please re-run with a more specific name." + Write-Host ($ProvisionedPackage | Select-Object DisplayName | Sort-Object DisplayName | Out-String) + ExitCode = 1 + continue + } + + try { + # Remove the provisioning package first. + if ($ProvisionedPackage) { + Write-Host "`nAttempting to remove provisioning package $($ProvisionedPackage.DisplayName)..." + Get-AppxProvisionedPackage -Online | Where-Object { $_.DisplayName -like "*$App*" } | Remove-AppxProvisionedPackage -Online -AllUsers | Out-Null + Write-Host "Successfully removed provisioning package $($ProvisionedPackage.DisplayName)." + } + + # Remove the installed instances. + if ($AppxPackage) { + Write-Host "`nAttempting to remove $($AppxPackage.Name)..." + Get-AppxPackage -AllUsers | Where-Object { $_.Name -Like "*$App*" } | Remove-AppxPackage -AllUsers + Write-Host "Successfully removed $($AppxPackage.Name)." + } + } + catch { + Write-Host "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + } + + exit $ExitCode +} +end { + + + +} diff --git a/Powershell Scripts/Rename Computer.ps1 b/Powershell Scripts/Rename Computer.ps1 index f4136e5..1cec0e1 100644 --- a/Powershell Scripts/Rename Computer.ps1 +++ b/Powershell Scripts/Rename Computer.ps1 @@ -1,233 +1,233 @@ # Renames domain-joined or non-domain-joined computers. For domain-joined computers, this operation requires either the username of a Domain Admin and the name of a secure field containing their password, or it must be executed with Domain Admin privileges. -#Requires -Version 4 - -<# -.SYNOPSIS - Renames domain-joined or non-domain-joined computers. For domain-joined computers, this operation requires either the username of a Domain Admin and the name of a secure field containing their password, or it must be executed with Domain Admin privileges. -.DESCRIPTION - Renames domain-joined or non-domain-joined computers. For domain-joined computers, this operation requires either the username of a Domain Admin and the name of a secure field containing their password, or it must be executed with Domain Admin privileges. -.EXAMPLE - -NewName "ReplaceWithNewName" - - WARNING: The changes will take effect after you restart the computer KYLE-WIN10-TEST. - - HasSucceeded OldComputerName NewComputerName - ------------ --------------- --------------- - True KYLE-WIN10-TEST ReplaceWithNewName - - - - WARNING: This script takes effect after a reboot. Use -Reboot to have this script reboot for you. - -PARAMETER: -DomainUser "UsernameForDomainAdmin" -DomainPasswordCustomField "SecureCustomField" - Domain Joined machines require a domain admins creds when not ran as a Domain Admin (System is not a Domain Admin). - -PARAMETER: -Reboot - Reboots the computer 5 minutes after the script is ran. -.EXAMPLE - -NewName "ReplaceWithNewName" -Reboot - - This is a domain joined machine. Testing for secure domain connection... - WARNING: The changes will take effect after you restart the computer KYLE-WIN10-TEST. - - HasSucceeded OldComputerName NewComputerName - ------------ --------------- --------------- - True KYLE-WIN10-TEST ReplaceWithNewName - - WARNING: Reboot specified scheduling reboot for 06/13/2023 12:09:53... - -.OUTPUTS - None -.NOTES - OS: Win 10+, Server 2012+ - Release Notes: Removed Password Variable, Switched to write-host instead of write-error -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$NewName, - [Parameter()] - [String]$DomainUser, - [Parameter()] - [String]$DomainPasswordCustomField, - [Parameter()] - [Switch]$Reboot = [System.Convert]::ToBoolean($env:reboot) -) - -begin { - # If script forms are used overwrite the params with those values. - if ($env:newComputerName -and $env:newComputerName -notlike "null") { $NewName = $env:newComputerName } - if ($env:domainAdminUsername -and $env:domainAdminUsername -notlike "null") { $DomainUser = $env:domainAdminUsername } - if ($env:domainAdminPasswordCustomField -and $env:domainAdminPasswordCustomField -notlike "null") { $DomainPasswordCustomField = $env:domainAdminPasswordCustomField } - - function Get-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - [String]$Name - ) - - # We'll redirect error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1 - - # If we received some sort of error it should have an exception property and we'll exit the function with that error information. - if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } - - if (-not $NinjaPropertyValue) { - throw [System.NullReferenceException]::New("The Custom Field '$Name' is empty!") - } - - $NinjaPropertyValue - } - - # If Domain Password Custom Field provided, retrieve the password, convert it to a secure a string and save it to a variable. - if ($DomainPasswordCustomField) { - try { - Write-Host -Object "Attempting to retrieve password from secure field '$DomainPasswordCustomField'..." - $DomainPassword = Get-NinjaProperty -Name $DomainPasswordCustomField -ErrorAction Stop | ConvertTo-SecureString -AsPlainText -Force - Write-Host -Object "Successfully retrieved password from '$DomainPasswordCustomField'." - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - exit 1 - } - } - - # Converts the username and password into a powershell credential object - if ($DomainUser -and $DomainPassword) { - $Credential = New-Object System.Management.Automation.PsCredential("$DomainUser", $DomainPassword) - } - - # Checks if script is running as an elevated user - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - # Check if machine is domain joined - function Test-IsDomainJoined { - if ($PSVersionTable.PSVersion.Major -lt 5) { - return $(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain - } - else { - return $(Get-CimInstance -Class Win32_ComputerSystem).PartOfDomain - } - } - - # Check if script is running as System - function Test-IsSystem { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - return $id.Name -like "NT AUTHORITY*" -or $id.IsSystem - } - - # Check if script is running as a domain admin - function Test-IsDomainAdmin { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - return $p.IsInRole("Domain Admins") - } - - # Check if running on a domain controller - function Test-IsDomainController { - $OS = if ($PSVersionTable.PSVersion.Major -lt 5) { - Get-WmiObject -Class Win32_OperatingSystem - } - else { - Get-CimInstance -ClassName Win32_OperatingSystem - } - - if ($OS.ProductType -eq "2") { - return $true - } - } - - # Double check that this script has something to do. - if ($NewName -eq $env:computername) { - Write-Host -Object "[Error] New name is the same as the current hostname." - exit 1 - } - - # Error out if not provided with a new name - if (-not $Newname) { - Write-Host -Object "[Error] Please specify a new name!" - exit 1 - } -} -process { - # If not running as the system user script needs to be running as an elevated user. - if (-not (Test-IsElevated) -and -not (Test-IsSystem)) { - Write-Host -Object "Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Warn end-users if theyre giving the computer too long of a name. - if ($NewName.Length -gt 15) { - Write-Warning -Message "The New Computer Name $NewName exceeds 15 characters! In some instances you may only see the first 15 characters." - } - - # Preparing Splat - $ArgumentList = @{ - "ComputerName" = $env:computername - "Force" = $True - "NewName" = $NewName - "PassThru" = $True - } - - # If it's domain joined we'll have to check a couple things to make sure this is possible - if (Test-IsDomainJoined) { - Write-Host -Object "This is a domain joined machine. Testing for secure domain connection..." - - # We're not going to allow renaming domain controllers - if (Test-IsDomainController) { - Write-Host -Object "[Error] This is a domain controller. Please rename manually." - exit 1 - } - - # The domain controller will need to be reachable for the rename to apply - if (-not (Test-ComputerSecureChannel)) { - Write-Host -Object "[Error] A secure connection to the domain controller cannot be established! Please ensure the domain is reachable and there are no machines with identical names!" - exit 1 - } - - # Domain joined machines require a domain admin to change the name - if (-not $Credential -and -not (Test-IsDomainAdmin)) { - Write-Host -Object "[Error] The Domain User and Domain Password is missing. The username and password for a domain admin is required for a domain joined machine!" - exit 1 - } - - # Adding credentials to the splat - if ($Credential) { - $ArgumentList["DomainCredential"] = $Credential - } - } - - # Saving the results to check later - $Result = Rename-Computer @ArgumentList - - # Letting the end-user know the result - $Result | Format-Table -AutoSize | Out-String | Write-Host - - # Error out on failure - if (-not $Result.HasSucceeded) { - Write-Host -Object "[Error] Failed to rename computer!" - exit 1 - } - - # If a reboot was specified schedule it for 5 minutes from now. - if ($Reboot) { - Write-Warning -Message "Reboot specified scheduling reboot for $((Get-Date).AddMinutes(5))..." - Start-Process "shutdown.exe" -ArgumentList "/r /t 300" -NoNewWindow -Wait - } - else { - Write-Warning -Message "This script takes effect after a reboot. Use the reboot checkbox to have this script reboot for you." - } - exit 0 -} -end { - - - -} - +#Requires -Version 4 + +<# +.SYNOPSIS + Renames domain-joined or non-domain-joined computers. For domain-joined computers, this operation requires either the username of a Domain Admin and the name of a secure field containing their password, or it must be executed with Domain Admin privileges. +.DESCRIPTION + Renames domain-joined or non-domain-joined computers. For domain-joined computers, this operation requires either the username of a Domain Admin and the name of a secure field containing their password, or it must be executed with Domain Admin privileges. +.EXAMPLE + -NewName "ReplaceWithNewName" + + WARNING: The changes will take effect after you restart the computer KYLE-WIN10-TEST. + + HasSucceeded OldComputerName NewComputerName + ------------ --------------- --------------- + True KYLE-WIN10-TEST ReplaceWithNewName + + + + WARNING: This script takes effect after a reboot. Use -Reboot to have this script reboot for you. + +PARAMETER: -DomainUser "UsernameForDomainAdmin" -DomainPasswordCustomField "SecureCustomField" + Domain Joined machines require a domain admins creds when not ran as a Domain Admin (System is not a Domain Admin). + +PARAMETER: -Reboot + Reboots the computer 5 minutes after the script is ran. +.EXAMPLE + -NewName "ReplaceWithNewName" -Reboot + + This is a domain joined machine. Testing for secure domain connection... + WARNING: The changes will take effect after you restart the computer KYLE-WIN10-TEST. + + HasSucceeded OldComputerName NewComputerName + ------------ --------------- --------------- + True KYLE-WIN10-TEST ReplaceWithNewName + + WARNING: Reboot specified scheduling reboot for 06/13/2023 12:09:53... + +.OUTPUTS + None +.NOTES + OS: Win 10+, Server 2012+ + Release Notes: Removed Password Variable, Switched to write-host instead of write-error +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$NewName, + [Parameter()] + [String]$DomainUser, + [Parameter()] + [String]$DomainPasswordCustomField, + [Parameter()] + [Switch]$Reboot = [System.Convert]::ToBoolean($env:reboot) +) + +begin { + # If script forms are used overwrite the params with those values. + if ($env:newComputerName -and $env:newComputerName -notlike "null") { $NewName = $env:newComputerName } + if ($env:domainAdminUsername -and $env:domainAdminUsername -notlike "null") { $DomainUser = $env:domainAdminUsername } + if ($env:domainAdminPasswordCustomField -and $env:domainAdminPasswordCustomField -notlike "null") { $DomainPasswordCustomField = $env:domainAdminPasswordCustomField } + + function Get-NinjaProperty { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [String]$Name + ) + + # We'll redirect error output to the success stream to make it easier to error out if nothing was found or something else went wrong. + # $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1 # Removed NinjaOne dependency + + # If we received some sort of error it should have an exception property and we'll exit the function with that error information. + if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } + + if (-not $NinjaPropertyValue) { + throw [System.NullReferenceException]::New("The Custom Field '$Name' is empty!") + } + + $NinjaPropertyValue + } + + # If Domain Password Custom Field provided, retrieve the password, convert it to a secure a string and save it to a variable. + if ($DomainPasswordCustomField) { + try { + Write-Host -Object "Attempting to retrieve password from secure field '$DomainPasswordCustomField'..." + $DomainPassword = Get-NinjaProperty -Name $DomainPasswordCustomField -ErrorAction Stop | ConvertTo-SecureString -AsPlainText -Force + Write-Host -Object "Successfully retrieved password from '$DomainPasswordCustomField'." + } + catch { + Write-Host "[Error] $($_.Exception.Message)" + exit 1 + } + } + + # Converts the username and password into a powershell credential object + if ($DomainUser -and $DomainPassword) { + $Credential = New-Object System.Management.Automation.PsCredential("$DomainUser", $DomainPassword) + } + + # Checks if script is running as an elevated user + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + # Check if machine is domain joined + function Test-IsDomainJoined { + if ($PSVersionTable.PSVersion.Major -lt 5) { + return $(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain + } + else { + return $(Get-CimInstance -Class Win32_ComputerSystem).PartOfDomain + } + } + + # Check if script is running as System + function Test-IsSystem { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + return $id.Name -like "NT AUTHORITY*" -or $id.IsSystem + } + + # Check if script is running as a domain admin + function Test-IsDomainAdmin { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + return $p.IsInRole("Domain Admins") + } + + # Check if running on a domain controller + function Test-IsDomainController { + $OS = if ($PSVersionTable.PSVersion.Major -lt 5) { + Get-WmiObject -Class Win32_OperatingSystem + } + else { + Get-CimInstance -ClassName Win32_OperatingSystem + } + + if ($OS.ProductType -eq "2") { + return $true + } + } + + # Double check that this script has something to do. + if ($NewName -eq $env:computername) { + Write-Host -Object "[Error] New name is the same as the current hostname." + exit 1 + } + + # Error out if not provided with a new name + if (-not $Newname) { + Write-Host -Object "[Error] Please specify a new name!" + exit 1 + } +} +process { + # If not running as the system user script needs to be running as an elevated user. + if (-not (Test-IsElevated) -and -not (Test-IsSystem)) { + Write-Host -Object "Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Warn end-users if theyre giving the computer too long of a name. + if ($NewName.Length -gt 15) { + Write-Warning -Message "The New Computer Name $NewName exceeds 15 characters! In some instances you may only see the first 15 characters." + } + + # Preparing Splat + $ArgumentList = @{ + "ComputerName" = $env:computername + "Force" = $True + "NewName" = $NewName + "PassThru" = $True + } + + # If it's domain joined we'll have to check a couple things to make sure this is possible + if (Test-IsDomainJoined) { + Write-Host -Object "This is a domain joined machine. Testing for secure domain connection..." + + # We're not going to allow renaming domain controllers + if (Test-IsDomainController) { + Write-Host -Object "[Error] This is a domain controller. Please rename manually." + exit 1 + } + + # The domain controller will need to be reachable for the rename to apply + if (-not (Test-ComputerSecureChannel)) { + Write-Host -Object "[Error] A secure connection to the domain controller cannot be established! Please ensure the domain is reachable and there are no machines with identical names!" + exit 1 + } + + # Domain joined machines require a domain admin to change the name + if (-not $Credential -and -not (Test-IsDomainAdmin)) { + Write-Host -Object "[Error] The Domain User and Domain Password is missing. The username and password for a domain admin is required for a domain joined machine!" + exit 1 + } + + # Adding credentials to the splat + if ($Credential) { + $ArgumentList["DomainCredential"] = $Credential + } + } + + # Saving the results to check later + $Result = Rename-Computer @ArgumentList + + # Letting the end-user know the result + $Result | Format-Table -AutoSize | Out-String | Write-Host + + # Error out on failure + if (-not $Result.HasSucceeded) { + Write-Host -Object "[Error] Failed to rename computer!" + exit 1 + } + + # If a reboot was specified schedule it for 5 minutes from now. + if ($Reboot) { + Write-Warning -Message "Reboot specified scheduling reboot for $((Get-Date).AddMinutes(5))..." + Start-Process "shutdown.exe" -ArgumentList "/r /t 300" -NoNewWindow -Wait + } + else { + Write-Warning -Message "This script takes effect after a reboot. Use the reboot checkbox to have this script reboot for you." + } + exit 0 +} +end { + + + +} + diff --git a/Powershell Scripts/Reset an Account Password.ps1 b/Powershell Scripts/Reset an Account Password.ps1 index 09ba269..0f24736 100644 --- a/Powershell Scripts/Reset an Account Password.ps1 +++ b/Powershell Scripts/Reset an Account Password.ps1 @@ -1,463 +1,463 @@ # Resets a users password. Please run on a domain controller to reset an active directory account; otherwise, it will reset a local account. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Resets a user's password. Please run on a domain controller to reset an active directory account; otherwise, it will reset a local account. -.DESCRIPTION - Resets a user's password. Please run on a domain controller to reset an active directory account; otherwise, it will reset a local account. -.EXAMPLE - -Username "Fred" -PasswordCustomField "secure" - - WARNING: This system is not a domain controller but is domain-joined. Resetting domain accounts is only supported on Domain Controllers. - WARNING: Assuming you're trying to reset a local account on a domain-joined machine. - - Attempting to set Custom Field 'secure'. - Successfully set Custom Field 'secure'! - - Successfully reset password for 'Fred'. - -PARAMETER: -Username "NameOfAccountYouWouldLikeToReset" - Username of the user you would like to reset the password for. - -PARAMETER: -PasswordCustomField "NameOfSecureFieldToStorePassword" - Name of a secure Custom Field to store the randomly generated password to. - -PARAMETER: -PasswordLength "20" - Desired length for the randomly generated password. - -PARAMETER: -PasswordExpireOption "User Must Change Password" - Specifies the password expiration policy. Options include "User Must Change Password" to require a password change at next login, "Password Never Expires" to keep the password from expiring, and "Neither" for standard password expiration. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Updated Calculated Name, removed checkbox for domain accounts, switched to generating password and storing it into a custom field. -.COMPONENT - ManageUsers -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$Username, - [Parameter()] - [String]$PasswordCustomField, - [Parameter()] - [long]$PasswordLength, - [Parameter()] - [String]$PasswordExpireOption -) - -begin { - # Replace preset parameters with script variables. - if ($env:resetUsername -and $env:resetUsername -notlike "null") { $Username = $env:resetUsername } - if ($env:customFieldToStorePassword -and $env:customFieldToStorePassword -notlike "null") { $PasswordCustomField = $env:customFieldToStorePassword } - if ($env:passwordLength -and $env:passwordLength -notlike "null") { $PasswordLength = $env:passwordLength } - if ($env:passwordExpireOptions -and $env:passwordExpireOptions -notlike "null") { $PasswordExpireOption = $env:passwordExpireOptions } - - # Check if the username variable is empty. If it is, output an error message and exit with status code 1. - if (!$Username) { - Write-Host -Object "[Error] The username of the user you would like to reset is required." - exit 1 - } - - # Ensure username does not contain illegal characters. - if ($Username -match '\[|\]|:|;|\||=|\+|\*|\?|<|>|/|\\|,|"|@') { - Write-Host -Object ("[Error] $Username contains one of the following invalid characters." + ' " [ ] : ; | = + * ? < > / \ , @') - exit 1 - } - - # Ensure the username does not contain spaces. - if ($Username -match '\s') { - Write-Host -Object "[Error] '$Username' contains a space." - exit 1 - } - - # Ensure the username is not longer than 20 characters. - $UsernameCharacters = $Username | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($UsernameCharacters -gt 20) { - Write-Host -Object "[Error] '$Username' is too long. The username needs to be less than or equal to 20 characters." - exit 1 - } - - # Verify that a password length has been specified, exit if not. - if (!$PasswordLength) { - Write-Host -Object "[Error] You must specify a password length." - exit 1 - } - - # Ensure the specified password length is between 8 and 199 characters, exit if outside this range. - if ($PasswordLength -lt 8 -or $PasswordLength -ge 200) { - Write-Host -Object "[Error] Password length must be at least 8 and less than 200. '$PasswordLength' is invalid." - exit 1 - } - - # Check for the presence of a custom field to store the password, exit if it is missing. - if (!$PasswordCustomField) { - Write-Host -Object "[Error] A custom field to store the password is required!" - exit 1 - } - - # Ensure the custom field does not contain spaces, exit if it does. - if ($PasswordCustomField -match '\s') { - Write-Host -Object "[Error] The script requires the name of the custom field not the label." - Write-Host -Object "https://ninjarmm.zendesk.com/hc/en-us/articles/360060920631-Custom-Fields-Configuration-Device-Role-Fields" - exit 1 - } - - # Validate that the password expiration option provided is one of the allowed values, exit if it is not. - $ValidExpireOption = "User Must Change Password", "Password Never Expires", "Neither" - if ($PasswordExpireOption -and $ValidExpireOption -notcontains $PasswordExpireOption) { - Write-Host -Object "[Error] Invalid password expire option given. Must be either 'User Must Change Password' or 'Password Never Expires' or 'Neither'" - exit 1 - } - - - # Microsoft default password policy - $PasswordPolicy = [PSCustomObject]@{ - MinimumLength = 0 - Complexity = 1 - } - - # Export the security policy to a file and wait for the process to complete. - $Arguments = @( - "/export" - "/cfg" - "$env:TEMP\secconfig.cfg" - ) - $SecurityExport = Start-Process -FilePath "secedit.exe" -ArgumentList $Arguments -PassThru -Wait -WindowStyle Hidden - - # Check if the export was successful; if not, assume default Microsoft policy. - if ($SecurityExport.ExitCode -ne 0) { - Write-Host -Object "[Error] Failed to retrieve password complexity policy. Assuming Microsoft Default policy is in effect." - } - else { - $SecurityPolicy = Get-Content -Path "$env:TEMP\secconfig.cfg" - - $PasswordLengthField = $SecurityPolicy | Select-String "MinimumPasswordLength" - $PasswordPolicy.MinimumLength = ($PasswordLengthField -split "=").Trim()[1] - } - - # Remove the exported security policy file if it exists. - if (Test-Path -Path "$env:TEMP\secconfig.cfg" -ErrorAction SilentlyContinue) { - Remove-Item -Path "$env:TEMP\secconfig.cfg" - } - - # Check if the requested password length meets the minimum requirements of the security policy, exit if it does not. - if ($PasswordLength -lt $PasswordPolicy.MinimumLength) { - Write-Host "[Error] The minimum password length of $($PasswordPolicy.MinimumLength) is greater than the password length you requested to generate ($PasswordLength)." - exit 1 - } - - # Check if script is running with local administrator rights. - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - if ($p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)) { - Write-Output $True - } - else { - Write-Output $False - } - } - - # Generate a cryptographically secure password. - function New-SecurePassword { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $false)] - [int]$Length = 16, - [Parameter(Mandatory = $false)] - [switch]$IncludeSpecialCharacters - ) - # .NET class for generating cryptographically secure random numbers - $cryptoProvider = New-Object System.Security.Cryptography.RNGCryptoServiceProvider - $baseChars = "abcdefghjknpqrstuvwxyzABCDEFGHIJKMNPQRSTUVWXYZ0123456789" - $SpecialCharacters = '!@#$%&-' - $passwordChars = $baseChars + $(if ($IncludeSpecialCharacters) { $SpecialCharacters } else { '' }) - $password = for ($i = 0; $i -lt $Length; $i++) { - $byte = [byte[]]::new(1) - $cryptoProvider.GetBytes($byte) - $charIndex = $byte[0] % $passwordChars.Length - $passwordChars[$charIndex] - } - - return $password -join '' - } - - # Check if the script is currently running on a Domain Controller. - function Test-IsDomainController { - $OS = if ($PSVersionTable.PSVersion.Major -lt 5) { - Get-WmiObject -Class Win32_OperatingSystem - } - else { - Get-CimInstance -ClassName Win32_OperatingSystem - } - - if ($OS.ProductType -eq "2") { - return $True - } - } - - # Check if the script is running on a Domain-Joined computer. - function Test-IsDomainJoined { - if ($PSVersionTable.PSVersion.Major -lt 5) { - return $(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain - } - else { - return $(Get-CimInstance -Class Win32_ComputerSystem).PartOfDomain - } - } - - # Check if the script is running on an Entra-Joined computer. - function Test-IsAzureJoined { - if ([environment]::OSVersion.Version.Major -ge 10) { - $dsreg = dsregcmd.exe /status | Select-String "AzureAdJoined : YES" - } - - if ($dsreg) { - return $True - } - else { - return $False - } - } - - # Function to set a Ninja custom field. - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded; the value is greater than or equal to 200,000 characters.") - } - - # If requested to set the field value for a Ninja document, we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is specified, it is assumed that the input does not need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to be set. - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # Redirect error output to the success stream to make it easier to handle errors if nothing is found or if something else goes wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received with an exception property, the function will exit with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below types require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # Although it's highly likely we were given a value like "True" or a boolean datatype, it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # Check if the script is running with elevated (Administrator) privileges; exit with an error if not. - if (!(Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Check if the script is running on a Domain Controller and set the $IsDomainUser flag to true if it is. - if (Test-IsDomainController) { - $IsDomainUser = $True - } - - # Check if the script is running on a system joined to Azure AD (Microsoft Entra) and output warnings that the script does not support operations on Entra accounts. - if (Test-IsAzureJoined) { - Write-Warning "This script does not support resetting Microsoft Entra accounts." - Write-Warning "Assuming you want to reset a local or domain account on an Entra-joined machine." - Write-Host "" - } - - # Check if the system is domain-joined but not a domain controller, and output warnings that domain account resets are only supported on domain controllers. - if (!(Test-IsDomainController) -and (Test-IsDomainJoined)) { - Write-Warning "This system is not a domain controller but is domain-joined. Resetting domain accounts is only supported on Domain Controllers." - Write-Warning "Assuming you're trying to reset a local account on a domain-joined machine." - Write-Host "" - } - - # Attempt to import the ActiveDirectory module if the script detected a domain environment earlier. - if ($IsDomainUser) { - try { - # Try to import the ActiveDirectory module - Import-Module -Name ActiveDirectory -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] Failed to import the ActiveDirectory PowerShell module. Please ensure it is installed on this system." - exit 1 - } - } - - # Attempt to generate a secure password that meets specified complexity requirements, retrying up to 1000 times. - $i = 0 - do { - $Password = New-SecurePassword -Length $PasswordLength -IncludeSpecialCharacters - $i++ - }while ($i -lt 1000 -and !($Password -match '[@!#$%&\-]+' -and $Password -match '[A-Z]+' -and $Password -match '[a-z]+' -and $Password -match '[0-9]+')) - - # Output an error if unable to generate a password after 1000 attempts. - if ($i -eq 1000) { - Write-Host "[Error] Unable to generate a secure password after 1000 tries." - exit 1 - } - - # Retrieve user account information based on whether the script is running in a domain environment or locally. - if ($IsDomainUser) { - $UserToReset = Get-ADUser -Identity $Username -ErrorAction SilentlyContinue - } - else { - $UserToReset = Get-LocalUser -Name $Username -ErrorAction SilentlyContinue - } - - # Exit with an error if no user account was found for the specified username. - if (!$UserToReset) { - Write-Host "[Error] Cannot reset the password to an account that does not exist!" - exit 1 - } - - # Check if multiple accounts matched the username; if so, provide detailed information and exit. - if ($UserToReset.Count -gt 1) { - Write-Host "[Error] Multiple accounts matched that username. Please be more specific." - $UserToReset | Format-Table | Out-String | Write-Host - exit 1 - } - - # Attempt to set a custom field with the newly generated password, handling any errors that occur. - try { - Write-Host "Attempting to set Custom Field '$PasswordCustomField'." - Set-NinjaProperty -Name $PasswordCustomField -Value $Password - Write-Host "Successfully set Custom Field '$PasswordCustomField'!`n" - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - exit 1 - } - - # Convert plaintext password to a secure string. - $Password = $Password | ConvertTo-SecureString -AsPlainText -Force - - try { - # If operating within a domain environment, use domain user operations. - if ($IsDomainUser) { - # Reset the user's password. - $UserToReset | Set-ADAccountPassword -NewPassword $Password -Reset -ErrorAction Stop - - # Set password expiration option according to the option selected. - if ($PasswordExpireOption -eq "Password Never Expires") { - $UserToReset | Set-ADUser -PasswordNeverExpires:$True - } - else { - $UserToReset | Set-ADUser -PasswordNeverExpires:$False - } - - # If user must change password at next logon, set that option. - if ($PasswordExpireOption -eq "User Must Change Password") { - $UserToReset = Get-ADUser -Identity $Username -ErrorAction SilentlyContinue - $UserToReset | Set-ADUser -ChangePasswordAtLogon:$True - } - - # Confirm password reset operation success. - Write-Host "Successfully reset password for '$Username'." - } - else { - $Arguments = @{ - Password = $Password - } - - # Set password policies for local users based on the specified expiration option. - if ($PasswordExpireOption -eq "Password Never Expires") { - $Arguments["PasswordNeverExpires"] = $true - } - else { - $Arguments["PasswordNeverExpires"] = $false - } - - # Reset the local user's password. - $UserToReset | Set-LocalUser @Arguments -Confirm:$false -ErrorAction Stop - - # If user must change password at next logon, execute the command. - if ($PasswordExpireOption -eq "User Must Change Password") { - Invoke-Command -ScriptBlock { net.exe user "$Username" /logonpasswordchg:yes } | Where-Object { $_ -AND $_ -notmatch "command completed successfully" } - } - - # Confirm password reset operation success. - Write-Host "Successfully reset password for '$Username'." - } - } - catch { - # Output errors if the try block fails. - Write-Host "[Error] Failed to reset the password for $Username." - Write-Host "[Error] $($_.Exception.Message)" - exit 1 - } - - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Resets a user's password. Please run on a domain controller to reset an active directory account; otherwise, it will reset a local account. +.DESCRIPTION + Resets a user's password. Please run on a domain controller to reset an active directory account; otherwise, it will reset a local account. +.EXAMPLE + -Username "Fred" -PasswordCustomField "secure" + + WARNING: This system is not a domain controller but is domain-joined. Resetting domain accounts is only supported on Domain Controllers. + WARNING: Assuming you're trying to reset a local account on a domain-joined machine. + + Attempting to set Custom Field 'secure'. + Successfully set Custom Field 'secure'! + + Successfully reset password for 'Fred'. + +PARAMETER: -Username "NameOfAccountYouWouldLikeToReset" + Username of the user you would like to reset the password for. + +PARAMETER: -PasswordCustomField "NameOfSecureFieldToStorePassword" + Name of a secure Custom Field to store the randomly generated password to. + +PARAMETER: -PasswordLength "20" + Desired length for the randomly generated password. + +PARAMETER: -PasswordExpireOption "User Must Change Password" + Specifies the password expiration policy. Options include "User Must Change Password" to require a password change at next login, "Password Never Expires" to keep the password from expiring, and "Neither" for standard password expiration. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Updated Calculated Name, removed checkbox for domain accounts, switched to generating password and storing it into a custom field. +.COMPONENT + ManageUsers +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$Username, + [Parameter()] + [String]$PasswordCustomField, + [Parameter()] + [long]$PasswordLength, + [Parameter()] + [String]$PasswordExpireOption +) + +begin { + # Replace preset parameters with script variables. + if ($env:resetUsername -and $env:resetUsername -notlike "null") { $Username = $env:resetUsername } + if ($env:customFieldToStorePassword -and $env:customFieldToStorePassword -notlike "null") { $PasswordCustomField = $env:customFieldToStorePassword } + if ($env:passwordLength -and $env:passwordLength -notlike "null") { $PasswordLength = $env:passwordLength } + if ($env:passwordExpireOptions -and $env:passwordExpireOptions -notlike "null") { $PasswordExpireOption = $env:passwordExpireOptions } + + # Check if the username variable is empty. If it is, output an error message and exit with status code 1. + if (!$Username) { + Write-Host -Object "[Error] The username of the user you would like to reset is required." + exit 1 + } + + # Ensure username does not contain illegal characters. + if ($Username -match '\[|\]|:|;|\||=|\+|\*|\?|<|>|/|\\|,|"|@') { + Write-Host -Object ("[Error] $Username contains one of the following invalid characters." + ' " [ ] : ; | = + * ? < > / \ , @') + exit 1 + } + + # Ensure the username does not contain spaces. + if ($Username -match '\s') { + Write-Host -Object "[Error] '$Username' contains a space." + exit 1 + } + + # Ensure the username is not longer than 20 characters. + $UsernameCharacters = $Username | Measure-Object -Character | Select-Object -ExpandProperty Characters + if ($UsernameCharacters -gt 20) { + Write-Host -Object "[Error] '$Username' is too long. The username needs to be less than or equal to 20 characters." + exit 1 + } + + # Verify that a password length has been specified, exit if not. + if (!$PasswordLength) { + Write-Host -Object "[Error] You must specify a password length." + exit 1 + } + + # Ensure the specified password length is between 8 and 199 characters, exit if outside this range. + if ($PasswordLength -lt 8 -or $PasswordLength -ge 200) { + Write-Host -Object "[Error] Password length must be at least 8 and less than 200. '$PasswordLength' is invalid." + exit 1 + } + + # Check for the presence of a custom field to store the password, exit if it is missing. + if (!$PasswordCustomField) { + Write-Host -Object "[Error] A custom field to store the password is required!" + exit 1 + } + + # Ensure the custom field does not contain spaces, exit if it does. + if ($PasswordCustomField -match '\s') { + Write-Host -Object "[Error] The script requires the name of the custom field not the label." + Write-Host -Object "https://ninjarmm.zendesk.com/hc/en-us/articles/360060920631-Custom-Fields-Configuration-Device-Role-Fields" + exit 1 + } + + # Validate that the password expiration option provided is one of the allowed values, exit if it is not. + $ValidExpireOption = "User Must Change Password", "Password Never Expires", "Neither" + if ($PasswordExpireOption -and $ValidExpireOption -notcontains $PasswordExpireOption) { + Write-Host -Object "[Error] Invalid password expire option given. Must be either 'User Must Change Password' or 'Password Never Expires' or 'Neither'" + exit 1 + } + + + # Microsoft default password policy + $PasswordPolicy = [PSCustomObject]@{ + MinimumLength = 0 + Complexity = 1 + } + + # Export the security policy to a file and wait for the process to complete. + $Arguments = @( + "/export" + "/cfg" + "$env:TEMP\secconfig.cfg" + ) + $SecurityExport = Start-Process -FilePath "secedit.exe" -ArgumentList $Arguments -PassThru -Wait -WindowStyle Hidden + + # Check if the export was successful; if not, assume default Microsoft policy. + if ($SecurityExport.ExitCode -ne 0) { + Write-Host -Object "[Error] Failed to retrieve password complexity policy. Assuming Microsoft Default policy is in effect." + } + else { + $SecurityPolicy = Get-Content -Path "$env:TEMP\secconfig.cfg" + + $PasswordLengthField = $SecurityPolicy | Select-String "MinimumPasswordLength" + $PasswordPolicy.MinimumLength = ($PasswordLengthField -split "=").Trim()[1] + } + + # Remove the exported security policy file if it exists. + if (Test-Path -Path "$env:TEMP\secconfig.cfg" -ErrorAction SilentlyContinue) { + Remove-Item -Path "$env:TEMP\secconfig.cfg" + } + + # Check if the requested password length meets the minimum requirements of the security policy, exit if it does not. + if ($PasswordLength -lt $PasswordPolicy.MinimumLength) { + Write-Host "[Error] The minimum password length of $($PasswordPolicy.MinimumLength) is greater than the password length you requested to generate ($PasswordLength)." + exit 1 + } + + # Check if script is running with local administrator rights. + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + if ($p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)) { + Write-Output $True + } + else { + Write-Output $False + } + } + + # Generate a cryptographically secure password. + function New-SecurePassword { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [int]$Length = 16, + [Parameter(Mandatory = $false)] + [switch]$IncludeSpecialCharacters + ) + # .NET class for generating cryptographically secure random numbers + $cryptoProvider = New-Object System.Security.Cryptography.RNGCryptoServiceProvider + $baseChars = "abcdefghjknpqrstuvwxyzABCDEFGHIJKMNPQRSTUVWXYZ0123456789" + $SpecialCharacters = '!@#$%&-' + $passwordChars = $baseChars + $(if ($IncludeSpecialCharacters) { $SpecialCharacters } else { '' }) + $password = for ($i = 0; $i -lt $Length; $i++) { + $byte = [byte[]]::new(1) + $cryptoProvider.GetBytes($byte) + $charIndex = $byte[0] % $passwordChars.Length + $passwordChars[$charIndex] + } + + return $password -join '' + } + + # Check if the script is currently running on a Domain Controller. + function Test-IsDomainController { + $OS = if ($PSVersionTable.PSVersion.Major -lt 5) { + Get-WmiObject -Class Win32_OperatingSystem + } + else { + Get-CimInstance -ClassName Win32_OperatingSystem + } + + if ($OS.ProductType -eq "2") { + return $True + } + } + + # Check if the script is running on a Domain-Joined computer. + function Test-IsDomainJoined { + if ($PSVersionTable.PSVersion.Major -lt 5) { + return $(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain + } + else { + return $(Get-CimInstance -Class Win32_ComputerSystem).PartOfDomain + } + } + + # Check if the script is running on an Entra-Joined computer. + function Test-IsAzureJoined { + if ([environment]::OSVersion.Version.Major -ge 10) { + $dsreg = dsregcmd.exe /status | Select-String "AzureAdJoined : YES" + } + + if ($dsreg) { + return $True + } + else { + return $False + } + } + + # Function to set a Ninja custom field. + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + # if ($Characters -ge 200000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded; the value is greater than or equal to 200,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If requested to set the field value for a Ninja document, we'll specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # This is a list of valid fields that can be set. If no type is specified, it is assumed that the input does not need to be changed. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # The field below requires additional information to be set. # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # Redirect error output to the success stream to make it easier to handle errors if nothing is found or if something else goes wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If an error is received with an exception property, the function will exit with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # The below types require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # Although it's highly likely we were given a value like "True" or a boolean datatype, it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # if (-not $Selection) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # We'll need to set the field differently depending on if its a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # Check if the script is running with elevated (Administrator) privileges; exit with an error if not. + if (!(Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Check if the script is running on a Domain Controller and set the $IsDomainUser flag to true if it is. + if (Test-IsDomainController) { + $IsDomainUser = $True + } + + # Check if the script is running on a system joined to Azure AD (Microsoft Entra) and output warnings that the script does not support operations on Entra accounts. + if (Test-IsAzureJoined) { + Write-Warning "This script does not support resetting Microsoft Entra accounts." + Write-Warning "Assuming you want to reset a local or domain account on an Entra-joined machine." + Write-Host "" + } + + # Check if the system is domain-joined but not a domain controller, and output warnings that domain account resets are only supported on domain controllers. + if (!(Test-IsDomainController) -and (Test-IsDomainJoined)) { + Write-Warning "This system is not a domain controller but is domain-joined. Resetting domain accounts is only supported on Domain Controllers." + Write-Warning "Assuming you're trying to reset a local account on a domain-joined machine." + Write-Host "" + } + + # Attempt to import the ActiveDirectory module if the script detected a domain environment earlier. + if ($IsDomainUser) { + try { + # Try to import the ActiveDirectory module + Import-Module -Name ActiveDirectory -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] Failed to import the ActiveDirectory PowerShell module. Please ensure it is installed on this system." + exit 1 + } + } + + # Attempt to generate a secure password that meets specified complexity requirements, retrying up to 1000 times. + $i = 0 + do { + $Password = New-SecurePassword -Length $PasswordLength -IncludeSpecialCharacters + $i++ + }while ($i -lt 1000 -and !($Password -match '[@!#$%&\-]+' -and $Password -match '[A-Z]+' -and $Password -match '[a-z]+' -and $Password -match '[0-9]+')) + + # Output an error if unable to generate a password after 1000 attempts. + if ($i -eq 1000) { + Write-Host "[Error] Unable to generate a secure password after 1000 tries." + exit 1 + } + + # Retrieve user account information based on whether the script is running in a domain environment or locally. + if ($IsDomainUser) { + $UserToReset = Get-ADUser -Identity $Username -ErrorAction SilentlyContinue + } + else { + $UserToReset = Get-LocalUser -Name $Username -ErrorAction SilentlyContinue + } + + # Exit with an error if no user account was found for the specified username. + if (!$UserToReset) { + Write-Host "[Error] Cannot reset the password to an account that does not exist!" + exit 1 + } + + # Check if multiple accounts matched the username; if so, provide detailed information and exit. + if ($UserToReset.Count -gt 1) { + Write-Host "[Error] Multiple accounts matched that username. Please be more specific." + $UserToReset | Format-Table | Out-String | Write-Host + exit 1 + } + + # Attempt to set a custom field with the newly generated password, handling any errors that occur. + try { + Write-Host "Attempting to set Custom Field '$PasswordCustomField'." + # Set-NinjaProperty -Name $PasswordCustomField -Value $Password # Removed NinjaOne dependency + Write-Host "Successfully set Custom Field '$PasswordCustomField'!`n" + } + catch { + Write-Host "[Error] $($_.Exception.Message)" + exit 1 + } + + # Convert plaintext password to a secure string. + $Password = $Password | ConvertTo-SecureString -AsPlainText -Force + + try { + # If operating within a domain environment, use domain user operations. + if ($IsDomainUser) { + # Reset the user's password. + $UserToReset | Set-ADAccountPassword -NewPassword $Password -Reset -ErrorAction Stop + + # Set password expiration option according to the option selected. + if ($PasswordExpireOption -eq "Password Never Expires") { + $UserToReset | Set-ADUser -PasswordNeverExpires:$True + } + else { + $UserToReset | Set-ADUser -PasswordNeverExpires:$False + } + + # If user must change password at next logon, set that option. + if ($PasswordExpireOption -eq "User Must Change Password") { + $UserToReset = Get-ADUser -Identity $Username -ErrorAction SilentlyContinue + $UserToReset | Set-ADUser -ChangePasswordAtLogon:$True + } + + # Confirm password reset operation success. + Write-Host "Successfully reset password for '$Username'." + } + else { + $Arguments = @{ + Password = $Password + } + + # Set password policies for local users based on the specified expiration option. + if ($PasswordExpireOption -eq "Password Never Expires") { + $Arguments["PasswordNeverExpires"] = $true + } + else { + $Arguments["PasswordNeverExpires"] = $false + } + + # Reset the local user's password. + $UserToReset | Set-LocalUser @Arguments -Confirm:$false -ErrorAction Stop + + # If user must change password at next logon, execute the command. + if ($PasswordExpireOption -eq "User Must Change Password") { + Invoke-Command -ScriptBlock { net.exe user "$Username" /logonpasswordchg:yes } | Where-Object { $_ -AND $_ -notmatch "command completed successfully" } + } + + # Confirm password reset operation success. + Write-Host "Successfully reset password for '$Username'." + } + } + catch { + # Output errors if the try block fails. + Write-Host "[Error] Failed to reset the password for $Username." + Write-Host "[Error] $($_.Exception.Message)" + exit 1 + } + + exit $ExitCode +} +end { + + + +} diff --git a/Powershell Scripts/Save Hard Drive Type to Custom Field.ps1 b/Powershell Scripts/Save Hard Drive Type to Custom Field.ps1 index 5bb7333..7a83cad 100644 --- a/Powershell Scripts/Save Hard Drive Type to Custom Field.ps1 +++ b/Powershell Scripts/Save Hard Drive Type to Custom Field.ps1 @@ -87,17 +87,19 @@ process { # Save the results to a custom field if ($CustomFieldName) { - Write-Host "[Info] Saving the results to the custom field. ($CustomFieldName)" - $CustomField = $( - $Drives | ForEach-Object { - "#:$($_.DiskNumber), Letter: $($_.DriveLetter), Media: $($_.MediaType), Bus: $($_.BusType), SN: $($_.SerialNumber)" - } - ) | Ninja-Property-Set-Piped -Name $CustomFieldName 2>&1 - if ($CustomField.Exception) { - Write-Host $CustomField.Exception.Message - Write-Host "[Error] Failed to save the results to the custom field. ($CustomFieldName)" - } - else { + Write-Host "[Info] Note: Custom field '$CustomFieldName' was specified but NinjaOne integration has been removed." + Write-Host "[Info] Drive information displayed above." + # NinjaOne integration removed + # $CustomField = $( + # $Drives | ForEach-Object { + # "#:$($_.DiskNumber), Letter: $($_.DriveLetter), Media: $($_.MediaType), Bus: $($_.BusType), SN: $($_.SerialNumber)" + # } + # ) | Ninja-Property-Set-Piped -Name $CustomFieldName 2>&1 + # if ($CustomField.Exception) { + # Write-Host $CustomField.Exception.Message + # Write-Host "[Error] Failed to save the results to the custom field. ($CustomFieldName)" + # } + # else { Write-Host "[Info] The results have been saved to the custom field. ($CustomFieldName)" } } diff --git a/Powershell Scripts/Scheduled Task Created Alert.ps1 b/Powershell Scripts/Scheduled Task Created Alert.ps1 index 14af07c..79fa81b 100644 --- a/Powershell Scripts/Scheduled Task Created Alert.ps1 +++ b/Powershell Scripts/Scheduled Task Created Alert.ps1 @@ -1,326 +1,326 @@ # Alert on new scheduled tasks created in the last X hours. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Alert on new scheduled tasks created in the last X hours. - -.DESCRIPTION - This script will query the Windows Event Log for new scheduled tasks created in the last X hours. It will save the results to a multiline custom field and/or a WYSIWYG custom field. - This does require the Microsoft-Windows-TaskScheduler/Operational event log to be enabled first before results can be retrieved. - If enabled the default maximum log size is 10MB and the default retention method is Overwrite oldest events as needed. - -.PARAMETER Hours - The number of hours to look back for new scheduled tasks. - -.PARAMETER MultilineCustomField - The name of the multiline custom field to save the results to. - -.PARAMETER WYSIWYGCustomField - The name of the WYSIWYG custom field to save the results to. - -.PARAMETER EnableEventLog - Enable the Microsoft-Windows-TaskScheduler/Operational event log if it is not already enabled. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [int]$CreatedInLastXHours, - - [Parameter()] - [string]$MultilineCustomField, - - [Parameter()] - [string]$WYSIWYGCustomField, - - [Parameter()] - [switch]$EnableEventLog -) -begin { - - # Check which Script Variables are used - if ($env:createdInLastXHours -and $env:createdInLastXHours -notlike "null") { - if ($env:createdInLastXHours -match '\D') { - Write-Host "[Error] CreatedInLastXHours must be an integer." - exit 1 - } - [int]$CreatedInLastXHours = $env:createdInLastXHours - } - if ($env:multilineCustomField -and $env:multilineCustomField -notlike "null") { - $MultilineCustomField = $env:multilineCustomField - } - if ($env:WYSIWYGCustomField -and $env:wysiwygCustomField -notlike "null") { - $WYSIWYGCustomField = $env:wysiwygCustomField - } - if ($env:enableEventLog -and $env:enableEventLog -like "true") { - $EnableEventLog = $true - } - - # Check if CreatedInLastXHours is not 0 nor negative - if ($CreatedInLastXHours -le 0) { - Write-Host "[Error] CreatedInLastXHours must be greater than 0." - exit 1 - } - - # Define the event log name, selected date, and event ID to query - $EventLogName = 'Microsoft-Windows-TaskScheduler/Operational' - $SelectedDate = $($(Get-Date).AddHours(0 - $CreatedInLastXHours)) - $EventID = 106 - - # Check if the event log is enabled - try { - $EventLogEnabled = Get-WinEvent -ListLog $EventLogName -ErrorAction Stop - } - catch { - Write-Host "[Error] Failed to retrieve event log '$EventLogName'." - Write-Host $_.Exception.Message - exit 1 - } - - # If the event log is not found, exit the script - if ($EventLogEnabled.IsEnabled) { - Write-Host "[Info] Event log '$EventLogName' is enabled." - } - else { - # Enable the event log if the switch is provided - if ($EnableEventLog) { - Write-Host "[Info] Enabling event log '$EventLogName'." - try { - $log = New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration $EventLogName - $log.IsEnabled = $true - $log.SaveChanges() - } - catch { - Write-Host "[Error] Failed to enable event log '$EventLogName'." - exit 1 - } - } - else { - Write-Host "[Error] Event log '$EventLogName' is not enabled." - exit 1 - } - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName, - [Parameter()] - [Switch]$Piped - ) - # Remove the non-breaking space character - if ($Type -eq "WYSIWYG") { - $Value = $Value -replace ' ', ' ' - } - - # Measure the number of characters in the provided value - $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # Throw an error if the value exceeds the character limit of 200,000 characters - if ($Piped -and $Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - if (!$Piped -and $Characters -ge 45000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") - } - - # Initialize a hashtable for additional documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define a list of valid field types - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - - # Warn the user if the provided type is not valid - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # Define types that require options to be retrieved - $NeedsOptions = "Dropdown" - - # If the property is being set in a document or field and the type needs options, retrieve them - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an error if there was an issue retrieving the property options - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Process the property value based on its type - switch ($Type) { - "Checkbox" { - # Convert the value to a boolean for Checkbox type - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Convert the value to a Unix timestamp for Date or Date Time type - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Convert the dropdown value to its corresponding GUID - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - # Throw an error if the value is not present in the dropdown options - if (!($Selection)) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # For other types, use the value as is - $NinjaValue = $Value - } - } - - # Set the property value in the document if a document name is provided - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - try { - # Otherwise, set the standard property value - if ($Piped) { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - } - catch { - Write-Host -Object "[Error] Failed to set custom field." - throw $_.Exception.Message - } - } - - # Throw an error if setting the property failed - if ($CustomField.Exception) { - throw $CustomField - } - } - - $ExitCode = 0 -} - -process { - # Get the scheduled tasks from the event log - try { - $EventLogEntries = Get-WinEvent -FilterHashtable @{ - LogName = $EventLogName - ID = $EventID - } -ErrorAction Stop | Where-Object { $_.TimeCreated -ge $SelectedDate } - } - catch { - Write-Host "[Error] Failed to retrieve event log '$EventLogName'." - Write-Host $_.Exception.Message - exit 1 - } - - # If there are no scheduled tasks, exit the script - if (-not $EventLogEntries) { - Write-Host "[Info] No scheduled tasks created in the last $CreatedInLastXHours hours." - exit 0 - } - - # Get the details of the scheduled tasks - $ScheduledTasks = $EventLogEntries | ForEach-Object { - $_Event = $_ - - if ($_Event) { - # Get the task path and name from the event properties - $TaskPath = $_Event | Select-Object -ExpandProperty Properties | Select-Object -ExpandProperty Value -First 1 # First property value is the task path - # Get the parent path of the task - $ParentPath = ("$TaskPath" -split '\\' | Select-Object -SkipLast 1) -join '\' - # If the task path is empty, set the parent path to the root - if ($ParentPath -eq "") { $ParentPath = "\" } - # Get the task name from the task path - $TaskName = "$TaskPath" -split '\\' | Select-Object -Last 1 - if ($TaskName -like "" -or $ParentPath -like "") { - Write-Host "[Error] Failed to get task path or name from event:" - Write-Host $TaskPath - } - else { - # Get the scheduled task details - $Task = Get-ScheduledTask -TaskPath "$ParentPath" -TaskName "$TaskName" -ErrorAction SilentlyContinue - # Get the last run time of the scheduled task - $LastRunTime = try { - Get-ScheduledTaskInfo -TaskPath "$ParentPath" -TaskName "$TaskName" -ErrorAction Stop | Select-Object -ExpandProperty LastRunTime - } - catch { [datetime]::MinValue } - - # Return the scheduled task details - [PSCustomObject]@{ - TimeCreated = $_Event.TimeCreated - TaskName = $TaskName - TaskCreationDate = $(if ($Task.Date) { $Task.Date } else { $_Event.TimeCreated }) - TaskPath = $TaskPath | Select-Object -First 1 - TaskLastRunTime = $(if ($LastRunTime.Year -lt 2000) { "Never" } else { $LastRunTime }) # If the last run time is before 2000, it has never run - } - } - } - } - - # Sort the scheduled tasks by TimeCreated in descending order - $ScheduledTasks = $ScheduledTasks | Sort-Object -Property TimeCreated -Descending - - # Output the scheduled tasks to the multiline custom field - if ($MultilineCustomField) { - try { - Write-Host "[Info] Attempting to set Custom Field '$MultilineCustomField'." - $ScheduledTasks | Format-List | Out-String -Width 4000 | Set-NinjaProperty -Name $MultilineCustomField -Type "MultiLine" -Piped - Write-Host "[Info] Successfully set Custom Field '$MultilineCustomField'!" - } - catch { - Write-Host "[Error] Failed to set multiline custom field." - $ExitCode = 1 - } - } - - # Output the scheduled tasks to the WYSIWYG custom field - if ($WYSIWYGCustomField) { - try { - Write-Host "[Info] Attempting to set Custom Field '$WYSIWYGCustomField'." - Set-NinjaProperty -Name $WYSIWYGCustomField -Value $($ScheduledTasks | ConvertTo-Html -Fragment) -Type "WYSIWYG" -Piped - Write-Host "[Info] Successfully set Custom Field '$WYSIWYGCustomField'!" - } - catch { - Write-Host "[Error] Failed to set WYSIWYG custom field." - $ExitCode = 1 - } - } - - # Output the scheduled tasks to the Activity Feed - $ScheduledTasks | Format-List | Out-String -Width 4000 | Write-Host - - exit $ExitCode -} - -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Alert on new scheduled tasks created in the last X hours. + +.DESCRIPTION + This script will query the Windows Event Log for new scheduled tasks created in the last X hours. It will save the results to a multiline custom field and/or a WYSIWYG custom field. + This does require the Microsoft-Windows-TaskScheduler/Operational event log to be enabled first before results can be retrieved. + If enabled the default maximum log size is 10MB and the default retention method is Overwrite oldest events as needed. + +.PARAMETER Hours + The number of hours to look back for new scheduled tasks. + +.PARAMETER MultilineCustomField + The name of the multiline custom field to save the results to. + +.PARAMETER WYSIWYGCustomField + The name of the WYSIWYG custom field to save the results to. + +.PARAMETER EnableEventLog + Enable the Microsoft-Windows-TaskScheduler/Operational event log if it is not already enabled. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [int]$CreatedInLastXHours, + + [Parameter()] + [string]$MultilineCustomField, + + [Parameter()] + [string]$WYSIWYGCustomField, + + [Parameter()] + [switch]$EnableEventLog +) +begin { + + # Check which Script Variables are used + if ($env:createdInLastXHours -and $env:createdInLastXHours -notlike "null") { + if ($env:createdInLastXHours -match '\D') { + Write-Host "[Error] CreatedInLastXHours must be an integer." + exit 1 + } + [int]$CreatedInLastXHours = $env:createdInLastXHours + } + if ($env:multilineCustomField -and $env:multilineCustomField -notlike "null") { + $MultilineCustomField = $env:multilineCustomField + } + if ($env:WYSIWYGCustomField -and $env:wysiwygCustomField -notlike "null") { + $WYSIWYGCustomField = $env:wysiwygCustomField + } + if ($env:enableEventLog -and $env:enableEventLog -like "true") { + $EnableEventLog = $true + } + + # Check if CreatedInLastXHours is not 0 nor negative + if ($CreatedInLastXHours -le 0) { + Write-Host "[Error] CreatedInLastXHours must be greater than 0." + exit 1 + } + + # Define the event log name, selected date, and event ID to query + $EventLogName = 'Microsoft-Windows-TaskScheduler/Operational' + $SelectedDate = $($(Get-Date).AddHours(0 - $CreatedInLastXHours)) + $EventID = 106 + + # Check if the event log is enabled + try { + $EventLogEnabled = Get-WinEvent -ListLog $EventLogName -ErrorAction Stop + } + catch { + Write-Host "[Error] Failed to retrieve event log '$EventLogName'." + Write-Host $_.Exception.Message + exit 1 + } + + # If the event log is not found, exit the script + if ($EventLogEnabled.IsEnabled) { + Write-Host "[Info] Event log '$EventLogName' is enabled." + } + else { + # Enable the event log if the switch is provided + if ($EnableEventLog) { + Write-Host "[Info] Enabling event log '$EventLogName'." + try { + $log = New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration $EventLogName + $log.IsEnabled = $true + $log.SaveChanges() + } + catch { + Write-Host "[Error] Failed to enable event log '$EventLogName'." + exit 1 + } + } + else { + Write-Host "[Error] Event log '$EventLogName' is not enabled." + exit 1 + } + } + + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [Switch]$Piped # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + # # Remove the non-breaking space character # Removed NinjaOne dependency + # if ($Type -eq "WYSIWYG") { # Removed NinjaOne dependency + # $Value = $Value -replace ' ', ' ' # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Measure the number of characters in the provided value # Removed NinjaOne dependency + # $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + + # # Throw an error if the value exceeds the character limit of 200,000 characters # Removed NinjaOne dependency + # if ($Piped -and $Characters -ge 200000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if (!$Piped -and $Characters -ge 45000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Initialize a hashtable for additional documentation parameters # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + + # # If a document name is provided, add it to the documentation parameters # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # Define a list of valid field types # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + + # # Warn the user if the provided type is not valid # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # Define types that require options to be retrieved # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + + # # If the property is being set in a document or field and the type needs options, retrieve them # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if there was an issue retrieving the property options # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # Process the property value based on its type # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # Convert the value to a boolean for Checkbox type # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Convert the value to a Unix timestamp for Date or Date Time type # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Convert the dropdown value to its corresponding GUID # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # # Throw an error if the value is not present in the dropdown options # Removed NinjaOne dependency + # if (!($Selection)) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # For other types, use the value as is # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Set the property value in the document if a document name is provided # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # try { # Removed NinjaOne dependency + # # Otherwise, set the standard property value # Removed NinjaOne dependency + # if ($Piped) { # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # catch { # Removed NinjaOne dependency + # Write-Host -Object "[Error] Failed to set custom field." # Removed NinjaOne dependency + # throw $_.Exception.Message # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if setting the property failed # Removed NinjaOne dependency + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + $ExitCode = 0 +} + +process { + # Get the scheduled tasks from the event log + try { + $EventLogEntries = Get-WinEvent -FilterHashtable @{ + LogName = $EventLogName + ID = $EventID + } -ErrorAction Stop | Where-Object { $_.TimeCreated -ge $SelectedDate } + } + catch { + Write-Host "[Error] Failed to retrieve event log '$EventLogName'." + Write-Host $_.Exception.Message + exit 1 + } + + # If there are no scheduled tasks, exit the script + if (-not $EventLogEntries) { + Write-Host "[Info] No scheduled tasks created in the last $CreatedInLastXHours hours." + exit 0 + } + + # Get the details of the scheduled tasks + $ScheduledTasks = $EventLogEntries | ForEach-Object { + $_Event = $_ + + if ($_Event) { + # Get the task path and name from the event properties + $TaskPath = $_Event | Select-Object -ExpandProperty Properties | Select-Object -ExpandProperty Value -First 1 # First property value is the task path + # Get the parent path of the task + $ParentPath = ("$TaskPath" -split '\\' | Select-Object -SkipLast 1) -join '\' + # If the task path is empty, set the parent path to the root + if ($ParentPath -eq "") { $ParentPath = "\" } + # Get the task name from the task path + $TaskName = "$TaskPath" -split '\\' | Select-Object -Last 1 + if ($TaskName -like "" -or $ParentPath -like "") { + Write-Host "[Error] Failed to get task path or name from event:" + Write-Host $TaskPath + } + else { + # Get the scheduled task details + $Task = Get-ScheduledTask -TaskPath "$ParentPath" -TaskName "$TaskName" -ErrorAction SilentlyContinue + # Get the last run time of the scheduled task + $LastRunTime = try { + Get-ScheduledTaskInfo -TaskPath "$ParentPath" -TaskName "$TaskName" -ErrorAction Stop | Select-Object -ExpandProperty LastRunTime + } + catch { [datetime]::MinValue } + + # Return the scheduled task details + [PSCustomObject]@{ + TimeCreated = $_Event.TimeCreated + TaskName = $TaskName + TaskCreationDate = $(if ($Task.Date) { $Task.Date } else { $_Event.TimeCreated }) + TaskPath = $TaskPath | Select-Object -First 1 + TaskLastRunTime = $(if ($LastRunTime.Year -lt 2000) { "Never" } else { $LastRunTime }) # If the last run time is before 2000, it has never run + } + } + } + } + + # Sort the scheduled tasks by TimeCreated in descending order + $ScheduledTasks = $ScheduledTasks | Sort-Object -Property TimeCreated -Descending + + # Output the scheduled tasks to the multiline custom field + if ($MultilineCustomField) { + try { + Write-Host "[Info] Attempting to set Custom Field '$MultilineCustomField'." + # $ScheduledTasks | Format-List | Out-String -Width 4000 | Set-NinjaProperty -Name $MultilineCustomField -Type "MultiLine" -Piped # Removed NinjaOne dependency + Write-Host "[Info] Successfully set Custom Field '$MultilineCustomField'!" + } + catch { + Write-Host "[Error] Failed to set multiline custom field." + $ExitCode = 1 + } + } + + # Output the scheduled tasks to the WYSIWYG custom field + if ($WYSIWYGCustomField) { + try { + Write-Host "[Info] Attempting to set Custom Field '$WYSIWYGCustomField'." + # Set-NinjaProperty -Name $WYSIWYGCustomField -Value $($ScheduledTasks | ConvertTo-Html -Fragment) -Type "WYSIWYG" -Piped # Removed NinjaOne dependency + Write-Host "[Info] Successfully set Custom Field '$WYSIWYGCustomField'!" + } + catch { + Write-Host "[Error] Failed to set WYSIWYG custom field." + $ExitCode = 1 + } + } + + # Output the scheduled tasks to the Activity Feed + $ScheduledTasks | Format-List | Out-String -Width 4000 | Write-Host + + exit $ExitCode +} + +end { + + + +} diff --git a/Powershell Scripts/Scheduled Task Report.ps1 b/Powershell Scripts/Scheduled Task Report.ps1 index 9789c2f..e5b396d 100644 --- a/Powershell Scripts/Scheduled Task Report.ps1 +++ b/Powershell Scripts/Scheduled Task Report.ps1 @@ -1,108 +1,108 @@ # Retrieves a list of scheduled tasks and outputs the list into the activity log. This list can optionally be saved to a Custom Field. - -<# -.SYNOPSIS - Retrieves a list of scheduled tasks and outputs the list into the activity log. This list can optionally be saved to a Custom Field. -.DESCRIPTION - Retrieves a list of scheduled tasks and outputs the list into the activity log. This list can optionally be saved to a Custom Field. -.EXAMPLE - (No Parameters) - - Scheduled Task(s) Found! - - TaskName TaskPath State - -------- -------- ----- - Firefox Background Update 3080... \Mozilla\ Ready - -PARAMETER: -IncludeMicrosoft - Includes Scheduled Tasks created by Microsoft in the report. - -PARAMETER: -IncludeDisabled - Includes Scheduled Tasks that are currently disabled in the report. - -PARAMETER: -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" - Name of a multiline custom field to save the results to. This is optional; results will also output to the activity log. - -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2012 R2 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [Switch]$IncludeMicrosoft = [System.Convert]::ToBoolean($env:includeMicrosoftTasks), - [Parameter()] - [Switch]$IncludeDisabled = [System.Convert]::ToBoolean($env:includeDisabledTasks), - [Parameter()] - [String]$CustomFieldName -) - -begin { - # Get CustomFieldName value from Dynamic Script Form. - if ($env:customFieldName -and $env:customFieldName -notlike "null" ) { $CustomFieldName = $env:customFieldName } - - # Some Scheduled Tasks require Local Admin Privileges to view. - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - # Initialize Generic List for Report - $Report = New-Object System.Collections.Generic.List[String] -} -process { - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." -Category PermissionDenied -Exception (New-Object -TypeName System.UnauthorizedAccessException) - exit 1 - } - - # By default, we'll exclude tasks made by Microsoft. They don't always put themselves down as an author. - if (-not $IncludeMicrosoft) { - $ScheduledTasks = Get-ScheduledTask | Where-Object { $_.Author -notlike "Microsoft*" -and $_.TaskPath -notlike "\Microsoft*" } - } - else { - $ScheduledTasks = Get-ScheduledTask - } - - # We should ignore disabled tasks unless told otherwise. - if (-not $IncludeDisabled) { - $ScheduledTasks = $ScheduledTasks | Where-Object { $_.State -notlike "Disabled" } - } - - # The activity log isn't going to fit all this output, so we'll trim it if it's too large. - if ($ScheduledTasks) { - $FormattedTasks = $ScheduledTasks | ForEach-Object { - $Name = if (($_.TaskName).Length -gt 30) { ($_.TaskName).Substring(0, 30) + "..." }else { $_.TaskName } - $Path = if (($_.TaskPath).Length -gt 30) { ($_.TaskPath).Substring(0, 30) + "..." }else { $_.TaskPath } - - [PSCustomObject]@{ - TaskName = $Name - TaskPath = $Path - State = $_.State - } - } - - Write-Host "Scheduled Task(s) Found!" - $Report.Add(( $FormattedTasks | Format-Table TaskName, TaskPath, State -AutoSize | Out-String )) - } - else { - $Report.Add("No Scheduled Tasks have been found.") - } - - # Output our results. - Write-Host $Report - - # Save our results to a custom field. - if ($CustomFieldName) { - Ninja-Property-Set -Name $CustomFieldName -Value $Report - } -} -end { - - - -} + +<# +.SYNOPSIS + Retrieves a list of scheduled tasks and outputs the list into the activity log. This list can optionally be saved to a Custom Field. +.DESCRIPTION + Retrieves a list of scheduled tasks and outputs the list into the activity log. This list can optionally be saved to a Custom Field. +.EXAMPLE + (No Parameters) + + Scheduled Task(s) Found! + + TaskName TaskPath State + -------- -------- ----- + Firefox Background Update 3080... \Mozilla\ Ready + +PARAMETER: -IncludeMicrosoft + Includes Scheduled Tasks created by Microsoft in the report. + +PARAMETER: -IncludeDisabled + Includes Scheduled Tasks that are currently disabled in the report. + +PARAMETER: -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" + Name of a multiline custom field to save the results to. This is optional; results will also output to the activity log. + +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2012 R2 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [Switch]$IncludeMicrosoft = [System.Convert]::ToBoolean($env:includeMicrosoftTasks), + [Parameter()] + [Switch]$IncludeDisabled = [System.Convert]::ToBoolean($env:includeDisabledTasks), + [Parameter()] + [String]$CustomFieldName +) + +begin { + # Get CustomFieldName value from Dynamic Script Form. + if ($env:customFieldName -and $env:customFieldName -notlike "null" ) { $CustomFieldName = $env:customFieldName } + + # Some Scheduled Tasks require Local Admin Privileges to view. + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + # Initialize Generic List for Report + $Report = New-Object System.Collections.Generic.List[String] +} +process { + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." -Category PermissionDenied -Exception (New-Object -TypeName System.UnauthorizedAccessException) + exit 1 + } + + # By default, we'll exclude tasks made by Microsoft. They don't always put themselves down as an author. + if (-not $IncludeMicrosoft) { + $ScheduledTasks = Get-ScheduledTask | Where-Object { $_.Author -notlike "Microsoft*" -and $_.TaskPath -notlike "\Microsoft*" } + } + else { + $ScheduledTasks = Get-ScheduledTask + } + + # We should ignore disabled tasks unless told otherwise. + if (-not $IncludeDisabled) { + $ScheduledTasks = $ScheduledTasks | Where-Object { $_.State -notlike "Disabled" } + } + + # The activity log isn't going to fit all this output, so we'll trim it if it's too large. + if ($ScheduledTasks) { + $FormattedTasks = $ScheduledTasks | ForEach-Object { + $Name = if (($_.TaskName).Length -gt 30) { ($_.TaskName).Substring(0, 30) + "..." }else { $_.TaskName } + $Path = if (($_.TaskPath).Length -gt 30) { ($_.TaskPath).Substring(0, 30) + "..." }else { $_.TaskPath } + + [PSCustomObject]@{ + TaskName = $Name + TaskPath = $Path + State = $_.State + } + } + + Write-Host "Scheduled Task(s) Found!" + $Report.Add(( $FormattedTasks | Format-Table TaskName, TaskPath, State -AutoSize | Out-String )) + } + else { + $Report.Add("No Scheduled Tasks have been found.") + } + + # Output our results. + Write-Host $Report + + # Save our results to a custom field. + if ($CustomFieldName) { + # Ninja-Property-Set -Name $CustomFieldName -Value $Report # Removed NinjaOne dependency + } +} +end { + + + +} diff --git a/Powershell Scripts/Search DNS Cache Entries.ps1 b/Powershell Scripts/Search DNS Cache Entries.ps1 index f657e6c..92c0c30 100644 --- a/Powershell Scripts/Search DNS Cache Entries.ps1 +++ b/Powershell Scripts/Search DNS Cache Entries.ps1 @@ -1,193 +1,193 @@ # Search for DNS cache record names that match the specified keywords. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Search for DNS cache record names that match the specified keywords. -.DESCRIPTION - Search for DNS cache record names that match the specified keywords. - The DNS cache is a temporary database maintained by the operating system that contains records of all the recent visits and attempted visits to websites and other internet domains. - This script searches the DNS cache for record names that match the specified keywords and outputs the results to the Activity Feed. - Optionally, the results can be saved to a multiline custom field. - -PARAMETER: -Keywords "ExampleInput" - Comma separated list of keywords to search for in the DNS cache. -.EXAMPLE - -Keywords "arpa" - ## EXAMPLE OUTPUT WITH Keywords ## - Entry: 1.80.19.172.in-addr.arpa, Record Name: 1.80.19.172.in-addr.arpa., Record Type: 12, Data: test.mshome.net, TTL: 598963 - -PARAMETER: -Keywords "arpa,mshome" -MultilineCustomField "ReplaceMeWithAnyMultilineCustomField" - The name of the multiline custom field to save the results to. -.EXAMPLE - -Keywords "arpa,mshome" -MultilineCustomField "ReplaceMeWithAnyMultilineCustomField" - ## EXAMPLE OUTPUT WITH MultilineCustomField ## - Entry: 1.80.19.172.in-addr.arpa, Record Name: 1.80.19.172.in-addr.arpa., Record Type: 12, Data: test.mshome.net, TTL: 598963 - Entry: test.mshome.net, Record Name: test.mshome.net., Record Type: 1, Data: 172.19.80.1, TTL: 598963 - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String[]]$Keywords, - [Parameter()] - [String]$MultilineCustomField -) - -begin { - $ExitCode = 0 - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 10000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") - } - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to be set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received it will have an exception property, the function will exit with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } -} -process { - # Get the keywords to search for in the DNS cache - $Keywords = if ($env:keywordsToSearch -and $env:keywordsToSearch -ne "null") { - $env:keywordsToSearch -split "," | ForEach-Object { $_.Trim() } - } - else { - $Keywords -split "," | ForEach-Object { $_.Trim() } - } - $Keywords = if ($Keywords -and $Keywords -ne "null") { - $Keywords | ForEach-Object { - Write-Host "[Info] Searching for DNS Cache Records Matching: $_" - Write-Output "*$_*" - } - } - else { - # Exit if Keywords is empty - Write-Host "[Error] No Keywords Provided" - $ExitCode = 1 - exit $ExitCode - } - # Get the multiline custom field to save the results to - $MultilineCustomField = if ($env:multilineCustomField -and $env:multilineCustomField -ne "null") { - $env:multilineCustomField -split "," | ForEach-Object { $_.Trim() } - } - else { - $MultilineCustomField -split "," | ForEach-Object { $_.Trim() } - } - - Write-Host "" - - # Get the DNS cache entries that match the keywords - $DnsCache = Get-DnsClientCache -Name $Keywords | Select-Object -Property Entry, Name, Type, Data, TimeToLive - - if ($null -eq $DnsCache) { - Write-Host "[Warn] No DNS Cache Entries Found" - } - else { - # Format the DNS cache entries - $Results = $DnsCache | ForEach-Object { - "Entry: $($_.Entry), Record Name: $($_.Name), Record Type: $($_.Type), Data: $($_.Data), TTL: $($_.TimeToLive)" - } - Write-Host "[Info] DNS Cache Entries Found" - # Save the results to a multiline custom field if specified - if ($MultilineCustomField -and $MultilineCustomField -ne "null") { - Write-Host "[Info] Attempting to set Custom Field '$MultilineCustomField'." - try { - Set-NinjaProperty -Name $MultilineCustomField -Value $($Results | Out-String) - Write-Host "[Info] Successfully set Custom Field '$MultilineCustomField'!" - } - catch { - Write-Host "[Warn] Failed to set Custom Field '$MultilineCustomField'." - $Results | Out-String | Write-Host - } - } - else { - # Output the results to the Activity Feed - $Results | Out-String | Write-Host - } - } - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Search for DNS cache record names that match the specified keywords. +.DESCRIPTION + Search for DNS cache record names that match the specified keywords. + The DNS cache is a temporary database maintained by the operating system that contains records of all the recent visits and attempted visits to websites and other internet domains. + This script searches the DNS cache for record names that match the specified keywords and outputs the results to the Activity Feed. + Optionally, the results can be saved to a multiline custom field. + +PARAMETER: -Keywords "ExampleInput" + Comma separated list of keywords to search for in the DNS cache. +.EXAMPLE + -Keywords "arpa" + ## EXAMPLE OUTPUT WITH Keywords ## + Entry: 1.80.19.172.in-addr.arpa, Record Name: 1.80.19.172.in-addr.arpa., Record Type: 12, Data: test.mshome.net, TTL: 598963 + +PARAMETER: -Keywords "arpa,mshome" -MultilineCustomField "ReplaceMeWithAnyMultilineCustomField" + The name of the multiline custom field to save the results to. +.EXAMPLE + -Keywords "arpa,mshome" -MultilineCustomField "ReplaceMeWithAnyMultilineCustomField" + ## EXAMPLE OUTPUT WITH MultilineCustomField ## + Entry: 1.80.19.172.in-addr.arpa, Record Name: 1.80.19.172.in-addr.arpa., Record Type: 12, Data: test.mshome.net, TTL: 598963 + Entry: test.mshome.net, Record Name: test.mshome.net., Record Type: 1, Data: 172.19.80.1, TTL: 598963 + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String[]]$Keywords, + [Parameter()] + [String]$MultilineCustomField +) + +begin { + $ExitCode = 0 + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + # if ($Characters -ge 10000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If we're requested to set the field value for a Ninja document we'll specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # The field below requires additional information to be set # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If an error is received it will have an exception property, the function will exit with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # if (-not $Selection) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # We'll need to set the field differently depending on if its a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency +} +process { + # Get the keywords to search for in the DNS cache + $Keywords = if ($env:keywordsToSearch -and $env:keywordsToSearch -ne "null") { + $env:keywordsToSearch -split "," | ForEach-Object { $_.Trim() } + } + else { + $Keywords -split "," | ForEach-Object { $_.Trim() } + } + $Keywords = if ($Keywords -and $Keywords -ne "null") { + $Keywords | ForEach-Object { + Write-Host "[Info] Searching for DNS Cache Records Matching: $_" + Write-Output "*$_*" + } + } + else { + # Exit if Keywords is empty + Write-Host "[Error] No Keywords Provided" + $ExitCode = 1 + exit $ExitCode + } + # Get the multiline custom field to save the results to + $MultilineCustomField = if ($env:multilineCustomField -and $env:multilineCustomField -ne "null") { + $env:multilineCustomField -split "," | ForEach-Object { $_.Trim() } + } + else { + $MultilineCustomField -split "," | ForEach-Object { $_.Trim() } + } + + Write-Host "" + + # Get the DNS cache entries that match the keywords + $DnsCache = Get-DnsClientCache -Name $Keywords | Select-Object -Property Entry, Name, Type, Data, TimeToLive + + if ($null -eq $DnsCache) { + Write-Host "[Warn] No DNS Cache Entries Found" + } + else { + # Format the DNS cache entries + $Results = $DnsCache | ForEach-Object { + "Entry: $($_.Entry), Record Name: $($_.Name), Record Type: $($_.Type), Data: $($_.Data), TTL: $($_.TimeToLive)" + } + Write-Host "[Info] DNS Cache Entries Found" + # Save the results to a multiline custom field if specified + if ($MultilineCustomField -and $MultilineCustomField -ne "null") { + Write-Host "[Info] Attempting to set Custom Field '$MultilineCustomField'." + try { + # Set-NinjaProperty -Name $MultilineCustomField -Value $($Results | Out-String) # Removed NinjaOne dependency + Write-Host "[Info] Successfully set Custom Field '$MultilineCustomField'!" + } + catch { + Write-Host "[Warn] Failed to set Custom Field '$MultilineCustomField'." + $Results | Out-String | Write-Host + } + } + else { + # Output the results to the Activity Feed + $Results | Out-String | Write-Host + } + } + exit $ExitCode +} +end { + + + +} diff --git a/Powershell Scripts/Search Event Log.ps1 b/Powershell Scripts/Search Event Log.ps1 index 36d8821..6b8714d 100644 --- a/Powershell Scripts/Search Event Log.ps1 +++ b/Powershell Scripts/Search Event Log.ps1 @@ -1,615 +1,615 @@ # Search for specific events in Event Viewer based on the event log they were in, the source of the event, or the specific event IDs used. One of these three options is required for the search. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Search for specific events in Event Viewer based on the event log they were in, the source of the event, or the specific event IDs used. One of these three options is required for the search. -.DESCRIPTION - Search for specific events in Event Viewer based on the event log they were in, the source of the event, or the specific event IDs used. One of these three options is required for the search. -.EXAMPLE - -EventLogName "Application" - - Matching Events Found! - - LogName : Application - ProviderName : Microsoft-Windows-Security-SPP - Id : 16384 - TimeCreated : 4/4/2024 10:00:48 AM - Message : Successfully scheduled Software Protection service for re-start at 2024-04-04T21:19:48Z. Reason: Rul... - - LogName : Application - ProviderName : Microsoft-Windows-Security-SPP - Id : 16394 - TimeCreated : 4/4/2024 10:00:17 AM - Message : Offline downlevel migration succeeded. - - LogName : Application - ProviderName : Microsoft-Windows-Security-SPP - Id : 16384 - TimeCreated : 4/4/2024 9:59:59 AM - Message : Successfully scheduled Software Protection service for re-start at 2024-04-04T21:19:59Z. Reason: Rul... - -PARAMETER: -EventLogName "Application" - Specify the name of the Event Log from which to retrieve events. - -PARAMETER: -EventLogSource "Microsoft-Windows-Kernel-General" - Determines the source of the events to retrieve. - -PARAMETER: -EventLogMessage "Alert" - Filters events by the text contained in the event's message. - -PARAMETER: -EventIDs "12, 13, 6008" - A comma-separated list of event IDs to include in the search. - -PARAMETER: -excludeEventIDs "13" - A comma-separated list of event IDs to exclude from the search. - -PARAMETER: -StartDate "12/24/2021" - Defines the start date and time for the event search. Events logged before this time will not be included in the results. - -PARAMETER: -EndDate "12/29/2021" - Sets the end date and time for the event search. Events logged after this time will not be included. - -PARAMETER: -MultilineCustomField "replaceMeWithAcustomFieldName" - Specify the name of a multiline custom field to optionally store the search results in. Leave blank to not set a multiline field. - -PARAMETER: -WysiwygCustomField "replaceMeWithACustomFieldName" - Specify the name of a WYSIWYG custom field to optionally store the search results in. Leave blank to not set a WYSIWYG field. -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$EventLogName, - [Parameter()] - [String]$EventLogSource, - [Parameter()] - [String]$EventLogMessage, - [Parameter()] - [String]$EventIDs, - [Parameter()] - [String]$ExcludeEventIDs, - [Parameter()] - [datetime]$StartDate, - [Parameter()] - [datetime]$EndDate, - [Parameter()] - [String]$MultilineCustomField, - [Parameter()] - [String]$WysiwygCustomField -) - -begin { - # Set parameters using dynamic script variables. - if ($env:eventLogName -and $env:eventLogName -notlike "null") { $EventLogName = $env:eventLogName } - if ($env:eventLogSource -and $env:eventLogSource -notlike "null") { $EventLogSource = $env:eventLogSource } - if ($env:eventLogMessage -and $env:eventLogMessage -notlike "null") { $EventLogMessage = $env:eventLogMessage } - if ($env:eventIds -and $env:eventIds -notlike "null") { $EventIDs = $env:eventIds } - if ($env:excludeEventIds -and $env:excludeEventIds -notlike "null") { $ExcludeEventIDs = $env:excludeEventIds } - if ($env:eventStart -and $env:eventStart -notlike "null") { $StartDate = $env:eventStart } - if ($env:eventEnd -and $env:eventEnd -notlike "null") { $EndDate = $env:eventEnd } - if ($env:multilineCustomFieldName -and $env:multilineCustomFieldName -notlike "null") { $MultilineCustomField = $env:multilineCustomFieldName } - if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $WysiwygCustomField = $env:wysiwygCustomFieldName } - - # Check if both StartDate and EndDate are provided and if StartDate is earlier than EndDate - if (($StartDate -and $EndDate) -and $StartDate -gt $EndDate) { - Write-Host -Object "[Error] Start date cannot be earlier than end date!" - exit 1 - } - - # Verify that WysiwygField and MultiLineField are not the same, exiting with an error if they are. - if ($WysiwygCustomField -and $MultilineCustomField -and ($WysiwygCustomField -eq $MultilineCustomField)) { - Write-Host -Object "[Error] Wysiwyg Field and Multiline Field are the same! Custom fields cannot be the same type." - Write-Host -Object "https://ninjarmm.zendesk.com/hc/en-us/articles/18601842971789-Custom-Fields-by-Type-and-Functionality" - exit 1 - } - - # Ensure that at least one of Event ID, Event Log Name, or Event Source is provided for the query - if (!$EventIDs -and !$EventLogName -and !$EventLogSource) { - Write-Host -Object "[Error] You must provide either an Event ID, Event Log Name or Event Source." - exit 1 - } - - # Trimming trailing spaces. - if ($EventLogName) { - $EventLogName = $EventLogName.Trim() - } - - # Retrieve and sort all event log names available on the system - $EventLogNamesOnSystem = Get-WinEvent -ListLog * -ErrorAction SilentlyContinue | Sort-Object LogName - # Check if the provided EventLogName exists in the system's event logs - if ($EventLogName -and ($EventLogNamesOnSystem).LogName -notcontains $EventLogName) { - # If not found, print an error message and a list of valid event log names, then exit the script - Write-Host -Object "[Error] Event Log '$EventLogName' doesn't exist! See the list below for valid event log names." - Write-Host -Object "### Valid Event Log Names ###" - $EventLogNamesOnSystem | Select-Object -ExpandProperty LogName | Write-Host - exit 1 - } - - $InvalidEventSourceCharacters = "[\\/<>&`"%\|']" - if ($EventLogSource) { - if ($EventLogSource -match $InvalidEventSourceCharacters) { - Write-Host -Object "[Error] Event Log Source '$EventLogSource' contains an invalid character!" - exit 1 - } - - if ($EventLogSource.Length -gt 255) { - Write-Host -Object "[Error] Event Log Source '$EventLogSource' is too large to be an event source!" - exit 1 - } - - # Trims the event log source for trailing spaces - $EventLogSource = $EventLogSource.Trim() - } - - # Prepare a list to hold valid event IDs to search for - $EventIdsToSearch = New-Object System.Collections.Generic.List[int] - # Process the input event IDs, removing any that are not purely numerical - if ($EventIDs -and $EventIDs -match ",") { - # If multiple event IDs are provided and separated by commas, split them - $EventIDs -split "," | ForEach-Object { - $EventId = $_.Trim() - # Validate each event ID to ensure it's numerical - if ($EventId -match '[a-zA-Z]|\W') { - # If not, print an error and skip adding this ID to the list - Write-Host "[Error] Event ID '$EventId' is not a valid event id. Removing it from the search." - $ExitCode = 1 - return - } - # Check size of event id - if ([long]$EventId -gt 65535 -or [long]$EventId -lt 0) { - Write-Host "[Error] Event ID '$EventId' is not a valid event id. Event ID's must be less than or equal to 65535 and greater than or equal to 0. Removing it from the search." - $ExitCode = 1 - return - } - - # Add the validated event ID to the list - $EventIdsToSearch.Add($EventId) - } - } - elseif ($EventIDs) { - $EventId = $EventIDs.Trim() - # Handle a single event ID input - if ($EventId -match '[a-zA-Z]|\W') { - Write-Host "[Error] Event ID '$EventId' is not a valid event id. Removing it from the search." - $ExitCode = 1 - } - elseif ([long]$EventId -gt 65535 -or [long]$EventId -lt 0) { - Write-Host "[Error] Event ID '$EventId' is not a valid event id. Event ID's must be less than or equal to 65535 and greater than or equal to 0. Removing it from the search." - $ExitCode = 1 - } - else { - $EventIdsToSearch.Add($EventId) - } - } - - # Prepare a list to hold event IDs that should be excluded from the search - $EventsToExclude = New-Object System.Collections.Generic.List[int] - - # Similar process for excluded event IDs as regular event IDs - if ($ExcludeEventIDs -and $ExcludeEventIDs -match ",") { - $ExcludeEventIDs -split "," | ForEach-Object { - $ExcludeEventId = $_.Trim() - if ($ExcludeEventId -match '[a-zA-Z]|\W') { - Write-Host "[Error] Event ID '$ExcludeEventId' is not a valid event id. Removing it from the exclusions." - $ExitCode = 1 - return - } - - if ([long]$ExcludeEventId -gt 65535 -or [long]$ExcludeEventId -lt 0) { - Write-Host "[Error] Event ID '$ExcludeEventId' is not a valid event id. Event ID's must be less than or equal to 65535 and greater than or equal to 0. Removing it from the exclusions." - $ExitCode = 1 - return - } - - $EventsToExclude.Add($ExcludeEventId) - } - } - elseif ($ExcludeEventIDs) { - $ExcludeEventId = $ExcludeEventIDs.Trim() - if ($ExcludeEventId -match '[a-zA-Z]|\W') { - Write-Host "[Error] Event ID '$ExcludeEventId' is not a valid event id. Removing it from the exclusions." - $ExitCode = 1 - } - elseif ([long]$ExcludeEventId -gt 65535 -or [long]$ExcludeEventId -lt 0) { - Write-Host "[Error] Event ID '$ExcludeEventId' is not a valid event id. Event ID's must be less than or equal to 65535 and greater than or equal to 0. Removing it from the exclusions." - $ExitCode = 1 - } - else { - $EventsToExclude.Add($ExcludeEventId) - } - } - - # Check if there are any event IDs to exclude and if the list of event IDs to search for is not empty. - if ($EventsToExclude.Count -gt 0 -and $EventIdsToSearch.Count -gt 0) { - $EventsToExclude | ForEach-Object { - # Check if the current event ID from the exclusion list is also in the list of event IDs to search for. - if ($EventIdsToSearch -contains $_) { - Write-Warning "Event ID $_ has been specified for both inclusion and exclusion. It will be excluded." - } - } - } - - # Check if there's no valid event ID, log name, or log source provided and exit if true - if ($EventIdsToSearch.Count -eq 0 -and !$EventLogName -and !$EventLogSource) { - Write-Host "[Error] No valid Event ID given and no Event Log Name or Event Log Source given." - exit 1 - } - - # Handy function to set a custom field. - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value with $Characters characters is greater than or equal to 200,000 characters.") - } - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to be set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received it will have an exception property, the function will exit with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # Check if the script is running with elevated (Administrator) privileges - if (!(Test-IsElevated)) { - # If not, display an error message and exit with status code 1 - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Prepare a list to hold event log names to search for - $EventLogNamesToSearch = New-Object System.Collections.Generic.List[string] - - # If no event log name was required we'll search all the event - if (!$EventLogName) { - $EventLogNamesOnSystem | Where-Object { $_.RecordCount -gt 0 } | Select-Object -ExpandProperty LogName | ForEach-Object { - $EventLogNamesToSearch.Add($_) - } - } - else { - $EventLogNamesToSearch.Add($EventLogName) - } - - # Create XML object. - [xml]$XML = New-Object System.Xml.XmlDocument - - # Add QueryList element to xml. - $QueryList = $XML.CreateElement("QueryList") - $QueryList = $XML.AppendChild($QueryList) - - # Create query element and nest it under QueryList. - $Query = $XML.CreateElement("Query") - $Query.SetAttribute("Id", "0") - $Query = $QueryList.AppendChild($Query) - - - # Foreach event log to search we're going to create a select element. - $EventLogNamesToSearch | ForEach-Object { - # We'll start each loop by selecting the query element to add to. - $Query = $XML.SelectSingleNode("//Query") - - # The select element starts off with the event log to search. - $Select = $XML.CreateElement("Select") - $Select.SetAttribute("Path", "$_") - - # Reset the inner text between runnings - $XMLInnerText = $Null - - # The inner text of each element (InnerText) will need to be built differently depending on the parameters. - if ($EventLogSource) { - $XMLInnerText = "*[System[Provider[@Name='$EventLogSource']]]" - } - - # If we're given a select number of event id's to search we'll filter them here. - if ($EventIdsToSearch.Count -gt 0) { - $EventIDSearchText = $Null - $EventIdsToSearch | ForEach-Object { - # We may have been given one event id or more than one. - if ($EventIDSearchText) { - $EventIDSearchText = "$EventIDSearchText or EventID=$_" - } - else { - $EventIDSearchText = "EventID=$_" - } - } - - # We'll replace the two ending brackets with our given search text - if ($XMLInnerText) { - $XMLInnerText = $XMLInnerText -replace ']]$', " and ($EventIDSearchText)]]" - } - else { - $XMLInnerText = "*[System[($EventIDSearchText)]]" - } - } - - # If we're also asked to filter based on the date the event was created we'll create the filter text here. - if ($StartDate -or $EndDate) { - $DateFilter = $Null - if ($StartDate) { - $XMLstartDate = Get-Date $StartDate -Format "yyyy-MM-ddTHH:mm:ss" - # PowerShell will convert the < or > symbol for us when we go to save to the xml. - $DateFilter = "@SystemTime>='$XMLstartDate'" - } - - # We may or may not have been given a start date. - if ($EndDate -and $DateFilter) { - $XMLendDate = Get-Date $EndDate -Format "yyyy-MM-ddTHH:mm:ss" - $DateFilter = "$DateFilter and @SystemTime<='$XMLendDate'" - } - elseif ($EndDate) { - $XMLendDate = Get-Date $EndDate -Format "yyyy-MM-ddTHH:mm:ss" - $DateFilter = "@SystemTime<='$XMLendDate'" - } - - # Replace the last two closing brackets and add our filter text. - if($XMLInnerText){ - $XMLInnerText = $XMLInnerText -replace ']]$', " and TimeCreated[$DateFilter]]]" - }else{ - $XMLInnerText = "*[System[TimeCreated[$DateFilter]]]" - } - } - - # If no filters were given (other than the event log name) we'll need to select everything in that log - if(!$XMLInnerText){ - $XMLInnerText = "*" - } - - # Save our filter text to the select statement - $Select.InnerText = $XMLInnerText - - # Append our select statement to our xml file - $Query.AppendChild($Select) | Out-Null - } - - # Search for matching events using the XML filter - $MatchingEvents = Get-WinEvent -FilterXml $XML -ErrorAction SilentlyContinue - - # Exclude events based on the excluded event IDs if any are specified - if ($EventsToExclude.Count -gt 0) { - $MatchingEvents = $MatchingEvents | Where-Object { $EventsToExclude -notcontains $_.ID } - } - - # Exclude events that do not match the keywords you specified. - if ($EventLogMessage) { - $MatchingEvents = $MatchingEvents | Where-Object { $_.Message -like "*$EventLogMessage*" } - } - - # If the event log message is larger than 100 characters trim it and add ... - if ($MatchingEvents) { - $MatchingEvents = $MatchingEvents | Select-Object LevelDisplayName, LogName, ProviderName, Id, TimeCreated, @{ - Name = 'Message' - Expression = { - $Characters = $_.Message | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -gt 100) { - "$(($_.Message).SubString(0,100))(...)" - } - else { - $_.Message - } - } - } - - # Sort the object by newest event to oldest - $MatchingEvents = $MatchingEvents | Sort-Object TimeCreated -Descending - } - - # Set a Wysiwyg custom field if any matching events are found and it was requested. - if ($WysiwygCustomField -and $MatchingEvents) { - try { - Write-Host "Attempting to set Custom Field '$WysiwygCustomField'." - - # Prepare the custom field output. - $CustomFieldValue = New-Object System.Collections.Generic.List[string] - - # Convert the matching events into an html report. - $htmlTable = $MatchingEvents | Select-Object -Property LevelDisplayName, LogName, ProviderName, Id, TimeCreated, Message | ConvertTo-Html -Fragment - - # Set color coding - $htmlTable = $htmlTable -replace "", "" - $htmlTable = $htmlTable -replace "", "" - $htmlTable = $htmlTable -replace "", "" - $htmlTable = $htmlTable -replace "", "" - - # Remove Level Display Name - $LevelDisplayNames = $MatchingEvents | Select-Object -Property LevelDisplayName -Unique - $LevelDisplayNames | ForEach-Object { - $htmlTable = $htmlTable -replace "" - } - $htmlTable = $htmlTable -replace "" - - # Add the newly created html into the custom field output. - $CustomFieldValue.Add($htmlTable) - - # Check that the output complies with the hard character limits. - $Characters = $CustomFieldValue | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 199500) { - Write-Warning "200,000 Character Limit has been reached! Trimming output until the character limit is satisified..." - - # If it doesn't comply with the limits we'll need to recreate it with some adjustments. - $i = 0 - do { - # Recreate the custom field output starting with a warning that we truncated the output. - $CustomFieldValue = New-Object System.Collections.Generic.List[string] - $CustomFieldValue.Add("

This info has been truncated to accommodate the 200,000 character limit.

") - - # The custom field information is sorted from newest to oldest. We'll remove the oldest first by flipping the array upside down. - [array]::Reverse($htmlTable) - # If the next entry is a row we'll delete it. - if ($htmlTable[$i] -match ' + +[CmdletBinding()] +param ( + [Parameter()] + [String]$EventLogName, + [Parameter()] + [String]$EventLogSource, + [Parameter()] + [String]$EventLogMessage, + [Parameter()] + [String]$EventIDs, + [Parameter()] + [String]$ExcludeEventIDs, + [Parameter()] + [datetime]$StartDate, + [Parameter()] + [datetime]$EndDate, + [Parameter()] + [String]$MultilineCustomField, + [Parameter()] + [String]$WysiwygCustomField +) + +begin { + # Set parameters using dynamic script variables. + if ($env:eventLogName -and $env:eventLogName -notlike "null") { $EventLogName = $env:eventLogName } + if ($env:eventLogSource -and $env:eventLogSource -notlike "null") { $EventLogSource = $env:eventLogSource } + if ($env:eventLogMessage -and $env:eventLogMessage -notlike "null") { $EventLogMessage = $env:eventLogMessage } + if ($env:eventIds -and $env:eventIds -notlike "null") { $EventIDs = $env:eventIds } + if ($env:excludeEventIds -and $env:excludeEventIds -notlike "null") { $ExcludeEventIDs = $env:excludeEventIds } + if ($env:eventStart -and $env:eventStart -notlike "null") { $StartDate = $env:eventStart } + if ($env:eventEnd -and $env:eventEnd -notlike "null") { $EndDate = $env:eventEnd } + if ($env:multilineCustomFieldName -and $env:multilineCustomFieldName -notlike "null") { $MultilineCustomField = $env:multilineCustomFieldName } + if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $WysiwygCustomField = $env:wysiwygCustomFieldName } + + # Check if both StartDate and EndDate are provided and if StartDate is earlier than EndDate + if (($StartDate -and $EndDate) -and $StartDate -gt $EndDate) { + Write-Host -Object "[Error] Start date cannot be earlier than end date!" + exit 1 + } + + # Verify that WysiwygField and MultiLineField are not the same, exiting with an error if they are. + if ($WysiwygCustomField -and $MultilineCustomField -and ($WysiwygCustomField -eq $MultilineCustomField)) { + Write-Host -Object "[Error] Wysiwyg Field and Multiline Field are the same! Custom fields cannot be the same type." + Write-Host -Object "https://ninjarmm.zendesk.com/hc/en-us/articles/18601842971789-Custom-Fields-by-Type-and-Functionality" + exit 1 + } + + # Ensure that at least one of Event ID, Event Log Name, or Event Source is provided for the query + if (!$EventIDs -and !$EventLogName -and !$EventLogSource) { + Write-Host -Object "[Error] You must provide either an Event ID, Event Log Name or Event Source." + exit 1 + } + + # Trimming trailing spaces. + if ($EventLogName) { + $EventLogName = $EventLogName.Trim() + } + + # Retrieve and sort all event log names available on the system + $EventLogNamesOnSystem = Get-WinEvent -ListLog * -ErrorAction SilentlyContinue | Sort-Object LogName + # Check if the provided EventLogName exists in the system's event logs + if ($EventLogName -and ($EventLogNamesOnSystem).LogName -notcontains $EventLogName) { + # If not found, print an error message and a list of valid event log names, then exit the script + Write-Host -Object "[Error] Event Log '$EventLogName' doesn't exist! See the list below for valid event log names." + Write-Host -Object "### Valid Event Log Names ###" + $EventLogNamesOnSystem | Select-Object -ExpandProperty LogName | Write-Host + exit 1 + } + + $InvalidEventSourceCharacters = "[\\/<>&`"%\|']" + if ($EventLogSource) { + if ($EventLogSource -match $InvalidEventSourceCharacters) { + Write-Host -Object "[Error] Event Log Source '$EventLogSource' contains an invalid character!" + exit 1 + } + + if ($EventLogSource.Length -gt 255) { + Write-Host -Object "[Error] Event Log Source '$EventLogSource' is too large to be an event source!" + exit 1 + } + + # Trims the event log source for trailing spaces + $EventLogSource = $EventLogSource.Trim() + } + + # Prepare a list to hold valid event IDs to search for + $EventIdsToSearch = New-Object System.Collections.Generic.List[int] + # Process the input event IDs, removing any that are not purely numerical + if ($EventIDs -and $EventIDs -match ",") { + # If multiple event IDs are provided and separated by commas, split them + $EventIDs -split "," | ForEach-Object { + $EventId = $_.Trim() + # Validate each event ID to ensure it's numerical + if ($EventId -match '[a-zA-Z]|\W') { + # If not, print an error and skip adding this ID to the list + Write-Host "[Error] Event ID '$EventId' is not a valid event id. Removing it from the search." + $ExitCode = 1 + return + } + # Check size of event id + if ([long]$EventId -gt 65535 -or [long]$EventId -lt 0) { + Write-Host "[Error] Event ID '$EventId' is not a valid event id. Event ID's must be less than or equal to 65535 and greater than or equal to 0. Removing it from the search." + $ExitCode = 1 + return + } + + # Add the validated event ID to the list + $EventIdsToSearch.Add($EventId) + } + } + elseif ($EventIDs) { + $EventId = $EventIDs.Trim() + # Handle a single event ID input + if ($EventId -match '[a-zA-Z]|\W') { + Write-Host "[Error] Event ID '$EventId' is not a valid event id. Removing it from the search." + $ExitCode = 1 + } + elseif ([long]$EventId -gt 65535 -or [long]$EventId -lt 0) { + Write-Host "[Error] Event ID '$EventId' is not a valid event id. Event ID's must be less than or equal to 65535 and greater than or equal to 0. Removing it from the search." + $ExitCode = 1 + } + else { + $EventIdsToSearch.Add($EventId) + } + } + + # Prepare a list to hold event IDs that should be excluded from the search + $EventsToExclude = New-Object System.Collections.Generic.List[int] + + # Similar process for excluded event IDs as regular event IDs + if ($ExcludeEventIDs -and $ExcludeEventIDs -match ",") { + $ExcludeEventIDs -split "," | ForEach-Object { + $ExcludeEventId = $_.Trim() + if ($ExcludeEventId -match '[a-zA-Z]|\W') { + Write-Host "[Error] Event ID '$ExcludeEventId' is not a valid event id. Removing it from the exclusions." + $ExitCode = 1 + return + } + + if ([long]$ExcludeEventId -gt 65535 -or [long]$ExcludeEventId -lt 0) { + Write-Host "[Error] Event ID '$ExcludeEventId' is not a valid event id. Event ID's must be less than or equal to 65535 and greater than or equal to 0. Removing it from the exclusions." + $ExitCode = 1 + return + } + + $EventsToExclude.Add($ExcludeEventId) + } + } + elseif ($ExcludeEventIDs) { + $ExcludeEventId = $ExcludeEventIDs.Trim() + if ($ExcludeEventId -match '[a-zA-Z]|\W') { + Write-Host "[Error] Event ID '$ExcludeEventId' is not a valid event id. Removing it from the exclusions." + $ExitCode = 1 + } + elseif ([long]$ExcludeEventId -gt 65535 -or [long]$ExcludeEventId -lt 0) { + Write-Host "[Error] Event ID '$ExcludeEventId' is not a valid event id. Event ID's must be less than or equal to 65535 and greater than or equal to 0. Removing it from the exclusions." + $ExitCode = 1 + } + else { + $EventsToExclude.Add($ExcludeEventId) + } + } + + # Check if there are any event IDs to exclude and if the list of event IDs to search for is not empty. + if ($EventsToExclude.Count -gt 0 -and $EventIdsToSearch.Count -gt 0) { + $EventsToExclude | ForEach-Object { + # Check if the current event ID from the exclusion list is also in the list of event IDs to search for. + if ($EventIdsToSearch -contains $_) { + Write-Warning "Event ID $_ has been specified for both inclusion and exclusion. It will be excluded." + } + } + } + + # Check if there's no valid event ID, log name, or log source provided and exit if true + if ($EventIdsToSearch.Count -eq 0 -and !$EventLogName -and !$EventLogSource) { + Write-Host "[Error] No valid Event ID given and no Event Log Name or Event Log Source given." + exit 1 + } + + # Handy function to set a custom field. + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + # if ($Characters -ge 200000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value with $Characters characters is greater than or equal to 200,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If we're requested to set the field value for a Ninja document we'll specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # The field below requires additional information to be set # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If an error is received it will have an exception property, the function will exit with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # if (-not $Selection) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # We'll need to set the field differently depending on if its a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # Check if the script is running with elevated (Administrator) privileges + if (!(Test-IsElevated)) { + # If not, display an error message and exit with status code 1 + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Prepare a list to hold event log names to search for + $EventLogNamesToSearch = New-Object System.Collections.Generic.List[string] + + # If no event log name was required we'll search all the event + if (!$EventLogName) { + $EventLogNamesOnSystem | Where-Object { $_.RecordCount -gt 0 } | Select-Object -ExpandProperty LogName | ForEach-Object { + $EventLogNamesToSearch.Add($_) + } + } + else { + $EventLogNamesToSearch.Add($EventLogName) + } + + # Create XML object. + [xml]$XML = New-Object System.Xml.XmlDocument + + # Add QueryList element to xml. + $QueryList = $XML.CreateElement("QueryList") + $QueryList = $XML.AppendChild($QueryList) + + # Create query element and nest it under QueryList. + $Query = $XML.CreateElement("Query") + $Query.SetAttribute("Id", "0") + $Query = $QueryList.AppendChild($Query) + + + # Foreach event log to search we're going to create a select element. + $EventLogNamesToSearch | ForEach-Object { + # We'll start each loop by selecting the query element to add to. + $Query = $XML.SelectSingleNode("//Query") + + # The select element starts off with the event log to search. + $Select = $XML.CreateElement("Select") + $Select.SetAttribute("Path", "$_") + + # Reset the inner text between runnings + $XMLInnerText = $Null + + # The inner text of each element (InnerText) will need to be built differently depending on the parameters. + if ($EventLogSource) { + $XMLInnerText = "*[System[Provider[@Name='$EventLogSource']]]" + } + + # If we're given a select number of event id's to search we'll filter them here. + if ($EventIdsToSearch.Count -gt 0) { + $EventIDSearchText = $Null + $EventIdsToSearch | ForEach-Object { + # We may have been given one event id or more than one. + if ($EventIDSearchText) { + $EventIDSearchText = "$EventIDSearchText or EventID=$_" + } + else { + $EventIDSearchText = "EventID=$_" + } + } + + # We'll replace the two ending brackets with our given search text + if ($XMLInnerText) { + $XMLInnerText = $XMLInnerText -replace ']]$', " and ($EventIDSearchText)]]" + } + else { + $XMLInnerText = "*[System[($EventIDSearchText)]]" + } + } + + # If we're also asked to filter based on the date the event was created we'll create the filter text here. + if ($StartDate -or $EndDate) { + $DateFilter = $Null + if ($StartDate) { + $XMLstartDate = Get-Date $StartDate -Format "yyyy-MM-ddTHH:mm:ss" + # PowerShell will convert the < or > symbol for us when we go to save to the xml. + $DateFilter = "@SystemTime>='$XMLstartDate'" + } + + # We may or may not have been given a start date. + if ($EndDate -and $DateFilter) { + $XMLendDate = Get-Date $EndDate -Format "yyyy-MM-ddTHH:mm:ss" + $DateFilter = "$DateFilter and @SystemTime<='$XMLendDate'" + } + elseif ($EndDate) { + $XMLendDate = Get-Date $EndDate -Format "yyyy-MM-ddTHH:mm:ss" + $DateFilter = "@SystemTime<='$XMLendDate'" + } + + # Replace the last two closing brackets and add our filter text. + if($XMLInnerText){ + $XMLInnerText = $XMLInnerText -replace ']]$', " and TimeCreated[$DateFilter]]]" + }else{ + $XMLInnerText = "*[System[TimeCreated[$DateFilter]]]" + } + } + + # If no filters were given (other than the event log name) we'll need to select everything in that log + if(!$XMLInnerText){ + $XMLInnerText = "*" + } + + # Save our filter text to the select statement + $Select.InnerText = $XMLInnerText + + # Append our select statement to our xml file + $Query.AppendChild($Select) | Out-Null + } + + # Search for matching events using the XML filter + $MatchingEvents = Get-WinEvent -FilterXml $XML -ErrorAction SilentlyContinue + + # Exclude events based on the excluded event IDs if any are specified + if ($EventsToExclude.Count -gt 0) { + $MatchingEvents = $MatchingEvents | Where-Object { $EventsToExclude -notcontains $_.ID } + } + + # Exclude events that do not match the keywords you specified. + if ($EventLogMessage) { + $MatchingEvents = $MatchingEvents | Where-Object { $_.Message -like "*$EventLogMessage*" } + } + + # If the event log message is larger than 100 characters trim it and add ... + if ($MatchingEvents) { + $MatchingEvents = $MatchingEvents | Select-Object LevelDisplayName, LogName, ProviderName, Id, TimeCreated, @{ + Name = 'Message' + Expression = { + $Characters = $_.Message | Measure-Object -Character | Select-Object -ExpandProperty Characters + if ($Characters -gt 100) { + "$(($_.Message).SubString(0,100))(...)" + } + else { + $_.Message + } + } + } + + # Sort the object by newest event to oldest + $MatchingEvents = $MatchingEvents | Sort-Object TimeCreated -Descending + } + + # Set a Wysiwyg custom field if any matching events are found and it was requested. + if ($WysiwygCustomField -and $MatchingEvents) { + try { + Write-Host "Attempting to set Custom Field '$WysiwygCustomField'." + + # Prepare the custom field output. + $CustomFieldValue = New-Object System.Collections.Generic.List[string] + + # Convert the matching events into an html report. + $htmlTable = $MatchingEvents | Select-Object -Property LevelDisplayName, LogName, ProviderName, Id, TimeCreated, Message | ConvertTo-Html -Fragment + + # Set color coding + $htmlTable = $htmlTable -replace "", "" + $htmlTable = $htmlTable -replace "", "" + $htmlTable = $htmlTable -replace "", "" + $htmlTable = $htmlTable -replace "", "" + + # Remove Level Display Name + $LevelDisplayNames = $MatchingEvents | Select-Object -Property LevelDisplayName -Unique + $LevelDisplayNames | ForEach-Object { + $htmlTable = $htmlTable -replace "" + } + $htmlTable = $htmlTable -replace "" + + # Add the newly created html into the custom field output. + $CustomFieldValue.Add($htmlTable) + + # Check that the output complies with the hard character limits. + $Characters = $CustomFieldValue | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters + if ($Characters -ge 199500) { + Write-Warning "200,000 Character Limit has been reached! Trimming output until the character limit is satisified..." + + # If it doesn't comply with the limits we'll need to recreate it with some adjustments. + $i = 0 + do { + # Recreate the custom field output starting with a warning that we truncated the output. + $CustomFieldValue = New-Object System.Collections.Generic.List[string] + $CustomFieldValue.Add("

This info has been truncated to accommodate the 200,000 character limit.

") + + # The custom field information is sorted from newest to oldest. We'll remove the oldest first by flipping the array upside down. + [array]::Reverse($htmlTable) + # If the next entry is a row we'll delete it. + if ($htmlTable[$i] -match ' - -[CmdletBinding()] -param ( - [Parameter()] - [String]$IpAddress, - [String]$CustomFieldName -) - -begin { - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 10000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") - } - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to be set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received it will have an exception property, the function will exit with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the Date-Time to be in Unix Epoch time so we'll convert it here. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - function Test-IPNetwork { - param([string]$Text) - $Ip, $Prefix = $Text -split '/' - $Ip -as [System.Net.IPAddress] -and - $Prefix -as [int] -and $Prefix -ge 0 -and $Prefix -le 32 - } - function Get-IPNetwork { - [CmdletBinding()] - - Param( - [Parameter(Mandatory, Position = 0)] - [ValidateScript({ $_ -eq ([IPAddress]$_).IPAddressToString })] - [string]$IPAddress, - - [Parameter(Mandatory, Position = 1, ParameterSetName = "SubnetMask")] - [ValidateScript({ $_ -eq ([IPAddress]$_).IPAddressToString })] - [ValidateScript({ - $SMReversed = [IPAddress]$_ - $SMReversed = $SMReversed.GetAddressBytes() - [array]::Reverse($SMReversed) - [IPAddress]$SMReversed = $SMReversed - [convert]::ToString($SMReversed.Address, 2) -match "^[1]*0{0,}$" - })] - [string]$SubnetMask, - - [Parameter(Mandatory, Position = 1, ParameterSetName = "CIDRNotation")] - [ValidateRange(0, 32)] - [int]$PrefixLength, - - [switch]$ReturnAllIPs - ) - - [IPAddress]$IPAddress = $IPAddress - - if ($SubnetMask) { - [IPAddress]$SubnetMask = $SubnetMask - $SMReversed = $SubnetMask.GetAddressBytes() - [array]::Reverse($SMReversed) - [IPAddress]$SMReversed = $SMReversed - - [int]$PrefixLength = [convert]::ToString($SMReversed.Address, 2).replace(0, '').length - } - else { - [IPAddress]$SubnetMask = ([Math]::Pow(2, $PrefixLength) - 1) * [Math]::Pow(2, (32 - $PrefixLength)) - } - - - $FullMask = [UInt32]'0xffffffff' - $WildcardMask = [IPAddress]($SubnetMask.Address -bxor $FullMask) - $NetworkId = [IPAddress]($IPAddress.Address -band $SubnetMask.Address) - $Broadcast = [IPAddress](($FullMask - $NetworkId.Address) -bxor $SubnetMask.Address) - - # Used for determining first usable IP Address - $FirstIPByteArray = $NetworkId.GetAddressBytes() - [Array]::Reverse($FirstIPByteArray) - - # Used for determining last usable IP Address - $LastIPByteArray = $Broadcast.GetAddressBytes() - [Array]::Reverse($LastIPByteArray) - - # Handler for /31, /30 CIDR prefix values, and default for all others. - switch ($PrefixLength) { - 31 { - $TotalIPs = 2 - $UsableIPs = 2 - $FirstIP = $NetworkId - $LastIP = $Broadcast - $FirstIPInt = ([IPAddress]$FirstIPByteArray).Address - $LastIPInt = ([IPAddress]$LastIPByteArray).Address - break - } - - 32 { - $TotalIPs = 1 - $UsableIPs = 1 - $FirstIP = $IPAddress - $LastIP = $IPAddress - $FirstIPInt = ([IPAddress]$FirstIPByteArray).Address - $LastIPInt = ([IPAddress]$LastIPByteArray).Address - break - } - - default { - - # Usable Address Space - $TotalIPs = [Math]::pow(2, (32 - $PrefixLength)) - $UsableIPs = $TotalIPs - 2 - - # First usable IP - $FirstIPInt = ([IPAddress]$FirstIPByteArray).Address + 1 - $FirstIP = [IPAddress]$FirstIPInt - $FirstIP = ($FirstIP).GetAddressBytes() - [Array]::Reverse($FirstIP) - $FirstIP = [IPAddress]$FirstIP - - # Last usable IP - $LastIPInt = ([IPAddress]$LastIPByteArray).Address - 1 - $LastIP = [IPAddress]$LastIPInt - $LastIP = ($LastIP).GetAddressBytes() - [Array]::Reverse($LastIP) - $LastIP = [IPAddress]$LastIP - } - } - - $AllIPs = if ($ReturnAllIPs) { - - if ($UsableIPs -ge 500000) { - Write-Host ('[Warn] Generating an array containing {0:N0} IPs, this may take a little while' -f $UsableIPs) - } - - $CurrentIPInt = $FirstIPInt - - Do { - $IP = [IPAddress]$CurrentIPInt - $IP = ($IP).GetAddressBytes() - [Array]::Reverse($IP) | Out-Null - $IP = ([IPAddress]$IP).IPAddressToString - $IP - - $CurrentIPInt++ - - } While ($CurrentIPInt -le $LastIPInt) - } - - - $obj = [PSCustomObject]@{ - NetworkId = ($NetworkId).IPAddressToString - Broadcast = ($Broadcast).IPAddressToString - SubnetMask = ($SubnetMask).IPAddressToString - PrefixLength = $PrefixLength - WildcardMask = ($WildcardMask).IPAddressToString - FirstIP = ($FirstIP).IPAddressToString - LastIP = ($LastIP).IPAddressToString - TotalIPs = $TotalIPs - UsableIPs = $UsableIPs - AllIPs = $AllIPs - } - - Write-Output $obj - } -} -process { - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - if ($env:ipAddress -and $env:ipAddress -ne 'null') { - $IpAddress = $env:ipAddress - } - if ($env:customFieldName -and $env:customFieldName -ne 'null') { - $CustomFieldName = $env:customFieldName - } - - # Parse the Addresses to check - $Addresses = if ($IpAddress) { - # Validate the IP Address - $IpAddress -split ',' | ForEach-Object { - "$_".Trim() - } | ForEach-Object { - if (($_ -as [System.Net.IPAddress])) { - Write-Host "[Info] Valid IP Address: $_" - [System.Net.IPAddress]::Parse($_) - } - elseif ($(Test-IPNetwork $_)) { - Write-Host "[Info] Valid IP Network: $_" - $Address, $PrefixLength = $_ -split '/' - try { - Get-IPNetwork -IPAddress $Address -PrefixLength $PrefixLength -ReturnAllIPs | Select-Object -ExpandProperty AllIPs - } - catch { - Write-Host "[Error] Invalid IP CIDR: $_" - exit 1 - } - } - else { - Write-Host "[Error] Invalid IP Address: $_" - exit 1 - } - } - } - else { $null } - - # Get the open ports - $FoundAddresses = $( - Get-NetTCPConnection | Select-Object @( - 'LocalAddress' - 'LocalPort' - @{Name = "RemoteAddress"; Expression = { if ($_.RemoteAddress) { $_.RemoteAddress }else { "None" } } } - @{Name = "RemotePort"; Expression = { if ($_.RemotePort) { $_.RemotePort }else { "None" } } } - 'State' - @{Name = "Protocol"; Expression = { "TCP" } } - 'OwningProcess' - @{Name = "Process"; Expression = { (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName } } - ) - Get-NetUDPEndpoint | Select-Object @( - 'LocalAddress' - 'LocalPort' - @{Name = "RemoteAddress"; Expression = { "None" } } - @{Name = "RemotePort"; Expression = { "None" } } - @{Name = "State"; Expression = { "None" } } - @{Name = "Protocol"; Expression = { "UDP" } } - 'OwningProcess' - @{Name = "Process"; Expression = { (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName } } - ) - ) | Where-Object { - $( - <# When Addresses are specified select just those addresses. #> - if ($Addresses) { - $_.LocalAddress -in $Addresses -or - $_.RemoteAddress -in $Addresses - } - else { $true } - ) -and - ( - <# Filter out anything that isn't listening or established. #> - $( - $_.Protocol -eq "TCP" -and - $( - $_.State -eq "Listen" -or - $_.State -eq "Established" - ) - ) -or - <# UDP is stateless, return all UDP connections. #> - $_.Protocol -eq "UDP" - ) - } | Sort-Object LocalAddress, RemoteAddress | Select-Object * -Unique - - if (-not $FoundAddresses -or $FoundAddresses.Count -eq 0) { - Write-Host "[Info] No Addresses were found listening or established with the specified network or address" - } - - # Output the found Addresses - $FoundAddresses | ForEach-Object { - Write-Host "[Alert] Found Local Address: $($_.LocalAddress), Local Port: $($_.LocalPort), Remote Address: $($_.RemoteAddress), Remote Port: $($_.RemotePort), PID: $($_.OwningProcess), Protocol: $($_.Protocol), State: $($_.State), Process: $($_.Process)" - } - # Save the results to a custom field if one was provided - if ($CustomFieldName -and $CustomFieldName -ne 'null') { - try { - Write-Host "[Info] Saving results to custom field: $CustomFieldName" - Set-NinjaProperty -Name $CustomFieldName -Value $( - $FoundAddresses | ForEach-Object { - "Local Address: $($_.LocalAddress), Local Port: $($_.LocalPort), Remote Address: $($_.RemoteAddress), Remote Port: $($_.RemotePort), PID: $($_.OwningProcess), Protocol: $($_.Protocol), State: $($_.State), Process: $($_.Process)" - } | Out-String - ) - Write-Host "[Info] Results saved to custom field: $CustomFieldName" - } - catch { - Write-Host $_.Exception.Message - Write-Host "[Warn] Failed to save results to custom field: $CustomFieldName" - exit 1 - } - } -} -end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + Alert on specified addresses that are Listening or Established and optionally save the results to a custom field. +.DESCRIPTION + Will alert on addresses, regardless if a firewall is blocking them or not. + Checks for addresses that are in a 'Listen' or 'Established' state. + UDP is a stateless protocol and will not have a state. + Outputs the addresses, process ID, state, protocol, local address, and process name. + When a Custom Field is provided this will save the results to that custom field. + +PARAMETER: -IpAddress "192.168.11.1, 192.168.1.1/24" + A comma separated list of IP Addresses to check. Can include IPv4 CIDR notation for ranges. IPv6 CIDR notation not supported. (e.g. 192.168.1.0/24, 10.0.10.12) +.EXAMPLE + -IpAddress "192.168.1.0/24, 10.0.10.12" + ## EXAMPLE OUTPUT WITH IpAddress ## + [Info] Valid IP Address: 192.168.11.1 + [Info] Valid IP Network: 192.168.1.1/24 + [Alert] Found Local Address: 192.168.1.18, Local Port: 139, Remote Address: 0.0.0.0, Remote Port: None, PID: 4, Protocol: TCP, State: Listen, Process: System + [Alert] Found Local Address: 192.168.1.18, Local Port: 138, Remote Address: None, Remote Port: None, PID: 4, Protocol: UDP, State: None, Process: System + [Alert] Found Local Address: 192.168.1.18, Local Port: 137, Remote Address: None, Remote Port: None, PID: 4, Protocol: UDP, State: None, Process: System + +PARAMETER: -CustomField "ReplaceMeWithAnyMultilineCustomField" + Name of the custom field to save the results to. +.EXAMPLE + -IpAddress "192.168.11.1, 192.168.1.1/24" -CustomField "ReplaceMeWithAnyMultilineCustomField" + ## EXAMPLE OUTPUT WITH CustomField ## + [Info] Valid IP Address: 192.168.11.1 + [Info] Valid IP Network: 192.168.1.1/24 + [Alert] Found Local Address: 192.168.1.18, Local Port: 139, Remote Address: 0.0.0.0, Remote Port: None, PID: 4, Protocol: TCP, State: Listen, Process: System + [Alert] Found Local Address: 192.168.1.18, Local Port: 138, Remote Address: None, Remote Port: None, PID: 4, Protocol: UDP, State: None, Process: System + [Alert] Found Local Address: 192.168.1.18, Local Port: 137, Remote Address: None, Remote Port: None, PID: 4, Protocol: UDP, State: None, Process: System + + [Info] Saving results to custom field: ReplaceMeWithAnyMultilineCustomField + [Info] Results saved to custom field: ReplaceMeWithAnyMultilineCustomField +.OUTPUTS + None +.NOTES + Supported Operating Systems: Windows 10/Windows Server 2016 or later with PowerShell 5.1 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$IpAddress, + [String]$CustomFieldName +) + +begin { + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + # if ($Characters -ge 10000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If we're requested to set the field value for a Ninja document we'll specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # The field below requires additional information to be set # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If an error is received it will have an exception property, the function will exit with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the Date-Time to be in Unix Epoch time so we'll convert it here. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # if (-not $Selection) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # We'll need to set the field differently depending on if its a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + function Test-IPNetwork { + param([string]$Text) + $Ip, $Prefix = $Text -split '/' + $Ip -as [System.Net.IPAddress] -and + $Prefix -as [int] -and $Prefix -ge 0 -and $Prefix -le 32 + } + function Get-IPNetwork { + [CmdletBinding()] + + Param( + [Parameter(Mandatory, Position = 0)] + [ValidateScript({ $_ -eq ([IPAddress]$_).IPAddressToString })] + [string]$IPAddress, + + [Parameter(Mandatory, Position = 1, ParameterSetName = "SubnetMask")] + [ValidateScript({ $_ -eq ([IPAddress]$_).IPAddressToString })] + [ValidateScript({ + $SMReversed = [IPAddress]$_ + $SMReversed = $SMReversed.GetAddressBytes() + [array]::Reverse($SMReversed) + [IPAddress]$SMReversed = $SMReversed + [convert]::ToString($SMReversed.Address, 2) -match "^[1]*0{0,}$" + })] + [string]$SubnetMask, + + [Parameter(Mandatory, Position = 1, ParameterSetName = "CIDRNotation")] + [ValidateRange(0, 32)] + [int]$PrefixLength, + + [switch]$ReturnAllIPs + ) + + [IPAddress]$IPAddress = $IPAddress + + if ($SubnetMask) { + [IPAddress]$SubnetMask = $SubnetMask + $SMReversed = $SubnetMask.GetAddressBytes() + [array]::Reverse($SMReversed) + [IPAddress]$SMReversed = $SMReversed + + [int]$PrefixLength = [convert]::ToString($SMReversed.Address, 2).replace(0, '').length + } + else { + [IPAddress]$SubnetMask = ([Math]::Pow(2, $PrefixLength) - 1) * [Math]::Pow(2, (32 - $PrefixLength)) + } + + + $FullMask = [UInt32]'0xffffffff' + $WildcardMask = [IPAddress]($SubnetMask.Address -bxor $FullMask) + $NetworkId = [IPAddress]($IPAddress.Address -band $SubnetMask.Address) + $Broadcast = [IPAddress](($FullMask - $NetworkId.Address) -bxor $SubnetMask.Address) + + # Used for determining first usable IP Address + $FirstIPByteArray = $NetworkId.GetAddressBytes() + [Array]::Reverse($FirstIPByteArray) + + # Used for determining last usable IP Address + $LastIPByteArray = $Broadcast.GetAddressBytes() + [Array]::Reverse($LastIPByteArray) + + # Handler for /31, /30 CIDR prefix values, and default for all others. + switch ($PrefixLength) { + 31 { + $TotalIPs = 2 + $UsableIPs = 2 + $FirstIP = $NetworkId + $LastIP = $Broadcast + $FirstIPInt = ([IPAddress]$FirstIPByteArray).Address + $LastIPInt = ([IPAddress]$LastIPByteArray).Address + break + } + + 32 { + $TotalIPs = 1 + $UsableIPs = 1 + $FirstIP = $IPAddress + $LastIP = $IPAddress + $FirstIPInt = ([IPAddress]$FirstIPByteArray).Address + $LastIPInt = ([IPAddress]$LastIPByteArray).Address + break + } + + default { + + # Usable Address Space + $TotalIPs = [Math]::pow(2, (32 - $PrefixLength)) + $UsableIPs = $TotalIPs - 2 + + # First usable IP + $FirstIPInt = ([IPAddress]$FirstIPByteArray).Address + 1 + $FirstIP = [IPAddress]$FirstIPInt + $FirstIP = ($FirstIP).GetAddressBytes() + [Array]::Reverse($FirstIP) + $FirstIP = [IPAddress]$FirstIP + + # Last usable IP + $LastIPInt = ([IPAddress]$LastIPByteArray).Address - 1 + $LastIP = [IPAddress]$LastIPInt + $LastIP = ($LastIP).GetAddressBytes() + [Array]::Reverse($LastIP) + $LastIP = [IPAddress]$LastIP + } + } + + $AllIPs = if ($ReturnAllIPs) { + + if ($UsableIPs -ge 500000) { + Write-Host ('[Warn] Generating an array containing {0:N0} IPs, this may take a little while' -f $UsableIPs) + } + + $CurrentIPInt = $FirstIPInt + + Do { + $IP = [IPAddress]$CurrentIPInt + $IP = ($IP).GetAddressBytes() + [Array]::Reverse($IP) | Out-Null + $IP = ([IPAddress]$IP).IPAddressToString + $IP + + $CurrentIPInt++ + + } While ($CurrentIPInt -le $LastIPInt) + } + + + $obj = [PSCustomObject]@{ + NetworkId = ($NetworkId).IPAddressToString + Broadcast = ($Broadcast).IPAddressToString + SubnetMask = ($SubnetMask).IPAddressToString + PrefixLength = $PrefixLength + WildcardMask = ($WildcardMask).IPAddressToString + FirstIP = ($FirstIP).IPAddressToString + LastIP = ($LastIP).IPAddressToString + TotalIPs = $TotalIPs + UsableIPs = $UsableIPs + AllIPs = $AllIPs + } + + Write-Output $obj + } +} +process { + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + if ($env:ipAddress -and $env:ipAddress -ne 'null') { + $IpAddress = $env:ipAddress + } + if ($env:customFieldName -and $env:customFieldName -ne 'null') { + $CustomFieldName = $env:customFieldName + } + + # Parse the Addresses to check + $Addresses = if ($IpAddress) { + # Validate the IP Address + $IpAddress -split ',' | ForEach-Object { + "$_".Trim() + } | ForEach-Object { + if (($_ -as [System.Net.IPAddress])) { + Write-Host "[Info] Valid IP Address: $_" + [System.Net.IPAddress]::Parse($_) + } + elseif ($(Test-IPNetwork $_)) { + Write-Host "[Info] Valid IP Network: $_" + $Address, $PrefixLength = $_ -split '/' + try { + Get-IPNetwork -IPAddress $Address -PrefixLength $PrefixLength -ReturnAllIPs | Select-Object -ExpandProperty AllIPs + } + catch { + Write-Host "[Error] Invalid IP CIDR: $_" + exit 1 + } + } + else { + Write-Host "[Error] Invalid IP Address: $_" + exit 1 + } + } + } + else { $null } + + # Get the open ports + $FoundAddresses = $( + Get-NetTCPConnection | Select-Object @( + 'LocalAddress' + 'LocalPort' + @{Name = "RemoteAddress"; Expression = { if ($_.RemoteAddress) { $_.RemoteAddress }else { "None" } } } + @{Name = "RemotePort"; Expression = { if ($_.RemotePort) { $_.RemotePort }else { "None" } } } + 'State' + @{Name = "Protocol"; Expression = { "TCP" } } + 'OwningProcess' + @{Name = "Process"; Expression = { (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName } } + ) + Get-NetUDPEndpoint | Select-Object @( + 'LocalAddress' + 'LocalPort' + @{Name = "RemoteAddress"; Expression = { "None" } } + @{Name = "RemotePort"; Expression = { "None" } } + @{Name = "State"; Expression = { "None" } } + @{Name = "Protocol"; Expression = { "UDP" } } + 'OwningProcess' + @{Name = "Process"; Expression = { (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName } } + ) + ) | Where-Object { + $( + <# When Addresses are specified select just those addresses. #> + if ($Addresses) { + $_.LocalAddress -in $Addresses -or + $_.RemoteAddress -in $Addresses + } + else { $true } + ) -and + ( + <# Filter out anything that isn't listening or established. #> + $( + $_.Protocol -eq "TCP" -and + $( + $_.State -eq "Listen" -or + $_.State -eq "Established" + ) + ) -or + <# UDP is stateless, return all UDP connections. #> + $_.Protocol -eq "UDP" + ) + } | Sort-Object LocalAddress, RemoteAddress | Select-Object * -Unique + + if (-not $FoundAddresses -or $FoundAddresses.Count -eq 0) { + Write-Host "[Info] No Addresses were found listening or established with the specified network or address" + } + + # Output the found Addresses + $FoundAddresses | ForEach-Object { + Write-Host "[Alert] Found Local Address: $($_.LocalAddress), Local Port: $($_.LocalPort), Remote Address: $($_.RemoteAddress), Remote Port: $($_.RemotePort), PID: $($_.OwningProcess), Protocol: $($_.Protocol), State: $($_.State), Process: $($_.Process)" + } + # Save the results to a custom field if one was provided + if ($CustomFieldName -and $CustomFieldName -ne 'null') { + try { + Write-Host "[Info] Saving results to custom field: $CustomFieldName" + # Set-NinjaProperty -Name $CustomFieldName -Value $( # Removed NinjaOne dependency + $FoundAddresses | ForEach-Object { + "Local Address: $($_.LocalAddress), Local Port: $($_.LocalPort), Remote Address: $($_.RemoteAddress), Remote Port: $($_.RemotePort), PID: $($_.OwningProcess), Protocol: $($_.Protocol), State: $($_.State), Process: $($_.Process)" + } | Out-String + ) + Write-Host "[Info] Results saved to custom field: $CustomFieldName" + } + catch { + Write-Host $_.Exception.Message + Write-Host "[Warn] Failed to save results to custom field: $CustomFieldName" + exit 1 + } + } +} +end { + + + +} + diff --git a/Powershell Scripts/Search for Listening and Established Ports.ps1 b/Powershell Scripts/Search for Listening and Established Ports.ps1 index c172050..ee45a14 100644 --- a/Powershell Scripts/Search for Listening and Established Ports.ps1 +++ b/Powershell Scripts/Search for Listening and Established Ports.ps1 @@ -1,256 +1,256 @@ # Alert on specified ports that are Listening or Established and optionally save the results to a custom field. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Alert on specified ports that are Listening or Established and optionally save the results to a custom field. -.DESCRIPTION - Will alert on open ports, regardless if a firewall is blocking them or not. - Checks for open ports that are in a 'Listen' or 'Established' state. - UDP is a stateless protocol and will not have a state. - Outputs the open ports, process ID, state, protocol, local address, and process name. - When a Custom Field is provided this will save the results to that custom field. - -.EXAMPLE - (No Parameters) - ## EXAMPLE OUTPUT WITHOUT PARAMS ## - [Alert] Found open port: 80, PID: 99, State: Listen, Local Address: 0.0.0.0, Process: nginx - [Alert] Found open port: 500, PID: 99, State: Listen, Local Address: 0.0.0.0, Process: nginx - -PARAMETER: -Port "100,200,300-350, 400" - A comma separated list of ports to check. Can include ranges (e.g. 100,200,300-350, 400) -.EXAMPLE - -Port "80,200,300-350, 400" - ## EXAMPLE OUTPUT WITH Port ## - [Alert] Found open port: 80, PID: 99, State: Listen, Local Address: 0.0.0.0, Process: nginx - -PARAMETER: -CustomField "ReplaceMeWithAnyMultilineCustomField" - Name of the custom field to save the results to. -.EXAMPLE - -Port "80,200,300-350, 400" -CustomField "ReplaceMeWithAnyMultilineCustomField" - ## EXAMPLE OUTPUT WITH CustomField ## - [Alert] Found open port: 80, PID: 99, State: Listen, Local Address: 0.0.0.0, Process: nginx - [Info] Saving results to custom field: ReplaceMeWithAnyMultilineCustomField - [Info] Results saved to custom field: ReplaceMeWithAnyMultilineCustomField -.OUTPUTS - None -.NOTES - Supported Operating Systems: Windows 10/Windows Server 2016 or later with PowerShell 5.1 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$PortsToCheck, - [String]$CustomFieldName -) - -begin { - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 10000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") - } - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to be set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received it will have an exception property, the function will exit with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the Date-Time to be in Unix Epoch time so we'll convert it here. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } -} -process { - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - if ($env:portsToCheck -and $env:portsToCheck -ne 'null') { - $PortsToCheck = $env:portsToCheck - } - if ($env:customFieldName -and $env:customFieldName -ne 'null') { - $CustomFieldName = $env:customFieldName - } - - # Remove any whitespace - $PortsToCheck = $PortsToCheck -replace '\s', '' - - # Parse the ports to check - $Ports = if ($PortsToCheck) { - # Split the ports by comma and handle ranges - $PortsToCheck -split ',' | ForEach-Object { - # Trim the whitespace - $Ports = "$_".Trim() - # If the port is a range, expand it - if ($Ports -match '-') { - # Split the range and expand it - $Range = $Ports -split '-' | ForEach-Object { "$_".Trim() } | Where-Object { $_ } - if ($Range.Count -ne 2) { - Write-Host "[Error] Invalid range formatting, must be two number with a dash in between them (eg 1-10): $PortsToCheck" - exit 1 - } - try { - $Range[0]..$Range[1] - } - catch { - Write-Host "[Error] Failed to parse range, must be two number with a dash in between them (eg 1-10): $PortsToCheck" - exit 1 - } - } - else { - $Ports - } - } - } - else { $null } - - if ($($Ports | Where-Object { [int]$_ -gt 65535 })) { - Write-Host "[Error] Can not search for ports above 65535. Must be with in the range of 1 to 65535." - exit 1 - } - - # Get the open ports - $FoundPorts = $( - Get-NetTCPConnection | Select-Object @( - 'LocalAddress' - 'LocalPort' - 'State' - @{Name = "Protocol"; Expression = { "TCP" } } - 'OwningProcess' - @{Name = "Process"; Expression = { (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName } } - ) - Get-NetUDPEndpoint | Select-Object @( - 'LocalAddress' - 'LocalPort' - @{Name = "State"; Expression = { "None" } } - @{Name = "Protocol"; Expression = { "UDP" } } - 'OwningProcess' - @{Name = "Process"; Expression = { (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName } } - ) - ) | Where-Object { - $( - <# When Ports are specified select just those ports. #> - if ($Ports) { $_.LocalPort -in $Ports }else { $true } - ) -and - ( - <# Filter out anything that isn't listening or established. #> - $( - $_.Protocol -eq "TCP" -and - $( - $_.State -eq "Listen" -or - $_.State -eq "Established" - ) - ) -or - <# UDP is stateless, return all UDP connections. #> - $_.Protocol -eq "UDP" - ) - } | Sort-Object LocalPort | Select-Object * -Unique - - if (-not $FoundPorts -or $FoundPorts.Count -eq 0) { - Write-Host "[Info] No ports were found listening or established with the specified: $PortsToCheck" - } - - # Output the found ports - $FoundPorts | ForEach-Object { - Write-Host "[Alert] Found open port: $($_.LocalPort), PID: $($_.OwningProcess), Protocol: $($_.Protocol), State: $($_.State), Local IP: $($_.LocalAddress), Process: $($_.Process)" - } - # Save the results to a custom field if one was provided - if ($CustomFieldName -and $CustomFieldName -ne 'null') { - try { - Write-Host "[Info] Saving results to custom field: $CustomFieldName" - Set-NinjaProperty -Name $CustomFieldName -Value $( - $FoundPorts | ForEach-Object { - "Open port: $($_.LocalPort), PID: $($_.OwningProcess), Protocol: $($_.Protocol), State: $($_.State), Local Address: $($_.LocalAddress), Process: $($_.Process)" - } | Out-String - ) - Write-Host "[Info] Results saved to custom field: $CustomFieldName" - } - catch { - Write-Host $_.Exception.Message - Write-Host "[Warn] Failed to save results to custom field: $CustomFieldName" - exit 1 - } - } -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Alert on specified ports that are Listening or Established and optionally save the results to a custom field. +.DESCRIPTION + Will alert on open ports, regardless if a firewall is blocking them or not. + Checks for open ports that are in a 'Listen' or 'Established' state. + UDP is a stateless protocol and will not have a state. + Outputs the open ports, process ID, state, protocol, local address, and process name. + When a Custom Field is provided this will save the results to that custom field. + +.EXAMPLE + (No Parameters) + ## EXAMPLE OUTPUT WITHOUT PARAMS ## + [Alert] Found open port: 80, PID: 99, State: Listen, Local Address: 0.0.0.0, Process: nginx + [Alert] Found open port: 500, PID: 99, State: Listen, Local Address: 0.0.0.0, Process: nginx + +PARAMETER: -Port "100,200,300-350, 400" + A comma separated list of ports to check. Can include ranges (e.g. 100,200,300-350, 400) +.EXAMPLE + -Port "80,200,300-350, 400" + ## EXAMPLE OUTPUT WITH Port ## + [Alert] Found open port: 80, PID: 99, State: Listen, Local Address: 0.0.0.0, Process: nginx + +PARAMETER: -CustomField "ReplaceMeWithAnyMultilineCustomField" + Name of the custom field to save the results to. +.EXAMPLE + -Port "80,200,300-350, 400" -CustomField "ReplaceMeWithAnyMultilineCustomField" + ## EXAMPLE OUTPUT WITH CustomField ## + [Alert] Found open port: 80, PID: 99, State: Listen, Local Address: 0.0.0.0, Process: nginx + [Info] Saving results to custom field: ReplaceMeWithAnyMultilineCustomField + [Info] Results saved to custom field: ReplaceMeWithAnyMultilineCustomField +.OUTPUTS + None +.NOTES + Supported Operating Systems: Windows 10/Windows Server 2016 or later with PowerShell 5.1 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$PortsToCheck, + [String]$CustomFieldName +) + +begin { + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + # if ($Characters -ge 10000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If we're requested to set the field value for a Ninja document we'll specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # The field below requires additional information to be set # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If an error is received it will have an exception property, the function will exit with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the Date-Time to be in Unix Epoch time so we'll convert it here. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # if (-not $Selection) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # We'll need to set the field differently depending on if its a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency +} +process { + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + if ($env:portsToCheck -and $env:portsToCheck -ne 'null') { + $PortsToCheck = $env:portsToCheck + } + if ($env:customFieldName -and $env:customFieldName -ne 'null') { + $CustomFieldName = $env:customFieldName + } + + # Remove any whitespace + $PortsToCheck = $PortsToCheck -replace '\s', '' + + # Parse the ports to check + $Ports = if ($PortsToCheck) { + # Split the ports by comma and handle ranges + $PortsToCheck -split ',' | ForEach-Object { + # Trim the whitespace + $Ports = "$_".Trim() + # If the port is a range, expand it + if ($Ports -match '-') { + # Split the range and expand it + $Range = $Ports -split '-' | ForEach-Object { "$_".Trim() } | Where-Object { $_ } + if ($Range.Count -ne 2) { + Write-Host "[Error] Invalid range formatting, must be two number with a dash in between them (eg 1-10): $PortsToCheck" + exit 1 + } + try { + $Range[0]..$Range[1] + } + catch { + Write-Host "[Error] Failed to parse range, must be two number with a dash in between them (eg 1-10): $PortsToCheck" + exit 1 + } + } + else { + $Ports + } + } + } + else { $null } + + if ($($Ports | Where-Object { [int]$_ -gt 65535 })) { + Write-Host "[Error] Can not search for ports above 65535. Must be with in the range of 1 to 65535." + exit 1 + } + + # Get the open ports + $FoundPorts = $( + Get-NetTCPConnection | Select-Object @( + 'LocalAddress' + 'LocalPort' + 'State' + @{Name = "Protocol"; Expression = { "TCP" } } + 'OwningProcess' + @{Name = "Process"; Expression = { (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName } } + ) + Get-NetUDPEndpoint | Select-Object @( + 'LocalAddress' + 'LocalPort' + @{Name = "State"; Expression = { "None" } } + @{Name = "Protocol"; Expression = { "UDP" } } + 'OwningProcess' + @{Name = "Process"; Expression = { (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName } } + ) + ) | Where-Object { + $( + <# When Ports are specified select just those ports. #> + if ($Ports) { $_.LocalPort -in $Ports }else { $true } + ) -and + ( + <# Filter out anything that isn't listening or established. #> + $( + $_.Protocol -eq "TCP" -and + $( + $_.State -eq "Listen" -or + $_.State -eq "Established" + ) + ) -or + <# UDP is stateless, return all UDP connections. #> + $_.Protocol -eq "UDP" + ) + } | Sort-Object LocalPort | Select-Object * -Unique + + if (-not $FoundPorts -or $FoundPorts.Count -eq 0) { + Write-Host "[Info] No ports were found listening or established with the specified: $PortsToCheck" + } + + # Output the found ports + $FoundPorts | ForEach-Object { + Write-Host "[Alert] Found open port: $($_.LocalPort), PID: $($_.OwningProcess), Protocol: $($_.Protocol), State: $($_.State), Local IP: $($_.LocalAddress), Process: $($_.Process)" + } + # Save the results to a custom field if one was provided + if ($CustomFieldName -and $CustomFieldName -ne 'null') { + try { + Write-Host "[Info] Saving results to custom field: $CustomFieldName" + # Set-NinjaProperty -Name $CustomFieldName -Value $( # Removed NinjaOne dependency + $FoundPorts | ForEach-Object { + "Open port: $($_.LocalPort), PID: $($_.OwningProcess), Protocol: $($_.Protocol), State: $($_.State), Local Address: $($_.LocalAddress), Process: $($_.Process)" + } | Out-String + ) + Write-Host "[Info] Results saved to custom field: $CustomFieldName" + } + catch { + Write-Host $_.Exception.Message + Write-Host "[Warn] Failed to save results to custom field: $CustomFieldName" + exit 1 + } + } +} +end { + + + +} diff --git a/Powershell Scripts/Set Custom Field if Path Exists.ps1 b/Powershell Scripts/Set Custom Field if Path Exists.ps1 index 43a458b..d41688c 100644 --- a/Powershell Scripts/Set Custom Field if Path Exists.ps1 +++ b/Powershell Scripts/Set Custom Field if Path Exists.ps1 @@ -1,156 +1,156 @@ # Updates a custom field with Yes or No, depending if the path exists or not. -#Requires -Version 3 - -<# -.SYNOPSIS - Updates a custom field with Yes or No, depending if the path exists or not. -.DESCRIPTION - Updates a custom field with Yes or No, depending if the path exists or not. -.EXAMPLE - -Path "C:\Program Files\VideoLAN\VLC\vlc.exe" -CustomField "VLC" - Check if VLC is installed. Set custom field "VLC" to "Yes" if the folder exists or "No" if it doesn't. -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2012 - Release Notes: Updated Calculated Name -#> -[CmdletBinding()] -param ( - # Path to file or folder - [Parameter()][String]$Path, - # THe custom field that we will be updating - [Parameter()][String]$CustomField, - # Text that will be saved to the custom field when file/folder exists - [Parameter(Mandatory = $false)][String]$Exists = "Yes", - # Text that will be saved to the custom field when file/folder does not exist - [Parameter(Mandatory = $false)][String]$NotExist = "No" -) - -begin { - if ($env:filePath) { - $Path = $env:filePath - } - if ($env:CustomField) { - $CustomField = $env:CustomField - } - if ($env:Exists) { - $Exists = $env:Exists - } - if ($env:NotExist) { - $NotExist = $env:NotExist - } - if (-not $Path -and -not $CustomField) { - Write-Host "Path and CustomField Parameters are required." - exit 1 - } - - # This function is to make it easier to set Ninja Custom Fields. - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields we can set. If no type is given we'll assume the input doesn't have to be changed in any way. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The below field requires additional information in order to set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If we received some sort of error it should have an exception property and we'll exit the function with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli is expecting the time to be representing as a Unix Epoch string. So we'll convert what we were given into that format. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw "Value is not present in dropdown" - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } -} -process { - $CustomFieldValue = $( - if ($(Test-Path -Path $Path -ErrorAction SilentlyContinue)) { - Write-Host "The Path $Path Exists!" - $Exists - } - else { - Write-Warning "The Path $Path Does Not Exist!" - $NotExist - } - ) - - try { - Set-NinjaProperty -Name $CustomField -Value $CustomFieldValue - } - catch { - # If we ran into some sort of error we'll output it here. - Write-Error -Message $_.ToString() -Category InvalidOperation -Exception (New-Object System.Exception) - exit 1 - } -} -end { - - - -} - - +#Requires -Version 3 + +<# +.SYNOPSIS + Updates a custom field with Yes or No, depending if the path exists or not. +.DESCRIPTION + Updates a custom field with Yes or No, depending if the path exists or not. +.EXAMPLE + -Path "C:\Program Files\VideoLAN\VLC\vlc.exe" -CustomField "VLC" + Check if VLC is installed. Set custom field "VLC" to "Yes" if the folder exists or "No" if it doesn't. +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2012 + Release Notes: Updated Calculated Name +#> +[CmdletBinding()] +param ( + # Path to file or folder + [Parameter()][String]$Path, + # THe custom field that we will be updating + [Parameter()][String]$CustomField, + # Text that will be saved to the custom field when file/folder exists + [Parameter(Mandatory = $false)][String]$Exists = "Yes", + # Text that will be saved to the custom field when file/folder does not exist + [Parameter(Mandatory = $false)][String]$NotExist = "No" +) + +begin { + if ($env:filePath) { + $Path = $env:filePath + } + if ($env:CustomField) { + $CustomField = $env:CustomField + } + if ($env:Exists) { + $Exists = $env:Exists + } + if ($env:NotExist) { + $NotExist = $env:NotExist + } + if (-not $Path -and -not $CustomField) { + Write-Host "Path and CustomField Parameters are required." + exit 1 + } + + # This function is to make it easier to set Ninja Custom Fields. + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # # If we're requested to set the field value for a Ninja document we'll specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # This is a list of valid fields we can set. If no type is given we'll assume the input doesn't have to be changed in any way. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL" # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # The below field requires additional information in order to set # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If we received some sort of error it should have an exception property and we'll exit the function with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli is expecting the time to be representing as a Unix Epoch string. So we'll convert what we were given into that format. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # if (-not $Selection) { # Removed NinjaOne dependency + # throw "Value is not present in dropdown" # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # We'll need to set the field differently depending on if its a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency +} +process { + $CustomFieldValue = $( + if ($(Test-Path -Path $Path -ErrorAction SilentlyContinue)) { + Write-Host "The Path $Path Exists!" + $Exists + } + else { + Write-Warning "The Path $Path Does Not Exist!" + $NotExist + } + ) + + try { + # Set-NinjaProperty -Name $CustomField -Value $CustomFieldValue # Removed NinjaOne dependency + } + catch { + # If we ran into some sort of error we'll output it here. + Write-Error -Message $_.ToString() -Category InvalidOperation -Exception (New-Object System.Exception) + exit 1 + } +} +end { + + + +} + + diff --git a/Powershell Scripts/Start Microsoft MSERT Scan.ps1 b/Powershell Scripts/Start Microsoft MSERT Scan.ps1 index 6a403b6..b6f752a 100644 --- a/Powershell Scripts/Start Microsoft MSERT Scan.ps1 +++ b/Powershell Scripts/Start Microsoft MSERT Scan.ps1 @@ -1,411 +1,411 @@ # Run the Microsoft Safety Scanner, collect the results, and optionally save the results to a multiline custom field. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Run the Microsoft Safety Scanner, collect the results, and optionally save the results to a multiline custom field. -.DESCRIPTION - Run the Microsoft Safety Scanner, collect the results, and optionally save the results to a multiline custom field. -.EXAMPLE - (No Parameters) - - Downloading MSERT from https://go.microsoft.com/fwlink/?LinkId=212732 - Waiting for 3 seconds. - Download Attempt 1 - Download Successful! - Initiating Scan - Exit Code: 7 - [Critical] Infections found! - - --------------------------------------------------------------------------------------- - Microsoft Safety Scanner v1.405, (build 1.405.445.0) - Started On Thu Feb 22 13:33:34 2024 - - Engine: 1.1.24010.10 - Signatures: 1.405.445.0 - MpGear: 1.1.16330.1 - Run Mode: Scan Run in Quiet Mode - - Quick Scan Results: - ------------------- - Threat Detected: Virus:DOS/EICAR_Test_File, not removed. - Action: NoAction, Result: 0x00000000 - file://C:\Windows\system32\eicarcom2.zip->eicar_com.zip->eicar.com - SigSeq: 0x00000555DC2DDDB0 - file://C:\Windows\system32\eicar.com - SigSeq: 0x00000555DC2DDDB0 - file://C:\Windows\eicar.com - SigSeq: 0x00000555DC2DDDB0 - containerfile://C:\Windows\system32\eicarcom2.zip - - Results Summary: - ---------------- - Found Virus:DOS/EICAR_Test_File, not removed. - Successfully Submitted MAPS Report - Successfully Submitted Heartbeat Report - Microsoft Safety Scanner Finished On Thu Feb 22 13:35:58 2024 - - - Return code: 7 (0x7) - -PARAMETER: -ScanType "Full" - Specifies the type of scan to perform. "Full" for a complete disk scan, or "Quick" for a scan of common exploit locations. - -PARAMETER: -Timeout "ReplaceMeWithANumber" - Sets a time limit for the scan in minutes. If the scan exceeds this duration, it is canceled, and an error is output. Replace "ReplaceMeWithANumber" with the desired time limit in minutes. - -PARAMETER: -CustomField "ReplaceWithNameOfCustomField" - Specifies the name of the multiline custom field where scan results are optionally saved. Enter the field name to enable this feature. -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 10, Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$ScanType = "Quick", - [Parameter()] - [Int]$Timeout = 30, - [Parameter()] - [String]$CustomField, - [Parameter()] - [String]$DownloadURL = "https://go.microsoft.com/fwlink/?LinkId=212732" -) - -begin { - # Set parameters using dynamic script variables. - if($env:scanType -and $env:scanType -notlike "null"){ $ScanType = $env:scanType } - if($env:scanTimeoutInMinutes -and $env:scanTimeoutInMinutes -notlike "null"){ $Timeout = $env:scanTimeoutInMinutes } - if($env:customFieldName -and $env:customFieldName -notlike "null"){ $CustomField = $env:customFieldName } - - # If a timeout is specified, check that it's in the valid range. - if($Timeout -lt 1 -or $Timeout -ge 120){ - Write-Host "[Error] Timeout must be greater than or equal to 1 minute and less than 120 minutes." - exit 1 - } - - # If we're not given a scan type, error out. - if(-not $ScanType){ - Write-Host "[Error] Please select a scan type (Quick or Full)." - exit 1 - } - - # Check that the scan type is valid. - switch($ScanType){ - "Quick" { Write-Verbose "Quick Scan Selected!"} - "Full" { Write-Verbose "Full Scan Selected!" } - default { - Write-Host "[Error] Invalid scan type selected!" - exit 1 - } - } - - # Checks for local administrator rights. - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - # Utility function for downloading files. - function Invoke-Download { - param( - [Parameter()] - [String]$URL, - [Parameter()] - [String]$Path, - [Parameter()] - [int]$Attempts = 3, - [Parameter()] - [Switch]$SkipSleep - ) - - $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType') - if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12 - } - elseif ( $SupportedTLSversions -contains 'Tls12' ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 - } - else { - # Not everything requires TLS 1.2, but we'll try anyway. - Write-Warning "TLS 1.2 and or TLS 1.3 are not supported on this system. This download may fail!" - if ($PSVersionTable.PSVersion.Major -lt 3) { - Write-Warning "PowerShell 2 / .NET 2.0 doesn't support TLS 1.2." - } - } - - $i = 1 - While ($i -le $Attempts) { - # Some cloud services have rate-limiting - if (-not ($SkipSleep)) { - $SleepTime = Get-Random -Minimum 3 -Maximum 15 - Write-Host "Waiting for $SleepTime seconds." - Start-Sleep -Seconds $SleepTime - } - - if ($i -ne 1) { Write-Host "" } - Write-Host "Download Attempt $i" - - $PreviousProgressPreference = $ProgressPreference - $ProgressPreference = 'SilentlyContinue' - try { - # Invoke-WebRequest is preferred because it supports links that redirect, e.g., https://t.ly - if ($PSVersionTable.PSVersion.Major -lt 4) { - # Downloads the file - $WebClient = New-Object System.Net.WebClient - $WebClient.DownloadFile($URL, $Path) - } - else { - # Standard options - $WebRequestArgs = @{ - Uri = $URL - OutFile = $Path - MaximumRedirection = 10 - UseBasicParsing = $true - } - - # Downloads the file - Invoke-WebRequest @WebRequestArgs - } - - $ProgressPreference = $PreviousProgressPreference - $File = Test-Path -Path $Path -ErrorAction SilentlyContinue - } - catch { - Write-Warning "An error has occurred while downloading!" - Write-Warning $_.Exception.Message - - if (Test-Path -Path $Path -ErrorAction SilentlyContinue) { - Remove-Item $Path -Force -Confirm:$false -ErrorAction SilentlyContinue - } - - $File = $False - } - - if ($File) { - $i = $Attempts - } - else { - Write-Warning "File failed to download." - Write-Host "" - } - - $i++ - } - - if (-not (Test-Path -Path $Path)) { - [PSCustomObject]@{ - ExitCode = 1 - } - } - else { - [PSCustomObject]@{ - ExitCode = 0 - } - } - } - - # Utility function to help set custom fields - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters - if($Characters -ge 10000){ - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") - } - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to be set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received it will have an exception property, the function will exit with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the Date-Time to be in Unix Epoch time so we'll convert it here. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - - $ExitCode = 0 - - # If the log file already exists remove it. - if(Test-Path -Path "$env:SYSTEMROOT\debug\msert.log"){ - Remove-Item -Path "$env:SYSTEMROOT\debug\msert.log" -Force -ErrorAction SilentlyContinue - } -} -process { - # Error out if we don't have local admin permissions. - if (-not (Test-IsElevated)) { - Write-Host "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Download MSERT. - Write-Host "Downloading MSERT from $DownloadURL" - $MSERTPath = "$env:TEMP\MSERT.exe" - $Download = Invoke-Download -Path $MSERTPath -URL $DownloadURL - if($Download.ExitCode -ne 0){ - Write-Host "[Error] Failed to download MSERT please check that $DownloadURL is reachable!" - exit 1 - } - - Write-Host "Download Successful!" - - # Start the MSERT Scan with the parameters given. - Write-Host "Initiating Scan" - $Arguments = New-Object System.Collections.Generic.List[string] - if($ScanType -eq "Full"){ - $Arguments.Add("/F") - } - $Arguments.Add("/Q") - $Arguments.Add("/N") - - try{ - # Run it with our specified timeout. - $TimeoutInSeconds = $Timeout * 60 - $MSERTProcess = Start-Process -FilePath $MSERTPath -ArgumentList $Arguments -NoNewWindow -PassThru - $MSERTProcess | Wait-Process -Timeout $TimeoutInSeconds -ErrorAction Stop - }catch{ - Write-Host "[Alert] The Microsoft Safety Scanner exceeded the specified timeout of $Timeout minutes, and the script is now terminating." - $MSERTProcess | Stop-Process -Force - $TimedOut = $True - $ExitCode = 1 - } - Write-Host "Exit Code: $($MSERTProcess.ExitCode)" - - # If the report is missing, something has clearly gone wrong. - if(-not (Test-Path -Path $env:SYSTEMROOT\debug\msert.log)){ - Write-Host "[Error] The report from MSERT.exe is missing?" - exit 1 - } - - # Get the contents of the MSERT log and error out if it's blank. - $Report = Get-Content -Path "$env:SYSTEMROOT\debug\msert.log" - if(-not $Report){ - Write-Host "[Error] The report from MSERT.exe is empty?" - exit 1 - } - - # If threats are detected, send out the alert. - $Report | ForEach-Object { - if($_ -match "No infection found"){ - $NoInfectionFoundTextPresent = $True - } - - if($_ -match "Threat Detected" ){ - $ThreatDetectedTextPresent = $True - } - } - - - if(($ThreatDetectedTextPresent -or -not $NoInfectionFoundTextPresent) -and -not $TimedOut){ - Write-Host "[Critical] Infections found!" - }elseif($ExitCode -ne 1 -and -not $TimedOut){ - Write-Host "[Success] Scan has completed no infections detected." - } - - # Save to a custom field upon request. - if($CustomField){ - try { - Write-Host "Attempting to set Custom Field '$CustomField'." - Set-NinjaProperty -Name $CustomField -Value ($Report | Out-String) - Write-Host "Successfully set Custom Field '$CustomField'!" - } - catch { - if($_.Exception.Message){ - Write-Host "[Error] $($_.Exception.Message)" - } - - if($_.Message){ - Write-Host "[Error] $($_.Message)" - } - - $ExitCode = 1 - } - } - - # Send out the report to the activity log. - $Report | Write-Host - - # Remove the old log file. - if(Test-Path -Path "$env:SYSTEMROOT\debug\msert.log"){ - Remove-Item -Path "$env:SYSTEMROOT\debug\msert.log" -Force -ErrorAction SilentlyContinue - } - - # Exit. - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Run the Microsoft Safety Scanner, collect the results, and optionally save the results to a multiline custom field. +.DESCRIPTION + Run the Microsoft Safety Scanner, collect the results, and optionally save the results to a multiline custom field. +.EXAMPLE + (No Parameters) + + Downloading MSERT from https://go.microsoft.com/fwlink/?LinkId=212732 + Waiting for 3 seconds. + Download Attempt 1 + Download Successful! + Initiating Scan + Exit Code: 7 + [Critical] Infections found! + + --------------------------------------------------------------------------------------- + Microsoft Safety Scanner v1.405, (build 1.405.445.0) + Started On Thu Feb 22 13:33:34 2024 + + Engine: 1.1.24010.10 + Signatures: 1.405.445.0 + MpGear: 1.1.16330.1 + Run Mode: Scan Run in Quiet Mode + + Quick Scan Results: + ------------------- + Threat Detected: Virus:DOS/EICAR_Test_File, not removed. + Action: NoAction, Result: 0x00000000 + file://C:\Windows\system32\eicarcom2.zip->eicar_com.zip->eicar.com + SigSeq: 0x00000555DC2DDDB0 + file://C:\Windows\system32\eicar.com + SigSeq: 0x00000555DC2DDDB0 + file://C:\Windows\eicar.com + SigSeq: 0x00000555DC2DDDB0 + containerfile://C:\Windows\system32\eicarcom2.zip + + Results Summary: + ---------------- + Found Virus:DOS/EICAR_Test_File, not removed. + Successfully Submitted MAPS Report + Successfully Submitted Heartbeat Report + Microsoft Safety Scanner Finished On Thu Feb 22 13:35:58 2024 + + + Return code: 7 (0x7) + +PARAMETER: -ScanType "Full" + Specifies the type of scan to perform. "Full" for a complete disk scan, or "Quick" for a scan of common exploit locations. + +PARAMETER: -Timeout "ReplaceMeWithANumber" + Sets a time limit for the scan in minutes. If the scan exceeds this duration, it is canceled, and an error is output. Replace "ReplaceMeWithANumber" with the desired time limit in minutes. + +PARAMETER: -CustomField "ReplaceWithNameOfCustomField" + Specifies the name of the multiline custom field where scan results are optionally saved. Enter the field name to enable this feature. +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 10, Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$ScanType = "Quick", + [Parameter()] + [Int]$Timeout = 30, + [Parameter()] + [String]$CustomField, + [Parameter()] + [String]$DownloadURL = "https://go.microsoft.com/fwlink/?LinkId=212732" +) + +begin { + # Set parameters using dynamic script variables. + if($env:scanType -and $env:scanType -notlike "null"){ $ScanType = $env:scanType } + if($env:scanTimeoutInMinutes -and $env:scanTimeoutInMinutes -notlike "null"){ $Timeout = $env:scanTimeoutInMinutes } + if($env:customFieldName -and $env:customFieldName -notlike "null"){ $CustomField = $env:customFieldName } + + # If a timeout is specified, check that it's in the valid range. + if($Timeout -lt 1 -or $Timeout -ge 120){ + Write-Host "[Error] Timeout must be greater than or equal to 1 minute and less than 120 minutes." + exit 1 + } + + # If we're not given a scan type, error out. + if(-not $ScanType){ + Write-Host "[Error] Please select a scan type (Quick or Full)." + exit 1 + } + + # Check that the scan type is valid. + switch($ScanType){ + "Quick" { Write-Verbose "Quick Scan Selected!"} + "Full" { Write-Verbose "Full Scan Selected!" } + default { + Write-Host "[Error] Invalid scan type selected!" + exit 1 + } + } + + # Checks for local administrator rights. + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + # Utility function for downloading files. + function Invoke-Download { + param( + [Parameter()] + [String]$URL, + [Parameter()] + [String]$Path, + [Parameter()] + [int]$Attempts = 3, + [Parameter()] + [Switch]$SkipSleep + ) + + $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType') + if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12 + } + elseif ( $SupportedTLSversions -contains 'Tls12' ) { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 + } + else { + # Not everything requires TLS 1.2, but we'll try anyway. + Write-Warning "TLS 1.2 and or TLS 1.3 are not supported on this system. This download may fail!" + if ($PSVersionTable.PSVersion.Major -lt 3) { + Write-Warning "PowerShell 2 / .NET 2.0 doesn't support TLS 1.2." + } + } + + $i = 1 + While ($i -le $Attempts) { + # Some cloud services have rate-limiting + if (-not ($SkipSleep)) { + $SleepTime = Get-Random -Minimum 3 -Maximum 15 + Write-Host "Waiting for $SleepTime seconds." + Start-Sleep -Seconds $SleepTime + } + + if ($i -ne 1) { Write-Host "" } + Write-Host "Download Attempt $i" + + $PreviousProgressPreference = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + try { + # Invoke-WebRequest is preferred because it supports links that redirect, e.g., https://t.ly + if ($PSVersionTable.PSVersion.Major -lt 4) { + # Downloads the file + $WebClient = New-Object System.Net.WebClient + $WebClient.DownloadFile($URL, $Path) + } + else { + # Standard options + $WebRequestArgs = @{ + Uri = $URL + OutFile = $Path + MaximumRedirection = 10 + UseBasicParsing = $true + } + + # Downloads the file + Invoke-WebRequest @WebRequestArgs + } + + $ProgressPreference = $PreviousProgressPreference + $File = Test-Path -Path $Path -ErrorAction SilentlyContinue + } + catch { + Write-Warning "An error has occurred while downloading!" + Write-Warning $_.Exception.Message + + if (Test-Path -Path $Path -ErrorAction SilentlyContinue) { + Remove-Item $Path -Force -Confirm:$false -ErrorAction SilentlyContinue + } + + $File = $False + } + + if ($File) { + $i = $Attempts + } + else { + Write-Warning "File failed to download." + Write-Host "" + } + + $i++ + } + + if (-not (Test-Path -Path $Path)) { + [PSCustomObject]@{ + ExitCode = 1 + } + } + else { + [PSCustomObject]@{ + ExitCode = 0 + } + } + } + + # Utility function to help set custom fields + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + # if($Characters -ge 10000){ # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If we're requested to set the field value for a Ninja document we'll specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # The field below requires additional information to be set # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If an error is received it will have an exception property, the function will exit with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the Date-Time to be in Unix Epoch time so we'll convert it here. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # if (-not $Selection) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # We'll need to set the field differently depending on if its a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + $ExitCode = 0 + + # If the log file already exists remove it. + if(Test-Path -Path "$env:SYSTEMROOT\debug\msert.log"){ + Remove-Item -Path "$env:SYSTEMROOT\debug\msert.log" -Force -ErrorAction SilentlyContinue + } +} +process { + # Error out if we don't have local admin permissions. + if (-not (Test-IsElevated)) { + Write-Host "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Download MSERT. + Write-Host "Downloading MSERT from $DownloadURL" + $MSERTPath = "$env:TEMP\MSERT.exe" + $Download = Invoke-Download -Path $MSERTPath -URL $DownloadURL + if($Download.ExitCode -ne 0){ + Write-Host "[Error] Failed to download MSERT please check that $DownloadURL is reachable!" + exit 1 + } + + Write-Host "Download Successful!" + + # Start the MSERT Scan with the parameters given. + Write-Host "Initiating Scan" + $Arguments = New-Object System.Collections.Generic.List[string] + if($ScanType -eq "Full"){ + $Arguments.Add("/F") + } + $Arguments.Add("/Q") + $Arguments.Add("/N") + + try{ + # Run it with our specified timeout. + $TimeoutInSeconds = $Timeout * 60 + $MSERTProcess = Start-Process -FilePath $MSERTPath -ArgumentList $Arguments -NoNewWindow -PassThru + $MSERTProcess | Wait-Process -Timeout $TimeoutInSeconds -ErrorAction Stop + }catch{ + Write-Host "[Alert] The Microsoft Safety Scanner exceeded the specified timeout of $Timeout minutes, and the script is now terminating." + $MSERTProcess | Stop-Process -Force + $TimedOut = $True + $ExitCode = 1 + } + Write-Host "Exit Code: $($MSERTProcess.ExitCode)" + + # If the report is missing, something has clearly gone wrong. + if(-not (Test-Path -Path $env:SYSTEMROOT\debug\msert.log)){ + Write-Host "[Error] The report from MSERT.exe is missing?" + exit 1 + } + + # Get the contents of the MSERT log and error out if it's blank. + $Report = Get-Content -Path "$env:SYSTEMROOT\debug\msert.log" + if(-not $Report){ + Write-Host "[Error] The report from MSERT.exe is empty?" + exit 1 + } + + # If threats are detected, send out the alert. + $Report | ForEach-Object { + if($_ -match "No infection found"){ + $NoInfectionFoundTextPresent = $True + } + + if($_ -match "Threat Detected" ){ + $ThreatDetectedTextPresent = $True + } + } + + + if(($ThreatDetectedTextPresent -or -not $NoInfectionFoundTextPresent) -and -not $TimedOut){ + Write-Host "[Critical] Infections found!" + }elseif($ExitCode -ne 1 -and -not $TimedOut){ + Write-Host "[Success] Scan has completed no infections detected." + } + + # Save to a custom field upon request. + if($CustomField){ + try { + Write-Host "Attempting to set Custom Field '$CustomField'." + # Set-NinjaProperty -Name $CustomField -Value ($Report | Out-String) # Removed NinjaOne dependency + Write-Host "Successfully set Custom Field '$CustomField'!" + } + catch { + if($_.Exception.Message){ + Write-Host "[Error] $($_.Exception.Message)" + } + + if($_.Message){ + Write-Host "[Error] $($_.Message)" + } + + $ExitCode = 1 + } + } + + # Send out the report to the activity log. + $Report | Write-Host + + # Remove the old log file. + if(Test-Path -Path "$env:SYSTEMROOT\debug\msert.log"){ + Remove-Item -Path "$env:SYSTEMROOT\debug\msert.log" -Force -ErrorAction SilentlyContinue + } + + # Exit. + exit $ExitCode +} +end { + + + +} diff --git a/Powershell Scripts/Startup Audit Using Autorunsc.ps1 b/Powershell Scripts/Startup Audit Using Autorunsc.ps1 index ebb94de..63e6f10 100644 --- a/Powershell Scripts/Startup Audit Using Autorunsc.ps1 +++ b/Powershell Scripts/Startup Audit Using Autorunsc.ps1 @@ -1,531 +1,531 @@ # Runs Autorunsc with your selected options and outputs the results to the activity log and optionally a WYSIWYG custom field. Please note that there is a limit to the number of results that can be set in Custom Fields or viewed in the Activity Log. -#Requires -Version 4 - -<# -.SYNOPSIS - Runs Autorunsc with your selected options and outputs the results to the activity log and optionally a WYSIWYG custom field. Please note that there is a limit to the number of results that can be set in Custom Fields or viewed in the Activity Log. -.DESCRIPTION - Runs Autorunsc with your selected options and outputs the results to the activity log and optionally a WYSIWYG custom field. Please note that there is a limit to the number of results that can be set in Custom Fields or viewed in the Activity Log. -.EXAMPLE - (No Parameters) - URL Given, Downloading the file... - Download Attempt 1 - HKCU:\SOFTWARE\Sysinternals\AutoRuns\EulaAccepted changed from 1 to 1 - - Sysinternals Autoruns v14.10 - Autostart program viewer - Copyright (C) 2002-2023 Mark Russinovich - Sysinternals - www.sysinternals.com - - WARNING: Script must be elevated in order to write to custom field. - - Entry : C:\Windows\system32\userinit.exe - Entry Location : HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Userinit - Image Path : c:\windows\system32\userinit.exe - Signer : (Verified) Microsoft Windows - MD5 : 9C4C281156040CF01EA35D759092F540 - - Entry : cmd.exe - Entry Location : HKLM\SYSTEM\CurrentControlSet\Control\SafeBoot\AlternateShell - Image Path : c:\windows\system32\cmd.exe - Signer : (Verified) Microsoft Windows - MD5 : 8A2122E8162DBEF04694B9C3E0B6CDEE - -PARAMETER: -CustomField "ReplaceWithAMultilineCustomField" - The name of the multiline custom field you would like to save the results to. - -PARAMETER: -Startup - Applications or scripts configured to run automatically after a user logs into their account. This is the default option for Autoruns. - E.g. Applications in the 'Startup' folder. - -PARAMETER: -Boot - Programs or commands that are set to execute during the system's boot-up sequence before a user logs in. - -PARAMETER: -WinLogon - Items that are configured to run during the Windows logon process. Often these items are critical to the logon UI. - -PARAMETER: -AppInit - DLLs that are automatically loaded by every process that calls the User32.dll file (anything with a GUI). - -PARAMETER: -Explorer - Plugins or extensions that integrate into the Windows Explorer shell. - -PARAMETER: -Sidebar - Mini applications or gadgets that load into the desktop sidebar in earlier versions of Windows (introduced in Windows Vista). - -PARAMETER: -ImageHijacks - Registry modifications that redirect the execution of specific executable files to a different program. - -PARAMETER: -IEAddons - Browser extensions or toolbars that Internet Explorer will load automatically when it starts. - -PARAMETER: -KnownDLLs - Crucial system DLLs that Windows will load into memory at startup. - -PARAMETER: -WMIentries - Entries related to WMI scripts or providers that are set to execute automatically. - -PARAMETER: -WinSockProtocols - Modules or services meant to load up with the Windows network stack. - -PARAMETER: -Codecs - Software components meant to be used for encoding or decoding digital media streams (often set to run at system startup). - -PARAMETER: -PrinterMonitor - DLL's associated with printer drivers. - -PARAMETER: -LSAProviders - Plugins that integrate with the Local Security Authority subsystem. - -PARAMETER: -Services - Windows Services set to start Automatically. - -PARAMETER: -ScheduledTasks - These are tasks set in Task Scheduler to do something automatically at a specified interval. - -PARAMETER: -HideMicrosoftEntries - Hides Signed Microsoft Entries from the results. - -PARAMETER: -DestinationFolder - By default this script downloads autorunsc to the temp folder. - -PARAMETER: -DownloadUrl - URL to download Autoruns from. - -PARAMETER: -SkipSleep - Skips sleeping prior to downloading autorunsc. -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 10, Server 2012 - Release Notes: Added support for WYSIWYG new character limit; now truncates results if results are too long. -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$CustomField, - [Parameter()] - [Switch]$Startup = [System.Convert]::ToBoolean($env:checkLogonStartupEntries), - [Parameter()] - [Switch]$Boot = [System.Convert]::ToBoolean($env:checkBootEntries), - [Parameter()] - [Switch]$WinLogon = [System.Convert]::ToBoolean($env:checkWinlogonEntries), - [Parameter()] - [Switch]$AppInit = [System.Convert]::ToBoolean($env:checkAppinitEntries), - [Parameter()] - [Switch]$Explorer = [System.Convert]::ToBoolean($env:checkExplorerAddons), - [Parameter()] - [Switch]$Sidebar = [System.Convert]::ToBoolean($env:checkSidebarGadgets), - [Parameter()] - [Switch]$ImageHijacks = [System.Convert]::ToBoolean($env:checkImageHijacks), - [Parameter()] - [Switch]$IEAddons = [System.Convert]::ToBoolean($env:checkInternetExplorerAddons), - [Parameter()] - [Switch]$KnownDLLs = [System.Convert]::ToBoolean($env:checkKnownDlls), - [Parameter()] - [Switch]$WMIentries = [System.Convert]::ToBoolean($env:checkWmiEntries), - [Parameter()] - [Switch]$WinSockProtocols = [System.Convert]::ToBoolean($env:checkWinsockProtocol), - [Parameter()] - [Switch]$Codecs = [System.Convert]::ToBoolean($env:checkCodecs), - [Parameter()] - [Switch]$PrinterMonitor = [System.Convert]::ToBoolean($env:checkPrinterMonitorDlls), - [Parameter()] - [Switch]$LSAProviders = [System.Convert]::ToBoolean($env:checkLsaSecurityProviders), - [Parameter()] - [Switch]$Services = [System.Convert]::ToBoolean($env:checkAutostartServices), - [Parameter()] - [Switch]$ScheduledTasks = [System.Convert]::ToBoolean($env:checkScheduledTasks), - [Parameter()] - [Switch]$HideMicrosoftEntries = [System.Convert]::ToBoolean($env:hideMicrosoftEntries), - [Parameter()] - [String]$DestinationFolder = "$env:Temp", - [Parameter()] - [String]$DownloadUrl = "https://download.sysinternals.com/files/Autoruns.zip", - [Parameter()] - [Switch]$SkipSleep = [System.Convert]::ToBoolean($env:skipSleep) -) - -begin { - - # If Script Forms are used replace the parameters - if ($env:destinationFolder -and $env:DestinationFolder -notlike "null") { $DestinationFolder = $env:destinationFolder } - if ($env:downloadUrl -and $env:downloadUrl -notlike "null") { $DownloadUrl = $env:downloadUrl } - if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } - - if ($PSVersionTable.PSVersion.Major -lt 5) { - function Expand-Archive { - [CmdletBinding()] - param( - [Parameter()] - [String]$Path, - [Parameter()] - [String]$DestinationPath, - [Parameter()] - [Switch]$Force - ) - begin { - Add-Type -assembly "System.IO.Compression.FileSystem" - } - process { - if ($Force -and (Test-Path $DestinationPath)) { - $ZipFile = [System.IO.Compression.ZipFile]::OpenRead($Path) - - $ZipFile.Entries | ForEach-Object { - $Destination = [System.IO.Path]::Combine($DestinationPath, $_.FullName) - $DestinationDir = [System.IO.Path]::GetDirectoryName($Destination) - if (-not (Test-Path $DestinationDir)) { - New-Item -ItemType Directory -Path $DestinationDir -Force - } - [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $Destination, $True) - } - $ZipFile.Dispose() - } - else { - [System.IO.Compression.ZipFile]::ExtractToDirectory($Path, $DestinationPath) - } - } - } - } - - # Handy download function - function Invoke-Download { - param( - [Parameter()] - [String]$URL, - [Parameter()] - [String]$Path, - [Parameter()] - [Switch]$SkipSleep - ) - - Write-Host "URL Given, Downloading the file..." - - $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType') - if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12 - } - elseif ( $SupportedTLSversions -contains 'Tls12' ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 - } - else { - # Not everything requires TLS 1.2, but we'll try anyways. - Write-Warning "TLS 1.2 and or TLS 1.3 isn't supported on this system. This download may fail!" - if ($PSVersionTable.PSVersion.Major -lt 3) { - Write-Warning "PowerShell 2 / .NET 2.0 doesn't support TLS 1.2." - } - } - - $i = 1 - While ($i -lt 4) { - if (-not ($SkipSleep)) { - $SleepTime = Get-Random -Minimum 3 -Maximum 60 - Start-Sleep -Seconds $SleepTime - } - - Write-Host "Download Attempt $i" - - try { - $WebClient = New-Object System.Net.WebClient - $WebClient.DownloadFile($URL, $Path) - } - catch { - Write-Warning "An error has occurred while downloading!" - } - - $File = Test-Path -Path $Path -ErrorAction SilentlyContinue - if ($File) { - $i = 4 - } - else { - $i++ - } - } - - if (-not $File) { - Write-Error -Message "File failed to download!" -Category DeviceError -Exception (New-Object System.Exception) - Exit 1 - } - } - - # Need to set Regkey to accept EULA - function Set-RegKey { - param ( - $Path, - $Name, - $Value, - [ValidateSet("DWord", "QWord", "String", "ExpandedString", "Binary", "MultiString", "Unknown")] - $PropertyType = "DWord" - ) - if (-not $(Test-Path -Path $Path)) { - # Check if path does not exist and create the path - New-Item -Path $Path -Force | Out-Null - } - if ((Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue)) { - # Update property and print out what it was changed from and changed to - $CurrentValue = (Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue).$Name - try { - Set-ItemProperty -Path $Path -Name $Name -Value $Value -Force -Confirm:$false -ErrorAction Stop | Out-Null - } - catch { - Write-Error -Message "[Error] Unable to Set registry key for $Name please see below error!" -Category DeviceError -Exception (New-Object System.Exception) - Write-Error $_ - exit 1 - } - Write-Host "$Path\$Name changed from $CurrentValue to $($(Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue).$Name)" - } - else { - # Create property with value - try { - New-ItemProperty -Path $Path -Name $Name -Value $Value -PropertyType $PropertyType -Force -Confirm:$false -ErrorAction Stop | Out-Null - } - catch { - Write-Error -Message "[Error] Unable to Set registry key for $Name please see below error!" -Category DeviceError -Exception (New-Object System.Exception) - Write-Error $_ - exit 1 - } - Write-Host "Set $Path\$Name to $($(Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue).$Name)" - } - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than or equal to 200,000 characters.") - } - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to be set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received it will have an exception property, the function will exit with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = $NinjaValue | Ninja-Property-Docs-Set -AttributeName $Name @DocumentationParams 2>&1 - } - else { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - - # Test for elevation - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - $ExitCode = 0 -} -process { - - # Take the script parameters and translate them into Autoruns options. - if ($Startup) { $AutorunOptions = "l" } - if ($Boot) { $AutorunOptions = "$($AutorunOptions)b" } - if ($Winlogon) { $AutorunOptions = "$($AutorunOptions)w" } - if ($AppInit) { $AutorunOptions = "$($AutorunOptions)d" } - if ($Explorer) { $AutorunOptions = "$($AutorunOptions)e" } - if ($Sidebar) { $AutorunOptions = "$($AutorunOptions)g" } - if ($ImageHijacks) { $AutorunOptions = "$($AutorunOptions)h" } - if ($IEAddons) { $AutorunOptions = "$($AutorunOptions)i" } - if ($KnownDLLs) { $AutorunOptions = "$($AutorunOptions)k" } - if ($WMIentries) { $AutorunOptions = "$($AutorunOptions)m" } - if ($WinSockProtocols) { $AutorunOptions = "$($AutorunOptions)n" } - if ($Codecs) { $AutorunOptions = "$($AutorunOptions)o" } - if ($PrinterMonitor) { $AutorunOptions = "$($AutorunOptions)p" } - if ($LSAProviders) { $AutorunOptions = "$($AutorunOptions)r" } - if ($Services) { $AutorunOptions = "$($AutorunOptions)s" } - if ($ScheduledTasks) { $AutorunOptions = "$($AutorunOptions)t" } - - if (-not $AutorunOptions) { - Write-Host -Object "[Error] No Autoruns options selected. Please at least select one option to search for autostart entries." - exit 1 - } - - # Download the file and unzip its contents - $DownloadArguments = @{ - URL = $DownloadUrl - Path = "$DestinationFolder\Autoruns.zip" - } - if ($SkipSleep) { $DownloadArguments["SkipSleep"] = $true } - - # Download and unzip - Invoke-Download @DownloadArguments - Expand-Archive -Path "$DestinationFolder\Autoruns.zip" -DestinationPath "$DestinationFolder\Autoruns" -Force - - if (-not (Test-Path "$DestinationFolder\Autoruns\autorunsc64.exe" -ErrorAction SilentlyContinue)) { - Write-Host -Object "[Error] Failed to unzip Autoruns" - exit 1 - } - - # Now that we have the options create an argument list using those options - $ArgumentList = New-Object System.Collections.Generic.List[string] - $ArgumentList.Add("-a $AutorunOptions") - $ArgumentList.Add("-h") - $ArgumentList.Add("-c") - $ArgumentList.Add("-s") - if ($HideMicrosoftEntries) { $ArgumentList.Add("-m") } - - # Accept EULA - Set-RegKey -Path "HKCU:\SOFTWARE\Sysinternals\AutoRuns" -Name "EulaAccepted" -Value 1 - - # Run autoruns and store the results as a csv and then import the results into powershell - Start-Process "$DestinationFolder\Autoruns\autorunsc64.exe" -ArgumentList $ArgumentList -NoNewWindow -RedirectStandardOutput "$DestinationFolder\Autoruns\autorunsc.csv" -Wait - $AutorunResults = Import-Csv "$DestinationFolder\Autoruns\autorunsc.csv" | Where-Object { $_.Entry } | Sort-Object Entry | Select-Object Entry, "Entry Location", "Image Path", Signer, MD5 - - if (-not ($AutorunResults)) { - Write-Host -Object "[Error] No startup entries found. Is Autorunsc being blocked?" - $ExitCode = 1 - } - - # Set the custom field with the Autoruns results. - if ($CustomField) { - if ($PSVersionTable.PSVersion.Major -lt 3) { - Write-Warning "Ninjarmm-cli does not support setting custom fields using PowerShell 2.0" - $ExitCode = 1 - } - - if ( -not (Test-IsElevated)) { - Write-Warning "Script must be elevated in order to write to custom field." - $ExitCode = 1 - } - - try { - Write-Host "Attempting to set Custom Field '$CustomField'." - - # Initialize html report. - $htmlReport = New-Object System.Collections.Generic.List[string] - - # Create html table based on our results. - $htmlTable = $AutorunResults | ConvertTo-Html -Fragment - - # Gather all the unsigned and unverified results. - $UnsignedResults = $AutorunResults | Where-Object { -not $_.Signer } - $UnverifiedResults = $AutorunResults | Where-Object { $_.Signer -like "*Not verified*" } - - # Loop through the html table and change the table row class for the unsigned entries. - $UnsignedResults | ForEach-Object { - $htmlTable = $htmlTable -replace "", "" - } - - # Loop through the html table and change the table row class for the unverified entries. - $UnverifiedResults | ForEach-Object { - $htmlTable = $htmlTable -replace "", "" - } - - # Check to see if we're at the character limit for WYSIWYG fields. - $Characters = $htmlTable | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # If we're within 500 characters of the limit we'll output a warning and add some text to the report to indicate that we're truncating the report. - if ($Characters -ge 199500) { - Write-Warning "200,000 Character Limit has been reached! Trimming rows until the character limit is satisfied..." - $htmlReport.Add("

This info has been truncated to accommodate the 200,000 character limit.

") - - # We'll want to truncate the last entries from the report so we'll reverse the string array and look for table row entries. - [array]::Reverse($htmlTable) - $i = 0 - do { - # If a table row entry has been found we'll remove it and then check if we satisfy the limit. - if ($htmlTable[$i] -match '", "" + } + + # Loop through the html table and change the table row class for the unverified entries. + $UnverifiedResults | ForEach-Object { + $htmlTable = $htmlTable -replace "", "" + } + + # Check to see if we're at the character limit for WYSIWYG fields. + $Characters = $htmlTable | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters + + # If we're within 500 characters of the limit we'll output a warning and add some text to the report to indicate that we're truncating the report. + if ($Characters -ge 199500) { + Write-Warning "200,000 Character Limit has been reached! Trimming rows until the character limit is satisfied..." + $htmlReport.Add("

This info has been truncated to accommodate the 200,000 character limit.

") + + # We'll want to truncate the last entries from the report so we'll reverse the string array and look for table row entries. + [array]::Reverse($htmlTable) + $i = 0 + do { + # If a table row entry has been found we'll remove it and then check if we satisfy the limit. + if ($htmlTable[$i] -match '", "" - $CPUProcessMetricTable = $CPUProcessMetricTable -replace "
Local Users
UNKNOWN
UNKNOWN
UNKNOWN
UNKNOWN
Verbose
Verbose
Warning
Warning
Error
Error
Critical Error
Critical Error$([Regex]::Escape($_.LevelDisplayName))LevelDisplayName
' -or $htmlTable[$i] -match '
Verbose
Verbose
Warning
Warning
Error
Error
Critical Error
Critical Error$([Regex]::Escape($_.LevelDisplayName))LevelDisplayName
' -or $htmlTable[$i] -match '
$([Regex]::Escape($_.Entry))$([Regex]::Escape($_.'Entry Location'))$([Regex]::Escape($_.'Image Path'))
$($_.Entry)$($_.'Entry Location')$($_.'Image Path')
$([Regex]::Escape($_.Entry))$([Regex]::Escape($_.'Entry Location'))$([Regex]::Escape($_.'Image Path'))$([Regex]::Escape($_.Signer))
$($_.Entry)$($_.'Entry Location')$($_.'Image Path')$($_.Signer)
') { - $htmlTable[$i] = $null - } - $i++ - $Characters = $htmlTable | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters - }while ($Characters -ge 199500) - - # Now that the limit has been satisfied we'll reverse the table/string array again. - [array]::Reverse($htmlTable) - } - - # Add the table to the report. - $htmlReport.Add($htmlTable) - - # Set the custom field. - Set-NinjaProperty -Name $CustomField -Value $htmlReport - Write-Host "Successfully set Custom Field '$CustomField'!" - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - - # Clean up our leftover files. - Remove-Item "$DestinationFolder\Autoruns" -Recurse -Force - Remove-Item "$DestinationFolder\Autoruns.zip" -Force - - # Output results into activity log. Using Format-List due to size of table. - $AutorunResults | Sort-Object Entry | Format-List - - exit $ExitCode -} -end { - - - -} +#Requires -Version 4 + +<# +.SYNOPSIS + Runs Autorunsc with your selected options and outputs the results to the activity log and optionally a WYSIWYG custom field. Please note that there is a limit to the number of results that can be set in Custom Fields or viewed in the Activity Log. +.DESCRIPTION + Runs Autorunsc with your selected options and outputs the results to the activity log and optionally a WYSIWYG custom field. Please note that there is a limit to the number of results that can be set in Custom Fields or viewed in the Activity Log. +.EXAMPLE + (No Parameters) + URL Given, Downloading the file... + Download Attempt 1 + HKCU:\SOFTWARE\Sysinternals\AutoRuns\EulaAccepted changed from 1 to 1 + + Sysinternals Autoruns v14.10 - Autostart program viewer + Copyright (C) 2002-2023 Mark Russinovich + Sysinternals - www.sysinternals.com + + WARNING: Script must be elevated in order to write to custom field. + + Entry : C:\Windows\system32\userinit.exe + Entry Location : HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Userinit + Image Path : c:\windows\system32\userinit.exe + Signer : (Verified) Microsoft Windows + MD5 : 9C4C281156040CF01EA35D759092F540 + + Entry : cmd.exe + Entry Location : HKLM\SYSTEM\CurrentControlSet\Control\SafeBoot\AlternateShell + Image Path : c:\windows\system32\cmd.exe + Signer : (Verified) Microsoft Windows + MD5 : 8A2122E8162DBEF04694B9C3E0B6CDEE + +PARAMETER: -CustomField "ReplaceWithAMultilineCustomField" + The name of the multiline custom field you would like to save the results to. + +PARAMETER: -Startup + Applications or scripts configured to run automatically after a user logs into their account. This is the default option for Autoruns. + E.g. Applications in the 'Startup' folder. + +PARAMETER: -Boot + Programs or commands that are set to execute during the system's boot-up sequence before a user logs in. + +PARAMETER: -WinLogon + Items that are configured to run during the Windows logon process. Often these items are critical to the logon UI. + +PARAMETER: -AppInit + DLLs that are automatically loaded by every process that calls the User32.dll file (anything with a GUI). + +PARAMETER: -Explorer + Plugins or extensions that integrate into the Windows Explorer shell. + +PARAMETER: -Sidebar + Mini applications or gadgets that load into the desktop sidebar in earlier versions of Windows (introduced in Windows Vista). + +PARAMETER: -ImageHijacks + Registry modifications that redirect the execution of specific executable files to a different program. + +PARAMETER: -IEAddons + Browser extensions or toolbars that Internet Explorer will load automatically when it starts. + +PARAMETER: -KnownDLLs + Crucial system DLLs that Windows will load into memory at startup. + +PARAMETER: -WMIentries + Entries related to WMI scripts or providers that are set to execute automatically. + +PARAMETER: -WinSockProtocols + Modules or services meant to load up with the Windows network stack. + +PARAMETER: -Codecs + Software components meant to be used for encoding or decoding digital media streams (often set to run at system startup). + +PARAMETER: -PrinterMonitor + DLL's associated with printer drivers. + +PARAMETER: -LSAProviders + Plugins that integrate with the Local Security Authority subsystem. + +PARAMETER: -Services + Windows Services set to start Automatically. + +PARAMETER: -ScheduledTasks + These are tasks set in Task Scheduler to do something automatically at a specified interval. + +PARAMETER: -HideMicrosoftEntries + Hides Signed Microsoft Entries from the results. + +PARAMETER: -DestinationFolder + By default this script downloads autorunsc to the temp folder. + +PARAMETER: -DownloadUrl + URL to download Autoruns from. + +PARAMETER: -SkipSleep + Skips sleeping prior to downloading autorunsc. +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 10, Server 2012 + Release Notes: Added support for WYSIWYG new character limit; now truncates results if results are too long. +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$CustomField, + [Parameter()] + [Switch]$Startup = [System.Convert]::ToBoolean($env:checkLogonStartupEntries), + [Parameter()] + [Switch]$Boot = [System.Convert]::ToBoolean($env:checkBootEntries), + [Parameter()] + [Switch]$WinLogon = [System.Convert]::ToBoolean($env:checkWinlogonEntries), + [Parameter()] + [Switch]$AppInit = [System.Convert]::ToBoolean($env:checkAppinitEntries), + [Parameter()] + [Switch]$Explorer = [System.Convert]::ToBoolean($env:checkExplorerAddons), + [Parameter()] + [Switch]$Sidebar = [System.Convert]::ToBoolean($env:checkSidebarGadgets), + [Parameter()] + [Switch]$ImageHijacks = [System.Convert]::ToBoolean($env:checkImageHijacks), + [Parameter()] + [Switch]$IEAddons = [System.Convert]::ToBoolean($env:checkInternetExplorerAddons), + [Parameter()] + [Switch]$KnownDLLs = [System.Convert]::ToBoolean($env:checkKnownDlls), + [Parameter()] + [Switch]$WMIentries = [System.Convert]::ToBoolean($env:checkWmiEntries), + [Parameter()] + [Switch]$WinSockProtocols = [System.Convert]::ToBoolean($env:checkWinsockProtocol), + [Parameter()] + [Switch]$Codecs = [System.Convert]::ToBoolean($env:checkCodecs), + [Parameter()] + [Switch]$PrinterMonitor = [System.Convert]::ToBoolean($env:checkPrinterMonitorDlls), + [Parameter()] + [Switch]$LSAProviders = [System.Convert]::ToBoolean($env:checkLsaSecurityProviders), + [Parameter()] + [Switch]$Services = [System.Convert]::ToBoolean($env:checkAutostartServices), + [Parameter()] + [Switch]$ScheduledTasks = [System.Convert]::ToBoolean($env:checkScheduledTasks), + [Parameter()] + [Switch]$HideMicrosoftEntries = [System.Convert]::ToBoolean($env:hideMicrosoftEntries), + [Parameter()] + [String]$DestinationFolder = "$env:Temp", + [Parameter()] + [String]$DownloadUrl = "https://download.sysinternals.com/files/Autoruns.zip", + [Parameter()] + [Switch]$SkipSleep = [System.Convert]::ToBoolean($env:skipSleep) +) + +begin { + + # If Script Forms are used replace the parameters + if ($env:destinationFolder -and $env:DestinationFolder -notlike "null") { $DestinationFolder = $env:destinationFolder } + if ($env:downloadUrl -and $env:downloadUrl -notlike "null") { $DownloadUrl = $env:downloadUrl } + if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } + + if ($PSVersionTable.PSVersion.Major -lt 5) { + function Expand-Archive { + [CmdletBinding()] + param( + [Parameter()] + [String]$Path, + [Parameter()] + [String]$DestinationPath, + [Parameter()] + [Switch]$Force + ) + begin { + Add-Type -assembly "System.IO.Compression.FileSystem" + } + process { + if ($Force -and (Test-Path $DestinationPath)) { + $ZipFile = [System.IO.Compression.ZipFile]::OpenRead($Path) + + $ZipFile.Entries | ForEach-Object { + $Destination = [System.IO.Path]::Combine($DestinationPath, $_.FullName) + $DestinationDir = [System.IO.Path]::GetDirectoryName($Destination) + if (-not (Test-Path $DestinationDir)) { + New-Item -ItemType Directory -Path $DestinationDir -Force + } + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $Destination, $True) + } + $ZipFile.Dispose() + } + else { + [System.IO.Compression.ZipFile]::ExtractToDirectory($Path, $DestinationPath) + } + } + } + } + + # Handy download function + function Invoke-Download { + param( + [Parameter()] + [String]$URL, + [Parameter()] + [String]$Path, + [Parameter()] + [Switch]$SkipSleep + ) + + Write-Host "URL Given, Downloading the file..." + + $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType') + if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12 + } + elseif ( $SupportedTLSversions -contains 'Tls12' ) { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 + } + else { + # Not everything requires TLS 1.2, but we'll try anyways. + Write-Warning "TLS 1.2 and or TLS 1.3 isn't supported on this system. This download may fail!" + if ($PSVersionTable.PSVersion.Major -lt 3) { + Write-Warning "PowerShell 2 / .NET 2.0 doesn't support TLS 1.2." + } + } + + $i = 1 + While ($i -lt 4) { + if (-not ($SkipSleep)) { + $SleepTime = Get-Random -Minimum 3 -Maximum 60 + Start-Sleep -Seconds $SleepTime + } + + Write-Host "Download Attempt $i" + + try { + $WebClient = New-Object System.Net.WebClient + $WebClient.DownloadFile($URL, $Path) + } + catch { + Write-Warning "An error has occurred while downloading!" + } + + $File = Test-Path -Path $Path -ErrorAction SilentlyContinue + if ($File) { + $i = 4 + } + else { + $i++ + } + } + + if (-not $File) { + Write-Error -Message "File failed to download!" -Category DeviceError -Exception (New-Object System.Exception) + Exit 1 + } + } + + # Need to set Regkey to accept EULA + function Set-RegKey { + param ( + $Path, + $Name, + $Value, + [ValidateSet("DWord", "QWord", "String", "ExpandedString", "Binary", "MultiString", "Unknown")] + $PropertyType = "DWord" + ) + if (-not $(Test-Path -Path $Path)) { + # Check if path does not exist and create the path + New-Item -Path $Path -Force | Out-Null + } + if ((Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue)) { + # Update property and print out what it was changed from and changed to + $CurrentValue = (Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue).$Name + try { + Set-ItemProperty -Path $Path -Name $Name -Value $Value -Force -Confirm:$false -ErrorAction Stop | Out-Null + } + catch { + Write-Error -Message "[Error] Unable to Set registry key for $Name please see below error!" -Category DeviceError -Exception (New-Object System.Exception) + Write-Error $_ + exit 1 + } + Write-Host "$Path\$Name changed from $CurrentValue to $($(Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue).$Name)" + } + else { + # Create property with value + try { + New-ItemProperty -Path $Path -Name $Name -Value $Value -PropertyType $PropertyType -Force -Confirm:$false -ErrorAction Stop | Out-Null + } + catch { + Write-Error -Message "[Error] Unable to Set registry key for $Name please see below error!" -Category DeviceError -Exception (New-Object System.Exception) + Write-Error $_ + exit 1 + } + Write-Host "Set $Path\$Name to $($(Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue).$Name)" + } + } + + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + # if ($Characters -ge 200000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than or equal to 200,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If we're requested to set the field value for a Ninja document we'll specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # The field below requires additional information to be set # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If an error is received it will have an exception property, the function will exit with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # if (-not $Selection) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # We'll need to set the field differently depending on if its a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Docs-Set -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # Test for elevation + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + $ExitCode = 0 +} +process { + + # Take the script parameters and translate them into Autoruns options. + if ($Startup) { $AutorunOptions = "l" } + if ($Boot) { $AutorunOptions = "$($AutorunOptions)b" } + if ($Winlogon) { $AutorunOptions = "$($AutorunOptions)w" } + if ($AppInit) { $AutorunOptions = "$($AutorunOptions)d" } + if ($Explorer) { $AutorunOptions = "$($AutorunOptions)e" } + if ($Sidebar) { $AutorunOptions = "$($AutorunOptions)g" } + if ($ImageHijacks) { $AutorunOptions = "$($AutorunOptions)h" } + if ($IEAddons) { $AutorunOptions = "$($AutorunOptions)i" } + if ($KnownDLLs) { $AutorunOptions = "$($AutorunOptions)k" } + if ($WMIentries) { $AutorunOptions = "$($AutorunOptions)m" } + if ($WinSockProtocols) { $AutorunOptions = "$($AutorunOptions)n" } + if ($Codecs) { $AutorunOptions = "$($AutorunOptions)o" } + if ($PrinterMonitor) { $AutorunOptions = "$($AutorunOptions)p" } + if ($LSAProviders) { $AutorunOptions = "$($AutorunOptions)r" } + if ($Services) { $AutorunOptions = "$($AutorunOptions)s" } + if ($ScheduledTasks) { $AutorunOptions = "$($AutorunOptions)t" } + + if (-not $AutorunOptions) { + Write-Host -Object "[Error] No Autoruns options selected. Please at least select one option to search for autostart entries." + exit 1 + } + + # Download the file and unzip its contents + $DownloadArguments = @{ + URL = $DownloadUrl + Path = "$DestinationFolder\Autoruns.zip" + } + if ($SkipSleep) { $DownloadArguments["SkipSleep"] = $true } + + # Download and unzip + Invoke-Download @DownloadArguments + Expand-Archive -Path "$DestinationFolder\Autoruns.zip" -DestinationPath "$DestinationFolder\Autoruns" -Force + + if (-not (Test-Path "$DestinationFolder\Autoruns\autorunsc64.exe" -ErrorAction SilentlyContinue)) { + Write-Host -Object "[Error] Failed to unzip Autoruns" + exit 1 + } + + # Now that we have the options create an argument list using those options + $ArgumentList = New-Object System.Collections.Generic.List[string] + $ArgumentList.Add("-a $AutorunOptions") + $ArgumentList.Add("-h") + $ArgumentList.Add("-c") + $ArgumentList.Add("-s") + if ($HideMicrosoftEntries) { $ArgumentList.Add("-m") } + + # Accept EULA + Set-RegKey -Path "HKCU:\SOFTWARE\Sysinternals\AutoRuns" -Name "EulaAccepted" -Value 1 + + # Run autoruns and store the results as a csv and then import the results into powershell + Start-Process "$DestinationFolder\Autoruns\autorunsc64.exe" -ArgumentList $ArgumentList -NoNewWindow -RedirectStandardOutput "$DestinationFolder\Autoruns\autorunsc.csv" -Wait + $AutorunResults = Import-Csv "$DestinationFolder\Autoruns\autorunsc.csv" | Where-Object { $_.Entry } | Sort-Object Entry | Select-Object Entry, "Entry Location", "Image Path", Signer, MD5 + + if (-not ($AutorunResults)) { + Write-Host -Object "[Error] No startup entries found. Is Autorunsc being blocked?" + $ExitCode = 1 + } + + # Set the custom field with the Autoruns results. + if ($CustomField) { + if ($PSVersionTable.PSVersion.Major -lt 3) { + Write-Warning "Ninjarmm-cli does not support setting custom fields using PowerShell 2.0" + $ExitCode = 1 + } + + if ( -not (Test-IsElevated)) { + Write-Warning "Script must be elevated in order to write to custom field." + $ExitCode = 1 + } + + try { + Write-Host "Attempting to set Custom Field '$CustomField'." + + # Initialize html report. + $htmlReport = New-Object System.Collections.Generic.List[string] + + # Create html table based on our results. + $htmlTable = $AutorunResults | ConvertTo-Html -Fragment + + # Gather all the unsigned and unverified results. + $UnsignedResults = $AutorunResults | Where-Object { -not $_.Signer } + $UnverifiedResults = $AutorunResults | Where-Object { $_.Signer -like "*Not verified*" } + + # Loop through the html table and change the table row class for the unsigned entries. + $UnsignedResults | ForEach-Object { + $htmlTable = $htmlTable -replace "
$([Regex]::Escape($_.Entry))$([Regex]::Escape($_.'Entry Location'))$([Regex]::Escape($_.'Image Path'))
$($_.Entry)$($_.'Entry Location')$($_.'Image Path')
$([Regex]::Escape($_.Entry))$([Regex]::Escape($_.'Entry Location'))$([Regex]::Escape($_.'Image Path'))$([Regex]::Escape($_.Signer))
$($_.Entry)$($_.'Entry Location')$($_.'Image Path')$($_.Signer)
') { + $htmlTable[$i] = $null + } + $i++ + $Characters = $htmlTable | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters + }while ($Characters -ge 199500) + + # Now that the limit has been satisfied we'll reverse the table/string array again. + [array]::Reverse($htmlTable) + } + + # Add the table to the report. + $htmlReport.Add($htmlTable) + + # Set the custom field. + # Set-NinjaProperty -Name $CustomField -Value $htmlReport # Removed NinjaOne dependency + Write-Host "Successfully set Custom Field '$CustomField'!" + } + catch { + Write-Host "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + } + + # Clean up our leftover files. + Remove-Item "$DestinationFolder\Autoruns" -Recurse -Force + Remove-Item "$DestinationFolder\Autoruns.zip" -Force + + # Output results into activity log. Using Format-List due to size of table. + $AutorunResults | Sort-Object Entry | Format-List + + exit $ExitCode +} +end { + + + +} diff --git a/Powershell Scripts/System Performance Check.ps1 b/Powershell Scripts/System Performance Check.ps1 index df36053..03356d4 100644 --- a/Powershell Scripts/System Performance Check.ps1 +++ b/Powershell Scripts/System Performance Check.ps1 @@ -1,1764 +1,1764 @@ # Collects system performance data (CPU, memory, disk, and network). The results can optionally be saved to a WYSIWYG custom field. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Collects system performance data (CPU, memory, disk, and network). The results can optionally be saved to a WYSIWYG custom field. -.DESCRIPTION - Collects system performance data (CPU, memory, disk, and network). The results can optionally be saved to a WYSIWYG custom field. -.EXAMPLE - -DaysSinceLastReboot "7" -DurationToPerformTests "5" -NumberOfEvents "5" -WysiwygCustomField "WYSIWYG" -DisplayUserMessage - - Sending message to all users. - ExitCode: 0 - Sending message to session Console, display time 150 - Async message sent to session Console - Sending message to session 31C5CE94259D4006A9E4#0, display time 150 - Async message sent to session 31C5CE94259D4006A9E4#0 - - [Alert] This computer was last started on 8/27/2024 at 5:21 PM which was 11.2 days ago. - - Collecting event logs. - Searching for performance counter localizations. - Collecting performance metrics for 5 minutes. - WARNING: The data in one of the performance counter samples is not valid. View the Status property for each - PerformanceCounterSample object to make sure it contains valid data. - WARNING: The data in one of the performance counter samples is not valid. View the Status property for each - PerformanceCounterSample object to make sure it contains valid data. - WARNING: The data in one of the performance counter samples is not valid. View the Status property for each - PerformanceCounterSample object to make sure it contains valid data. - WARNING: The data in one of the performance counter samples is not valid. View the Status property for each - PerformanceCounterSample object to make sure it contains valid data. - WARNING: The data in one of the performance counter samples is not valid. View the Status property for each - PerformanceCounterSample object to make sure it contains valid data. - - ### 12th Gen Intel(R) Core(TM) i9-12900H 2.918 GHz ### - CPU Average % CPU Minimum % CPU Maximum % - ------------- ------------- ------------- - 1.09% 0.27% 1.89% - - ### Memory Usage ### - Total Memory Installed: 4 GB - RAM Average % RAM Minimum % RAM Maximum % - ------------- ------------- ------------- - 68.51% 68.39% 68.68% - - ### Top 5 CPU Processes ### - Process Name Average CPU % Used Minimum CPU % Used Maximum CPU % Used - ------------ ------------------ ------------------ ------------------ - msmpeng 0.85% 0.13% 1.59% - svchost 0.1% 0.03% 0.21% - mssense 0.09% 0% 0.16% - ninjarmmagent 0.03% 0% 0.05% - teamviewer_service 0.02% 0% 0.03% - - ### Top 5 RAM Processes ### - Process Name Average RAM % Used Minimum RAM % Used Maximum RAM % Used - ------------ ------------------ ------------------ ------------------ - svchost 5.42% 5.37% 5.44% - msmpeng 3.37% 3.3% 3.5% - powershell 1.79% 1.62% 1.91% - mssense 1.61% 1.59% 1.62% - sensendr 0.67% 0.67% 0.67% - - ### Network Usage ### - NetworkAdapter : Ethernet - MacAddress : 00-17-FB-00-00-04 - Type : Wired - Average Sent & Received : 0.01 Mbps - Minimum Sent & Received : 0.01 Mbps - Maximum Sent & Received : 0.02 Mbps - - ### Disk Usage ### - DriveLetter FreeSpace TotalSpace PhysicalDisk MediaType Average IOPS Minimum IOPS Maximum IOPS - ----------- --------- ---------- ------------ --------- ------------ ------------ ------------ - C 27.31 GB (55.2%) 49.47 GB NVMe PC801 NVMe SK hynix 2TB SSD 47.04 IOPS 2.02 IOPS 112.24 IOPS - - ### Top 5 IO Processes (Network & Disk Combined) ### - Process Name Average IO Used Minimum IO Used Maximum IO Used - ------------ --------------- --------------- --------------- - svchost 0.0718 Mbps 0.0027 Mbps 0.1742 Mbps - system 0.0409 Mbps 0.0323 Mbps 0.0502 Mbps - ninjarmmagent 0.0335 Mbps 0.0174 Mbps 0.0464 Mbps - registry 0.0169 Mbps 0.0021 Mbps 0.0521 Mbps - msmpeng 0.015 Mbps 0.0063 Mbps 0.0264 Mbps - - Running WinSAT Assessements. - More info: https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-8.1-and-8/hh825488(v=win.10) - ExitCode: 0 - Retrieving WinSAT assessment data. - Successfully retrieved assessment data. - - ### WinSAT Scores ### - CPUScore D3DScore DiskScore GraphicsScore MemoryScore - -------- -------- --------- ------------- ----------- - 9.1 9.9 9.7 8.3 9.1 - - Attempting to set Custom Field 'WYSIWYG'. - Successfully set Custom Field 'WYSIWYG'! - - ### Last 5 errors in Application, Security, Setup and System Log. ### - LogName : Application - ProviderName : Microsoft-Windows-Security-SPP - Id : 8198 - TimeCreated : 11/18/2024 4:19:19 PM - Message : License Activation (slui.exe) failed with the following error code: - hr=0x80004005 - Command-line arguments: - RuleId=eeba1977-569e-4571-b639-7623d8bfecc0;Action=AutoActivate;AppId=55c92734-d682-4d71-983e-d6ec3f1605 - 9f;SkuId=2de67392-b7a7-462a-b1ca-108dd189f588;NotificationInterval=1440;Trigger=TimerEvent - - LogName : System - ProviderName : Microsoft-Windows-Time-Service - Id : 34 - TimeCreated : 11/18/2024 6:07:36 AM - Message : The time service has detected that the system time needs to be changed by 0 seconds. The time service - will not change the system time by more than 54000 seconds. Verify that your time and time zone are - correct, and that the time source VM IC Time Synchronization Provider is working properly. - - LogName : Application - ProviderName : Microsoft-Windows-Security-SPP - Id : 8198 - TimeCreated : 11/17/2024 4:19:19 PM - Message : License Activation (slui.exe) failed with the following error code: - hr=0x80004005 - Command-line arguments: - RuleId=eeba1977-569e-4571-b639-7623d8bfecc0;Action=AutoActivate;AppId=55c92734-d682-4d71-983e-d6ec3f1605 - 9f;SkuId=2de67392-b7a7-462a-b1ca-108dd189f588;NotificationInterval=1440;Trigger=TimerEvent - - LogName : System - ProviderName : Microsoft-Windows-Time-Service - Id : 34 - TimeCreated : 11/17/2024 10:53:51 AM - Message : The time service has detected that the system time needs to be changed by 0 seconds. The time service - will not change the system time by more than 54000 seconds. Verify that your time and time zone are - correct, and that the time source VM IC Time Synchronization Provider is working properly. - - LogName : Application - ProviderName : Microsoft-Windows-Security-SPP - Id : 8198 - TimeCreated : 11/16/2024 4:19:52 PM - Message : License Activation (slui.exe) failed with the following error code: - hr=0x80004005 - Command-line arguments: - RuleId=eeba1977-569e-4571-b639-7623d8bfecc0;Action=AutoActivate;AppId=55c92734-d682-4d71-983e-d6ec3f1605 - 9f;SkuId=2de67392-b7a7-462a-b1ca-108dd189f588;NotificationInterval=1440;Trigger=TimerEvent - - LogName : Application - ProviderName : Microsoft-Windows-Defrag - Id : 264 - TimeCreated : 11/16/2024 12:09:28 PM - Message : The storage optimizer couldn't complete slab consolidation on System (C:) because: The slab - consolidation operation was aborted because an insufficient number of slabs could be reclaimed (based - on the limits specified in the registry). (0x89000028) - - LogName : Application - ProviderName : Microsoft-Windows-Security-SPP - Id : 8198 - TimeCreated : 11/15/2024 4:09:12 PM - Message : License Activation (slui.exe) failed with the following error code: - hr=0x80004005 - Command-line arguments: - RuleId=eeba1977-569e-4571-b639-7623d8bfecc0;Action=AutoActivate;AppId=55c92734-d682-4d71-983e-d6ec3f1605 - 9f;SkuId=2de67392-b7a7-462a-b1ca-108dd189f588;NotificationInterval=1440;Trigger=UserLogon;SessionId=2 - - LogName : System - ProviderName : Service Control Manager - Id : 7000 - TimeCreated : 11/15/2024 11:58:50 AM - Message : The luafv service failed to start due to the following error: - This driver has been blocked from loading - - LogName : System - ProviderName : Service Control Manager - Id : 7043 - TimeCreated : 11/15/2024 11:58:39 AM - Message : The Windows Defender Advanced Threat Protection Service service did not shut down properly after - receiving a preshutdown control. - - LogName : System - ProviderName : Service Control Manager - Id : 7031 - TimeCreated : 11/15/2024 11:43:58 AM - Message : The Microsoft Intune Management Extension service terminated unexpectedly. It has done this 1 time(s). - The following corrective action will be taken in 60000 milliseconds: Restart the service. - - Sending message to all users. - ExitCode: 0 - Sending message to session Console, display time 3600 - Async message sent to session Console - Sending message to session 31C5CE94259D4006A9E4#0, display time 3600 - Async message sent to session 31C5CE94259D4006A9E4#0 - -PARAMETER: -DisplayUserMessage - Display a message to the end-user informing them that you are collecting performance metrics and that they should not restart the computer. - -PARAMETER: -DaysSinceLastReboot "7" - Specify the number of days by which the system should have been rebooted. - -PARAMETER: -DurationToPerformTests "5" - The duration (in minutes) for which the performance tests should be executed. - -PARAMETER: -NumberOfEvents "5" - The number of error events to retrieve from the Application, Security, Setup, and System event logs. - -PARAMETER: -WysiwygCustomField "ReplaceMeWithAnyWYSIWYGCustomField" - Optionally specify the name of a WYSIWYG custom field to store the formatted performance data. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Removed internet speedtest -#> - -[CmdletBinding()] -param ( - [Parameter()] - $DaysSinceLastReboot, - [Parameter()] - [Float]$DurationToPerformTests = 5, - [Parameter()] - $NumberOfEvents, - [Parameter()] - [String]$WysiwygCustomField, - [Parameter()] - [Switch]$DisplayUserMessage = [System.Convert]::ToBoolean($env:displayUserMessage) -) - -begin { - # If script form variables are used, replace command line parameters with their values. - if ($env:daysSinceLastReboot -and $env:daysSinceLastReboot -notlike "null") { $DaysSinceLastReboot = $env:daysSinceLastReboot } - if ($env:durationToPerformTests -and $env:durationToPerformTests -notlike "null") { $DurationToPerformTests = $env:durationToPerformTests } - if ($env:numberOfEvents -and $env:numberOfEvents -notlike "null") { $NumberOfEvents = $env:numberOfEvents } - if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $WysiwygCustomField = $env:wysiwygCustomFieldName } - - # Validate the 'Days Since Last Reboot' input. - if ($DaysSinceLastReboot) { - try { - $ErrorActionPreference = "Stop" - # Attempt to cast the value to a floating-point number. - $DaysSinceLastReboot = [float]$DaysSinceLastReboot - $ErrorActionPreference = "Continue" - } - catch { - # If the conversion fails, display an error message and exit the script. - Write-Host -Object "[Error] The 'Days Since Last Reboot' value of '$DaysSinceLastReboot' is invalid. Please provide a positive whole number or 0." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - } - - # Ensure the value is a whole number (i.e., not a fraction). - if ($DaysSinceLastReboot -and ($DaysSinceLastReboot % 1) -ne 0) { - Write-Host -Object "[Error] The 'Days Since Last Reboot' value of '$DaysSinceLastReboot' is invalid. Please provide a positive whole number or 0." - exit 1 - } - - # Ensure the value is non-negative (greater than or equal to 0). - if ($DaysSinceLastReboot -and $DaysSinceLastReboot -lt 0) { - Write-Host -Object "[Error] The 'Days Since Last Reboot' value of '$DaysSinceLastReboot' is invalid. Please provide a positive whole number or 0." - exit 1 - } - - # Validate the 'Duration To Perform Tests' input. - if (!$DurationToPerformTests) { - Write-Host -Object "[Error] Please provide the duration for which you would like to perform the tests using the 'Duration To Perform Tests' box." - exit 1 - } - - # Ensure the duration is a whole number (i.e., not a fraction). - if ($DurationToPerformTests -and ($DurationToPerformTests % 1) -ne 0) { - Write-Host -Object "[Error] The 'Duration To Perform Tests' value of '$DurationToPerformTests' is invalid." - Write-Host -Object "[Error] Please provide a positive whole number that's greater than 0 and less than or equal to 60." - exit 1 - } - - # Ensure the duration is between 1 and 60. - if ($DurationToPerformTests -and ($DurationToPerformTests -lt 1 -or $DurationToPerformTests -gt 60)) { - Write-Host -Object "[Error] The 'Duration To Perform Tests' value of '$DurationToPerformTests' is invalid." - Write-Host -Object "[Error] Please provide a positive whole number that's greater than 0 and less than or equal to 60." - exit 1 - } - - # Validate the 'Number of Events' input. - if ($NumberOfEvents) { - try { - $ErrorActionPreference = "Stop" - # Attempt to cast the value to a floating-point number. - $NumberOfEvents = [float]$NumberOfEvents - $ErrorActionPreference = "Continue" - } - catch { - # If the conversion fails, display an error message and exit the script. - Write-Host -Object "[Error] The 'Number of Events' value of '$NumberOfEvents' is invalid. Please provide a positive whole number or 0." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - } - - # Ensure the value is a whole number (i.e., not a fraction). - if ($NumberOfEvents -and ($NumberOfEvents % 1) -ne 0) { - Write-Host -Object "[Error] The 'Number of Events' value of '$NumberOfEvents' is invalid. Please provide a positive whole number or 0." - exit 1 - } - - # Ensure the value is non-negative (greater than or equal to 0). - if ($NumberOfEvents -and $NumberOfEvents -lt 0) { - Write-Host -Object "[Error] The 'Number of Events' value of '$NumberOfEvents' is invalid. Please provide a positive whole number or 0." - exit 1 - } - - function Test-IsServer { - # Determine the method to retrieve the operating system information based on PowerShell version - - try { - $OS = if ($PSVersionTable.PSVersion.Major -lt 3) { - Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop - } - else { - Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop - } - } - catch { - Write-Host -Object "[Error] Failed to identity if this device is a workstation or server." - throw $_ - } - - # Check if the ProductType is "3" or "2", which indicates that the system is a server - if ($OS.ProductType -eq "3" -or $OS.ProductType -eq "2") { - return $true - } - } - - # Check if the script is running on a server. - try { - $IsServer = Test-IsServer - } - catch { - Write-Host -Object "[Error] Unable to identify device type." - Write-Host -Object "[Error] $($_.Exception.Message)`n" - $ExitCode = 1 - } - - if ($IsServer -and $DisplayUserMessage) { - # Attempt to check if the RDS role is installed. - try { - # Retrieve the RDS role feature and check if it is installed. - $RDSRole = Get-WindowsFeature -Name RDS-RD-Server | Where-Object { $_.Installed } - } - catch { - # If an error occurs during the check, output an error message and exit the script. - Write-Host -Object "[Error] Unable to check if the RDS role is installed." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # If the RDS role is installed, output an error message and exit the script. - if ($RDSRole) { - Write-Host -Object "[Error] This script doesn't support sending a message on RDS servers because the message would show for all logged-in users, potentially creating a source of confusion." - exit 1 - } - } - - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - # Utility function for downloading files. - function Invoke-Download { - param( - [Parameter()] - [String]$URL, - [Parameter()] - [String]$Path, - [Parameter()] - [int]$Attempts = 3, - [Parameter()] - [Switch]$SkipSleep - ) - - # Display the URL being used for the download - Write-Host -Object "URL '$URL' was given." - Write-Host -Object "Downloading the file..." - - # Determine the supported TLS versions and set the appropriate security protocol - $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType') - if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12 - } - elseif ( $SupportedTLSversions -contains 'Tls12' ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 - } - else { - # Warn the user if TLS 1.2 and 1.3 are not supported, which may cause the download to fail - Write-Warning "TLS 1.2 and/or TLS 1.3 are not supported on this system. This download may fail!" - if ($PSVersionTable.PSVersion.Major -lt 3) { - Write-Warning "PowerShell 2 / .NET 2.0 doesn't support TLS 1.2." - } - } - - # Initialize the attempt counter - $i = 1 - While ($i -le $Attempts) { - # If SkipSleep is not set, wait for a random time between 3 and 15 seconds before each attempt - if (!($SkipSleep)) { - $SleepTime = Get-Random -Minimum 3 -Maximum 15 - Write-Host "Waiting for $SleepTime seconds." - Start-Sleep -Seconds $SleepTime - } - - # Provide a visual break between attempts - if ($i -ne 1) { Write-Host "" } - Write-Host "Download Attempt $i" - - # Temporarily disable progress reporting to speed up script performance - $PreviousProgressPreference = $ProgressPreference - $ProgressPreference = 'SilentlyContinue' - try { - if ($PSVersionTable.PSVersion.Major -lt 4) { - # For older versions of PowerShell, use WebClient to download the file - $WebClient = New-Object System.Net.WebClient - $WebClient.DownloadFile($URL, $Path) - } - else { - # For PowerShell 4.0 and above, use Invoke-WebRequest with specified arguments - $WebRequestArgs = @{ - Uri = $URL - OutFile = $Path - MaximumRedirection = 10 - UseBasicParsing = $True - } - - Invoke-WebRequest @WebRequestArgs - } - - # Verify if the file was successfully downloaded - $File = Test-Path -Path $Path -ErrorAction SilentlyContinue - } - catch { - # Handle any errors that occur during the download attempt - Write-Warning "An error has occurred while downloading!" - Write-Warning $_.Exception.Message - - # If the file partially downloaded, delete it to avoid corruption - if (Test-Path -Path $Path -ErrorAction SilentlyContinue) { - Remove-Item $Path -Force -Confirm:$false -ErrorAction SilentlyContinue - } - - $File = $False - } - - # Restore the original progress preference setting - $ProgressPreference = $PreviousProgressPreference - # If the file was successfully downloaded, exit the loop - if ($File) { - $i = $Attempts - } - else { - # Warn the user if the download attempt failed - Write-Warning "File failed to download." - Write-Host "" - } - - # Increment the attempt counter - $i++ - } - - # Final check: if the file still doesn't exist, report an error and exit - if (!(Test-Path $Path)) { - Write-Host -Object "[Error] Failed to download file." - Write-Host -Object "Please verify the URL of '$URL'." - exit 1 - } - else { - # If the download succeeded, return the path to the downloaded file - return $Path - } - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName, - [Parameter()] - [Switch]$Piped - ) - # Remove the non-breaking space character - if ($Type -eq "WYSIWYG") { - $Value = $Value -replace ' ', ' ' - } - - # Measure the number of characters in the provided value - $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # Throw an error if the value exceeds the character limit of 200,000 characters - if ($Piped -and $Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - if (!$Piped -and $Characters -ge 45000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") - } - - # Initialize a hashtable for additional documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define a list of valid field types - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - - # Warn the user if the provided type is not valid - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # Define types that require options to be retrieved - $NeedsOptions = "Dropdown" - - # If the property is being set in a document or field and the type needs options, retrieve them - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an error if there was an issue retrieving the property options - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Process the property value based on its type - switch ($Type) { - "Checkbox" { - # Convert the value to a boolean for Checkbox type - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Convert the value to a Unix timestamp for Date or Date Time type - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Convert the dropdown value to its corresponding GUID - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - # Throw an error if the value is not present in the dropdown options - if (!($Selection)) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # For other types, use the value as is - $NinjaValue = $Value - } - } - - # Set the property value in the document if a document name is provided - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - try { - # Otherwise, set the standard property value - if ($Piped) { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - } - catch { - Write-Host -Object "[Error] Failed to set custom field." - throw $_.Exception.Message - } - } - - # Throw an error if setting the property failed - if ($CustomField.Exception) { - throw $CustomField - } - } - - if (!$ExitCode) { - $ExitCode = 0 - } - - $StartedDateTime = Get-Date -} -process { - # Check if the script is being run with elevated (Administrator) privileges. - # If not, display an error message and exit the script. - if (!(Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Check if the lock file exists to prevent multiple instances of the script from running. - # If it exists, read the process ID from the lock file and check if the process is still running. - if (Test-Path -Path "$env:ProgramData\NinjaRMMAgent\SystemPerformance.lock.txt" -ErrorAction SilentlyContinue) { - try { - Write-Host -Object "Process lock file found at '$env:ProgramData\NinjaRMMAgent\SystemPerformance.lock.txt'. Checking if the process is still running." - - # Retrieve the process ID from the lock file. - $OtherScript = Get-Content -Path "$env:ProgramData\NinjaRMMAgent\SystemPerformance.lock.txt" -ErrorAction Stop - - # Check if the process ID exists, indicating the script is already running. - if (Get-Process -Id $OtherScript -ErrorAction SilentlyContinue) { - Write-Host -Object "[Error] This script is already running in another process with the process id (PID) '$OtherScript'." - exit 1 - } - } - catch { - # If there is an error accessing the lock file, display an error message and exit. - Write-Host -Object "[Error] Unable to access the lock file at '$env:ProgramData\NinjaRMMAgent\SystemPerformance.lock.txt'." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - } - - # Attempt to write the current process ID to the lock file, preventing multiple instances of the script from running. - try { - [System.Diagnostics.Process]::GetCurrentProcess().Id | Out-File -FilePath "$env:ProgramData\NinjaRMMAgent\SystemPerformance.lock.txt" -Force -ErrorAction Stop - } - catch { - # If the lock file cannot be created, display an error message and exit. - Write-Host -Object "[Error] Failed to create lock file at '$env:ProgramData\NinjaRMMAgent\SystemPerformance.lock.txt'." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - $TotalMessageTime = $($DurationToPerformTests * 60 / 2) - $TotalCollectionTime = $DurationToPerformTests - - if ($DisplayUserMessage) { - # Define arguments for the 'msg.exe' command to display a system message to the user. - $MSGArguments = @( - "*" - "/TIME:$TotalMessageTime" - "/V" - "System performance metrics are currently being collected. Collection should complete in approximately $TotalCollectionTime minutes and the results will be sent to your IT Administrator. Please do not restart the computer until this collection has completed." - ) - - # Generate unique log file names for capturing the stdout and stderr of the 'msg.exe' process. - $FirstMsgStandardOutLog = "$env:TEMP\$(New-Guid)_1STMSG_stdout.log" - $FirstMsgStandardErrLog = "$env:TEMP\$(New-Guid)_1STMSG_stderr.log" - - - # Attempt to display the system message to all users. - try { - Write-Host -Object "Sending message to all users." - - # Start the 'msg.exe' process with the arguments defined above. - # The process will run in the background without opening a new window (-NoNewWindow). - # Standard output and error will be redirected to log files. - # -Wait ensures the script waits for the process to finish before proceeding. - # -PassThru allows us to capture the process object and access its exit code. - $FirstMsgProcess = Start-Process -FilePath "$env:SystemRoot\System32\msg.exe" -ArgumentList $MSGArguments -Wait -NoNewWindow -PassThru -RedirectStandardOutput $FirstMsgStandardOutLog -RedirectStandardError $FirstMsgStandardErrLog -ErrorAction Stop - } - catch { - # If the 'msg.exe' process fails to start, output an error message and exit with a failure code. - Write-Host -Object "[Error] Failed to send message to all users." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Output the exit code of the 'msg.exe' process. - Write-Host -Object "ExitCode: $($FirstMsgProcess.ExitCode)" - - # If the exit code is non-zero (indicating an error occurred), display an error message. - if ($FirstMsgProcess.ExitCode -ne 0) { - Write-Host -Object "[Error] ExitCode does not indicate success." - } - - # Check if the standard output log file exists. - if (Test-Path -Path $FirstMsgStandardOutLog -ErrorAction SilentlyContinue) { - # Display the contents of the stdout log. - Get-Content -Path $FirstMsgStandardOutLog -Encoding Oem -ErrorAction SilentlyContinue | Write-Host - - try { - # Attempt to delete the stdout log file after displaying its contents. - Remove-Item -Path $FirstMsgStandardOutLog -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] Failed to remove standard output log at '$FirstMsgStandardOutLog'" - exit 1 - } - } - - # Check if the standard error log file exists. - if (Test-Path -Path $FirstMsgStandardErrLog -ErrorAction SilentlyContinue) { - # Read the contents of the stderr log into a variable. - $FirstMessageErrors = Get-Content -Path $FirstMsgStandardErrLog -Encoding Oem -ErrorAction SilentlyContinue - - # Attempt to delete the stderr log file after reading its contents. - try { - Remove-Item -Path $FirstMsgStandardErrLog -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] Failed to remove standard error log at '$FirstMsgStandardErrLog'" - exit 1 - } - } - - # If there were any errors captured in the stderr log, display them and exit with an error code. - if ($FirstMessageErrors) { - Write-Host -Object "[Error] Sending message to all users." - - $FirstMsgStandardErrLog | ForEach-Object { - Write-Host -Object "[Error] $_" - } - - exit 1 - } - - # If the 'msg.exe' process exit code is non-zero, exit the script with an error code. - if ($FirstMsgProcess.ExitCode -ne 0) { - exit 1 - } - } - - Write-Host -Object "" - - # Get the last reboot time of the system. - try { - $LastStartTime = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop | Select-Object -ExpandProperty LastBootUpTime - } - catch { - Write-Host -Object "[Error] Failed to get last start up time." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - - # If the 'DaysSinceLastReboot' parameter is set, calculate the time difference since the last reboot. - if ($DaysSinceLastReboot -ge 0) { - $TimeDifference = New-TimeSpan -Start $LastStartTime -End (Get-Date) - - # If the time since the last reboot exceeds the limit, display an alert to the user. - if ($TimeDifference.TotalDays -gt $DaysSinceLastReboot) { - Write-Host -Object "[Alert] This computer was last started on $($LastStartTime.ToShortDateString()) at $($LastStartTime.ToShortTimeString()) which was $([math]::Round($TimeDifference.TotalDays,2)) days ago." - $ExceededLastStartupLimit = $True - } - } - - # Initialize an empty list to store event logs. - $EventLogs = New-Object System.Collections.Generic.List[object] - - # Define XML queries for Application, Security, Setup, and System event logs that have error level events (Level=2). - [xml]$ApplicationXML = @" - - - - - -"@ - - [xml]$SecurityLogs = @" - - - - - -"@ - - [xml]$SetupLogs = @" - - - - - -"@ - - [xml]$SystemLogs = @" - - - - - -"@ - - # If the 'NumberOfEvents' parameter is set, collect the specified number of error logs from each log category. - if ($NumberOfEvents) { - Write-Host -Object "`nCollecting event logs." - - # Collect logs from each category and store them in the EventLogs list. - Get-WinEvent -MaxEvents $NumberOfEvents -FilterXml $ApplicationXML -ErrorAction SilentlyContinue -ErrorVariable EventLogErrors | ForEach-Object { $EventLogs.Add($_) } - Get-WinEvent -MaxEvents $NumberOfEvents -FilterXml $SecurityLogs -ErrorAction SilentlyContinue -ErrorVariable EventLogErrors | ForEach-Object { $EventLogs.Add($_) } - Get-WinEvent -MaxEvents $NumberOfEvents -FilterXml $SetupLogs -ErrorAction SilentlyContinue -ErrorVariable EventLogErrors | ForEach-Object { $EventLogs.Add($_) } - Get-WinEvent -MaxEvents $NumberOfEvents -FilterXml $SystemLogs -ErrorAction SilentlyContinue -ErrorVariable EventLogErrors | ForEach-Object { $EventLogs.Add($_) } - - # If any errors occurred during log collection, display warnings with the error details. - if ($EventLogErrors) { - $EventLogErrors | ForEach-Object { - Write-Warning -Message "$($_.Exception.Message)" - } - } - - # If no error logs were found, display a warning message. - if ($EventLogs.Count -eq 0) { - Write-Warning -Message "No error events were found in the event log." - } - else { - $EventLogs = $EventLogs | Select-Object LogName, ProviderName, Id, TimeCreated, Message | Sort-Object -Property TimeCreated -Descending - } - } - - # Display a message to the user indicating the start of the search for performance counter localizations. - Write-Host -Object "Searching for performance counter localizations." - - # Attempt to retrieve the "Counter" property from the CurrentLanguage registry key, which contains the localized performance counter names. - # If the retrieval fails, catch the error, display an error message, and exit the script. - try { - $CurrentLanguageKey = Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\CurrentLanguage" -Name "Counter" -ErrorAction Stop | Select-Object -ExpandProperty Counter - } - catch { - Write-Host -Object "[Error] Failed to retrieve performance counter localizations." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Initialize an empty hash table to store the performance counter localizations. - $LocalizationCounterTable = @{} - - # Loop through the array of performance counters in the registry. - # The counter array consists of alternating key-value pairs (even indexes are keys, odd indexes are values), - # so this loop increments by 2 to match each key with its corresponding value. - for ($i = 0; $i -lt $CurrentLanguageKey.Length; $i += 2) { - $LocalizationCounterTable[$CurrentLanguageKey[$i]] = $CurrentLanguageKey[$i + 1] - } - - # Define the paths for various performance counters using the localized counter names from the hash table. - # These paths are dynamically created by retrieving the localized names for each counter ID. - $OverallProcessCounterPath = "\$($LocalizationCounterTable['238'])(*)\$($LocalizationCounterTable['6'])" - $OverallMemoryCounterPath = "\$($LocalizationCounterTable['4'])\$($LocalizationCounterTable['1406'])" - $ProcessorCounterPath = "\$($LocalizationCounterTable['230'])(*)\$($LocalizationCounterTable['142'])" - $MemoryCounterPath = "\$($LocalizationCounterTable['230'])(*)\$($LocalizationCounterTable['1478'])" - $IOUsageCounterPath = "\$($LocalizationCounterTable['230'])(*)\$($LocalizationCounterTable['1424'])" - $DiskUsageCounterPath = "\$($LocalizationCounterTable['234'])(*)\$($LocalizationCounterTable['212'])" - $NetworkUsageCounterPath = "\$($LocalizationCounterTable['510'])(*)\$($LocalizationCounterTable['388'])" - - # Notify the user that performance metrics are being collected for the specified duration. - Write-Host -Object "Collecting performance metrics for $DurationToPerformTests minutes." - - # Collect performance metrics (CPU, memory, disk, and network usage) at a 60-second interval for the specified duration. - $PerformanceMetrics = Get-Counter -MaxSamples $DurationToPerformTests -SampleInterval 60 -Counter $OverallProcessCounterPath, $OverallMemoryCounterPath, - $ProcessorCounterPath, $MemoryCounterPath, $IOUsageCounterPath, $DiskUsageCounterPath, $NetworkUsageCounterPath -ErrorAction SilentlyContinue -ErrorVariable PerformanceMetricErrors - - # Extract performance metrics for CPU, memory, I/O, disk, and network usage from the collected data. - $OverallProcessorUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match "$([Regex]::Escape($($LocalizationCounterTable['6'])))$" } - $OverallMemoryUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match "$([Regex]::Escape($($LocalizationCounterTable['1406'])))$" } - $ProcessorUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match "$([Regex]::Escape($($LocalizationCounterTable['142'])))$" } - $MemoryUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match "$([Regex]::Escape($($LocalizationCounterTable['1478'])))$" } - $IOUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match "$([Regex]::Escape($($LocalizationCounterTable['1424'])))$" } - $DiskUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match "$([Regex]::Escape($($LocalizationCounterTable['212'])))$" } - $NetworkUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match "$([Regex]::Escape($($LocalizationCounterTable['388'])))$" } - - # If there were errors during the collection of performance metrics, display a warning message for each error. - if ($PerformanceMetricErrors) { - $PerformanceMetricErrors | ForEach-Object { - Write-Warning -Message "$($_.Exception.Message)" - } - } - - # Ensure that performance metrics for CPU, memory, I/O, disk, and network usage were successfully retrieved. - # If any of the metrics are missing, display an error message and exit the script. - if (!$OverallProcessorUsage -or !$OverallMemoryUsage -or !$ProcessorUsage -or !$MemoryUsage -or !$IOUsage -or !$DiskUsage -or !$NetworkUsage) { - Write-Host -Object "[Error] Failed to retrieve performance metrics." - exit 1 - } - - # Retrieve CPU information such as name and clock speed (in GHz). - try { - $CPU = "$(Get-CimInstance -ClassName Win32_Processor -ErrorAction Stop | Select-Object -ExpandProperty Name) $((Get-CimInstance -ClassName Win32_Processor -ErrorAction Stop | Select-Object -ExpandProperty MaxClockSpeed)/1000) GHz" - - # Retrieve the total amount of installed physical memory (RAM) in bytes and convert it to GB. - $TotalMemoryBytes = Get-CimInstance -ClassName Win32_PhysicalMemory -ErrorAction Stop | Measure-Object -Property Capacity -Sum | Select-Object -ExpandProperty Sum - $TotalMemoryGB = "$($TotalMemoryBytes/1GB) GB" - } - catch { - Write-Host -Object "[Error] Unable to get CPU or Memory details." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - - # Display the CPU information. - Write-Host -Object "`n### $CPU ###" - - # Filter and sort the relevant CPU performance metrics for the "_total" instance (overall system usage). - $RelevantMetrics = $OverallProcessorUsage | Where-Object { $_.InstanceName -eq "_total" } | Sort-Object CookedValue - - # Calculate average, minimum, and maximum CPU usage. - $CPUPerformance = [PSCustomObject]@{ - Avg = [math]::Round((($RelevantMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) / $DurationToPerformTests), 2) - Min = [math]::Round(($RelevantMetrics | Select-Object -ExpandProperty CookedValue -First 1), 2) - Max = [math]::Round(($RelevantMetrics | Select-Object -ExpandProperty CookedValue -Last 1), 2) - } - - # Format the CPU performance metrics for display. - $FormattedCPUPerformance = [PSCustomObject]@{ - "CPU Average %" = "$($CPUPerformance.Avg)%" - "CPU Minimum %" = "$($CPUPerformance.Min)%" - "CPU Maximum %" = "$($CPUPerformance.Max)%" - } - - # Display the formatted CPU performance metrics. - ($FormattedCPUPerformance | Format-Table -AutoSize | Out-String).Trim() | Write-Host - - # Display memory usage header. - Write-Host -Object "`n### Memory Usage ###" - Write-Host -Object "Total Memory Installed: $TotalMemoryGB" - - # Filter and sort the relevant memory usage metrics. - $RelevantMetrics = $OverallMemoryUsage | Sort-Object CookedValue - - # Calculate average, minimum, and maximum memory usage. - $MemoryPerformance = [PSCustomObject]@{ - Avg = [math]::Round((($RelevantMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) / $DurationToPerformTests), 2) - Min = [math]::Round(($RelevantMetrics | Select-Object -ExpandProperty CookedValue -First 1), 2) - Max = [math]::Round(($RelevantMetrics | Select-Object -ExpandProperty CookedValue -Last 1), 2) - } - - # Format the memory performance metrics for display. - $OverallMemoryMetrics = [PSCustomObject]@{ - "RAM Average %" = "$($MemoryPerformance.Avg)%" - "RAM Minimum %" = "$($MemoryPerformance.Min)%" - "RAM Maximum %" = "$($MemoryPerformance.Max)%" - } - - # Display the formatted memory performance metrics. - ($OverallMemoryMetrics | Format-Table -AutoSize | Out-String).Trim() | Write-Host - - # Display the header for the top 5 CPU processes. - Write-Host "`n### Top 5 CPU Processes ###" - - # Get a unique list of all process names excluding the "_total" instance. - $AllProcessNames = $ProcessorUsage | Where-Object { $_.InstanceName -ne "_total" } | Sort-Object InstanceName -Unique | Select-Object -ExpandProperty InstanceName - - # Initialize an empty list to store process metrics. - $Processes = New-Object -TypeName System.Collections.Generic.List[object] - - # Loop through each process name to calculate the CPU usage (min, max, avg) for each process. - foreach ($ProcessName in $AllProcessNames) { - $RelevantMetrics = $ProcessorUsage | Where-Object { $_.InstanceName -eq $ProcessName } - - # Group metrics by timestamp and calculate the total CPU usage for each timestamp. - $GroupedMetrics = $RelevantMetrics | Group-Object Timestamp | Select-Object @{Name = "InstanceName"; Expression = { $ProcessName } }, @{Name = "CookedValue"; Expression = { $_.Group | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum } } | Sort-Object CookedValue - - # Add the CPU usage metrics (min, max, avg) for each process to the list. - $Processes.Add( - [PSCustomObject]@{ - "InstanceName" = $ProcessName - "Min" = $GroupedMetrics | Select-Object -ExpandProperty CookedValue -First 1 - "Max" = $GroupedMetrics | Select-Object -ExpandProperty CookedValue -Last 1 - "Avg" = ($GroupedMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) / $DurationToPerformTests - } - ) - } - - # Sort the processes by average CPU usage in descending order and select the top 5. - $Top5CPUProcesses = $Processes | Sort-Object "Avg" -Descending | Select-Object -First 5 - - # Format the top 5 CPU processes for display. - $FormattedProcesses = $Top5CPUProcesses | ForEach-Object { - [PSCustomObject]@{ - "Process Name" = $_.InstanceName - "Average CPU % Used" = "$([math]::Round($_.Avg, 2))%" - "Minimum CPU % Used" = "$([math]::Round($_.Min, 2))%" - "Maximum CPU % Used" = "$([math]::Round($_.Max, 2))%" - } - } - - # Display the formatted CPU process usage metrics. - ($FormattedProcesses | Format-Table -AutoSize | Out-String).Trim() | Write-Host - - # Display the header for the top 5 RAM processes. - Write-Host -Object "`n### Top 5 RAM Processes ###" - - # Get a unique list of process names that are not "_total" or "memory compression". - $AllMemoryProcessNames = $MemoryUsage | Where-Object { $_.InstanceName -ne "_total" -and $_.InstanceName -ne "memory compression" } | Sort-Object InstanceName -Unique | Select-Object -ExpandProperty InstanceName - - # Initialize an empty list to store memory process metrics. - $MemoryProcesses = New-Object -TypeName System.Collections.Generic.List[object] - - # Loop through each process to calculate the memory usage (min, max, avg) for each process. - foreach ($ProcessName in $AllMemoryProcessNames) { - $RelevantMetrics = $MemoryUsage | Where-Object { $_.InstanceName -eq $ProcessName } - - # Group metrics by timestamp and calculate the total memory usage for each timestamp. - $GroupedMetrics = $RelevantMetrics | Group-Object Timestamp | Select-Object @{Name = "InstanceName"; Expression = { $ProcessName } }, @{Name = "CookedValue"; Expression = { $_.Group | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum } } | Sort-Object CookedValue - - # Add the memory usage metrics (min, max, avg) for each process to the list. - $MemoryProcesses.Add( - [PSCustomObject]@{ - "InstanceName" = $ProcessName - "Min" = $GroupedMetrics | Select-Object -ExpandProperty CookedValue -First 1 - "Max" = $GroupedMetrics | Select-Object -ExpandProperty CookedValue -Last 1 - "Avg" = ($GroupedMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) / $DurationToPerformTests - } - ) - } - - # Sort the processes by average memory usage in descending order and select the top 5. - $Top5RAMProcesses = $MemoryProcesses | Sort-Object "Avg" -Descending | Select-Object -First 5 | ForEach-Object { - if (!$TotalMemoryBytes) { - return - } - - [PSCustomObject]@{ - "InstanceName" = $_.InstanceName - "Min" = $_.Min / $TotalMemoryBytes * 100 - "Max" = $_.Max / $TotalMemoryBytes * 100 - "Avg" = $_.Avg / $TotalMemoryBytes * 100 - } - } - - # Format the top 5 RAM processes for display. - $FormattedMemoryProcesses = $Top5RAMProcesses | ForEach-Object { - if (!$TotalMemoryBytes) { - return - } - - [PSCustomObject]@{ - "Process Name" = $_.InstanceName - "Average RAM % Used" = "$([math]::Round($_.Avg, 2))%" - "Minimum RAM % Used" = "$([math]::Round($_.Min, 2))%" - "Maximum RAM % Used" = "$([math]::Round($_.Max, 2))%" - } - } - - # Display the formatted memory process usage metrics. - ($FormattedMemoryProcesses | Format-Table -AutoSize | Out-String).Trim() | Write-Host - - # Display the header for network usage. - Write-Host -Object "`n### Network Usage ###" - - # Get a unique list of network interfaces and initialize an empty list for storing network metrics. - $NetworkInterfaces = $NetworkUsage | Sort-Object InstanceName -Unique | Select-Object -ExpandProperty InstanceName - $NetworkInterfaceUsage = New-Object -TypeName System.Collections.Generic.List[object] - - # Loop through each network interface to calculate the network usage (min, max, avg) for each interface. - foreach ($NetworkInterface in $NetworkInterfaces) { - $RelevantMetrics = $NetworkUsage | Where-Object { $_.InstanceName -eq $NetworkInterface } | Sort-Object CookedValue - - try { - # Correct the network interface name if necessary to match the system's adapter description. - if (!(Get-NetAdapter -ErrorAction Stop | Where-Object { $_.InterfaceDescription -eq $NetworkInterface })) { - $NetworkInterface = $NetworkInterface -replace '\[', '(' -replace '\]', ')' - } - - # Retrieve the network adapter details and determine if it's wired, Wi-Fi, or another type. - $NetAdapter = Get-NetAdapter -ErrorAction Stop | Where-Object { $_.InterfaceDescription -eq $NetworkInterface } | Select-Object -First 1 - switch -Wildcard ($NetAdapter.MediaType) { - "802.3" { $AdapterType = "Wired" } - "*802.11" { $AdapterType = "Wi-Fi" } - default { $AdapterType = "Other" } - } - } - catch { - Write-Host -Object "[Error] Failed to get details on the network interface '$NetworkInterface'." - Write-Host -Object "[Error] $($_.Exception.Message)`n" - $ExitCode = 1 - continue - } - - # Add the network adapter usage metrics to the list. - $NetworkInterfaceUsage.Add( - [PSCustomObject]@{ - "NetworkAdapter" = $NetworkInterface - "MacAddress" = $NetAdapter.MacAddress - "Type" = $AdapterType - "Min" = $RelevantMetrics | Select-Object -ExpandProperty CookedValue -First 1 - "Max" = $RelevantMetrics | Select-Object -ExpandProperty CookedValue -Last 1 - "Avg" = ($RelevantMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) / $DurationToPerformTests - } - ) - } - - # Format the network usage metrics for display. - $FormattedNetworkUsage = $NetworkInterfaceUsage | Sort-Object "Avg" -Descending | ForEach-Object { - [PSCustomObject]@{ - "NetworkAdapter" = $_.NetworkAdapter - "MacAddress" = $_.MacAddress - "Type" = $_.Type - "Average Sent & Received" = "$([math]::Round(($_.Avg / 1MB * 8), 2)) Mbps" - "Minimum Sent & Received" = "$([math]::Round(($_.Min / 1MB * 8), 2)) Mbps" - "Maximum Sent & Received" = "$([math]::Round(($_.Max / 1MB * 8), 2)) Mbps" - } - } - - # Display the formatted network usage metrics. - ($FormattedNetworkUsage | Format-List | Out-String).Trim() | Write-Host - - # Display the header for disk usage. - Write-Host -Object "`n### Disk Usage ###" - - # Get a unique list of relevant disks and initialize an empty list for storing disk metrics. - $RelevantDisks = $DiskUsage | Where-Object { $_.InstanceName -ne "_total" } | Sort-Object InstanceName -Unique | Select-Object -ExpandProperty InstanceName - $DiskMetrics = New-Object -TypeName System.Collections.Generic.List[object] - - try { - $AllDiskNumbers = Get-Partition -ErrorAction Stop | Select-Object -ExpandProperty DiskNumber -Unique - } - catch { - Write-Host -Object "[Error] Unable to retrieve disk numbers." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - - # Loop through each disk to calculate the disk usage (min, max, avg) for each disk. - foreach ($RelevantDisk in $RelevantDisks) { - $RelevantMetrics = $DiskUsage | Where-Object { $_.InstanceName -eq $RelevantDisk } | Sort-Object CookedValue - - # Parse the disk number and drive letter from the instance name. - $DiskNumber = $RelevantDisk -split '\s' | Where-Object { $_ -match "^[0-9]$" } - $DriveLetters = ($RelevantDisk -split '\s' | Where-Object { $_ -match "^[A-z]:$" }) -replace ':' - - # Retrieve the physical disk based on the provided DiskNumber. - $PhysicalDisk = Get-PhysicalDisk -ErrorAction SilentlyContinue | Where-Object { $_.DeviceId -eq $DiskNumber } - - # Check if the disk number is part of the list of all disk numbers. - if ($AllDiskNumbers -and $AllDiskNumbers -notcontains $DiskNumber) { - - # If the physical disk has a FriendlyName (meaning it was found), warn that no partitions were found on this disk. - if ($PhysicalDisk.FriendlyName) { - Write-Warning -Message "No partitions found on disk '$($PhysicalDisk.FriendlyName)'." - } - else { - # If the physical disk has no FriendlyName, display a warning message using the DiskNumber. - Write-Warning -Message "No partitions found on disk '$DiskNumber'." - } - - Write-Host -Object "" - - # Continue to the next iteration in the loop, skipping the remaining code for this disk number. - continue - } - - # Attempt to retrieve the partitions for the specified disk number. - try { - $Partitions = Get-Partition -DiskNumber $DiskNumber -ErrorAction Stop - } - catch { - # If an error occurs while getting the partitions, display an error message. - Write-Host -Object "[Error] Accessing Partitions on disk '$DiskNumber'" - - # Display the exception message from the caught error. - Write-Host -Object "[Error] $($_.Exception.Message)`n" - - # Set the exit code to indicate an error occurred. - $ExitCode = 1 - - # Continue to the next iteration in the loop, skipping further actions for this disk number. - continue - } - - # Retrieve partition information and add the disk usage metrics to the list. - foreach ($DriveLetter in $DriveLetters) { - $Partitions | Where-Object { $_.DriveLetter -eq $DriveLetter } | ForEach-Object { - try { - $FreeSpace = Get-Volume -ErrorAction Stop | Where-Object { $_.DriveLetter -eq $DriveLetter } | Select-Object -ExpandProperty SizeRemaining - $TotalSize = Get-Volume -ErrorAction Stop | Where-Object { $_.DriveLetter -eq $DriveLetter } | Select-Object -ExpandProperty Size - } - catch { - Write-Host -Object "[Error] Unable to determine the total size or free space of drive '$DriveLetter'." - Write-Host -Object "[Error] $($_.Exception.Message)`n" - $ExitCode = 1 - continue - } - - $FreeSpaceGB = [math]::Round(($FreeSpace / 1GB), 2) - $FreeSpacePercent = [math]::Round(($FreeSpace / $TotalSize * 100), 2) - $TotalSpaceGB = [math]::Round(($TotalSize / 1GB), 2) - - # Add the disk metrics to the list. - $DiskMetrics.Add( - [PSCustomObject]@{ - "DriveLetter" = $_.DriveLetter - "FreeSpaceGB" = $FreeSpaceGB - "FreeSpacePercent" = $FreeSpacePercent - "TotalSpace" = "$TotalSpaceGB GB" - "PhysicalDisk" = $PhysicalDisk | Select-Object -ExpandProperty FriendlyName - "MediaType" = $PhysicalDisk | Select-Object -ExpandProperty MediaType - "Min" = $RelevantMetrics | Select-Object -ExpandProperty CookedValue -First 1 - "Max" = $RelevantMetrics | Select-Object -ExpandProperty CookedValue -Last 1 - "Avg" = ($RelevantMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) / $DurationToPerformTests - } - ) - } - } - } - - # Add the disk metrics to the list. - $FormattedDiskMetrics = $DiskMetrics | Sort-Object "Avg" -Descending | ForEach-Object { - [PSCustomObject]@{ - "DriveLetter" = $_.DriveLetter - "FreeSpace" = "$($_.FreeSpaceGB) GB ($($_.FreeSpacePercent)%)" - "TotalSpace" = $_.TotalSpace - "PhysicalDisk" = $_.PhysicalDisk - "MediaType" = $_.MediaType - "Average IOPS" = "$([math]::Round(($_.Avg), 2)) IOPS" - "Minimum IOPS" = "$([math]::Round(($_.Min), 2)) IOPS" - "Maximum IOPS" = "$([math]::Round(($_.Max), 2)) IOPS" - } - } - - # Display the formatted disk usage metrics. - ($FormattedDiskMetrics | Format-Table | Out-String).Trim() | Write-Host - - # Display the header for top 5 I/O processes (network and disk combined). - Write-Host -Object "`n### Top 5 IO Processes (Network & Disk Combined) ###" - - # Get a unique list of I/O process names excluding the "_total" instance. - $AllIOProcessNames = $IOUsage | Where-Object { $_.InstanceName -ne "_total" } | Sort-Object InstanceName -Unique | Select-Object -ExpandProperty InstanceName - $IOProcesses = New-Object -TypeName System.Collections.Generic.List[object] - - # Loop through each process to calculate the I/O usage (min, max, avg) for each process. - foreach ($ProcessName in $AllIOProcessNames) { - $RelevantMetrics = $IOUsage | Where-Object { $_.InstanceName -eq $ProcessName } - - # Group metrics by timestamp and calculate the total I/O usage for each timestamp. - $GroupedMetrics = $RelevantMetrics | Group-Object Timestamp | Select-Object @{Name = "InstanceName"; Expression = { $ProcessName } }, @{Name = "CookedValue"; Expression = { $_.Group | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum } } | Sort-Object CookedValue - - # Add the I/O usage metrics to the list. - $IOProcesses.Add( - [PSCustomObject]@{ - "InstanceName" = $ProcessName - "Min" = $GroupedMetrics | Select-Object -ExpandProperty CookedValue -First 1 - "Max" = $GroupedMetrics | Select-Object -ExpandProperty CookedValue -Last 1 - "Avg" = ($GroupedMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) / $DurationToPerformTests - } - ) - } - - # Sort the I/O processes by average I/O usage and select the top 5. - $Top5IOProcesses = $IOProcesses | Sort-Object "Avg" -Descending | Select-Object -First 5 - - # Format the top 5 I/O processes for display. - $FormattedIOProcesses = $Top5IOProcesses | ForEach-Object { - [PSCustomObject]@{ - "Process Name" = $_.InstanceName - "Average IO Used" = "$([math]::Round(($_.Avg / 1MB * 8), 4)) Mbps" - "Minimum IO Used" = "$([math]::Round(($_.Min / 1MB * 8), 4)) Mbps" - "Maximum IO Used" = "$([math]::Round(($_.Max / 1MB * 8), 4)) Mbps" - } - } - - # Display the formatted I/O process usage metrics. - ($FormattedIOProcesses | Format-Table -AutoSize | Out-String).Trim() | Write-Host - - # Inform the user that WinSAT assessments are running. - Write-Host -Object "`nRetrieving WinSAT assessment data." - Write-Host -Object "More info: https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-8.1-and-8/hh825488(v=win.10)" - - # Retrieve the WinSAT assessment scores. - try { - $WinSatScores = Get-CimInstance -ClassName Win32_WinSAT -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] Unable to retrieve WinSat assessment results." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - - # Handle the different possible states of the WinSAT assessment. - switch ($WinSatScores.WinSATAssessmentState) { - 0 { Write-Host -Object "[Error] WinSAT assessment data is not available on this computer" ; $ExitCode = 1 } - 1 { Write-Host -Object "Successfully retrieved assessment data." } - 2 { Write-Warning -Message "The WinSAT assessment data does not match the current computer configuration." } - 3 { Write-Host -Object "[Error] WinSAT assessment data is not available on this computer" ; $ExitCode = 1 } - 4 { Write-Host -Object "[Error] The WinSAT assessment data is not valid!" ; $ExitCode = 1 } - default { - Write-Host -Object "[Error] WinSAT assessment data is not available on this computer" ; $ExitCode = 1 - } - } - - # If the WinSAT assessment state is valid, display the assessment scores. - $ValidAssessmentStates = "1", "2" - if ($ValidAssessmentStates -contains $WinSatScores.WinSATAssessmentState) { - Write-Host -Object "`n### WinSAT Scores ###" - ($WinSatScores | Format-Table -Property CPUScore, D3DScore, DiskScore, GraphicsScore, MemoryScore | Out-String).Trim() | Write-Host - } - - # If the WYSIWYG custom field is given, proceed to set and format the custom field. - if ($WysiwygCustomField) { - try { - # Inform the user that the custom field is being set. - Write-Host "`nAttempting to set Custom Field '$WysiwygCustomField'." - - $CompletedDateTime = Get-Date - - # Initialize the custom field value as a list of strings. - $CustomFieldValue = New-Object System.Collections.Generic.List[String] - - # Convert the formatted CPU processes table to HTML and add custom formatting. - $CPUProcessMetricTable = $FormattedProcesses | ConvertTo-Html -Fragment - $CPUProcessMetricTable = $CPUProcessMetricTable -replace "", "" -replace "
", "
" - $CPUProcessMetricTable = $CPUProcessMetricTable -replace "Average CPU % Used", "  Average CPU % Used" - $CPUProcessMetricTable = $CPUProcessMetricTable -replace "Minimum CPU % Used", "  Minimum CPU % Used" - $CPUProcessMetricTable = $CPUProcessMetricTable -replace "Maximum CPU % Used", "  Maximum CPU % Used" - - # Highlight rows in the CPU table based on CPU usage thresholds (warnings and danger levels). - $Top5CPUProcesses | ForEach-Object { - if ($_.Avg -ge 20 -and $_.Avg -lt 50) { $CPUProcessMetricTable = $CPUProcessMetricTable -replace "", "" - $RAMProcessMetricTable = $RAMProcessMetricTable -replace "
Top 5 CPU Processes
$($_.InstanceName)", "
$($_.InstanceName)" } - if ($_.Min -ge 20 -and $_.Min -lt 50) { $CPUProcessMetricTable = $CPUProcessMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } - if ($_.Max -ge 20 -and $_.Max -lt 50) { $CPUProcessMetricTable = $CPUProcessMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } - - if ($_.Avg -ge 50) { $CPUProcessMetricTable = $CPUProcessMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } - if ($_.Min -ge 50) { $CPUProcessMetricTable = $CPUProcessMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } - if ($_.Max -ge 50) { $CPUProcessMetricTable = $CPUProcessMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } - } - - # Convert the formatted RAM processes table to HTML and add custom formatting. - $RAMProcessMetricTable = $FormattedMemoryProcesses | ConvertTo-Html -Fragment - $RAMProcessMetricTable = $RAMProcessMetricTable -replace "", "" -replace "
", "
" - $RAMProcessMetricTable = $RAMProcessMetricTable -replace "Average RAM % Used", "  Average RAM % Used" - $RAMProcessMetricTable = $RAMProcessMetricTable -replace "Minimum RAM % Used", "  Minimum RAM % Used" - $RAMProcessMetricTable = $RAMProcessMetricTable -replace "Maximum RAM % Used", "  Maximum RAM % Used" - - # Highlight rows in the RAM table based on RAM usage thresholds (warnings and danger levels). - $Top5RAMProcesses | ForEach-Object { - if ($_.Avg -ge 10 -and $_.Avg -lt 30) { $RAMProcessMetricTable = $RAMProcessMetricTable -replace "", "" - $IOProcessesMetricTable = $IOProcessesMetricTable -replace "
Top 5 RAM Processes
$($_.InstanceName)", "
$($_.InstanceName)" } - if ($_.Min -ge 10 -and $_.Min -lt 30) { $RAMProcessMetricTable = $RAMProcessMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } - if ($_.Max -ge 10 -and $_.Max -lt 30) { $RAMProcessMetricTable = $RAMProcessMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } - - if ($_.Avg -ge 30) { $RAMProcessMetricTable = $RAMProcessMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } - if ($_.Min -ge 30) { $RAMProcessMetricTable = $RAMProcessMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } - if ($_.Max -ge 30) { $RAMProcessMetricTable = $RAMProcessMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } - } - - # Convert the formatted I/O processes table to HTML and add custom formatting. - $IOProcessesMetricTable = $FormattedIOProcesses | ConvertTo-Html -Fragment - $IOProcessesMetricTable = $IOProcessesMetricTable -replace "", "" -replace "
", "
" - $IOProcessesMetricTable = $IOProcessesMetricTable -replace "Average IO Used", "  Average IO Used" - $IOProcessesMetricTable = $IOProcessesMetricTable -replace "Minimum IO Used", "  Minimum IO Used" - $IOProcessesMetricTable = $IOProcessesMetricTable -replace "Maximum IO Used", "  Maximum IO Used" - - # Highlight rows in the I/O table based on I/O usage thresholds (warnings and danger levels). - $Top5IOProcesses | ForEach-Object { - if ($_.Avg -ge 1250000 -and $_.Avg -lt 12500000) { $IOProcessesMetricTable = $IOProcessesMetricTable -replace "", "" - $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "
Top 5 IO Processes (Network & Disk Combined)
$($_.InstanceName)", "
$($_.InstanceName)" } - if ($_.Min -ge 1250000 -and $_.Min -lt 12500000) { $IOProcessesMetricTable = $IOProcessesMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } - if ($_.Max -ge 1250000 -and $_.Max -lt 12500000) { $IOProcessesMetricTable = $IOProcessesMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } - - if ($_.Avg -ge 12500000) { $IOProcessesMetricTable = $IOProcessesMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } - if ($_.Min -ge 12500000) { $IOProcessesMetricTable = $IOProcessesMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } - if ($_.Max -ge 12500000) { $IOProcessesMetricTable = $IOProcessesMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } - } - - # Convert the formatted network usage table to HTML and add custom formatting. - $NetworkUsageMetricTable = $FormattedNetworkUsage | ConvertTo-Html -Fragment - $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "", "" -replace "
", "
" - $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "Average Sent & Received", "  Average Sent & Received" - $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "Minimum Sent & Received", "  Minimum Sent & Received" - $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "Maximum Sent & Received", "  Maximum Sent & Received" - - # Add network type icons for wired, Wi-Fi, and other network interfaces. - $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "", "" - $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "", "" - $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "", "" - $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "", "" - - # Highlight network interfaces based on network usage thresholds and interface types. - $NetworkInterfaceUsage | ForEach-Object { - if ($_.Avg -ge 1250000 -and $_.Avg -lt 12500000) { $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "", "" - $DiskMetricTable = $DiskMetricTable -replace "
Network Usage
Type  TypeWired  WiredWi-Fi  Wi-FiOther  Other
$($_.NetworkAdapter)", "
$($_.NetworkAdapter)" } - if ($_.Min -ge 1250000 -and $_.Min -lt 12500000) { $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "
$($_.NetworkAdapter)", "
$($_.NetworkAdapter)" } - if ($_.Max -ge 1250000 -and $_.Max -lt 12500000) { $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "
$($_.NetworkAdapter)", "
$($_.NetworkAdapter)" } - - if ($_.Avg -ge 12500000) { $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "
$($_.NetworkAdapter)", "
$($_.NetworkAdapter)" } - if ($_.Min -ge 12500000) { $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "
$($_.NetworkAdapter)", "
$($_.NetworkAdapter)" } - if ($_.Max -ge 12500000) { $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "
$($_.NetworkAdapter)", "
$($_.NetworkAdapter)" } - - # Highlight Wi-Fi or "Other" types as warnings. - if ($_.Type -eq "Wi-Fi" -or $_.Type -eq "Other") { - $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "
$($_.NetworkAdapter)", "
$($_.NetworkAdapter)" - } - } - - # Convert the formatted disk usage table to HTML and add custom formatting. - $DiskMetricTable = $FormattedDiskMetrics | ConvertTo-Html -Fragment - $DiskMetricTable = $DiskMetricTable -replace "", "" -replace "
", "
" - $DiskMetricTable = $DiskMetricTable -replace "Average IOPS", "  Average IOPS" - $DiskMetricTable = $DiskMetricTable -replace "Minimum IOPS", "  Minimum IOPS" - $DiskMetricTable = $DiskMetricTable -replace "Maximum IOPS", "  Maximum IOPS" - - # Highlight rows in the disk usage table based on drive type and available space thresholds. - $DiskMetrics | ForEach-Object { - if ($_.MediaType -ne "SSD" -and $_.MediaType -ne "Unspecified") { - $DiskMetricTable = $DiskMetricTable -replace "", "" - $WinSATMetricTable = $WinSATMetricTable -replace "
Disk Usage
$($_.DriveLetter)", "
$($_.DriveLetter)" - } - - if ($_.FreeSpaceGB -lt 100) { - $DiskMetricTable = $DiskMetricTable -replace "
$($_.DriveLetter)", "
$($_.DriveLetter)" - } - - if ($_.FreeSpaceGB -lt 10) { - $DiskMetricTable = $DiskMetricTable -replace "
$($_.DriveLetter)", "
$($_.DriveLetter)" - } - } - - # Handle WinSAT assessment data if it's valid and add the WinSAT scores to the table. - $ValidAssessmentStates = "1", "2" - if ($ValidAssessmentStates -contains $WinSatScores.WinSATAssessmentState) { - $WinSATMetricTable = $WinSatScores | Select-Object -Property CPUScore, D3DScore, DiskScore, GraphicsScore, MemoryScore | ConvertTo-Html -Fragment - $WinSATMetricTable = $WinSATMetricTable -replace "", "" -replace "
", "
" - - # Highlight rows in the WinSAT table based on score thresholds. - if ($WinSatScores.CPUScore -lt 7 -or $WinSatScores.D3DScore -lt 7 -or $WinSatScores.DiskScore -lt 7 -or $WinSatScores.GraphicsScore -lt 7 -or $WinSatScores.MemoryScore -lt 7) { - $WinSATMetricTable = $WinSATMetricTable -replace "", "" - - # Set specific column widths for better presentation. - $EventLogTableMetrics = $EventLogTableMetrics -replace " -
-
  Recent Error Events
-
-
- $NewEventLogTable -
-" - - # Add a truncation notice and the truncated event log card. - $CustomFieldValue.Add("

This info has been truncated to accommodate the 45,000 character limit.

") - $CustomFieldValue.Add($EventLogCard) - - # Check the character count again; repeat if still too long. - $HTMLCharacters = $CustomFieldValue | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters - $ElapsedTime = (Get-Date) - $TrimStart - if ($ElapsedTime.TotalMinutes -ge 5) { - Write-Host -Object "[Error] 5 minute timeout reached. Unable to trim the output to comply with the character limit." - exit 1 - } - }while ($HTMLCharacters -ge 43000) - } - - # Set the custom field with the finalized HTML content. - Set-NinjaProperty -Name $WysiwygCustomField -Value $CustomFieldValue -Type "WYSIWYG" - Write-Host "Successfully set Custom Field '$WysiwygCustomField'!" - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - } - - # If the $NumberOfEvents variable has a value, proceed to display the event logs. - if ($NumberOfEvents) { - # Display a message indicating the number of errors retrieved from the event logs. - Write-Host -Object "`n### Last $NumberOfEvents errors in Application, Security, Setup and System Log. ###" - - # Format and display the collected event logs in a list format. - ($EventLogs | Format-List | Out-String).Trim() | Write-Host - } - - # Try to remove the lock file to ensure no other instance of the script is running. - try { - Remove-Item -Path "$env:ProgramData\NinjaRMMAgent\SystemPerformance.lock.txt" -Force -ErrorAction Stop - } - catch { - # If the removal of the lock file fails, catch the exception and display error messages. - Write-Host -Object "[Error] Failed to remove lock file at '$env:ProgramData\NinjaRMMAgent\SystemPerformance.lock.txt'." - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - - if ($DisplayUserMessage) { - # Arguments for sending a message to notify the user that performance metrics have been recorded. - $MSGArguments = @( - "*" - "/TIME:3600" - "/V" - "Performance metrics have been recorded and forwarded to your IT Administrator." - ) - - # Display an empty line for better readability in the output. - Write-Host -Object "" - - # Generate unique file paths for stdout and stderr logs in the TEMP directory. - $SecondMsgStandardOutLog = "$env:TEMP\$(New-Guid)_2NDMSG_stdout.log" - $SecondMsgStandardErrLog = "$env:TEMP\$(New-Guid)_2NDMSG_stderr.log" - - # Start the process of sending a message to the users using msg.exe. - try { - Write-Host -Object "Sending message to all users." - - # Start the 'msg.exe' process with the provided arguments and capture stdout and stderr into log files. - # -Wait ensures the script waits until the process completes. - # -PassThru returns the process object so that the exit code can be captured. - $SecondMsgProcess = Start-Process -FilePath "$env:SystemRoot\System32\msg.exe" -ArgumentList $MSGArguments -Wait -NoNewWindow -PassThru -RedirectStandardOutput $SecondMsgStandardOutLog -RedirectStandardError $SecondMsgStandardErrLog -ErrorAction Stop - } - catch { - # If the process fails to start, output an error message and exit the script with an error code. - Write-Host -Object "[Error] Failed to send message to all users." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Output the exit code of the msg.exe process. - Write-Host -Object "ExitCode: $($SecondMsgProcess.ExitCode)" - - # If the exit code is non-zero (indicating an error), display an error message. - if ($SecondMsgProcess.ExitCode -ne 0) { - Write-Host -Object "[Error] ExitCode does not indicate success." - } - - # Check if the standard output log file exists. - if (Test-Path -Path $SecondMsgStandardOutLog -ErrorAction SilentlyContinue) { - # Display the contents of the stdout log. - Get-Content -Path $SecondMsgStandardOutLog -Encoding Oem -ErrorAction SilentlyContinue | Write-Host - - # Attempt to delete the stdout log file after displaying its contents. - try { - Remove-Item -Path $SecondMsgStandardOutLog -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] Failed to remove standard output log at '$SecondMsgStandardOutLog'" - exit 1 - } - } - - # Check if the standard error log file exists. - if (Test-Path -Path $SecondMsgStandardErrLog -ErrorAction SilentlyContinue) { - # Read the contents of the stderr log into a variable. - $SecondMessageErrors = Get-Content -Path $SecondMsgStandardErrLog -Encoding Oem -ErrorAction SilentlyContinue - - # Attempt to delete the stderr log file after reading its contents. - try { - Remove-Item -Path $SecondMsgStandardErrLog -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] Failed to remove standard error log at '$SecondMsgStandardErrLog'" - exit 1 - } - } - - # If any errors were found in the stderr log, display them and exit with an error code. - if ($SecondMessageErrors) { - Write-Host -Object "[Error] Sending message to all users." - - # Iterate over each error and display it. - $SecondMessageErrors | ForEach-Object { - Write-Host -Object "[Error] $_" - } - - exit 1 - } - - # If the msg.exe process exit code is non-zero, exit the script with an error code. - if ($SecondMsgProcess.ExitCode -ne 0) { - exit 1 - } - } - - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Collects system performance data (CPU, memory, disk, and network). The results can optionally be saved to a WYSIWYG custom field. +.DESCRIPTION + Collects system performance data (CPU, memory, disk, and network). The results can optionally be saved to a WYSIWYG custom field. +.EXAMPLE + -DaysSinceLastReboot "7" -DurationToPerformTests "5" -NumberOfEvents "5" -WysiwygCustomField "WYSIWYG" -DisplayUserMessage + + Sending message to all users. + ExitCode: 0 + Sending message to session Console, display time 150 + Async message sent to session Console + Sending message to session 31C5CE94259D4006A9E4#0, display time 150 + Async message sent to session 31C5CE94259D4006A9E4#0 + + [Alert] This computer was last started on 8/27/2024 at 5:21 PM which was 11.2 days ago. + + Collecting event logs. + Searching for performance counter localizations. + Collecting performance metrics for 5 minutes. + WARNING: The data in one of the performance counter samples is not valid. View the Status property for each + PerformanceCounterSample object to make sure it contains valid data. + WARNING: The data in one of the performance counter samples is not valid. View the Status property for each + PerformanceCounterSample object to make sure it contains valid data. + WARNING: The data in one of the performance counter samples is not valid. View the Status property for each + PerformanceCounterSample object to make sure it contains valid data. + WARNING: The data in one of the performance counter samples is not valid. View the Status property for each + PerformanceCounterSample object to make sure it contains valid data. + WARNING: The data in one of the performance counter samples is not valid. View the Status property for each + PerformanceCounterSample object to make sure it contains valid data. + + ### 12th Gen Intel(R) Core(TM) i9-12900H 2.918 GHz ### + CPU Average % CPU Minimum % CPU Maximum % + ------------- ------------- ------------- + 1.09% 0.27% 1.89% + + ### Memory Usage ### + Total Memory Installed: 4 GB + RAM Average % RAM Minimum % RAM Maximum % + ------------- ------------- ------------- + 68.51% 68.39% 68.68% + + ### Top 5 CPU Processes ### + Process Name Average CPU % Used Minimum CPU % Used Maximum CPU % Used + ------------ ------------------ ------------------ ------------------ + msmpeng 0.85% 0.13% 1.59% + svchost 0.1% 0.03% 0.21% + mssense 0.09% 0% 0.16% + ninjarmmagent 0.03% 0% 0.05% + teamviewer_service 0.02% 0% 0.03% + + ### Top 5 RAM Processes ### + Process Name Average RAM % Used Minimum RAM % Used Maximum RAM % Used + ------------ ------------------ ------------------ ------------------ + svchost 5.42% 5.37% 5.44% + msmpeng 3.37% 3.3% 3.5% + powershell 1.79% 1.62% 1.91% + mssense 1.61% 1.59% 1.62% + sensendr 0.67% 0.67% 0.67% + + ### Network Usage ### + NetworkAdapter : Ethernet + MacAddress : 00-17-FB-00-00-04 + Type : Wired + Average Sent & Received : 0.01 Mbps + Minimum Sent & Received : 0.01 Mbps + Maximum Sent & Received : 0.02 Mbps + + ### Disk Usage ### + DriveLetter FreeSpace TotalSpace PhysicalDisk MediaType Average IOPS Minimum IOPS Maximum IOPS + ----------- --------- ---------- ------------ --------- ------------ ------------ ------------ + C 27.31 GB (55.2%) 49.47 GB NVMe PC801 NVMe SK hynix 2TB SSD 47.04 IOPS 2.02 IOPS 112.24 IOPS + + ### Top 5 IO Processes (Network & Disk Combined) ### + Process Name Average IO Used Minimum IO Used Maximum IO Used + ------------ --------------- --------------- --------------- + svchost 0.0718 Mbps 0.0027 Mbps 0.1742 Mbps + system 0.0409 Mbps 0.0323 Mbps 0.0502 Mbps + ninjarmmagent 0.0335 Mbps 0.0174 Mbps 0.0464 Mbps + registry 0.0169 Mbps 0.0021 Mbps 0.0521 Mbps + msmpeng 0.015 Mbps 0.0063 Mbps 0.0264 Mbps + + Running WinSAT Assessements. + More info: https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-8.1-and-8/hh825488(v=win.10) + ExitCode: 0 + Retrieving WinSAT assessment data. + Successfully retrieved assessment data. + + ### WinSAT Scores ### + CPUScore D3DScore DiskScore GraphicsScore MemoryScore + -------- -------- --------- ------------- ----------- + 9.1 9.9 9.7 8.3 9.1 + + Attempting to set Custom Field 'WYSIWYG'. + Successfully set Custom Field 'WYSIWYG'! + + ### Last 5 errors in Application, Security, Setup and System Log. ### + LogName : Application + ProviderName : Microsoft-Windows-Security-SPP + Id : 8198 + TimeCreated : 11/18/2024 4:19:19 PM + Message : License Activation (slui.exe) failed with the following error code: + hr=0x80004005 + Command-line arguments: + RuleId=eeba1977-569e-4571-b639-7623d8bfecc0;Action=AutoActivate;AppId=55c92734-d682-4d71-983e-d6ec3f1605 + 9f;SkuId=2de67392-b7a7-462a-b1ca-108dd189f588;NotificationInterval=1440;Trigger=TimerEvent + + LogName : System + ProviderName : Microsoft-Windows-Time-Service + Id : 34 + TimeCreated : 11/18/2024 6:07:36 AM + Message : The time service has detected that the system time needs to be changed by 0 seconds. The time service + will not change the system time by more than 54000 seconds. Verify that your time and time zone are + correct, and that the time source VM IC Time Synchronization Provider is working properly. + + LogName : Application + ProviderName : Microsoft-Windows-Security-SPP + Id : 8198 + TimeCreated : 11/17/2024 4:19:19 PM + Message : License Activation (slui.exe) failed with the following error code: + hr=0x80004005 + Command-line arguments: + RuleId=eeba1977-569e-4571-b639-7623d8bfecc0;Action=AutoActivate;AppId=55c92734-d682-4d71-983e-d6ec3f1605 + 9f;SkuId=2de67392-b7a7-462a-b1ca-108dd189f588;NotificationInterval=1440;Trigger=TimerEvent + + LogName : System + ProviderName : Microsoft-Windows-Time-Service + Id : 34 + TimeCreated : 11/17/2024 10:53:51 AM + Message : The time service has detected that the system time needs to be changed by 0 seconds. The time service + will not change the system time by more than 54000 seconds. Verify that your time and time zone are + correct, and that the time source VM IC Time Synchronization Provider is working properly. + + LogName : Application + ProviderName : Microsoft-Windows-Security-SPP + Id : 8198 + TimeCreated : 11/16/2024 4:19:52 PM + Message : License Activation (slui.exe) failed with the following error code: + hr=0x80004005 + Command-line arguments: + RuleId=eeba1977-569e-4571-b639-7623d8bfecc0;Action=AutoActivate;AppId=55c92734-d682-4d71-983e-d6ec3f1605 + 9f;SkuId=2de67392-b7a7-462a-b1ca-108dd189f588;NotificationInterval=1440;Trigger=TimerEvent + + LogName : Application + ProviderName : Microsoft-Windows-Defrag + Id : 264 + TimeCreated : 11/16/2024 12:09:28 PM + Message : The storage optimizer couldn't complete slab consolidation on System (C:) because: The slab + consolidation operation was aborted because an insufficient number of slabs could be reclaimed (based + on the limits specified in the registry). (0x89000028) + + LogName : Application + ProviderName : Microsoft-Windows-Security-SPP + Id : 8198 + TimeCreated : 11/15/2024 4:09:12 PM + Message : License Activation (slui.exe) failed with the following error code: + hr=0x80004005 + Command-line arguments: + RuleId=eeba1977-569e-4571-b639-7623d8bfecc0;Action=AutoActivate;AppId=55c92734-d682-4d71-983e-d6ec3f1605 + 9f;SkuId=2de67392-b7a7-462a-b1ca-108dd189f588;NotificationInterval=1440;Trigger=UserLogon;SessionId=2 + + LogName : System + ProviderName : Service Control Manager + Id : 7000 + TimeCreated : 11/15/2024 11:58:50 AM + Message : The luafv service failed to start due to the following error: + This driver has been blocked from loading + + LogName : System + ProviderName : Service Control Manager + Id : 7043 + TimeCreated : 11/15/2024 11:58:39 AM + Message : The Windows Defender Advanced Threat Protection Service service did not shut down properly after + receiving a preshutdown control. + + LogName : System + ProviderName : Service Control Manager + Id : 7031 + TimeCreated : 11/15/2024 11:43:58 AM + Message : The Microsoft Intune Management Extension service terminated unexpectedly. It has done this 1 time(s). + The following corrective action will be taken in 60000 milliseconds: Restart the service. + + Sending message to all users. + ExitCode: 0 + Sending message to session Console, display time 3600 + Async message sent to session Console + Sending message to session 31C5CE94259D4006A9E4#0, display time 3600 + Async message sent to session 31C5CE94259D4006A9E4#0 + +PARAMETER: -DisplayUserMessage + Display a message to the end-user informing them that you are collecting performance metrics and that they should not restart the computer. + +PARAMETER: -DaysSinceLastReboot "7" + Specify the number of days by which the system should have been rebooted. + +PARAMETER: -DurationToPerformTests "5" + The duration (in minutes) for which the performance tests should be executed. + +PARAMETER: -NumberOfEvents "5" + The number of error events to retrieve from the Application, Security, Setup, and System event logs. + +PARAMETER: -WysiwygCustomField "ReplaceMeWithAnyWYSIWYGCustomField" + Optionally specify the name of a WYSIWYG custom field to store the formatted performance data. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Removed internet speedtest +#> + +[CmdletBinding()] +param ( + [Parameter()] + $DaysSinceLastReboot, + [Parameter()] + [Float]$DurationToPerformTests = 5, + [Parameter()] + $NumberOfEvents, + [Parameter()] + [String]$WysiwygCustomField, + [Parameter()] + [Switch]$DisplayUserMessage = [System.Convert]::ToBoolean($env:displayUserMessage) +) + +begin { + # If script form variables are used, replace command line parameters with their values. + if ($env:daysSinceLastReboot -and $env:daysSinceLastReboot -notlike "null") { $DaysSinceLastReboot = $env:daysSinceLastReboot } + if ($env:durationToPerformTests -and $env:durationToPerformTests -notlike "null") { $DurationToPerformTests = $env:durationToPerformTests } + if ($env:numberOfEvents -and $env:numberOfEvents -notlike "null") { $NumberOfEvents = $env:numberOfEvents } + if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $WysiwygCustomField = $env:wysiwygCustomFieldName } + + # Validate the 'Days Since Last Reboot' input. + if ($DaysSinceLastReboot) { + try { + $ErrorActionPreference = "Stop" + # Attempt to cast the value to a floating-point number. + $DaysSinceLastReboot = [float]$DaysSinceLastReboot + $ErrorActionPreference = "Continue" + } + catch { + # If the conversion fails, display an error message and exit the script. + Write-Host -Object "[Error] The 'Days Since Last Reboot' value of '$DaysSinceLastReboot' is invalid. Please provide a positive whole number or 0." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + } + + # Ensure the value is a whole number (i.e., not a fraction). + if ($DaysSinceLastReboot -and ($DaysSinceLastReboot % 1) -ne 0) { + Write-Host -Object "[Error] The 'Days Since Last Reboot' value of '$DaysSinceLastReboot' is invalid. Please provide a positive whole number or 0." + exit 1 + } + + # Ensure the value is non-negative (greater than or equal to 0). + if ($DaysSinceLastReboot -and $DaysSinceLastReboot -lt 0) { + Write-Host -Object "[Error] The 'Days Since Last Reboot' value of '$DaysSinceLastReboot' is invalid. Please provide a positive whole number or 0." + exit 1 + } + + # Validate the 'Duration To Perform Tests' input. + if (!$DurationToPerformTests) { + Write-Host -Object "[Error] Please provide the duration for which you would like to perform the tests using the 'Duration To Perform Tests' box." + exit 1 + } + + # Ensure the duration is a whole number (i.e., not a fraction). + if ($DurationToPerformTests -and ($DurationToPerformTests % 1) -ne 0) { + Write-Host -Object "[Error] The 'Duration To Perform Tests' value of '$DurationToPerformTests' is invalid." + Write-Host -Object "[Error] Please provide a positive whole number that's greater than 0 and less than or equal to 60." + exit 1 + } + + # Ensure the duration is between 1 and 60. + if ($DurationToPerformTests -and ($DurationToPerformTests -lt 1 -or $DurationToPerformTests -gt 60)) { + Write-Host -Object "[Error] The 'Duration To Perform Tests' value of '$DurationToPerformTests' is invalid." + Write-Host -Object "[Error] Please provide a positive whole number that's greater than 0 and less than or equal to 60." + exit 1 + } + + # Validate the 'Number of Events' input. + if ($NumberOfEvents) { + try { + $ErrorActionPreference = "Stop" + # Attempt to cast the value to a floating-point number. + $NumberOfEvents = [float]$NumberOfEvents + $ErrorActionPreference = "Continue" + } + catch { + # If the conversion fails, display an error message and exit the script. + Write-Host -Object "[Error] The 'Number of Events' value of '$NumberOfEvents' is invalid. Please provide a positive whole number or 0." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + } + + # Ensure the value is a whole number (i.e., not a fraction). + if ($NumberOfEvents -and ($NumberOfEvents % 1) -ne 0) { + Write-Host -Object "[Error] The 'Number of Events' value of '$NumberOfEvents' is invalid. Please provide a positive whole number or 0." + exit 1 + } + + # Ensure the value is non-negative (greater than or equal to 0). + if ($NumberOfEvents -and $NumberOfEvents -lt 0) { + Write-Host -Object "[Error] The 'Number of Events' value of '$NumberOfEvents' is invalid. Please provide a positive whole number or 0." + exit 1 + } + + function Test-IsServer { + # Determine the method to retrieve the operating system information based on PowerShell version + + try { + $OS = if ($PSVersionTable.PSVersion.Major -lt 3) { + Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop + } + else { + Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop + } + } + catch { + Write-Host -Object "[Error] Failed to identity if this device is a workstation or server." + throw $_ + } + + # Check if the ProductType is "3" or "2", which indicates that the system is a server + if ($OS.ProductType -eq "3" -or $OS.ProductType -eq "2") { + return $true + } + } + + # Check if the script is running on a server. + try { + $IsServer = Test-IsServer + } + catch { + Write-Host -Object "[Error] Unable to identify device type." + Write-Host -Object "[Error] $($_.Exception.Message)`n" + $ExitCode = 1 + } + + if ($IsServer -and $DisplayUserMessage) { + # Attempt to check if the RDS role is installed. + try { + # Retrieve the RDS role feature and check if it is installed. + $RDSRole = Get-WindowsFeature -Name RDS-RD-Server | Where-Object { $_.Installed } + } + catch { + # If an error occurs during the check, output an error message and exit the script. + Write-Host -Object "[Error] Unable to check if the RDS role is installed." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # If the RDS role is installed, output an error message and exit the script. + if ($RDSRole) { + Write-Host -Object "[Error] This script doesn't support sending a message on RDS servers because the message would show for all logged-in users, potentially creating a source of confusion." + exit 1 + } + } + + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + # Utility function for downloading files. + function Invoke-Download { + param( + [Parameter()] + [String]$URL, + [Parameter()] + [String]$Path, + [Parameter()] + [int]$Attempts = 3, + [Parameter()] + [Switch]$SkipSleep + ) + + # Display the URL being used for the download + Write-Host -Object "URL '$URL' was given." + Write-Host -Object "Downloading the file..." + + # Determine the supported TLS versions and set the appropriate security protocol + $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType') + if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12 + } + elseif ( $SupportedTLSversions -contains 'Tls12' ) { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 + } + else { + # Warn the user if TLS 1.2 and 1.3 are not supported, which may cause the download to fail + Write-Warning "TLS 1.2 and/or TLS 1.3 are not supported on this system. This download may fail!" + if ($PSVersionTable.PSVersion.Major -lt 3) { + Write-Warning "PowerShell 2 / .NET 2.0 doesn't support TLS 1.2." + } + } + + # Initialize the attempt counter + $i = 1 + While ($i -le $Attempts) { + # If SkipSleep is not set, wait for a random time between 3 and 15 seconds before each attempt + if (!($SkipSleep)) { + $SleepTime = Get-Random -Minimum 3 -Maximum 15 + Write-Host "Waiting for $SleepTime seconds." + Start-Sleep -Seconds $SleepTime + } + + # Provide a visual break between attempts + if ($i -ne 1) { Write-Host "" } + Write-Host "Download Attempt $i" + + # Temporarily disable progress reporting to speed up script performance + $PreviousProgressPreference = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + try { + if ($PSVersionTable.PSVersion.Major -lt 4) { + # For older versions of PowerShell, use WebClient to download the file + $WebClient = New-Object System.Net.WebClient + $WebClient.DownloadFile($URL, $Path) + } + else { + # For PowerShell 4.0 and above, use Invoke-WebRequest with specified arguments + $WebRequestArgs = @{ + Uri = $URL + OutFile = $Path + MaximumRedirection = 10 + UseBasicParsing = $True + } + + Invoke-WebRequest @WebRequestArgs + } + + # Verify if the file was successfully downloaded + $File = Test-Path -Path $Path -ErrorAction SilentlyContinue + } + catch { + # Handle any errors that occur during the download attempt + Write-Warning "An error has occurred while downloading!" + Write-Warning $_.Exception.Message + + # If the file partially downloaded, delete it to avoid corruption + if (Test-Path -Path $Path -ErrorAction SilentlyContinue) { + Remove-Item $Path -Force -Confirm:$false -ErrorAction SilentlyContinue + } + + $File = $False + } + + # Restore the original progress preference setting + $ProgressPreference = $PreviousProgressPreference + # If the file was successfully downloaded, exit the loop + if ($File) { + $i = $Attempts + } + else { + # Warn the user if the download attempt failed + Write-Warning "File failed to download." + Write-Host "" + } + + # Increment the attempt counter + $i++ + } + + # Final check: if the file still doesn't exist, report an error and exit + if (!(Test-Path $Path)) { + Write-Host -Object "[Error] Failed to download file." + Write-Host -Object "Please verify the URL of '$URL'." + exit 1 + } + else { + # If the download succeeded, return the path to the downloaded file + return $Path + } + } + + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [Switch]$Piped # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + # # Remove the non-breaking space character # Removed NinjaOne dependency + # if ($Type -eq "WYSIWYG") { # Removed NinjaOne dependency + # $Value = $Value -replace ' ', ' ' # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Measure the number of characters in the provided value # Removed NinjaOne dependency + # $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + + # # Throw an error if the value exceeds the character limit of 200,000 characters # Removed NinjaOne dependency + # if ($Piped -and $Characters -ge 200000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if (!$Piped -and $Characters -ge 45000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Initialize a hashtable for additional documentation parameters # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + + # # If a document name is provided, add it to the documentation parameters # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # Define a list of valid field types # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + + # # Warn the user if the provided type is not valid # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # Define types that require options to be retrieved # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + + # # If the property is being set in a document or field and the type needs options, retrieve them # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if there was an issue retrieving the property options # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # Process the property value based on its type # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # Convert the value to a boolean for Checkbox type # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Convert the value to a Unix timestamp for Date or Date Time type # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Convert the dropdown value to its corresponding GUID # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # # Throw an error if the value is not present in the dropdown options # Removed NinjaOne dependency + # if (!($Selection)) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # For other types, use the value as is # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Set the property value in the document if a document name is provided # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # try { # Removed NinjaOne dependency + # # Otherwise, set the standard property value # Removed NinjaOne dependency + # if ($Piped) { # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # catch { # Removed NinjaOne dependency + # Write-Host -Object "[Error] Failed to set custom field." # Removed NinjaOne dependency + # throw $_.Exception.Message # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if setting the property failed # Removed NinjaOne dependency + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + if (!$ExitCode) { + $ExitCode = 0 + } + + $StartedDateTime = Get-Date +} +process { + # Check if the script is being run with elevated (Administrator) privileges. + # If not, display an error message and exit the script. + if (!(Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Check if the lock file exists to prevent multiple instances of the script from running. + # If it exists, read the process ID from the lock file and check if the process is still running. + if (Test-Path -Path "$env:ProgramData\NinjaRMMAgent\SystemPerformance.lock.txt" -ErrorAction SilentlyContinue) { + try { + Write-Host -Object "Process lock file found at '$env:ProgramData\NinjaRMMAgent\SystemPerformance.lock.txt'. Checking if the process is still running." + + # Retrieve the process ID from the lock file. + $OtherScript = Get-Content -Path "$env:ProgramData\NinjaRMMAgent\SystemPerformance.lock.txt" -ErrorAction Stop + + # Check if the process ID exists, indicating the script is already running. + if (Get-Process -Id $OtherScript -ErrorAction SilentlyContinue) { + Write-Host -Object "[Error] This script is already running in another process with the process id (PID) '$OtherScript'." + exit 1 + } + } + catch { + # If there is an error accessing the lock file, display an error message and exit. + Write-Host -Object "[Error] Unable to access the lock file at '$env:ProgramData\NinjaRMMAgent\SystemPerformance.lock.txt'." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + } + + # Attempt to write the current process ID to the lock file, preventing multiple instances of the script from running. + try { + [System.Diagnostics.Process]::GetCurrentProcess().Id | Out-File -FilePath "$env:ProgramData\NinjaRMMAgent\SystemPerformance.lock.txt" -Force -ErrorAction Stop + } + catch { + # If the lock file cannot be created, display an error message and exit. + Write-Host -Object "[Error] Failed to create lock file at '$env:ProgramData\NinjaRMMAgent\SystemPerformance.lock.txt'." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + $TotalMessageTime = $($DurationToPerformTests * 60 / 2) + $TotalCollectionTime = $DurationToPerformTests + + if ($DisplayUserMessage) { + # Define arguments for the 'msg.exe' command to display a system message to the user. + $MSGArguments = @( + "*" + "/TIME:$TotalMessageTime" + "/V" + "System performance metrics are currently being collected. Collection should complete in approximately $TotalCollectionTime minutes and the results will be sent to your IT Administrator. Please do not restart the computer until this collection has completed." + ) + + # Generate unique log file names for capturing the stdout and stderr of the 'msg.exe' process. + $FirstMsgStandardOutLog = "$env:TEMP\$(New-Guid)_1STMSG_stdout.log" + $FirstMsgStandardErrLog = "$env:TEMP\$(New-Guid)_1STMSG_stderr.log" + + + # Attempt to display the system message to all users. + try { + Write-Host -Object "Sending message to all users." + + # Start the 'msg.exe' process with the arguments defined above. + # The process will run in the background without opening a new window (-NoNewWindow). + # Standard output and error will be redirected to log files. + # -Wait ensures the script waits for the process to finish before proceeding. + # -PassThru allows us to capture the process object and access its exit code. + $FirstMsgProcess = Start-Process -FilePath "$env:SystemRoot\System32\msg.exe" -ArgumentList $MSGArguments -Wait -NoNewWindow -PassThru -RedirectStandardOutput $FirstMsgStandardOutLog -RedirectStandardError $FirstMsgStandardErrLog -ErrorAction Stop + } + catch { + # If the 'msg.exe' process fails to start, output an error message and exit with a failure code. + Write-Host -Object "[Error] Failed to send message to all users." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Output the exit code of the 'msg.exe' process. + Write-Host -Object "ExitCode: $($FirstMsgProcess.ExitCode)" + + # If the exit code is non-zero (indicating an error occurred), display an error message. + if ($FirstMsgProcess.ExitCode -ne 0) { + Write-Host -Object "[Error] ExitCode does not indicate success." + } + + # Check if the standard output log file exists. + if (Test-Path -Path $FirstMsgStandardOutLog -ErrorAction SilentlyContinue) { + # Display the contents of the stdout log. + Get-Content -Path $FirstMsgStandardOutLog -Encoding Oem -ErrorAction SilentlyContinue | Write-Host + + try { + # Attempt to delete the stdout log file after displaying its contents. + Remove-Item -Path $FirstMsgStandardOutLog -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] Failed to remove standard output log at '$FirstMsgStandardOutLog'" + exit 1 + } + } + + # Check if the standard error log file exists. + if (Test-Path -Path $FirstMsgStandardErrLog -ErrorAction SilentlyContinue) { + # Read the contents of the stderr log into a variable. + $FirstMessageErrors = Get-Content -Path $FirstMsgStandardErrLog -Encoding Oem -ErrorAction SilentlyContinue + + # Attempt to delete the stderr log file after reading its contents. + try { + Remove-Item -Path $FirstMsgStandardErrLog -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] Failed to remove standard error log at '$FirstMsgStandardErrLog'" + exit 1 + } + } + + # If there were any errors captured in the stderr log, display them and exit with an error code. + if ($FirstMessageErrors) { + Write-Host -Object "[Error] Sending message to all users." + + $FirstMsgStandardErrLog | ForEach-Object { + Write-Host -Object "[Error] $_" + } + + exit 1 + } + + # If the 'msg.exe' process exit code is non-zero, exit the script with an error code. + if ($FirstMsgProcess.ExitCode -ne 0) { + exit 1 + } + } + + Write-Host -Object "" + + # Get the last reboot time of the system. + try { + $LastStartTime = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop | Select-Object -ExpandProperty LastBootUpTime + } + catch { + Write-Host -Object "[Error] Failed to get last start up time." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + + # If the 'DaysSinceLastReboot' parameter is set, calculate the time difference since the last reboot. + if ($DaysSinceLastReboot -ge 0) { + $TimeDifference = New-TimeSpan -Start $LastStartTime -End (Get-Date) + + # If the time since the last reboot exceeds the limit, display an alert to the user. + if ($TimeDifference.TotalDays -gt $DaysSinceLastReboot) { + Write-Host -Object "[Alert] This computer was last started on $($LastStartTime.ToShortDateString()) at $($LastStartTime.ToShortTimeString()) which was $([math]::Round($TimeDifference.TotalDays,2)) days ago." + $ExceededLastStartupLimit = $True + } + } + + # Initialize an empty list to store event logs. + $EventLogs = New-Object System.Collections.Generic.List[object] + + # Define XML queries for Application, Security, Setup, and System event logs that have error level events (Level=2). + [xml]$ApplicationXML = @" + + + + + +"@ + + [xml]$SecurityLogs = @" + + + + + +"@ + + [xml]$SetupLogs = @" + + + + + +"@ + + [xml]$SystemLogs = @" + + + + + +"@ + + # If the 'NumberOfEvents' parameter is set, collect the specified number of error logs from each log category. + if ($NumberOfEvents) { + Write-Host -Object "`nCollecting event logs." + + # Collect logs from each category and store them in the EventLogs list. + Get-WinEvent -MaxEvents $NumberOfEvents -FilterXml $ApplicationXML -ErrorAction SilentlyContinue -ErrorVariable EventLogErrors | ForEach-Object { $EventLogs.Add($_) } + Get-WinEvent -MaxEvents $NumberOfEvents -FilterXml $SecurityLogs -ErrorAction SilentlyContinue -ErrorVariable EventLogErrors | ForEach-Object { $EventLogs.Add($_) } + Get-WinEvent -MaxEvents $NumberOfEvents -FilterXml $SetupLogs -ErrorAction SilentlyContinue -ErrorVariable EventLogErrors | ForEach-Object { $EventLogs.Add($_) } + Get-WinEvent -MaxEvents $NumberOfEvents -FilterXml $SystemLogs -ErrorAction SilentlyContinue -ErrorVariable EventLogErrors | ForEach-Object { $EventLogs.Add($_) } + + # If any errors occurred during log collection, display warnings with the error details. + if ($EventLogErrors) { + $EventLogErrors | ForEach-Object { + Write-Warning -Message "$($_.Exception.Message)" + } + } + + # If no error logs were found, display a warning message. + if ($EventLogs.Count -eq 0) { + Write-Warning -Message "No error events were found in the event log." + } + else { + $EventLogs = $EventLogs | Select-Object LogName, ProviderName, Id, TimeCreated, Message | Sort-Object -Property TimeCreated -Descending + } + } + + # Display a message to the user indicating the start of the search for performance counter localizations. + Write-Host -Object "Searching for performance counter localizations." + + # Attempt to retrieve the "Counter" property from the CurrentLanguage registry key, which contains the localized performance counter names. + # If the retrieval fails, catch the error, display an error message, and exit the script. + try { + $CurrentLanguageKey = Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\CurrentLanguage" -Name "Counter" -ErrorAction Stop | Select-Object -ExpandProperty Counter + } + catch { + Write-Host -Object "[Error] Failed to retrieve performance counter localizations." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Initialize an empty hash table to store the performance counter localizations. + $LocalizationCounterTable = @{} + + # Loop through the array of performance counters in the registry. + # The counter array consists of alternating key-value pairs (even indexes are keys, odd indexes are values), + # so this loop increments by 2 to match each key with its corresponding value. + for ($i = 0; $i -lt $CurrentLanguageKey.Length; $i += 2) { + $LocalizationCounterTable[$CurrentLanguageKey[$i]] = $CurrentLanguageKey[$i + 1] + } + + # Define the paths for various performance counters using the localized counter names from the hash table. + # These paths are dynamically created by retrieving the localized names for each counter ID. + $OverallProcessCounterPath = "\$($LocalizationCounterTable['238'])(*)\$($LocalizationCounterTable['6'])" + $OverallMemoryCounterPath = "\$($LocalizationCounterTable['4'])\$($LocalizationCounterTable['1406'])" + $ProcessorCounterPath = "\$($LocalizationCounterTable['230'])(*)\$($LocalizationCounterTable['142'])" + $MemoryCounterPath = "\$($LocalizationCounterTable['230'])(*)\$($LocalizationCounterTable['1478'])" + $IOUsageCounterPath = "\$($LocalizationCounterTable['230'])(*)\$($LocalizationCounterTable['1424'])" + $DiskUsageCounterPath = "\$($LocalizationCounterTable['234'])(*)\$($LocalizationCounterTable['212'])" + $NetworkUsageCounterPath = "\$($LocalizationCounterTable['510'])(*)\$($LocalizationCounterTable['388'])" + + # Notify the user that performance metrics are being collected for the specified duration. + Write-Host -Object "Collecting performance metrics for $DurationToPerformTests minutes." + + # Collect performance metrics (CPU, memory, disk, and network usage) at a 60-second interval for the specified duration. + $PerformanceMetrics = Get-Counter -MaxSamples $DurationToPerformTests -SampleInterval 60 -Counter $OverallProcessCounterPath, $OverallMemoryCounterPath, + $ProcessorCounterPath, $MemoryCounterPath, $IOUsageCounterPath, $DiskUsageCounterPath, $NetworkUsageCounterPath -ErrorAction SilentlyContinue -ErrorVariable PerformanceMetricErrors + + # Extract performance metrics for CPU, memory, I/O, disk, and network usage from the collected data. + $OverallProcessorUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match "$([Regex]::Escape($($LocalizationCounterTable['6'])))$" } + $OverallMemoryUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match "$([Regex]::Escape($($LocalizationCounterTable['1406'])))$" } + $ProcessorUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match "$([Regex]::Escape($($LocalizationCounterTable['142'])))$" } + $MemoryUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match "$([Regex]::Escape($($LocalizationCounterTable['1478'])))$" } + $IOUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match "$([Regex]::Escape($($LocalizationCounterTable['1424'])))$" } + $DiskUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match "$([Regex]::Escape($($LocalizationCounterTable['212'])))$" } + $NetworkUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match "$([Regex]::Escape($($LocalizationCounterTable['388'])))$" } + + # If there were errors during the collection of performance metrics, display a warning message for each error. + if ($PerformanceMetricErrors) { + $PerformanceMetricErrors | ForEach-Object { + Write-Warning -Message "$($_.Exception.Message)" + } + } + + # Ensure that performance metrics for CPU, memory, I/O, disk, and network usage were successfully retrieved. + # If any of the metrics are missing, display an error message and exit the script. + if (!$OverallProcessorUsage -or !$OverallMemoryUsage -or !$ProcessorUsage -or !$MemoryUsage -or !$IOUsage -or !$DiskUsage -or !$NetworkUsage) { + Write-Host -Object "[Error] Failed to retrieve performance metrics." + exit 1 + } + + # Retrieve CPU information such as name and clock speed (in GHz). + try { + $CPU = "$(Get-CimInstance -ClassName Win32_Processor -ErrorAction Stop | Select-Object -ExpandProperty Name) $((Get-CimInstance -ClassName Win32_Processor -ErrorAction Stop | Select-Object -ExpandProperty MaxClockSpeed)/1000) GHz" + + # Retrieve the total amount of installed physical memory (RAM) in bytes and convert it to GB. + $TotalMemoryBytes = Get-CimInstance -ClassName Win32_PhysicalMemory -ErrorAction Stop | Measure-Object -Property Capacity -Sum | Select-Object -ExpandProperty Sum + $TotalMemoryGB = "$($TotalMemoryBytes/1GB) GB" + } + catch { + Write-Host -Object "[Error] Unable to get CPU or Memory details." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + + # Display the CPU information. + Write-Host -Object "`n### $CPU ###" + + # Filter and sort the relevant CPU performance metrics for the "_total" instance (overall system usage). + $RelevantMetrics = $OverallProcessorUsage | Where-Object { $_.InstanceName -eq "_total" } | Sort-Object CookedValue + + # Calculate average, minimum, and maximum CPU usage. + $CPUPerformance = [PSCustomObject]@{ + Avg = [math]::Round((($RelevantMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) / $DurationToPerformTests), 2) + Min = [math]::Round(($RelevantMetrics | Select-Object -ExpandProperty CookedValue -First 1), 2) + Max = [math]::Round(($RelevantMetrics | Select-Object -ExpandProperty CookedValue -Last 1), 2) + } + + # Format the CPU performance metrics for display. + $FormattedCPUPerformance = [PSCustomObject]@{ + "CPU Average %" = "$($CPUPerformance.Avg)%" + "CPU Minimum %" = "$($CPUPerformance.Min)%" + "CPU Maximum %" = "$($CPUPerformance.Max)%" + } + + # Display the formatted CPU performance metrics. + ($FormattedCPUPerformance | Format-Table -AutoSize | Out-String).Trim() | Write-Host + + # Display memory usage header. + Write-Host -Object "`n### Memory Usage ###" + Write-Host -Object "Total Memory Installed: $TotalMemoryGB" + + # Filter and sort the relevant memory usage metrics. + $RelevantMetrics = $OverallMemoryUsage | Sort-Object CookedValue + + # Calculate average, minimum, and maximum memory usage. + $MemoryPerformance = [PSCustomObject]@{ + Avg = [math]::Round((($RelevantMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) / $DurationToPerformTests), 2) + Min = [math]::Round(($RelevantMetrics | Select-Object -ExpandProperty CookedValue -First 1), 2) + Max = [math]::Round(($RelevantMetrics | Select-Object -ExpandProperty CookedValue -Last 1), 2) + } + + # Format the memory performance metrics for display. + $OverallMemoryMetrics = [PSCustomObject]@{ + "RAM Average %" = "$($MemoryPerformance.Avg)%" + "RAM Minimum %" = "$($MemoryPerformance.Min)%" + "RAM Maximum %" = "$($MemoryPerformance.Max)%" + } + + # Display the formatted memory performance metrics. + ($OverallMemoryMetrics | Format-Table -AutoSize | Out-String).Trim() | Write-Host + + # Display the header for the top 5 CPU processes. + Write-Host "`n### Top 5 CPU Processes ###" + + # Get a unique list of all process names excluding the "_total" instance. + $AllProcessNames = $ProcessorUsage | Where-Object { $_.InstanceName -ne "_total" } | Sort-Object InstanceName -Unique | Select-Object -ExpandProperty InstanceName + + # Initialize an empty list to store process metrics. + $Processes = New-Object -TypeName System.Collections.Generic.List[object] + + # Loop through each process name to calculate the CPU usage (min, max, avg) for each process. + foreach ($ProcessName in $AllProcessNames) { + $RelevantMetrics = $ProcessorUsage | Where-Object { $_.InstanceName -eq $ProcessName } + + # Group metrics by timestamp and calculate the total CPU usage for each timestamp. + $GroupedMetrics = $RelevantMetrics | Group-Object Timestamp | Select-Object @{Name = "InstanceName"; Expression = { $ProcessName } }, @{Name = "CookedValue"; Expression = { $_.Group | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum } } | Sort-Object CookedValue + + # Add the CPU usage metrics (min, max, avg) for each process to the list. + $Processes.Add( + [PSCustomObject]@{ + "InstanceName" = $ProcessName + "Min" = $GroupedMetrics | Select-Object -ExpandProperty CookedValue -First 1 + "Max" = $GroupedMetrics | Select-Object -ExpandProperty CookedValue -Last 1 + "Avg" = ($GroupedMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) / $DurationToPerformTests + } + ) + } + + # Sort the processes by average CPU usage in descending order and select the top 5. + $Top5CPUProcesses = $Processes | Sort-Object "Avg" -Descending | Select-Object -First 5 + + # Format the top 5 CPU processes for display. + $FormattedProcesses = $Top5CPUProcesses | ForEach-Object { + [PSCustomObject]@{ + "Process Name" = $_.InstanceName + "Average CPU % Used" = "$([math]::Round($_.Avg, 2))%" + "Minimum CPU % Used" = "$([math]::Round($_.Min, 2))%" + "Maximum CPU % Used" = "$([math]::Round($_.Max, 2))%" + } + } + + # Display the formatted CPU process usage metrics. + ($FormattedProcesses | Format-Table -AutoSize | Out-String).Trim() | Write-Host + + # Display the header for the top 5 RAM processes. + Write-Host -Object "`n### Top 5 RAM Processes ###" + + # Get a unique list of process names that are not "_total" or "memory compression". + $AllMemoryProcessNames = $MemoryUsage | Where-Object { $_.InstanceName -ne "_total" -and $_.InstanceName -ne "memory compression" } | Sort-Object InstanceName -Unique | Select-Object -ExpandProperty InstanceName + + # Initialize an empty list to store memory process metrics. + $MemoryProcesses = New-Object -TypeName System.Collections.Generic.List[object] + + # Loop through each process to calculate the memory usage (min, max, avg) for each process. + foreach ($ProcessName in $AllMemoryProcessNames) { + $RelevantMetrics = $MemoryUsage | Where-Object { $_.InstanceName -eq $ProcessName } + + # Group metrics by timestamp and calculate the total memory usage for each timestamp. + $GroupedMetrics = $RelevantMetrics | Group-Object Timestamp | Select-Object @{Name = "InstanceName"; Expression = { $ProcessName } }, @{Name = "CookedValue"; Expression = { $_.Group | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum } } | Sort-Object CookedValue + + # Add the memory usage metrics (min, max, avg) for each process to the list. + $MemoryProcesses.Add( + [PSCustomObject]@{ + "InstanceName" = $ProcessName + "Min" = $GroupedMetrics | Select-Object -ExpandProperty CookedValue -First 1 + "Max" = $GroupedMetrics | Select-Object -ExpandProperty CookedValue -Last 1 + "Avg" = ($GroupedMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) / $DurationToPerformTests + } + ) + } + + # Sort the processes by average memory usage in descending order and select the top 5. + $Top5RAMProcesses = $MemoryProcesses | Sort-Object "Avg" -Descending | Select-Object -First 5 | ForEach-Object { + if (!$TotalMemoryBytes) { + return + } + + [PSCustomObject]@{ + "InstanceName" = $_.InstanceName + "Min" = $_.Min / $TotalMemoryBytes * 100 + "Max" = $_.Max / $TotalMemoryBytes * 100 + "Avg" = $_.Avg / $TotalMemoryBytes * 100 + } + } + + # Format the top 5 RAM processes for display. + $FormattedMemoryProcesses = $Top5RAMProcesses | ForEach-Object { + if (!$TotalMemoryBytes) { + return + } + + [PSCustomObject]@{ + "Process Name" = $_.InstanceName + "Average RAM % Used" = "$([math]::Round($_.Avg, 2))%" + "Minimum RAM % Used" = "$([math]::Round($_.Min, 2))%" + "Maximum RAM % Used" = "$([math]::Round($_.Max, 2))%" + } + } + + # Display the formatted memory process usage metrics. + ($FormattedMemoryProcesses | Format-Table -AutoSize | Out-String).Trim() | Write-Host + + # Display the header for network usage. + Write-Host -Object "`n### Network Usage ###" + + # Get a unique list of network interfaces and initialize an empty list for storing network metrics. + $NetworkInterfaces = $NetworkUsage | Sort-Object InstanceName -Unique | Select-Object -ExpandProperty InstanceName + $NetworkInterfaceUsage = New-Object -TypeName System.Collections.Generic.List[object] + + # Loop through each network interface to calculate the network usage (min, max, avg) for each interface. + foreach ($NetworkInterface in $NetworkInterfaces) { + $RelevantMetrics = $NetworkUsage | Where-Object { $_.InstanceName -eq $NetworkInterface } | Sort-Object CookedValue + + try { + # Correct the network interface name if necessary to match the system's adapter description. + if (!(Get-NetAdapter -ErrorAction Stop | Where-Object { $_.InterfaceDescription -eq $NetworkInterface })) { + $NetworkInterface = $NetworkInterface -replace '\[', '(' -replace '\]', ')' + } + + # Retrieve the network adapter details and determine if it's wired, Wi-Fi, or another type. + $NetAdapter = Get-NetAdapter -ErrorAction Stop | Where-Object { $_.InterfaceDescription -eq $NetworkInterface } | Select-Object -First 1 + switch -Wildcard ($NetAdapter.MediaType) { + "802.3" { $AdapterType = "Wired" } + "*802.11" { $AdapterType = "Wi-Fi" } + default { $AdapterType = "Other" } + } + } + catch { + Write-Host -Object "[Error] Failed to get details on the network interface '$NetworkInterface'." + Write-Host -Object "[Error] $($_.Exception.Message)`n" + $ExitCode = 1 + continue + } + + # Add the network adapter usage metrics to the list. + $NetworkInterfaceUsage.Add( + [PSCustomObject]@{ + "NetworkAdapter" = $NetworkInterface + "MacAddress" = $NetAdapter.MacAddress + "Type" = $AdapterType + "Min" = $RelevantMetrics | Select-Object -ExpandProperty CookedValue -First 1 + "Max" = $RelevantMetrics | Select-Object -ExpandProperty CookedValue -Last 1 + "Avg" = ($RelevantMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) / $DurationToPerformTests + } + ) + } + + # Format the network usage metrics for display. + $FormattedNetworkUsage = $NetworkInterfaceUsage | Sort-Object "Avg" -Descending | ForEach-Object { + [PSCustomObject]@{ + "NetworkAdapter" = $_.NetworkAdapter + "MacAddress" = $_.MacAddress + "Type" = $_.Type + "Average Sent & Received" = "$([math]::Round(($_.Avg / 1MB * 8), 2)) Mbps" + "Minimum Sent & Received" = "$([math]::Round(($_.Min / 1MB * 8), 2)) Mbps" + "Maximum Sent & Received" = "$([math]::Round(($_.Max / 1MB * 8), 2)) Mbps" + } + } + + # Display the formatted network usage metrics. + ($FormattedNetworkUsage | Format-List | Out-String).Trim() | Write-Host + + # Display the header for disk usage. + Write-Host -Object "`n### Disk Usage ###" + + # Get a unique list of relevant disks and initialize an empty list for storing disk metrics. + $RelevantDisks = $DiskUsage | Where-Object { $_.InstanceName -ne "_total" } | Sort-Object InstanceName -Unique | Select-Object -ExpandProperty InstanceName + $DiskMetrics = New-Object -TypeName System.Collections.Generic.List[object] + + try { + $AllDiskNumbers = Get-Partition -ErrorAction Stop | Select-Object -ExpandProperty DiskNumber -Unique + } + catch { + Write-Host -Object "[Error] Unable to retrieve disk numbers." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + + # Loop through each disk to calculate the disk usage (min, max, avg) for each disk. + foreach ($RelevantDisk in $RelevantDisks) { + $RelevantMetrics = $DiskUsage | Where-Object { $_.InstanceName -eq $RelevantDisk } | Sort-Object CookedValue + + # Parse the disk number and drive letter from the instance name. + $DiskNumber = $RelevantDisk -split '\s' | Where-Object { $_ -match "^[0-9]$" } + $DriveLetters = ($RelevantDisk -split '\s' | Where-Object { $_ -match "^[A-z]:$" }) -replace ':' + + # Retrieve the physical disk based on the provided DiskNumber. + $PhysicalDisk = Get-PhysicalDisk -ErrorAction SilentlyContinue | Where-Object { $_.DeviceId -eq $DiskNumber } + + # Check if the disk number is part of the list of all disk numbers. + if ($AllDiskNumbers -and $AllDiskNumbers -notcontains $DiskNumber) { + + # If the physical disk has a FriendlyName (meaning it was found), warn that no partitions were found on this disk. + if ($PhysicalDisk.FriendlyName) { + Write-Warning -Message "No partitions found on disk '$($PhysicalDisk.FriendlyName)'." + } + else { + # If the physical disk has no FriendlyName, display a warning message using the DiskNumber. + Write-Warning -Message "No partitions found on disk '$DiskNumber'." + } + + Write-Host -Object "" + + # Continue to the next iteration in the loop, skipping the remaining code for this disk number. + continue + } + + # Attempt to retrieve the partitions for the specified disk number. + try { + $Partitions = Get-Partition -DiskNumber $DiskNumber -ErrorAction Stop + } + catch { + # If an error occurs while getting the partitions, display an error message. + Write-Host -Object "[Error] Accessing Partitions on disk '$DiskNumber'" + + # Display the exception message from the caught error. + Write-Host -Object "[Error] $($_.Exception.Message)`n" + + # Set the exit code to indicate an error occurred. + $ExitCode = 1 + + # Continue to the next iteration in the loop, skipping further actions for this disk number. + continue + } + + # Retrieve partition information and add the disk usage metrics to the list. + foreach ($DriveLetter in $DriveLetters) { + $Partitions | Where-Object { $_.DriveLetter -eq $DriveLetter } | ForEach-Object { + try { + $FreeSpace = Get-Volume -ErrorAction Stop | Where-Object { $_.DriveLetter -eq $DriveLetter } | Select-Object -ExpandProperty SizeRemaining + $TotalSize = Get-Volume -ErrorAction Stop | Where-Object { $_.DriveLetter -eq $DriveLetter } | Select-Object -ExpandProperty Size + } + catch { + Write-Host -Object "[Error] Unable to determine the total size or free space of drive '$DriveLetter'." + Write-Host -Object "[Error] $($_.Exception.Message)`n" + $ExitCode = 1 + continue + } + + $FreeSpaceGB = [math]::Round(($FreeSpace / 1GB), 2) + $FreeSpacePercent = [math]::Round(($FreeSpace / $TotalSize * 100), 2) + $TotalSpaceGB = [math]::Round(($TotalSize / 1GB), 2) + + # Add the disk metrics to the list. + $DiskMetrics.Add( + [PSCustomObject]@{ + "DriveLetter" = $_.DriveLetter + "FreeSpaceGB" = $FreeSpaceGB + "FreeSpacePercent" = $FreeSpacePercent + "TotalSpace" = "$TotalSpaceGB GB" + "PhysicalDisk" = $PhysicalDisk | Select-Object -ExpandProperty FriendlyName + "MediaType" = $PhysicalDisk | Select-Object -ExpandProperty MediaType + "Min" = $RelevantMetrics | Select-Object -ExpandProperty CookedValue -First 1 + "Max" = $RelevantMetrics | Select-Object -ExpandProperty CookedValue -Last 1 + "Avg" = ($RelevantMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) / $DurationToPerformTests + } + ) + } + } + } + + # Add the disk metrics to the list. + $FormattedDiskMetrics = $DiskMetrics | Sort-Object "Avg" -Descending | ForEach-Object { + [PSCustomObject]@{ + "DriveLetter" = $_.DriveLetter + "FreeSpace" = "$($_.FreeSpaceGB) GB ($($_.FreeSpacePercent)%)" + "TotalSpace" = $_.TotalSpace + "PhysicalDisk" = $_.PhysicalDisk + "MediaType" = $_.MediaType + "Average IOPS" = "$([math]::Round(($_.Avg), 2)) IOPS" + "Minimum IOPS" = "$([math]::Round(($_.Min), 2)) IOPS" + "Maximum IOPS" = "$([math]::Round(($_.Max), 2)) IOPS" + } + } + + # Display the formatted disk usage metrics. + ($FormattedDiskMetrics | Format-Table | Out-String).Trim() | Write-Host + + # Display the header for top 5 I/O processes (network and disk combined). + Write-Host -Object "`n### Top 5 IO Processes (Network & Disk Combined) ###" + + # Get a unique list of I/O process names excluding the "_total" instance. + $AllIOProcessNames = $IOUsage | Where-Object { $_.InstanceName -ne "_total" } | Sort-Object InstanceName -Unique | Select-Object -ExpandProperty InstanceName + $IOProcesses = New-Object -TypeName System.Collections.Generic.List[object] + + # Loop through each process to calculate the I/O usage (min, max, avg) for each process. + foreach ($ProcessName in $AllIOProcessNames) { + $RelevantMetrics = $IOUsage | Where-Object { $_.InstanceName -eq $ProcessName } + + # Group metrics by timestamp and calculate the total I/O usage for each timestamp. + $GroupedMetrics = $RelevantMetrics | Group-Object Timestamp | Select-Object @{Name = "InstanceName"; Expression = { $ProcessName } }, @{Name = "CookedValue"; Expression = { $_.Group | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum } } | Sort-Object CookedValue + + # Add the I/O usage metrics to the list. + $IOProcesses.Add( + [PSCustomObject]@{ + "InstanceName" = $ProcessName + "Min" = $GroupedMetrics | Select-Object -ExpandProperty CookedValue -First 1 + "Max" = $GroupedMetrics | Select-Object -ExpandProperty CookedValue -Last 1 + "Avg" = ($GroupedMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) / $DurationToPerformTests + } + ) + } + + # Sort the I/O processes by average I/O usage and select the top 5. + $Top5IOProcesses = $IOProcesses | Sort-Object "Avg" -Descending | Select-Object -First 5 + + # Format the top 5 I/O processes for display. + $FormattedIOProcesses = $Top5IOProcesses | ForEach-Object { + [PSCustomObject]@{ + "Process Name" = $_.InstanceName + "Average IO Used" = "$([math]::Round(($_.Avg / 1MB * 8), 4)) Mbps" + "Minimum IO Used" = "$([math]::Round(($_.Min / 1MB * 8), 4)) Mbps" + "Maximum IO Used" = "$([math]::Round(($_.Max / 1MB * 8), 4)) Mbps" + } + } + + # Display the formatted I/O process usage metrics. + ($FormattedIOProcesses | Format-Table -AutoSize | Out-String).Trim() | Write-Host + + # Inform the user that WinSAT assessments are running. + Write-Host -Object "`nRetrieving WinSAT assessment data." + Write-Host -Object "More info: https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-8.1-and-8/hh825488(v=win.10)" + + # Retrieve the WinSAT assessment scores. + try { + $WinSatScores = Get-CimInstance -ClassName Win32_WinSAT -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] Unable to retrieve WinSat assessment results." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + + # Handle the different possible states of the WinSAT assessment. + switch ($WinSatScores.WinSATAssessmentState) { + 0 { Write-Host -Object "[Error] WinSAT assessment data is not available on this computer" ; $ExitCode = 1 } + 1 { Write-Host -Object "Successfully retrieved assessment data." } + 2 { Write-Warning -Message "The WinSAT assessment data does not match the current computer configuration." } + 3 { Write-Host -Object "[Error] WinSAT assessment data is not available on this computer" ; $ExitCode = 1 } + 4 { Write-Host -Object "[Error] The WinSAT assessment data is not valid!" ; $ExitCode = 1 } + default { + Write-Host -Object "[Error] WinSAT assessment data is not available on this computer" ; $ExitCode = 1 + } + } + + # If the WinSAT assessment state is valid, display the assessment scores. + $ValidAssessmentStates = "1", "2" + if ($ValidAssessmentStates -contains $WinSatScores.WinSATAssessmentState) { + Write-Host -Object "`n### WinSAT Scores ###" + ($WinSatScores | Format-Table -Property CPUScore, D3DScore, DiskScore, GraphicsScore, MemoryScore | Out-String).Trim() | Write-Host + } + + # If the WYSIWYG custom field is given, proceed to set and format the custom field. + if ($WysiwygCustomField) { + try { + # Inform the user that the custom field is being set. + Write-Host "`nAttempting to set Custom Field '$WysiwygCustomField'." + + $CompletedDateTime = Get-Date + + # Initialize the custom field value as a list of strings. + $CustomFieldValue = New-Object System.Collections.Generic.List[String] + + # Convert the formatted CPU processes table to HTML and add custom formatting. + $CPUProcessMetricTable = $FormattedProcesses | ConvertTo-Html -Fragment + $CPUProcessMetricTable = $CPUProcessMetricTable -replace "", "" + $CPUProcessMetricTable = $CPUProcessMetricTable -replace "
WinSAT Scores
", "
" - } - - if ($WinSatScores.CPUScore -lt 4 -or $WinSatScores.D3DScore -lt 4 -or $WinSatScores.DiskScore -lt 4 -or $WinSatScores.GraphicsScore -lt 4 -or $WinSatScores.MemoryScore -lt 4) { - $WinSATMetricTable = $WinSATMetricTable -replace "
", "
" - } - } - else { - # If WinSAT data is not available, display a message. - $WinSATMetricTable = "

The WinSAT assessment data is either invalid or not available for this computer.

" - } - - # Create the HTML content for the performance metrics section. - $HTMLCard = "
-
-
  System Performance Metrics
-
-
- - - - - - - -
-

Start Date and Time
$($StartedDateTime.ToShortDateString()) $($StartedDateTime.ToShortTimeString())

-
-

Completed Date and Time
$($CompletedDateTime.ToShortDateString()) $($CompletedDateTime.ToShortTimeString())

-
-

Last Startup Time
$($LastStartTime.ToShortDateString()) $($LastStartTime.ToShortTimeString())

-

$CPU

- - - - - - - - -
-
-
$($FormattedCPUPerformance."CPU Average %")
-
  Average CPU % Used
-
-
-
-
$($FormattedCPUPerformance."CPU Minimum %")
-
  Minimum CPU % Used
-
-
-
-
$($FormattedCPUPerformance."CPU Maximum %")
-
  Maximum CPU % Used
-
-
-

Total Memory: $TotalMemoryGB

- - - - - - - - -
-
-
$($OverallMemoryMetrics."RAM Average %")
-
  Average RAM % Used
-
-
-
-
$($OverallMemoryMetrics."RAM Minimum %")
-
  Minimum RAM % Used
-
-
-
-
$($OverallMemoryMetrics."RAM Maximum %")
-
  Maximum RAM % Used
-
-
- $CPUProcessMetricTable -
- $RAMProcessMetricTable -
- $NetworkUsageMetricTable -
- $DiskMetricTable -
- $IOProcessesMetricTable - $(if($ValidAssessmentStates -notcontains $WinSatScores.WinSATAssessmentState) {"

WinSAT Scores

"}) - $WinSATMetricTable -
-
" - # Modify the last startup time section based on whether the startup limit was exceeded or not. - if ($ExceededLastStartupLimit) { - $HTMLCard = $HTMLCard -replace "id='lastStartup' class='card-text'>Last Startup Time
$($LastStartTime.ToShortDateString()) $($LastStartTime.ToShortTimeString())", "id='lastStartup' class='card-text'>Last Startup Time
$($LastStartTime.ToShortDateString()) $($LastStartTime.ToShortTimeString())  " - } - elseif ($DaysSinceLastReboot -ge 0) { - $HTMLCard = $HTMLCard -replace "id='lastStartup' class='card-text'>Last Startup Time
$($LastStartTime.ToShortDateString()) $($LastStartTime.ToShortTimeString())", "id='lastStartup' class='card-text'>Last Startup Time
$($LastStartTime.ToShortDateString()) $($LastStartTime.ToShortTimeString())  " - } - - # Highlight CPU performance metrics based on threshold values (color coding). - if ($CPUPerformance.Avg -ge 60 -and $CPUPerformance.Avg -lt 90) { $HTMLCard = $HTMLCard -replace "id='cpuOverallAvg' style='color: #008001;'", "id='cpuOverallAvg' style='color: #FAC905;'" } - if ($CPUPerformance.Min -ge 60 -and $CPUPerformance.Min -lt 90) { $HTMLCard = $HTMLCard -replace "id='cpuOverallMin' style='color: #008001;'", "id='cpuOverallMin' style='color: #FAC905;'" } - if ($CPUPerformance.Max -ge 60 -and $CPUPerformance.Max -lt 90) { $HTMLCard = $HTMLCard -replace "id='cpuOverallMax' style='color: #008001;'", "id='cpuOverallMax' style='color: #FAC905;'" } - - if ($CPUPerformance.Avg -ge 90) { $HTMLCard = $HTMLCard -replace "id='cpuOverallAvg' style='color: #008001;'", "id='cpuOverallAvg' style='color: #D53948;'" } - if ($CPUPerformance.Min -ge 90) { $HTMLCard = $HTMLCard -replace "id='cpuOverallMin' style='color: #008001;'", "id='cpuOverallMin' style='color: #D53948;'" } - if ($CPUPerformance.Max -ge 90) { $HTMLCard = $HTMLCard -replace "id='cpuOverallMax' style='color: #008001;'", "id='cpuOverallMax' style='color: #D53948;'" } - - # Highlight RAM performance metrics based on threshold values (color coding). - if ($MemoryPerformance.Avg -ge 60 -and $MemoryPerformance.Avg -lt 90) { $HTMLCard = $HTMLCard -replace "id='ramOverallAvg' style='color: #008001;'", "id='ramOverallAvg' style='color: #FAC905;'" } - if ($MemoryPerformance.Min -ge 60 -and $MemoryPerformance.Min -lt 90) { $HTMLCard = $HTMLCard -replace "id='ramOverallMin' style='color: #008001;'", "id='ramOverallMin' style='color: #FAC905;'" } - if ($MemoryPerformance.Max -ge 60 -and $MemoryPerformance.Max -lt 90) { $HTMLCard = $HTMLCard -replace "id='ramOverallMax' style='color: #008001;'", "id='ramOverallMax' style='color: #FAC905;'" } - - if ($MemoryPerformance.Avg -ge 90) { $HTMLCard = $HTMLCard -replace "id='ramOverallAvg' style='color: #008001;'", "id='ramOverallAvg' style='color: #D53948;'" } - if ($MemoryPerformance.Min -ge 90) { $HTMLCard = $HTMLCard -replace "id='ramOverallMin' style='color: #008001;'", "id='ramOverallMin' style='color: #D53948;'" } - if ($MemoryPerformance.Max -ge 90) { $HTMLCard = $HTMLCard -replace "id='ramOverallMax' style='color: #008001;'", "id='ramOverallMax' style='color: #D53948;'" } - - # Add the created HTML card to the custom field. - $CustomFieldValue.Add($HTMLCard) - - # Check if there are any event logs to display. - if ($NumberOfEvents -gt 0 -and $EventLogs.Count -gt 0) { - # Convert the event logs into an HTML fragment for displaying in the output. - $EventLogTableMetrics = $EventLogs | ConvertTo-Html -Fragment - - # Apply custom styles to the HTML table headers. - $EventLogTableMetrics = $EventLogTableMetrics -replace "
", "" -replace "LogName", "Log Name" - $EventLogTableMetrics = $EventLogTableMetrics -replace "ProviderName", "Provider Name" - $EventLogTableMetrics = $EventLogTableMetrics -replace "Id", "Id" - $EventLogTableMetrics = $EventLogTableMetrics -replace "TimeCreated", "Time Created" - } - elseif ($NumberOfEvents -gt 0) { - # If no events were found, display a message instead of the table. - $EventLogTableMetrics = "

No error events were found in the event log.

" - } - - # If event logs exist, create a card to display them. - if ($NumberOfEvents -gt 0) { - # Create the HTML structure for the event log card. - $EventLogCard = "
-
-
  Recent Error Events
-
-
- $EventLogTableMetrics -
-
" - # Add the event log card to the custom field value. - $CustomFieldValue.Add($EventLogCard) - } - - # Check if the HTML content exceeds the character limit (45,000 characters). - $HTMLCharacters = $CustomFieldValue | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($HTMLCharacters -ge 43000) { - Write-Host -Object "The current character count is '$HTMLCharacters'." - Write-Warning "45,000 Character Limit has been reached! Trimming output until the character limit is satisfied..." - - # Truncate the output if it exceeds the limit. - $i = 0 - $Attempts = 0 - [array]$NewEventLogTable = $EventLogTableMetrics - $TrimStart = Get-Date - do { - # Recreate the custom field output - $CustomFieldValue = New-Object System.Collections.Generic.List[string] - if (!$NumberOfEvents -or !$NumberOfEvents -gt 0 -or !$EventLogs.Count -gt 0) { - Write-Host -Object "[Error] No events to trim." - exit 1 - } - - # Add the main performance metrics card to the custom field. - $CustomFieldValue.Add($HTMLCard) - - # Reverse the event log array so that the last entry is at the top. - [array]::Reverse($NewEventLogTable) - # Delete rows until the character count is reduced. - if ($NewEventLogTable[$i] -match '
' -or $NewEventLogTable[$i] -match '
", "" -replace "
", "
" + $CPUProcessMetricTable = $CPUProcessMetricTable -replace "Average CPU % Used", "  Average CPU % Used" + $CPUProcessMetricTable = $CPUProcessMetricTable -replace "Minimum CPU % Used", "  Minimum CPU % Used" + $CPUProcessMetricTable = $CPUProcessMetricTable -replace "Maximum CPU % Used", "  Maximum CPU % Used" + + # Highlight rows in the CPU table based on CPU usage thresholds (warnings and danger levels). + $Top5CPUProcesses | ForEach-Object { + if ($_.Avg -ge 20 -and $_.Avg -lt 50) { $CPUProcessMetricTable = $CPUProcessMetricTable -replace "", "" + $RAMProcessMetricTable = $RAMProcessMetricTable -replace "
Top 5 CPU Processes
$($_.InstanceName)", "
$($_.InstanceName)" } + if ($_.Min -ge 20 -and $_.Min -lt 50) { $CPUProcessMetricTable = $CPUProcessMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } + if ($_.Max -ge 20 -and $_.Max -lt 50) { $CPUProcessMetricTable = $CPUProcessMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } + + if ($_.Avg -ge 50) { $CPUProcessMetricTable = $CPUProcessMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } + if ($_.Min -ge 50) { $CPUProcessMetricTable = $CPUProcessMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } + if ($_.Max -ge 50) { $CPUProcessMetricTable = $CPUProcessMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } + } + + # Convert the formatted RAM processes table to HTML and add custom formatting. + $RAMProcessMetricTable = $FormattedMemoryProcesses | ConvertTo-Html -Fragment + $RAMProcessMetricTable = $RAMProcessMetricTable -replace "", "" -replace "
", "
" + $RAMProcessMetricTable = $RAMProcessMetricTable -replace "Average RAM % Used", "  Average RAM % Used" + $RAMProcessMetricTable = $RAMProcessMetricTable -replace "Minimum RAM % Used", "  Minimum RAM % Used" + $RAMProcessMetricTable = $RAMProcessMetricTable -replace "Maximum RAM % Used", "  Maximum RAM % Used" + + # Highlight rows in the RAM table based on RAM usage thresholds (warnings and danger levels). + $Top5RAMProcesses | ForEach-Object { + if ($_.Avg -ge 10 -and $_.Avg -lt 30) { $RAMProcessMetricTable = $RAMProcessMetricTable -replace "", "" + $IOProcessesMetricTable = $IOProcessesMetricTable -replace "
Top 5 RAM Processes
$($_.InstanceName)", "
$($_.InstanceName)" } + if ($_.Min -ge 10 -and $_.Min -lt 30) { $RAMProcessMetricTable = $RAMProcessMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } + if ($_.Max -ge 10 -and $_.Max -lt 30) { $RAMProcessMetricTable = $RAMProcessMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } + + if ($_.Avg -ge 30) { $RAMProcessMetricTable = $RAMProcessMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } + if ($_.Min -ge 30) { $RAMProcessMetricTable = $RAMProcessMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } + if ($_.Max -ge 30) { $RAMProcessMetricTable = $RAMProcessMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } + } + + # Convert the formatted I/O processes table to HTML and add custom formatting. + $IOProcessesMetricTable = $FormattedIOProcesses | ConvertTo-Html -Fragment + $IOProcessesMetricTable = $IOProcessesMetricTable -replace "", "" -replace "
", "
" + $IOProcessesMetricTable = $IOProcessesMetricTable -replace "Average IO Used", "  Average IO Used" + $IOProcessesMetricTable = $IOProcessesMetricTable -replace "Minimum IO Used", "  Minimum IO Used" + $IOProcessesMetricTable = $IOProcessesMetricTable -replace "Maximum IO Used", "  Maximum IO Used" + + # Highlight rows in the I/O table based on I/O usage thresholds (warnings and danger levels). + $Top5IOProcesses | ForEach-Object { + if ($_.Avg -ge 1250000 -and $_.Avg -lt 12500000) { $IOProcessesMetricTable = $IOProcessesMetricTable -replace "", "" + $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "
Top 5 IO Processes (Network & Disk Combined)
$($_.InstanceName)", "
$($_.InstanceName)" } + if ($_.Min -ge 1250000 -and $_.Min -lt 12500000) { $IOProcessesMetricTable = $IOProcessesMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } + if ($_.Max -ge 1250000 -and $_.Max -lt 12500000) { $IOProcessesMetricTable = $IOProcessesMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } + + if ($_.Avg -ge 12500000) { $IOProcessesMetricTable = $IOProcessesMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } + if ($_.Min -ge 12500000) { $IOProcessesMetricTable = $IOProcessesMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } + if ($_.Max -ge 12500000) { $IOProcessesMetricTable = $IOProcessesMetricTable -replace "
$($_.InstanceName)", "
$($_.InstanceName)" } + } + + # Convert the formatted network usage table to HTML and add custom formatting. + $NetworkUsageMetricTable = $FormattedNetworkUsage | ConvertTo-Html -Fragment + $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "", "" -replace "
", "
" + $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "Average Sent & Received", "  Average Sent & Received" + $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "Minimum Sent & Received", "  Minimum Sent & Received" + $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "Maximum Sent & Received", "  Maximum Sent & Received" + + # Add network type icons for wired, Wi-Fi, and other network interfaces. + $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "", "" + $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "", "" + $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "", "" + $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "", "" + + # Highlight network interfaces based on network usage thresholds and interface types. + $NetworkInterfaceUsage | ForEach-Object { + if ($_.Avg -ge 1250000 -and $_.Avg -lt 12500000) { $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "", "" + $DiskMetricTable = $DiskMetricTable -replace "
Network Usage
Type  TypeWired  WiredWi-Fi  Wi-FiOther  Other
$($_.NetworkAdapter)", "
$($_.NetworkAdapter)" } + if ($_.Min -ge 1250000 -and $_.Min -lt 12500000) { $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "
$($_.NetworkAdapter)", "
$($_.NetworkAdapter)" } + if ($_.Max -ge 1250000 -and $_.Max -lt 12500000) { $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "
$($_.NetworkAdapter)", "
$($_.NetworkAdapter)" } + + if ($_.Avg -ge 12500000) { $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "
$($_.NetworkAdapter)", "
$($_.NetworkAdapter)" } + if ($_.Min -ge 12500000) { $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "
$($_.NetworkAdapter)", "
$($_.NetworkAdapter)" } + if ($_.Max -ge 12500000) { $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "
$($_.NetworkAdapter)", "
$($_.NetworkAdapter)" } + + # Highlight Wi-Fi or "Other" types as warnings. + if ($_.Type -eq "Wi-Fi" -or $_.Type -eq "Other") { + $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace "
$($_.NetworkAdapter)", "
$($_.NetworkAdapter)" + } + } + + # Convert the formatted disk usage table to HTML and add custom formatting. + $DiskMetricTable = $FormattedDiskMetrics | ConvertTo-Html -Fragment + $DiskMetricTable = $DiskMetricTable -replace "", "" -replace "
", "
" + $DiskMetricTable = $DiskMetricTable -replace "Average IOPS", "  Average IOPS" + $DiskMetricTable = $DiskMetricTable -replace "Minimum IOPS", "  Minimum IOPS" + $DiskMetricTable = $DiskMetricTable -replace "Maximum IOPS", "  Maximum IOPS" + + # Highlight rows in the disk usage table based on drive type and available space thresholds. + $DiskMetrics | ForEach-Object { + if ($_.MediaType -ne "SSD" -and $_.MediaType -ne "Unspecified") { + $DiskMetricTable = $DiskMetricTable -replace "", "" + $WinSATMetricTable = $WinSATMetricTable -replace "
Disk Usage
$($_.DriveLetter)", "
$($_.DriveLetter)" + } + + if ($_.FreeSpaceGB -lt 100) { + $DiskMetricTable = $DiskMetricTable -replace "
$($_.DriveLetter)", "
$($_.DriveLetter)" + } + + if ($_.FreeSpaceGB -lt 10) { + $DiskMetricTable = $DiskMetricTable -replace "
$($_.DriveLetter)", "
$($_.DriveLetter)" + } + } + + # Handle WinSAT assessment data if it's valid and add the WinSAT scores to the table. + $ValidAssessmentStates = "1", "2" + if ($ValidAssessmentStates -contains $WinSatScores.WinSATAssessmentState) { + $WinSATMetricTable = $WinSatScores | Select-Object -Property CPUScore, D3DScore, DiskScore, GraphicsScore, MemoryScore | ConvertTo-Html -Fragment + $WinSATMetricTable = $WinSATMetricTable -replace "", "" -replace "
", "
" + + # Highlight rows in the WinSAT table based on score thresholds. + if ($WinSatScores.CPUScore -lt 7 -or $WinSatScores.D3DScore -lt 7 -or $WinSatScores.DiskScore -lt 7 -or $WinSatScores.GraphicsScore -lt 7 -or $WinSatScores.MemoryScore -lt 7) { + $WinSATMetricTable = $WinSATMetricTable -replace "", "" + + # Set specific column widths for better presentation. + $EventLogTableMetrics = $EventLogTableMetrics -replace " +
+
  Recent Error Events
+
+
+ $NewEventLogTable +
+" + + # Add a truncation notice and the truncated event log card. + $CustomFieldValue.Add("

This info has been truncated to accommodate the 45,000 character limit.

") + $CustomFieldValue.Add($EventLogCard) + + # Check the character count again; repeat if still too long. + $HTMLCharacters = $CustomFieldValue | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters + $ElapsedTime = (Get-Date) - $TrimStart + if ($ElapsedTime.TotalMinutes -ge 5) { + Write-Host -Object "[Error] 5 minute timeout reached. Unable to trim the output to comply with the character limit." + exit 1 + } + }while ($HTMLCharacters -ge 43000) + } + + # Set the custom field with the finalized HTML content. + # Set-NinjaProperty -Name $WysiwygCustomField -Value $CustomFieldValue -Type "WYSIWYG" # Removed NinjaOne dependency + Write-Host "Successfully set Custom Field '$WysiwygCustomField'!" + } + catch { + Write-Host "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + } + + # If the $NumberOfEvents variable has a value, proceed to display the event logs. + if ($NumberOfEvents) { + # Display a message indicating the number of errors retrieved from the event logs. + Write-Host -Object "`n### Last $NumberOfEvents errors in Application, Security, Setup and System Log. ###" + + # Format and display the collected event logs in a list format. + ($EventLogs | Format-List | Out-String).Trim() | Write-Host + } + + # Try to remove the lock file to ensure no other instance of the script is running. + try { + Remove-Item -Path "$env:ProgramData\NinjaRMMAgent\SystemPerformance.lock.txt" -Force -ErrorAction Stop + } + catch { + # If the removal of the lock file fails, catch the exception and display error messages. + Write-Host -Object "[Error] Failed to remove lock file at '$env:ProgramData\NinjaRMMAgent\SystemPerformance.lock.txt'." + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + + if ($DisplayUserMessage) { + # Arguments for sending a message to notify the user that performance metrics have been recorded. + $MSGArguments = @( + "*" + "/TIME:3600" + "/V" + "Performance metrics have been recorded and forwarded to your IT Administrator." + ) + + # Display an empty line for better readability in the output. + Write-Host -Object "" + + # Generate unique file paths for stdout and stderr logs in the TEMP directory. + $SecondMsgStandardOutLog = "$env:TEMP\$(New-Guid)_2NDMSG_stdout.log" + $SecondMsgStandardErrLog = "$env:TEMP\$(New-Guid)_2NDMSG_stderr.log" + + # Start the process of sending a message to the users using msg.exe. + try { + Write-Host -Object "Sending message to all users." + + # Start the 'msg.exe' process with the provided arguments and capture stdout and stderr into log files. + # -Wait ensures the script waits until the process completes. + # -PassThru returns the process object so that the exit code can be captured. + $SecondMsgProcess = Start-Process -FilePath "$env:SystemRoot\System32\msg.exe" -ArgumentList $MSGArguments -Wait -NoNewWindow -PassThru -RedirectStandardOutput $SecondMsgStandardOutLog -RedirectStandardError $SecondMsgStandardErrLog -ErrorAction Stop + } + catch { + # If the process fails to start, output an error message and exit the script with an error code. + Write-Host -Object "[Error] Failed to send message to all users." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Output the exit code of the msg.exe process. + Write-Host -Object "ExitCode: $($SecondMsgProcess.ExitCode)" + + # If the exit code is non-zero (indicating an error), display an error message. + if ($SecondMsgProcess.ExitCode -ne 0) { + Write-Host -Object "[Error] ExitCode does not indicate success." + } + + # Check if the standard output log file exists. + if (Test-Path -Path $SecondMsgStandardOutLog -ErrorAction SilentlyContinue) { + # Display the contents of the stdout log. + Get-Content -Path $SecondMsgStandardOutLog -Encoding Oem -ErrorAction SilentlyContinue | Write-Host + + # Attempt to delete the stdout log file after displaying its contents. + try { + Remove-Item -Path $SecondMsgStandardOutLog -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] Failed to remove standard output log at '$SecondMsgStandardOutLog'" + exit 1 + } + } + + # Check if the standard error log file exists. + if (Test-Path -Path $SecondMsgStandardErrLog -ErrorAction SilentlyContinue) { + # Read the contents of the stderr log into a variable. + $SecondMessageErrors = Get-Content -Path $SecondMsgStandardErrLog -Encoding Oem -ErrorAction SilentlyContinue + + # Attempt to delete the stderr log file after reading its contents. + try { + Remove-Item -Path $SecondMsgStandardErrLog -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] Failed to remove standard error log at '$SecondMsgStandardErrLog'" + exit 1 + } + } + + # If any errors were found in the stderr log, display them and exit with an error code. + if ($SecondMessageErrors) { + Write-Host -Object "[Error] Sending message to all users." + + # Iterate over each error and display it. + $SecondMessageErrors | ForEach-Object { + Write-Host -Object "[Error] $_" + } + + exit 1 + } + + # If the msg.exe process exit code is non-zero, exit the script with an error code. + if ($SecondMsgProcess.ExitCode -ne 0) { + exit 1 + } + } + + exit $ExitCode +} +end { + + + +} diff --git a/Powershell Scripts/System Restore - Check Status.ps1 b/Powershell Scripts/System Restore - Check Status.ps1 index 1f41387..a69df72 100644 --- a/Powershell Scripts/System Restore - Check Status.ps1 +++ b/Powershell Scripts/System Restore - Check Status.ps1 @@ -1,154 +1,154 @@ # Checks the status of System Restore on the device. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Checks the status of System Restore on the device. -.DESCRIPTION - Checks the status of System Restore on the device. - When a Custom Field is specified the results will be saved to the Custom Field as "Enabled" or "Disabled". - -.EXAMPLE - (No Parameters) - ## EXAMPLE OUTPUT WITHOUT PARAMS ## - [Info] System Restore is Disabled - -PARAMETER: -CustomFieldName "SystemRestore" - Saves the results to a custom field. -.EXAMPLE - -CustomFieldName "SystemRestore" - ## EXAMPLE OUTPUT WITH CustomFieldName ## - [Info] Attempting to set Custom Field 'SystemRestore'. - [Info] Successfully set Custom Field 'SystemRestore'! - [Info] System Restore is Enabled - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Added description to script variable 'Custom Field Name' -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$CustomFieldName -) - -begin { - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 10000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") - } - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to be set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received it will have an exception property, the function will exit with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below types require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - if ($env:customFieldName -and $env:customFieldName -ne "null") { - $CustomFieldName = $env:customFieldName - } -} -process { - # If the registry value is 1, System Restore is enabled. - $RegValue = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRestore\" -Name "RPSessionInterval" -ErrorAction SilentlyContinue - - $SystemRestoreStatus = if ($RegValue -ge 1) { - # If either of the above conditions are met, System Restore is enabled. - Write-Output "Enabled" - } - else { - Write-Output "Disabled" - } - - # If a Custom Field Name is provided, set the Custom Field with the System Restore Status. - if ($CustomFieldName) { - try { - Write-Host "[Info] Attempting to set Custom Field '$CustomFieldName'." - Set-NinjaProperty -Name $CustomFieldName -Value $SystemRestoreStatus - Write-Host "[Info] Successfully set Custom Field '$CustomFieldName'!" - } - catch { - Write-Host "[Error] Failed to set Custom Field '$CustomFieldName'." - } - } - Write-Host "[Info] System Restore is $SystemRestoreStatus" -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Checks the status of System Restore on the device. +.DESCRIPTION + Checks the status of System Restore on the device. + When a Custom Field is specified the results will be saved to the Custom Field as "Enabled" or "Disabled". + +.EXAMPLE + (No Parameters) + ## EXAMPLE OUTPUT WITHOUT PARAMS ## + [Info] System Restore is Disabled + +PARAMETER: -CustomFieldName "SystemRestore" + Saves the results to a custom field. +.EXAMPLE + -CustomFieldName "SystemRestore" + ## EXAMPLE OUTPUT WITH CustomFieldName ## + [Info] Attempting to set Custom Field 'SystemRestore'. + [Info] Successfully set Custom Field 'SystemRestore'! + [Info] System Restore is Enabled + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Added description to script variable 'Custom Field Name' +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$CustomFieldName +) + +begin { + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + # if ($Characters -ge 10000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If we're requested to set the field value for a Ninja document we'll specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # The field below requires additional information to be set # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If an error is received it will have an exception property, the function will exit with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # The below types require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # if (-not $Selection) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # We'll need to set the field differently depending on if its a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + if ($env:customFieldName -and $env:customFieldName -ne "null") { + $CustomFieldName = $env:customFieldName + } +} +process { + # If the registry value is 1, System Restore is enabled. + $RegValue = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRestore\" -Name "RPSessionInterval" -ErrorAction SilentlyContinue + + $SystemRestoreStatus = if ($RegValue -ge 1) { + # If either of the above conditions are met, System Restore is enabled. + Write-Output "Enabled" + } + else { + Write-Output "Disabled" + } + + # If a Custom Field Name is provided, set the Custom Field with the System Restore Status. + if ($CustomFieldName) { + try { + Write-Host "[Info] Attempting to set Custom Field '$CustomFieldName'." + # Set-NinjaProperty -Name $CustomFieldName -Value $SystemRestoreStatus # Removed NinjaOne dependency + Write-Host "[Info] Successfully set Custom Field '$CustomFieldName'!" + } + catch { + Write-Host "[Error] Failed to set Custom Field '$CustomFieldName'." + } + } + Write-Host "[Info] System Restore is $SystemRestoreStatus" +} +end { + + + +} diff --git a/Powershell Scripts/System Restore - Restore Point Report.ps1 b/Powershell Scripts/System Restore - Restore Point Report.ps1 index 7036336..f9b06b1 100644 --- a/Powershell Scripts/System Restore - Restore Point Report.ps1 +++ b/Powershell Scripts/System Restore - Restore Point Report.ps1 @@ -1,234 +1,234 @@ # Reports the status of System Restore on the system. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Reports the status of System Restore on the system. -.DESCRIPTION - Reports the status of System Restore on the system. - The report can be saved to a WYSIWYG Custom Field. - -.EXAMPLE - (No Parameters) - ## EXAMPLE OUTPUT WITHOUT PARAMS ## - [Info] System Restore Points Found! - - CreationTime Description SequenceNumber EventType RestorePointType - ------------ ----------- -------------- --------- ---------------- - 5/7/2024 3:51:11 PM Test Description 1 A system change has begun. An application has been installed. - 5/7/2024 3:53:11 PM Test Description 2 A system change has completed. An application has been uninstalled. - 5/7/2024 3:54:11 PM Test Description 3 A system change has begun and is nested. An application needs to delete the restore point it created. - 5/7/2024 3:59:11 PM Test Description 4 A system change has completed and is nested. A device driver has been installed. - -PARAMETER: -WysiwygCustomField "ReplaceMeWithAnyWysiwygCustomField" - Reports the status of System Restore on the system. -.EXAMPLE - -WysiwygCustomField "ReplaceMeWithAnyWysiwygCustomField" - ## EXAMPLE OUTPUT WITH WysiwygCustomField ## - [Info] System Restore Points Found! - - CreationTime Description SequenceNumber EventType RestorePointType - ------------ ----------- -------------- --------- ---------------- - 5/7/2024 3:51:11 PM Test Description 1 A system change has begun. An application has been installed. - 5/7/2024 3:53:11 PM Test Description 2 A system change has completed. An application has been uninstalled. - 5/7/2024 3:54:11 PM Test Description 3 A system change has begun and is nested. An application needs to delete the restore point it created. - 5/7/2024 3:59:11 PM Test Description 4 A system change has completed and is nested. A device driver has been installed. - - - [Info] Attempting to set Custom Field 'ReplaceMeWithAnyWysiwygCustomField'. - [Info] Successfully set Custom Field 'ReplaceMeWithAnyWysiwygCustomField'. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$WysiwygCustomField -) - -begin { - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 10000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") - } - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to be set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received it will have an exception property, the function will exit with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below types require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - # Splatting the properties to be used in the Select-Object command - $RestorePointSplatting = @{ - Property = @{ Label = "CreationTime"; Expression = { - # Convert the CreationTime to a DateTime object from example: '20240507155911.581535-000' - $CreationTime = $_.CreationTime -split "\." - [DateTime]::ParseExact($CreationTime[0], "yyyyMMddHHmmss", $null) - } - }, - @{ Label = "Description"; Expression = { $_.Description } }, - @{ Label = "SequenceNumber"; Expression = { $_.SequenceNumber } }, - @{ - Label = "EventType" - Expression = { - # Event Type IDs: https://learn.microsoft.com/en-us/windows/win32/sr/systemrestore - $ET = $_.EventType - switch ($ET) { - 100 { "Begun system change" } - 101 { "Completed system change" } - 102 { "Begun nested system change" } - 103 { "Completed nested system change" } - Default { "Unknown EventType: $($ET)" } - } - } - }, - @{ - Label = "RestorePointType" - Expression = { - # Restore Point Types: https://learn.microsoft.com/en-us/windows/win32/sr/systemrestore - $RPT = $_.RestorePointType - switch ($RPT) { - 0 { "An application has been installed." } - 1 { "An application has been uninstalled." } - 13 { "An application needs to delete the restore point it created." } - 10 { "A device driver has been installed." } - 12 { "An application has had features added or removed." } - Default { "Unknown RestorePointType: $($RPT)" } - } - } - } - } -} -process { - if (-not (Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $WysiwygCustomField = $env:wysiwygCustomFieldName } - - # Get System Restore Points - $RestorePoints = Get-ComputerRestorePoint - - # Output the results to the Activity Feed - if ($RestorePoints.Count -gt 0) { - Write-Host "[Info] System Restore Points Found!" - # Splatting from $RestorePointSplatting, auto sized to show all columns with a width of 4096 characters - $RestorePoints | Select-Object @RestorePointSplatting | Format-Table -AutoSize | Out-String -Width 4096 | Write-Host - } - else { - Write-Host "[Info] No System Restore Points Found or System Restore is disabled!" - # Continue on to update the custom field - } - - # Save the results to a custom field - if ($WysiwygCustomField) { - $Report = $( - if ($RestorePoints.Count -gt 0) { - # Format the report into an HTML table - # Splatting from $RestorePointSplatting - $RestorePoints | Select-Object @RestorePointSplatting | ConvertTo-Html -Fragment - } - else { - "

No System Restore Points Found or System Restore is disabled!

" - } - ) - # Minimum width of the table - $Report = $Report -replace "
WinSAT Scores
", "
" + } + + if ($WinSatScores.CPUScore -lt 4 -or $WinSatScores.D3DScore -lt 4 -or $WinSatScores.DiskScore -lt 4 -or $WinSatScores.GraphicsScore -lt 4 -or $WinSatScores.MemoryScore -lt 4) { + $WinSATMetricTable = $WinSATMetricTable -replace "
", "
" + } + } + else { + # If WinSAT data is not available, display a message. + $WinSATMetricTable = "

The WinSAT assessment data is either invalid or not available for this computer.

" + } + + # Create the HTML content for the performance metrics section. + $HTMLCard = "
+
+
  System Performance Metrics
+
+
+ + + + + + + +
+

Start Date and Time
$($StartedDateTime.ToShortDateString()) $($StartedDateTime.ToShortTimeString())

+
+

Completed Date and Time
$($CompletedDateTime.ToShortDateString()) $($CompletedDateTime.ToShortTimeString())

+
+

Last Startup Time
$($LastStartTime.ToShortDateString()) $($LastStartTime.ToShortTimeString())

+

$CPU

+ + + + + + + + +
+
+
$($FormattedCPUPerformance."CPU Average %")
+
  Average CPU % Used
+
+
+
+
$($FormattedCPUPerformance."CPU Minimum %")
+
  Minimum CPU % Used
+
+
+
+
$($FormattedCPUPerformance."CPU Maximum %")
+
  Maximum CPU % Used
+
+
+

Total Memory: $TotalMemoryGB

+ + + + + + + + +
+
+
$($OverallMemoryMetrics."RAM Average %")
+
  Average RAM % Used
+
+
+
+
$($OverallMemoryMetrics."RAM Minimum %")
+
  Minimum RAM % Used
+
+
+
+
$($OverallMemoryMetrics."RAM Maximum %")
+
  Maximum RAM % Used
+
+
+ $CPUProcessMetricTable +
+ $RAMProcessMetricTable +
+ $NetworkUsageMetricTable +
+ $DiskMetricTable +
+ $IOProcessesMetricTable + $(if($ValidAssessmentStates -notcontains $WinSatScores.WinSATAssessmentState) {"

WinSAT Scores

"}) + $WinSATMetricTable +
+
" + # Modify the last startup time section based on whether the startup limit was exceeded or not. + if ($ExceededLastStartupLimit) { + $HTMLCard = $HTMLCard -replace "id='lastStartup' class='card-text'>Last Startup Time
$($LastStartTime.ToShortDateString()) $($LastStartTime.ToShortTimeString())", "id='lastStartup' class='card-text'>Last Startup Time
$($LastStartTime.ToShortDateString()) $($LastStartTime.ToShortTimeString())  " + } + elseif ($DaysSinceLastReboot -ge 0) { + $HTMLCard = $HTMLCard -replace "id='lastStartup' class='card-text'>Last Startup Time
$($LastStartTime.ToShortDateString()) $($LastStartTime.ToShortTimeString())", "id='lastStartup' class='card-text'>Last Startup Time
$($LastStartTime.ToShortDateString()) $($LastStartTime.ToShortTimeString())  " + } + + # Highlight CPU performance metrics based on threshold values (color coding). + if ($CPUPerformance.Avg -ge 60 -and $CPUPerformance.Avg -lt 90) { $HTMLCard = $HTMLCard -replace "id='cpuOverallAvg' style='color: #008001;'", "id='cpuOverallAvg' style='color: #FAC905;'" } + if ($CPUPerformance.Min -ge 60 -and $CPUPerformance.Min -lt 90) { $HTMLCard = $HTMLCard -replace "id='cpuOverallMin' style='color: #008001;'", "id='cpuOverallMin' style='color: #FAC905;'" } + if ($CPUPerformance.Max -ge 60 -and $CPUPerformance.Max -lt 90) { $HTMLCard = $HTMLCard -replace "id='cpuOverallMax' style='color: #008001;'", "id='cpuOverallMax' style='color: #FAC905;'" } + + if ($CPUPerformance.Avg -ge 90) { $HTMLCard = $HTMLCard -replace "id='cpuOverallAvg' style='color: #008001;'", "id='cpuOverallAvg' style='color: #D53948;'" } + if ($CPUPerformance.Min -ge 90) { $HTMLCard = $HTMLCard -replace "id='cpuOverallMin' style='color: #008001;'", "id='cpuOverallMin' style='color: #D53948;'" } + if ($CPUPerformance.Max -ge 90) { $HTMLCard = $HTMLCard -replace "id='cpuOverallMax' style='color: #008001;'", "id='cpuOverallMax' style='color: #D53948;'" } + + # Highlight RAM performance metrics based on threshold values (color coding). + if ($MemoryPerformance.Avg -ge 60 -and $MemoryPerformance.Avg -lt 90) { $HTMLCard = $HTMLCard -replace "id='ramOverallAvg' style='color: #008001;'", "id='ramOverallAvg' style='color: #FAC905;'" } + if ($MemoryPerformance.Min -ge 60 -and $MemoryPerformance.Min -lt 90) { $HTMLCard = $HTMLCard -replace "id='ramOverallMin' style='color: #008001;'", "id='ramOverallMin' style='color: #FAC905;'" } + if ($MemoryPerformance.Max -ge 60 -and $MemoryPerformance.Max -lt 90) { $HTMLCard = $HTMLCard -replace "id='ramOverallMax' style='color: #008001;'", "id='ramOverallMax' style='color: #FAC905;'" } + + if ($MemoryPerformance.Avg -ge 90) { $HTMLCard = $HTMLCard -replace "id='ramOverallAvg' style='color: #008001;'", "id='ramOverallAvg' style='color: #D53948;'" } + if ($MemoryPerformance.Min -ge 90) { $HTMLCard = $HTMLCard -replace "id='ramOverallMin' style='color: #008001;'", "id='ramOverallMin' style='color: #D53948;'" } + if ($MemoryPerformance.Max -ge 90) { $HTMLCard = $HTMLCard -replace "id='ramOverallMax' style='color: #008001;'", "id='ramOverallMax' style='color: #D53948;'" } + + # Add the created HTML card to the custom field. + $CustomFieldValue.Add($HTMLCard) + + # Check if there are any event logs to display. + if ($NumberOfEvents -gt 0 -and $EventLogs.Count -gt 0) { + # Convert the event logs into an HTML fragment for displaying in the output. + $EventLogTableMetrics = $EventLogs | ConvertTo-Html -Fragment + + # Apply custom styles to the HTML table headers. + $EventLogTableMetrics = $EventLogTableMetrics -replace "
", "" -replace "LogName", "Log Name" + $EventLogTableMetrics = $EventLogTableMetrics -replace "ProviderName", "Provider Name" + $EventLogTableMetrics = $EventLogTableMetrics -replace "Id", "Id" + $EventLogTableMetrics = $EventLogTableMetrics -replace "TimeCreated", "Time Created" + } + elseif ($NumberOfEvents -gt 0) { + # If no events were found, display a message instead of the table. + $EventLogTableMetrics = "

No error events were found in the event log.

" + } + + # If event logs exist, create a card to display them. + if ($NumberOfEvents -gt 0) { + # Create the HTML structure for the event log card. + $EventLogCard = "
+
+
  Recent Error Events
+
+
+ $EventLogTableMetrics +
+
" + # Add the event log card to the custom field value. + $CustomFieldValue.Add($EventLogCard) + } + + # Check if the HTML content exceeds the character limit (45,000 characters). + $HTMLCharacters = $CustomFieldValue | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters + if ($HTMLCharacters -ge 43000) { + Write-Host -Object "The current character count is '$HTMLCharacters'." + Write-Warning "45,000 Character Limit has been reached! Trimming output until the character limit is satisfied..." + + # Truncate the output if it exceeds the limit. + $i = 0 + $Attempts = 0 + [array]$NewEventLogTable = $EventLogTableMetrics + $TrimStart = Get-Date + do { + # Recreate the custom field output + $CustomFieldValue = New-Object System.Collections.Generic.List[string] + if (!$NumberOfEvents -or !$NumberOfEvents -gt 0 -or !$EventLogs.Count -gt 0) { + Write-Host -Object "[Error] No events to trim." + exit 1 + } + + # Add the main performance metrics card to the custom field. + $CustomFieldValue.Add($HTMLCard) + + # Reverse the event log array so that the last entry is at the top. + [array]::Reverse($NewEventLogTable) + # Delete rows until the character count is reduced. + if ($NewEventLogTable[$i] -match '
' -or $NewEventLogTable[$i] -match '
", "
" - try { - Write-Host "[Info] Attempting to set Custom Field '$WysiwygCustomField'." - Set-NinjaProperty -Name $WysiwygCustomField -Value $Report - Write-Host "[Info] Successfully set Custom Field '$WysiwygCustomField'." - } - catch { - Write-Host "[Error] Failed to set Custom Field '$WysiwygCustomField'." - $ExitCode = 1 - } - } - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Reports the status of System Restore on the system. +.DESCRIPTION + Reports the status of System Restore on the system. + The report can be saved to a WYSIWYG Custom Field. + +.EXAMPLE + (No Parameters) + ## EXAMPLE OUTPUT WITHOUT PARAMS ## + [Info] System Restore Points Found! + + CreationTime Description SequenceNumber EventType RestorePointType + ------------ ----------- -------------- --------- ---------------- + 5/7/2024 3:51:11 PM Test Description 1 A system change has begun. An application has been installed. + 5/7/2024 3:53:11 PM Test Description 2 A system change has completed. An application has been uninstalled. + 5/7/2024 3:54:11 PM Test Description 3 A system change has begun and is nested. An application needs to delete the restore point it created. + 5/7/2024 3:59:11 PM Test Description 4 A system change has completed and is nested. A device driver has been installed. + +PARAMETER: -WysiwygCustomField "ReplaceMeWithAnyWysiwygCustomField" + Reports the status of System Restore on the system. +.EXAMPLE + -WysiwygCustomField "ReplaceMeWithAnyWysiwygCustomField" + ## EXAMPLE OUTPUT WITH WysiwygCustomField ## + [Info] System Restore Points Found! + + CreationTime Description SequenceNumber EventType RestorePointType + ------------ ----------- -------------- --------- ---------------- + 5/7/2024 3:51:11 PM Test Description 1 A system change has begun. An application has been installed. + 5/7/2024 3:53:11 PM Test Description 2 A system change has completed. An application has been uninstalled. + 5/7/2024 3:54:11 PM Test Description 3 A system change has begun and is nested. An application needs to delete the restore point it created. + 5/7/2024 3:59:11 PM Test Description 4 A system change has completed and is nested. A device driver has been installed. + + + [Info] Attempting to set Custom Field 'ReplaceMeWithAnyWysiwygCustomField'. + [Info] Successfully set Custom Field 'ReplaceMeWithAnyWysiwygCustomField'. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$WysiwygCustomField +) + +begin { + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + # if ($Characters -ge 10000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If we're requested to set the field value for a Ninja document we'll specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # The field below requires additional information to be set # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If an error is received it will have an exception property, the function will exit with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # The below types require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # if (-not $Selection) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # We'll need to set the field differently depending on if its a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # Splatting the properties to be used in the Select-Object command + $RestorePointSplatting = @{ + Property = @{ Label = "CreationTime"; Expression = { + # Convert the CreationTime to a DateTime object from example: '20240507155911.581535-000' + $CreationTime = $_.CreationTime -split "\." + [DateTime]::ParseExact($CreationTime[0], "yyyyMMddHHmmss", $null) + } + }, + @{ Label = "Description"; Expression = { $_.Description } }, + @{ Label = "SequenceNumber"; Expression = { $_.SequenceNumber } }, + @{ + Label = "EventType" + Expression = { + # Event Type IDs: https://learn.microsoft.com/en-us/windows/win32/sr/systemrestore + $ET = $_.EventType + switch ($ET) { + 100 { "Begun system change" } + 101 { "Completed system change" } + 102 { "Begun nested system change" } + 103 { "Completed nested system change" } + Default { "Unknown EventType: $($ET)" } + } + } + }, + @{ + Label = "RestorePointType" + Expression = { + # Restore Point Types: https://learn.microsoft.com/en-us/windows/win32/sr/systemrestore + $RPT = $_.RestorePointType + switch ($RPT) { + 0 { "An application has been installed." } + 1 { "An application has been uninstalled." } + 13 { "An application needs to delete the restore point it created." } + 10 { "A device driver has been installed." } + 12 { "An application has had features added or removed." } + Default { "Unknown RestorePointType: $($RPT)" } + } + } + } + } +} +process { + if (-not (Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $WysiwygCustomField = $env:wysiwygCustomFieldName } + + # Get System Restore Points + $RestorePoints = Get-ComputerRestorePoint + + # Output the results to the Activity Feed + if ($RestorePoints.Count -gt 0) { + Write-Host "[Info] System Restore Points Found!" + # Splatting from $RestorePointSplatting, auto sized to show all columns with a width of 4096 characters + $RestorePoints | Select-Object @RestorePointSplatting | Format-Table -AutoSize | Out-String -Width 4096 | Write-Host + } + else { + Write-Host "[Info] No System Restore Points Found or System Restore is disabled!" + # Continue on to update the custom field + } + + # Save the results to a custom field + if ($WysiwygCustomField) { + $Report = $( + if ($RestorePoints.Count -gt 0) { + # Format the report into an HTML table + # Splatting from $RestorePointSplatting + $RestorePoints | Select-Object @RestorePointSplatting | ConvertTo-Html -Fragment + } + else { + "

No System Restore Points Found or System Restore is disabled!

" + } + ) + # Minimum width of the table + $Report = $Report -replace "
", "
" + try { + Write-Host "[Info] Attempting to set Custom Field '$WysiwygCustomField'." + # Set-NinjaProperty -Name $WysiwygCustomField -Value $Report # Removed NinjaOne dependency + Write-Host "[Info] Successfully set Custom Field '$WysiwygCustomField'." + } + catch { + Write-Host "[Error] Failed to set Custom Field '$WysiwygCustomField'." + $ExitCode = 1 + } + } + exit $ExitCode +} +end { + + + +} diff --git a/Powershell Scripts/Troubleshoot Printers and Clear Print Queue.ps1 b/Powershell Scripts/Troubleshoot Printers and Clear Print Queue.ps1 index 947d7f8..736e183 100644 --- a/Powershell Scripts/Troubleshoot Printers and Clear Print Queue.ps1 +++ b/Powershell Scripts/Troubleshoot Printers and Clear Print Queue.ps1 @@ -1,261 +1,261 @@ # Clear print queues and list printers to help troubleshoot printing issues. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Clear print queues and list printers to help troubleshoot printing issues. -.DESCRIPTION - Clear print queues and list printers to help troubleshoot printing issues. - This script will stop the printer spooler service, clear all print jobs, and start the printer spooler service. - If some print jobs are not cleared, then a reboot might be needed before running this script again. -.EXAMPLE - No parameters needed -.OUTPUTS - String -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Renamed script -.COMPONENT - Printer -#> - -[CmdletBinding()] -param ( - [Parameter()] - [string]$CustomFieldName -) - -begin { - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - # Measure the number of characters in the provided value - $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # Throw an error if the value exceeds the character limit of 200,000 characters - if ($Characters -ge 200000) { - throw "Character limit exceeded: the value is greater than or equal to 200,000 characters." - } - - # Initialize a hashtable for additional documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define a list of valid field types - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - - # Warn the user if the provided type is not valid - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # Define types that require options to be retrieved - $NeedsOptions = "Dropdown" - - # If the property is being set in a document or field and the type needs options, retrieve them - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an error if there was an issue retrieving the property options - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Process the property value based on its type - switch ($Type) { - "Checkbox" { - # Convert the value to a boolean for Checkbox type - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Convert the value to a Unix timestamp for Date or Date Time type - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Convert the dropdown value to its corresponding GUID - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - # Throw an error if the value is not present in the dropdown options - if (!($Selection)) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # For other types, use the value as is - $NinjaValue = $Value - } - } - - # Set the property value in the document if a document name is provided - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - # Otherwise, set the standard property value - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - - # Throw an error if setting the property failed - if ($CustomField.Exception) { - throw $CustomField - } - } -} - -process { - if ($env:CustomFieldName -and $env:CustomFieldName -notlike "null") { $CustomFieldName = $env:CustomFieldName } - - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - $Status = [PSCustomObject]@{ - Cleared = $false - ServiceRestarted = $false - ListOfWSDPrinters = Get-Printer | Where-Object { $_.PortName -like "WSD*" } - PrintersWithIpAddressOrUncPath = Get-Printer | Where-Object { $_.PortName -like "*IP*" -or $_.PortName -like "*\\*" } - } - - Write-Host "[Info] Stopping print spooler service" - try { - Stop-Service -Name spooler -Force -ErrorAction Stop - } - catch { - $Status.ServiceRestarted = $false - } - # Exit Code 2 usually means the service is already stopped - if ((Get-Service -Name spooler).Status -eq "Stopped") { - Write-Host "[Info] Stopped print spooler service" - # Sleep just in case the spooler service is taking some time to stop - Start-Sleep -Seconds 10 - Write-Host "[Info] Clearing all print queues" - try { - Remove-Item -Path "$env:SystemRoot\System32\spool\PRINTERS\*" -Force -ErrorAction SilentlyContinue - $Status.Cleared = $true - } - catch { - Write-Host "[Warn] Failed to clear all print queues." - } - Write-Host "[Info] Cleared all print queues." - - Write-Host "[Info] Starting print spooler service" - try { - Start-Service -Name spooler -ErrorAction Stop - } - catch { - Write-Host "[Warn] Failed to start print spooler service. Attempting to stop and start it again." - Stop-Service -Name spooler -Force -ErrorAction SilentlyContinue - $Service = Start-Service -Name spooler -ErrorAction SilentlyContinue -PassThru - if ($Service.Status -ne "Stopped") { - $Status.ServiceRestarted = $false - } - } - Start-Sleep -Seconds 10 - if ((Get-Service -Name spooler).Status -eq "Running") { - Write-Host "[Info] Restarted print spooler service." - $Status.ServiceRestarted = $true - } - else { - Write-Host "[Error] Could not start Print Spooler service." - } - } - else { - Write-Host "[Error] Could not stop Print Spooler service." - } - - - $Output = New-Object System.Collections.Generic.List[string] - if ($Status.Cleared) { - $Output.Add("Cleared all print queues.") - } - else { - $Output.Add("Failed to clear all print queues.") - } - - if ($Status.ServiceRestarted) { - $Output.Add("Restarted print spooler service.") - } - else { - $Output.Add("Failed to restart print spooler service.") - } - - if ($Status.ListOfWSDPrinters) { - Write-Host "[Info] Found WSD printer:" - - $Output.Add("Found WSD printer:") - $Status.ListOfWSDPrinters | ForEach-Object { - $Output.Add("$($_.Name)") - Write-Host " $($_.Name)" - } - } - else { - $Output.Add("No WSD printers found.") - } - - if ($Status.PrintersWithIpAddressOrUncPath) { - Write-Host "[Info] Found printer with IP address or UNC path:" - - $Output.Add("Found printer with IP address or UNC path:") - $Status.PrintersWithIpAddressOrUncPath | ForEach-Object { - if ($_.PortName -like "*\\*" -and $(Test-Connection $($_.PortName -split '\\' | Select-Object -Skip 2 -First 1) -Count 3 -Quiet -ErrorAction SilentlyContinue)) { - $Output.Add("$($_.Name) (Connected)") - Write-Host " $($_.Name) (Connected)" - } - elseif ($_.PortName -like "*IP_*" -and $(Test-Connection $($_.PortName -split 'IP_' | Select-Object -Skip 1 -First 1) -Count 3 -Quiet -ErrorAction SilentlyContinue)) { - $Output.Add("$($_.Name) (Connected)") - Write-Host " $($_.Name) (Connected)" - } - else { - $Output.Add("$_ (Disconnected)") - Write-Host " $_ (Disconnected)" - } - } - } - else { - $Output.Add("No printers with IP address or UNC path found.") - } - if ($CustomFieldName) { - try { - Write-Host "[Info] Attempting to set Custom Field '$CustomFieldName'." - Set-NinjaProperty -Name $CustomFieldName -Value $($Output -join [System.Environment]::NewLine | Out-String -Width 4000) - Write-Host "[Info] Successfully set Custom Field '$CustomFieldName'!" - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - } - } -} - -end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + Clear print queues and list printers to help troubleshoot printing issues. +.DESCRIPTION + Clear print queues and list printers to help troubleshoot printing issues. + This script will stop the printer spooler service, clear all print jobs, and start the printer spooler service. + If some print jobs are not cleared, then a reboot might be needed before running this script again. +.EXAMPLE + No parameters needed +.OUTPUTS + String +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Renamed script +.COMPONENT + Printer +#> + +[CmdletBinding()] +param ( + [Parameter()] + [string]$CustomFieldName +) + +begin { + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # # Measure the number of characters in the provided value # Removed NinjaOne dependency + # $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + + # # Throw an error if the value exceeds the character limit of 200,000 characters # Removed NinjaOne dependency + # if ($Characters -ge 200000) { # Removed NinjaOne dependency + # throw "Character limit exceeded: the value is greater than or equal to 200,000 characters." # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Initialize a hashtable for additional documentation parameters # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + + # # If a document name is provided, add it to the documentation parameters # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # Define a list of valid field types # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + + # # Warn the user if the provided type is not valid # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # Define types that require options to be retrieved # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + + # # If the property is being set in a document or field and the type needs options, retrieve them # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if there was an issue retrieving the property options # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # Process the property value based on its type # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # Convert the value to a boolean for Checkbox type # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Convert the value to a Unix timestamp for Date or Date Time type # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Convert the dropdown value to its corresponding GUID # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # # Throw an error if the value is not present in the dropdown options # Removed NinjaOne dependency + # if (!($Selection)) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # For other types, use the value as is # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Set the property value in the document if a document name is provided # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # # Otherwise, set the standard property value # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if setting the property failed # Removed NinjaOne dependency + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency +} + +process { + if ($env:CustomFieldName -and $env:CustomFieldName -notlike "null") { $CustomFieldName = $env:CustomFieldName } + + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + $Status = [PSCustomObject]@{ + Cleared = $false + ServiceRestarted = $false + ListOfWSDPrinters = Get-Printer | Where-Object { $_.PortName -like "WSD*" } + PrintersWithIpAddressOrUncPath = Get-Printer | Where-Object { $_.PortName -like "*IP*" -or $_.PortName -like "*\\*" } + } + + Write-Host "[Info] Stopping print spooler service" + try { + Stop-Service -Name spooler -Force -ErrorAction Stop + } + catch { + $Status.ServiceRestarted = $false + } + # Exit Code 2 usually means the service is already stopped + if ((Get-Service -Name spooler).Status -eq "Stopped") { + Write-Host "[Info] Stopped print spooler service" + # Sleep just in case the spooler service is taking some time to stop + Start-Sleep -Seconds 10 + Write-Host "[Info] Clearing all print queues" + try { + Remove-Item -Path "$env:SystemRoot\System32\spool\PRINTERS\*" -Force -ErrorAction SilentlyContinue + $Status.Cleared = $true + } + catch { + Write-Host "[Warn] Failed to clear all print queues." + } + Write-Host "[Info] Cleared all print queues." + + Write-Host "[Info] Starting print spooler service" + try { + Start-Service -Name spooler -ErrorAction Stop + } + catch { + Write-Host "[Warn] Failed to start print spooler service. Attempting to stop and start it again." + Stop-Service -Name spooler -Force -ErrorAction SilentlyContinue + $Service = Start-Service -Name spooler -ErrorAction SilentlyContinue -PassThru + if ($Service.Status -ne "Stopped") { + $Status.ServiceRestarted = $false + } + } + Start-Sleep -Seconds 10 + if ((Get-Service -Name spooler).Status -eq "Running") { + Write-Host "[Info] Restarted print spooler service." + $Status.ServiceRestarted = $true + } + else { + Write-Host "[Error] Could not start Print Spooler service." + } + } + else { + Write-Host "[Error] Could not stop Print Spooler service." + } + + + $Output = New-Object System.Collections.Generic.List[string] + if ($Status.Cleared) { + $Output.Add("Cleared all print queues.") + } + else { + $Output.Add("Failed to clear all print queues.") + } + + if ($Status.ServiceRestarted) { + $Output.Add("Restarted print spooler service.") + } + else { + $Output.Add("Failed to restart print spooler service.") + } + + if ($Status.ListOfWSDPrinters) { + Write-Host "[Info] Found WSD printer:" + + $Output.Add("Found WSD printer:") + $Status.ListOfWSDPrinters | ForEach-Object { + $Output.Add("$($_.Name)") + Write-Host " $($_.Name)" + } + } + else { + $Output.Add("No WSD printers found.") + } + + if ($Status.PrintersWithIpAddressOrUncPath) { + Write-Host "[Info] Found printer with IP address or UNC path:" + + $Output.Add("Found printer with IP address or UNC path:") + $Status.PrintersWithIpAddressOrUncPath | ForEach-Object { + if ($_.PortName -like "*\\*" -and $(Test-Connection $($_.PortName -split '\\' | Select-Object -Skip 2 -First 1) -Count 3 -Quiet -ErrorAction SilentlyContinue)) { + $Output.Add("$($_.Name) (Connected)") + Write-Host " $($_.Name) (Connected)" + } + elseif ($_.PortName -like "*IP_*" -and $(Test-Connection $($_.PortName -split 'IP_' | Select-Object -Skip 1 -First 1) -Count 3 -Quiet -ErrorAction SilentlyContinue)) { + $Output.Add("$($_.Name) (Connected)") + Write-Host " $($_.Name) (Connected)" + } + else { + $Output.Add("$_ (Disconnected)") + Write-Host " $_ (Disconnected)" + } + } + } + else { + $Output.Add("No printers with IP address or UNC path found.") + } + if ($CustomFieldName) { + try { + Write-Host "[Info] Attempting to set Custom Field '$CustomFieldName'." + # Set-NinjaProperty -Name $CustomFieldName -Value $($Output -join [System.Environment]::NewLine | Out-String -Width 4000) # Removed NinjaOne dependency + Write-Host "[Info] Successfully set Custom Field '$CustomFieldName'!" + } + catch { + Write-Host "[Error] $($_.Exception.Message)" + } + } +} + +end { + + + +} + diff --git a/Powershell Scripts/USB Drive Alert.ps1 b/Powershell Scripts/USB Drive Alert.ps1 index b9d5a3d..73c5c2b 100644 --- a/Powershell Scripts/USB Drive Alert.ps1 +++ b/Powershell Scripts/USB Drive Alert.ps1 @@ -1,101 +1,101 @@ # Alerts when a USB drive is detected and optionally saves the results to a Custom Field. - -<# -.SYNOPSIS - Alerts when a USB drive is detected and optionally saves the results to a Custom Field. -.DESCRIPTION - Alerts when a USB drive is detected and optionally saves the results to a Custom Field. -.EXAMPLE - (No Parameters) - - No USB Drives are present. -.EXAMPLE - (No Parameters) - - C:\Users\KyleBohlander\Documents\bitbucket_clientscripts\client_scripts\src\Test-USBDrive.ps1 : A USB Drive has been detected! - At line:1 char:1 - + .\src\Test-USBDrive.ps1 - + ~~~~~~~~~~~~~~~~~~~~~~~ - + CategoryInfo : LimitsExceeded: (:) [Write-Error], Exception - + FullyQualifiedErrorId : System.Exception,Test-USBDrive.ps1 - - Index Caption SerialNumber Partitions - ----- ------- ------------ ---------- - 1 Samsung Flash Drive USB Device AA00000000000489 1 - -PARAMETER: -CustomFieldName "replaceMeWithACustomFieldName" - Name of a custom field to save the results to. This is optional; results will also output to the activity log. - -.OUTPUTS - None -.NOTES - Minimum supported OS: Windows 10, Server 2012 R2 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$CustomFieldName -) - -begin { - # Grab CustomFieldName from dynamic script form - if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomFieldName = $env:customFieldName } - - # Initialize exit code - $ExitCode = 0 - - # Initialize generic list for the report - $Report = New-Object System.Collections.Generic.List[String] - $CustomFieldReport = New-Object System.Collections.Generic.List[String] -} -process { - - # Get a list of USB drives - $USBDrives = if ($PSVersionTable.PSVersion.Major -ge 5) { - Get-CimInstance win32_diskdrive | Where-Object { $_.InterfaceType -eq 'USB' } - } - else { - Get-WmiObject win32_diskdrive | Where-Object { $_.InterfaceType -eq 'USB' } - } - - # Alert if a USB drive is detected - if ($USBDrives) { - Write-Error -Message "A USB Drive has been detected!" -Category LimitsExceeded -Exception (New-Object -TypeName System.Exception) - - # Grab relevant information about the USB Drive - $USBDrives | ForEach-Object { - $Report.Add( ($_ | Format-Table Index, Caption, SerialNumber, Partitions | Out-String) ) - if ($CustomFieldName) { $CustomFieldReport.Add( ($_ | Format-List Index, Caption, SerialNumber, Partitions | Out-String) ) } - - $Report.Add( (Get-Partition -DiskNumber $_.Index | Get-Volume | Format-Table DriveLetter, FriendlyName, DriveType, HealthStatus, SizeRemaining, Size | Out-String) ) - if ($CustomFieldName) { $CustomFieldReport.Add( (Get-Partition -DiskNumber $_.Index | Get-Volume | Format-List DriveLetter, FriendlyName, DriveType, HealthStatus, SizeRemaining, Size | Out-String) ) } - } - - # Change exit code to indicate failure/alert - $ExitCode = 1 - } - else { - # If no drives were found we'll need to indicate that. - $Report.Add("No USB Drives are present.") - if ($CustomFieldName) { $CustomFieldReport.Add("No USB Drives are present.") } - } - - # Write to the activity log - Write-Host $Report - - # Save to custom field if given one - if ($CustomFieldName) { - Write-Host "" - Ninja-Property-Set -Name $CustomFieldName -Value $CustomFieldReport - } - - # Exit with appropriate exit code - Exit $ExitCode -} -end { - - - -} + +<# +.SYNOPSIS + Alerts when a USB drive is detected and optionally saves the results to a Custom Field. +.DESCRIPTION + Alerts when a USB drive is detected and optionally saves the results to a Custom Field. +.EXAMPLE + (No Parameters) + + No USB Drives are present. +.EXAMPLE + (No Parameters) + + C:\Users\KyleBohlander\Documents\bitbucket_clientscripts\client_scripts\src\Test-USBDrive.ps1 : A USB Drive has been detected! + At line:1 char:1 + + .\src\Test-USBDrive.ps1 + + ~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : LimitsExceeded: (:) [Write-Error], Exception + + FullyQualifiedErrorId : System.Exception,Test-USBDrive.ps1 + + Index Caption SerialNumber Partitions + ----- ------- ------------ ---------- + 1 Samsung Flash Drive USB Device AA00000000000489 1 + +PARAMETER: -CustomFieldName "replaceMeWithACustomFieldName" + Name of a custom field to save the results to. This is optional; results will also output to the activity log. + +.OUTPUTS + None +.NOTES + Minimum supported OS: Windows 10, Server 2012 R2 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$CustomFieldName +) + +begin { + # Grab CustomFieldName from dynamic script form + if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomFieldName = $env:customFieldName } + + # Initialize exit code + $ExitCode = 0 + + # Initialize generic list for the report + $Report = New-Object System.Collections.Generic.List[String] + $CustomFieldReport = New-Object System.Collections.Generic.List[String] +} +process { + + # Get a list of USB drives + $USBDrives = if ($PSVersionTable.PSVersion.Major -ge 5) { + Get-CimInstance win32_diskdrive | Where-Object { $_.InterfaceType -eq 'USB' } + } + else { + Get-WmiObject win32_diskdrive | Where-Object { $_.InterfaceType -eq 'USB' } + } + + # Alert if a USB drive is detected + if ($USBDrives) { + Write-Error -Message "A USB Drive has been detected!" -Category LimitsExceeded -Exception (New-Object -TypeName System.Exception) + + # Grab relevant information about the USB Drive + $USBDrives | ForEach-Object { + $Report.Add( ($_ | Format-Table Index, Caption, SerialNumber, Partitions | Out-String) ) + if ($CustomFieldName) { $CustomFieldReport.Add( ($_ | Format-List Index, Caption, SerialNumber, Partitions | Out-String) ) } + + $Report.Add( (Get-Partition -DiskNumber $_.Index | Get-Volume | Format-Table DriveLetter, FriendlyName, DriveType, HealthStatus, SizeRemaining, Size | Out-String) ) + if ($CustomFieldName) { $CustomFieldReport.Add( (Get-Partition -DiskNumber $_.Index | Get-Volume | Format-List DriveLetter, FriendlyName, DriveType, HealthStatus, SizeRemaining, Size | Out-String) ) } + } + + # Change exit code to indicate failure/alert + $ExitCode = 1 + } + else { + # If no drives were found we'll need to indicate that. + $Report.Add("No USB Drives are present.") + if ($CustomFieldName) { $CustomFieldReport.Add("No USB Drives are present.") } + } + + # Write to the activity log + Write-Host $Report + + # Save to custom field if given one + if ($CustomFieldName) { + Write-Host "" + # Ninja-Property-Set -Name $CustomFieldName -Value $CustomFieldReport # Removed NinjaOne dependency + } + + # Exit with appropriate exit code + Exit $ExitCode +} +end { + + + +} diff --git a/Powershell Scripts/USB Storage Status.ps1 b/Powershell Scripts/USB Storage Status.ps1 index 43d4f05..d26f9b0 100644 --- a/Powershell Scripts/USB Storage Status.ps1 +++ b/Powershell Scripts/USB Storage Status.ps1 @@ -1,284 +1,284 @@ # Checks if the workstation currently allows access to USB storage devices and optionally saves the results to a custom field. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Checks if the workstation currently allows access to USB storage devices and optionally saves the results to a custom field. -.DESCRIPTION - Checks if the workstation currently allows access to USB storage devices and optionally saves the results to a custom field. - -.EXAMPLE - -CustomField "usbStorageStatus" - - Checking if the USB Mass Storage Driver service is enabled. - Checking if the USB Mass Storage Driver is disabled via the registry. - Checking if USB is disabled via group policy. - - USB is currently disabled. - - Attempting to set Custom Field 'usbStorageStatus'. - Successfully set Custom Field 'usbStorageStatus'! - -.NOTES - Minimum OS Architecture Supported: Windows 10 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$CustomField -) - -begin { - if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } - - if ($CustomField) { - $CustomField = $CustomField.Trim() - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName, - [Parameter()] - [Switch]$Piped - ) - # Remove the non-breaking space character - if ($Type -eq "WYSIWYG") { - $Value = $Value -replace ' ', ' ' - } - - # Measure the number of characters in the provided value - $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # Throw an error if the value exceeds the character limit of 200,000 characters - if ($Piped -and $Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - if (!$Piped -and $Characters -ge 45000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") - } - - # Initialize a hashtable for additional documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define a list of valid field types - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - - # Warn the user if the provided type is not valid - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # Define types that require options to be retrieved - $NeedsOptions = "Dropdown" - - # If the property is being set in a document or field and the type needs options, retrieve them - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an error if there was an issue retrieving the property options - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Process the property value based on its type - switch ($Type) { - "Checkbox" { - # Convert the value to a boolean for Checkbox type - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Convert the value to a Unix timestamp for Date or Date Time type - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Convert the dropdown value to its corresponding GUID - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - # Throw an error if the value is not present in the dropdown options - if (!($Selection)) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # For other types, use the value as is - $NinjaValue = $Value - } - } - - # Set the property value in the document if a document name is provided - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - try { - # Otherwise, set the standard property value - if ($Piped) { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - } - catch { - Write-Host -Object "[Error] Failed to set custom field." - throw $_.Exception.Message - } - } - - # Throw an error if setting the property failed - if ($CustomField.Exception) { - throw $CustomField - } - } - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # Check if the script is running with elevated (Administrator) privileges. - if (!(Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Notify that the script is checking if the USB Mass Storage Driver service is enabled. - Write-Host -Object "Checking if the USB Mass Storage Driver service is enabled." - - try { - $USBService = Get-Service -Name "USBStor" -ErrorAction Stop - } - catch { - Write-Host -Object "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - - # If the USB Mass Storage Driver service was not found, print an error message and exit. - if (!$USBService) { - Write-Host -Object "[Error] Accessing the 'USB Mass Storage Driver' service status." - - if ($CustomField) { - try { - Write-Host -Object "Attempting to set Custom Field '$CustomField'." - Set-NinjaProperty -Name $CustomField -Value "Unable to Determine" - Write-Host -Object "Successfully set Custom Field '$CustomField'!" - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - } - } - exit 1 - } - - # Notify that the script is checking if the USB Mass Storage Driver is disabled via the registry. - Write-Host -Object "Checking if the USB Mass Storage Driver is disabled via the registry." - - # Check if the registry path for the USB Mass Storage Driver exists. - if (!(Test-Path -Path "Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\USBSTOR")) { - Write-Host -Object "[Error] The 'USB Mass Storage Driver' Service is missing it's registry key." - - if ($CustomField) { - try { - Write-Host -Object "Attempting to set Custom Field '$CustomField'." - Set-NinjaProperty -Name $CustomField -Value "Unable to Determine" - Write-Host -Object "Successfully set Custom Field '$CustomField'!" - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - } - } - exit 1 - } - - # Get the registry value for the USB Mass Storage Driver service start type. - $USBRegKey = Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\USBSTOR" -Name "Start" | Select-Object -ExpandProperty Start - - # Notify that the script is checking if USB is disabled via group policy. - Write-Host -Object "Checking if USB is disabled via group policy." - - # Check if the group policy registry path for removable usb devices exists. - if (Test-Path -Path "Registry::HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}" -ErrorAction SilentlyContinue) { - $USBPolicyRegKey = Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}" - - # If all group policy settings deny read, write, and execute access, set the USB policy status to "Disabled". - if ($USBPolicyRegKey.Deny_Read -eq 1 -and $USBPolicyRegKey.Deny_Write -eq 1 -and $USBPolicyRegKey.Deny_Execute -eq 1) { - $USBPolicy = "Disabled" - } - - } - - # Check if the group policy registry path for all removable storage devices exists. - if (Test-Path -Path "Registry::HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\RemovableStorageDevices" -ErrorAction SilentlyContinue) { - $USBPolicyRegKey = Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\RemovableStorageDevices" - - # If group olicy is set to deny all removable devices, set the USB policy status to "Disabled". - if ($USBPolicyRegKey.Deny_All -eq 1) { - $USBPolicy = "Disabled" - } - - } - - try { - # If the USB service is disabled, the registry value indicates it is disabled, or the group policy disables it, set the custom field to "Disabled". - if ($USBService.StartType -eq "Disabled" -or $USBRegKey -eq 4 -or $USBPolicy -eq "Disabled") { - Write-Host -Object "`nUSB is currently disabled.`n" - - if ($CustomField) { - Write-Host -Object "Attempting to set Custom Field '$CustomField'." - Set-NinjaProperty -Name $CustomField -Value "Disabled" - Write-Host -Object "Successfully set Custom Field '$CustomField'!" - } - } - else { - # If none of the conditions indicate the USB is disabled, set the custom field to "Enabled". - Write-Host -Object "`nUSB is currently enabled.`n" - - if ($CustomField) { - Write-Host -Object "Attempting to set Custom Field '$CustomField'." - Set-NinjaProperty -Name $CustomField -Value "Enabled" - Write-Host -Object "Successfully set Custom Field '$CustomField'!" - } - } - } - catch { - # Catch any exceptions that occur and print an error message. - Write-Host "[Error] $($_.Exception.Message)" - $ExitCode = 1 - } - - # Exit the script with the appropriate exit code. - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Checks if the workstation currently allows access to USB storage devices and optionally saves the results to a custom field. +.DESCRIPTION + Checks if the workstation currently allows access to USB storage devices and optionally saves the results to a custom field. + +.EXAMPLE + -CustomField "usbStorageStatus" + + Checking if the USB Mass Storage Driver service is enabled. + Checking if the USB Mass Storage Driver is disabled via the registry. + Checking if USB is disabled via group policy. + + USB is currently disabled. + + Attempting to set Custom Field 'usbStorageStatus'. + Successfully set Custom Field 'usbStorageStatus'! + +.NOTES + Minimum OS Architecture Supported: Windows 10 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$CustomField +) + +begin { + if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } + + if ($CustomField) { + $CustomField = $CustomField.Trim() + } + + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [Switch]$Piped # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + # # Remove the non-breaking space character # Removed NinjaOne dependency + # if ($Type -eq "WYSIWYG") { # Removed NinjaOne dependency + # $Value = $Value -replace ' ', ' ' # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Measure the number of characters in the provided value # Removed NinjaOne dependency + # $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + + # # Throw an error if the value exceeds the character limit of 200,000 characters # Removed NinjaOne dependency + # if ($Piped -and $Characters -ge 200000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if (!$Piped -and $Characters -ge 45000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Initialize a hashtable for additional documentation parameters # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + + # # If a document name is provided, add it to the documentation parameters # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # Define a list of valid field types # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + + # # Warn the user if the provided type is not valid # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # Define types that require options to be retrieved # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + + # # If the property is being set in a document or field and the type needs options, retrieve them # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if there was an issue retrieving the property options # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # Process the property value based on its type # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # Convert the value to a boolean for Checkbox type # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Convert the value to a Unix timestamp for Date or Date Time type # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Convert the dropdown value to its corresponding GUID # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # # Throw an error if the value is not present in the dropdown options # Removed NinjaOne dependency + # if (!($Selection)) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # For other types, use the value as is # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Set the property value in the document if a document name is provided # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # try { # Removed NinjaOne dependency + # # Otherwise, set the standard property value # Removed NinjaOne dependency + # if ($Piped) { # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # catch { # Removed NinjaOne dependency + # Write-Host -Object "[Error] Failed to set custom field." # Removed NinjaOne dependency + # throw $_.Exception.Message # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if setting the property failed # Removed NinjaOne dependency + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # Check if the script is running with elevated (Administrator) privileges. + if (!(Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Notify that the script is checking if the USB Mass Storage Driver service is enabled. + Write-Host -Object "Checking if the USB Mass Storage Driver service is enabled." + + try { + $USBService = Get-Service -Name "USBStor" -ErrorAction Stop + } + catch { + Write-Host -Object "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + + # If the USB Mass Storage Driver service was not found, print an error message and exit. + if (!$USBService) { + Write-Host -Object "[Error] Accessing the 'USB Mass Storage Driver' service status." + + if ($CustomField) { + try { + Write-Host -Object "Attempting to set Custom Field '$CustomField'." + # Set-NinjaProperty -Name $CustomField -Value "Unable to Determine" # Removed NinjaOne dependency + Write-Host -Object "Successfully set Custom Field '$CustomField'!" + } + catch { + Write-Host "[Error] $($_.Exception.Message)" + } + } + exit 1 + } + + # Notify that the script is checking if the USB Mass Storage Driver is disabled via the registry. + Write-Host -Object "Checking if the USB Mass Storage Driver is disabled via the registry." + + # Check if the registry path for the USB Mass Storage Driver exists. + if (!(Test-Path -Path "Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\USBSTOR")) { + Write-Host -Object "[Error] The 'USB Mass Storage Driver' Service is missing it's registry key." + + if ($CustomField) { + try { + Write-Host -Object "Attempting to set Custom Field '$CustomField'." + # Set-NinjaProperty -Name $CustomField -Value "Unable to Determine" # Removed NinjaOne dependency + Write-Host -Object "Successfully set Custom Field '$CustomField'!" + } + catch { + Write-Host "[Error] $($_.Exception.Message)" + } + } + exit 1 + } + + # Get the registry value for the USB Mass Storage Driver service start type. + $USBRegKey = Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\USBSTOR" -Name "Start" | Select-Object -ExpandProperty Start + + # Notify that the script is checking if USB is disabled via group policy. + Write-Host -Object "Checking if USB is disabled via group policy." + + # Check if the group policy registry path for removable usb devices exists. + if (Test-Path -Path "Registry::HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}" -ErrorAction SilentlyContinue) { + $USBPolicyRegKey = Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}" + + # If all group policy settings deny read, write, and execute access, set the USB policy status to "Disabled". + if ($USBPolicyRegKey.Deny_Read -eq 1 -and $USBPolicyRegKey.Deny_Write -eq 1 -and $USBPolicyRegKey.Deny_Execute -eq 1) { + $USBPolicy = "Disabled" + } + + } + + # Check if the group policy registry path for all removable storage devices exists. + if (Test-Path -Path "Registry::HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\RemovableStorageDevices" -ErrorAction SilentlyContinue) { + $USBPolicyRegKey = Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\RemovableStorageDevices" + + # If group olicy is set to deny all removable devices, set the USB policy status to "Disabled". + if ($USBPolicyRegKey.Deny_All -eq 1) { + $USBPolicy = "Disabled" + } + + } + + try { + # If the USB service is disabled, the registry value indicates it is disabled, or the group policy disables it, set the custom field to "Disabled". + if ($USBService.StartType -eq "Disabled" -or $USBRegKey -eq 4 -or $USBPolicy -eq "Disabled") { + Write-Host -Object "`nUSB is currently disabled.`n" + + if ($CustomField) { + Write-Host -Object "Attempting to set Custom Field '$CustomField'." + # Set-NinjaProperty -Name $CustomField -Value "Disabled" # Removed NinjaOne dependency + Write-Host -Object "Successfully set Custom Field '$CustomField'!" + } + } + else { + # If none of the conditions indicate the USB is disabled, set the custom field to "Enabled". + Write-Host -Object "`nUSB is currently enabled.`n" + + if ($CustomField) { + Write-Host -Object "Attempting to set Custom Field '$CustomField'." + # Set-NinjaProperty -Name $CustomField -Value "Enabled" # Removed NinjaOne dependency + Write-Host -Object "Successfully set Custom Field '$CustomField'!" + } + } + } + catch { + # Catch any exceptions that occur and print an error message. + Write-Host "[Error] $($_.Exception.Message)" + $ExitCode = 1 + } + + # Exit the script with the appropriate exit code. + exit $ExitCode +} +end { + + + +} diff --git a/Powershell Scripts/Unsigned Driver Alert.ps1 b/Powershell Scripts/Unsigned Driver Alert.ps1 index 2caa5e3..ba782ce 100644 --- a/Powershell Scripts/Unsigned Driver Alert.ps1 +++ b/Powershell Scripts/Unsigned Driver Alert.ps1 @@ -1,181 +1,181 @@ # Get a list of unsigned drivers on the system. - -<# - -.SYNOPSIS - Get a list of unsigned drivers on the system. -.DESCRIPTION - Get a list of unsigned drivers on the system. -.EXAMPLE - (No Parameters) - - [Info] Unsigned Drivers Found - - Device Name INF Name Is Signed Manufacturer - ----------- -------- --------- ------------ - Local Print Queue printqueue.inf False Microsoft - Microsoft Print to PDF mspdf.inf False Microsoft - Microsoft XPS Document msxpsdrv.inf False Microsoft - -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$WYSIWYGCustomField -) - -begin { - # If using script form variables, replace command line parameters with the form variables. - if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $WYSIWYGCustomField = $env:wysiwygCustomFieldName } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - # Measure the number of characters in the provided value - $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters - - # Throw an error if the value exceeds the character limit of 200,000 characters - if ($Characters -ge 200000) { - throw "Character limit exceeded: the value is greater than or equal to 200,000 characters." - } - - # Initialize a hashtable for additional documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define a list of valid field types - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - - # Warn the user if the provided type is not valid - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # Define types that require options to be retrieved - $NeedsOptions = "Dropdown" - - # If the property is being set in a document or field and the type needs options, retrieve them - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an error if there was an issue retrieving the property options - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Process the property value based on its type - switch ($Type) { - "Checkbox" { - # Convert the value to a boolean for Checkbox type - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Convert the value to a Unix timestamp for Date or Date Time type - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Convert the dropdown value to its corresponding GUID - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - # Throw an error if the value is not present in the dropdown options - if (!($Selection)) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # For other types, use the value as is - $NinjaValue = $Value - } - } - - # Set the property value in the document if a document name is provided - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - # Otherwise, set the standard property value - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - - # Throw an error if setting the property failed - if ($CustomField.Exception) { - throw $CustomField - } - } -} -process { - # Get the list of unsigned drivers - $UnsignedDrivers = driverquery.exe /si /FO CSV | - ConvertFrom-Csv | - Select-Object @{ - label = "Device Name"; expression = { $_.DeviceName } - }, @{ - label = "INF Name"; expression = { $_.InfName } - }, @{ - label = "Is Signed"; expression = { if ($_.IsSigned -eq "TRUE") { $true }else { $false } } - }, Manufacturer | - Where-Object { -Not $_."Is Signed" } - - # Create Html Table - $HtmlTable = [System.Collections.Generic.List[String]]::new() - - # Add header - $HtmlTable.Add("

Unsigned Drivers

") - - # Output the list of unsigned drivers to the table - if ($UnsignedDrivers) { - Write-Host "[Info] Unsigned Drivers Found" - # Add table of unsigned drivers - $HtmlTable.Add($($UnsignedDrivers | ConvertTo-Html -Fragment | Out-String)) - # Output the list of unsigned drivers - $UnsignedDrivers | Format-Table -AutoSize | Out-String -Width 4000 | Write-Host - } - else { - Write-Host "[Info] No Unsigned Drivers Found" - $HtmlTable.Add("

No unsigned drivers found.

") - } - - # If a custom field name is provided, set the custom field with the list of unsigned drivers - if ($WYSIWYGCustomField) { - try { - Write-Host "[Info] Attempting to set Custom Field '$WYSIWYGCustomField'." - Set-NinjaProperty -Name $WYSIWYGCustomField -Value $($HtmlTable -join [System.Environment]::NewLine | Out-String) - Write-Host "[Info] Successfully set Custom Field '$WYSIWYGCustomField'!" - } - catch { - Write-Host "[Warn] Failed to set Custom Field '$WYSIWYGCustomField'." - } - } -} -end { - - - -} + +<# + +.SYNOPSIS + Get a list of unsigned drivers on the system. +.DESCRIPTION + Get a list of unsigned drivers on the system. +.EXAMPLE + (No Parameters) + + [Info] Unsigned Drivers Found + + Device Name INF Name Is Signed Manufacturer + ----------- -------- --------- ------------ + Local Print Queue printqueue.inf False Microsoft + Microsoft Print to PDF mspdf.inf False Microsoft + Microsoft XPS Document msxpsdrv.inf False Microsoft + +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$WYSIWYGCustomField +) + +begin { + # If using script form variables, replace command line parameters with the form variables. + if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $WYSIWYGCustomField = $env:wysiwygCustomFieldName } + + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # # Measure the number of characters in the provided value # Removed NinjaOne dependency + # $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + + # # Throw an error if the value exceeds the character limit of 200,000 characters # Removed NinjaOne dependency + # if ($Characters -ge 200000) { # Removed NinjaOne dependency + # throw "Character limit exceeded: the value is greater than or equal to 200,000 characters." # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Initialize a hashtable for additional documentation parameters # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + + # # If a document name is provided, add it to the documentation parameters # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # Define a list of valid field types # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + + # # Warn the user if the provided type is not valid # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # Define types that require options to be retrieved # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + + # # If the property is being set in a document or field and the type needs options, retrieve them # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if there was an issue retrieving the property options # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # Process the property value based on its type # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # Convert the value to a boolean for Checkbox type # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Convert the value to a Unix timestamp for Date or Date Time type # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Convert the dropdown value to its corresponding GUID # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # # Throw an error if the value is not present in the dropdown options # Removed NinjaOne dependency + # if (!($Selection)) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # For other types, use the value as is # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Set the property value in the document if a document name is provided # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # # Otherwise, set the standard property value # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Throw an error if setting the property failed # Removed NinjaOne dependency + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency +} +process { + # Get the list of unsigned drivers + $UnsignedDrivers = driverquery.exe /si /FO CSV | + ConvertFrom-Csv | + Select-Object @{ + label = "Device Name"; expression = { $_.DeviceName } + }, @{ + label = "INF Name"; expression = { $_.InfName } + }, @{ + label = "Is Signed"; expression = { if ($_.IsSigned -eq "TRUE") { $true }else { $false } } + }, Manufacturer | + Where-Object { -Not $_."Is Signed" } + + # Create Html Table + $HtmlTable = [System.Collections.Generic.List[String]]::new() + + # Add header + $HtmlTable.Add("

Unsigned Drivers

") + + # Output the list of unsigned drivers to the table + if ($UnsignedDrivers) { + Write-Host "[Info] Unsigned Drivers Found" + # Add table of unsigned drivers + $HtmlTable.Add($($UnsignedDrivers | ConvertTo-Html -Fragment | Out-String)) + # Output the list of unsigned drivers + $UnsignedDrivers | Format-Table -AutoSize | Out-String -Width 4000 | Write-Host + } + else { + Write-Host "[Info] No Unsigned Drivers Found" + $HtmlTable.Add("

No unsigned drivers found.

") + } + + # If a custom field name is provided, set the custom field with the list of unsigned drivers + if ($WYSIWYGCustomField) { + try { + Write-Host "[Info] Attempting to set Custom Field '$WYSIWYGCustomField'." + # Set-NinjaProperty -Name $WYSIWYGCustomField -Value $($HtmlTable -join [System.Environment]::NewLine | Out-String) # Removed NinjaOne dependency + Write-Host "[Info] Successfully set Custom Field '$WYSIWYGCustomField'!" + } + catch { + Write-Host "[Warn] Failed to set Custom Field '$WYSIWYGCustomField'." + } + } +} +end { + + + +} diff --git a/Powershell Scripts/Update Group Policy (gpupdate).ps1 b/Powershell Scripts/Update Group Policy (gpupdate).ps1 index 9edf5d6..f1d6658 100644 --- a/Powershell Scripts/Update Group Policy (gpupdate).ps1 +++ b/Powershell Scripts/Update Group Policy (gpupdate).ps1 @@ -298,7 +298,11 @@ process { # Output Report Write-Host $Report - if ($CustomFieldName) { Ninja-Property-Set -Name $CustomFieldName -Value $Report } + if ($CustomFieldName) { + Write-Host "" + Write-Host "Note: Custom field '$CustomFieldName' was specified but NinjaOne integration has been removed." + # Ninja-Property-Set -Name $CustomFieldName -Value $Report + } # If we had any kind of failures its best to not reboot the system or logoff any users diff --git a/Powershell Scripts/Update Location Custom Field based on GeoIP.ps1 b/Powershell Scripts/Update Location Custom Field based on GeoIP.ps1 index 092ee21..dd9a09c 100644 --- a/Powershell Scripts/Update Location Custom Field based on GeoIP.ps1 +++ b/Powershell Scripts/Update Location Custom Field based on GeoIP.ps1 @@ -1,252 +1,252 @@ # Retrieves the approximate location of a device using the Google GeoLocation API and optionally saves it to a multiline custom field. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Retrieves the approximate location of a device using the Google GeoLocation API and optionally saves it to a multiline custom field. -.DESCRIPTION - Retrieves the approximate location of a device using the Google GeoLocation API and optionally saves it to a multiline custom field. -.EXAMPLE - -GoogleApiKey "" -CustomFieldName "Location" - - Approximate Address: 871 N Oak Park Blvd, Pismo Beach, CA 93449, USA - Approximate GPS Coordinates: 35.1324183,-120.6068538 -.INPUTS - None -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 10, Server 2016 - Release Notes: - Updated to work with either Parameters or Script Variables, switched it to use one custom field instead of two. -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$GoogleApiKey, - [Parameter()] - [String]$CustomFieldName -) - -begin { - # Check if Script Variables are being used - if ($env:googleApiKey -and $env:googleApiKey -notlike "null") { - $GoogleApiKey = $env:googleApiKey - } - - if ($env:customFieldName -and $env:customFieldName -notlike "null") { - $CustomFieldName = $env:customFieldName - } - - function Test-StringEmpty { - param([string]$Text) - # Returns true if string is empty, null, or whitespace - process { [string]::IsNullOrEmpty($Text) -or [string]::IsNullOrWhiteSpace($Text) } - } - - # Check if api key is set, error if not set - if ($(Test-StringEmpty -Text $GoogleApiKey)) { - # Both Parameter and Script Variable are empty - # Can not combine Parameter "[Parameter(Mandatory)]" and Script Variable Required - Write-Error "GoogleApiKey is required." - exit 1 - } - - $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType') - if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12 - } - elseif ( $SupportedTLSversions -contains 'Tls12' ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 - } - else { - # Not everything requires TLS 1.2, but we'll try anyway. - Write-Warning "TLS 1.2 and or TLS 1.3 are not supported on this system. Getting the location may fail!" - if ($PSVersionTable.PSVersion.Major -lt 3) { - Write-Warning "PowerShell 2 / .NET 2.0 doesn't support TLS 1.2." - } - } - - # Build URL with API key - $Url = "https://www.googleapis.com/geolocation/v1/geolocate?key=$GoogleApiKey" - - function Get-NearestCity { - param ( - [double]$lat, - [double]$lon, - [string]$GoogleApi - ) - try { - $Response = Invoke-RestMethod -Uri "https://maps.googleapis.com/maps/api/geocode/json?latlng=$lat,$lon&key=$GoogleApi" - } - catch { - throw $Error[0] - } - return $Response.results[0].formatted_address - } - function Get-WifiNetwork { - end { - try { - netsh.exe wlan sh net mode=bssid | ForEach-Object -Process { - if ($_ -match '^SSID (\d+) : (.*)$') { - $current = @{} - $networks += $current - $current.Index = $matches[1].trim() - $current.SSID = $matches[2].trim() - } - else { - if ($_ -match '^\s+(.*)\s+:\s+(.*)\s*$') { - $current[$matches[1].trim()] = $matches[2].trim() - } - } - } -Begin { $networks = @() } -End { $networks | ForEach-Object { New-Object -TypeName "PSObject" -Property $_ } } - } - catch { - # return nothing - } - } - } - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields we can set. If no type is given we'll assume the input doesn't have to be changed in any way. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL" - - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The below field requires additional information in order to set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If we received some sort of error it should have an exception property and we'll exit the function with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli is expecting the time to be representing as a Unix Epoch string. So we'll convert what we were given into that format. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw "Value is not present in dropdown" - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } -} -process { - # Get WIFI network data nearby - $WiFiData = Get-WifiNetwork | - Select-Object @{name = 'age'; expression = { 0 } }, - @{name = 'macAddress'; expression = { $_.'BSSID 1' } }, - @{name = 'channel'; expression = { $_.Channel } }, - @{name = 'signalStrength'; expression = { (($_.Signal -replace "%") / 2) - 100 } } - - # Check if we got any number access points - $Body = if ($WiFiData -and $WiFiData.Count -gt 0) { - @{ - considerIp = $true - wifiAccessPoints = $WiFiData - } | ConvertTo-Json - } - else { - @{ - considerIp = $true - } | ConvertTo-Json - } - - # Get our lat,lng position - try { - $Response = Invoke-RestMethod -Method Post -Uri $Url -Body $Body -ContentType "application/json" - - # Save the relevant results to variable that have shorter names - $Lat = $Response.location.lat - $Lon = $Response.location.lng - - # Get City from Google API's - # Google API: https://developers.google.com/maps/documentation/geocoding/requests-reverse-geocoding - $Address = Get-NearestCity -lat $Lat -lon $Lon -GoogleApi $GoogleApiKey - - $Report = "Approximate Address: $Address`nApproximate GPS Coordinates: $Lat,$Lon" - Write-Host $Report - } - catch { - Write-Error $_ - exit 1 - } - - if (-not $CustomFieldName) { - exit 0 - } - - # Set a custom field - try { - Set-NinjaProperty -Name $CustomFieldName -Value $Report - } - catch { - Write-Error -Message $_.ToString() -Category InvalidOperation -Exception (New-Object System.Exception) - exit 1 - } - - exit 0 -} -end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + Retrieves the approximate location of a device using the Google GeoLocation API and optionally saves it to a multiline custom field. +.DESCRIPTION + Retrieves the approximate location of a device using the Google GeoLocation API and optionally saves it to a multiline custom field. +.EXAMPLE + -GoogleApiKey "" -CustomFieldName "Location" + + Approximate Address: 871 N Oak Park Blvd, Pismo Beach, CA 93449, USA + Approximate GPS Coordinates: 35.1324183,-120.6068538 +.INPUTS + None +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 10, Server 2016 + Release Notes: + Updated to work with either Parameters or Script Variables, switched it to use one custom field instead of two. +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$GoogleApiKey, + [Parameter()] + [String]$CustomFieldName +) + +begin { + # Check if Script Variables are being used + if ($env:googleApiKey -and $env:googleApiKey -notlike "null") { + $GoogleApiKey = $env:googleApiKey + } + + if ($env:customFieldName -and $env:customFieldName -notlike "null") { + $CustomFieldName = $env:customFieldName + } + + function Test-StringEmpty { + param([string]$Text) + # Returns true if string is empty, null, or whitespace + process { [string]::IsNullOrEmpty($Text) -or [string]::IsNullOrWhiteSpace($Text) } + } + + # Check if api key is set, error if not set + if ($(Test-StringEmpty -Text $GoogleApiKey)) { + # Both Parameter and Script Variable are empty + # Can not combine Parameter "[Parameter(Mandatory)]" and Script Variable Required + Write-Error "GoogleApiKey is required." + exit 1 + } + + $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType') + if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12 + } + elseif ( $SupportedTLSversions -contains 'Tls12' ) { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 + } + else { + # Not everything requires TLS 1.2, but we'll try anyway. + Write-Warning "TLS 1.2 and or TLS 1.3 are not supported on this system. Getting the location may fail!" + if ($PSVersionTable.PSVersion.Major -lt 3) { + Write-Warning "PowerShell 2 / .NET 2.0 doesn't support TLS 1.2." + } + } + + # Build URL with API key + $Url = "https://www.googleapis.com/geolocation/v1/geolocate?key=$GoogleApiKey" + + function Get-NearestCity { + param ( + [double]$lat, + [double]$lon, + [string]$GoogleApi + ) + try { + $Response = Invoke-RestMethod -Uri "https://maps.googleapis.com/maps/api/geocode/json?latlng=$lat,$lon&key=$GoogleApi" + } + catch { + throw $Error[0] + } + return $Response.results[0].formatted_address + } + function Get-WifiNetwork { + end { + try { + netsh.exe wlan sh net mode=bssid | ForEach-Object -Process { + if ($_ -match '^SSID (\d+) : (.*)$') { + $current = @{} + $networks += $current + $current.Index = $matches[1].trim() + $current.SSID = $matches[2].trim() + } + else { + if ($_ -match '^\s+(.*)\s+:\s+(.*)\s*$') { + $current[$matches[1].trim()] = $matches[2].trim() + } + } + } -Begin { $networks = @() } -End { $networks | ForEach-Object { New-Object -TypeName "PSObject" -Property $_ } } + } + catch { + # return nothing + } + } + } + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # # If we're requested to set the field value for a Ninja document we'll specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # This is a list of valid fields we can set. If no type is given we'll assume the input doesn't have to be changed in any way. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL" # Removed NinjaOne dependency + + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # The below field requires additional information in order to set # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If we received some sort of error it should have an exception property and we'll exit the function with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli is expecting the time to be representing as a Unix Epoch string. So we'll convert what we were given into that format. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # if (-not $Selection) { # Removed NinjaOne dependency + # throw "Value is not present in dropdown" # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # We'll need to set the field differently depending on if its a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency +} +process { + # Get WIFI network data nearby + $WiFiData = Get-WifiNetwork | + Select-Object @{name = 'age'; expression = { 0 } }, + @{name = 'macAddress'; expression = { $_.'BSSID 1' } }, + @{name = 'channel'; expression = { $_.Channel } }, + @{name = 'signalStrength'; expression = { (($_.Signal -replace "%") / 2) - 100 } } + + # Check if we got any number access points + $Body = if ($WiFiData -and $WiFiData.Count -gt 0) { + @{ + considerIp = $true + wifiAccessPoints = $WiFiData + } | ConvertTo-Json + } + else { + @{ + considerIp = $true + } | ConvertTo-Json + } + + # Get our lat,lng position + try { + $Response = Invoke-RestMethod -Method Post -Uri $Url -Body $Body -ContentType "application/json" + + # Save the relevant results to variable that have shorter names + $Lat = $Response.location.lat + $Lon = $Response.location.lng + + # Get City from Google API's + # Google API: https://developers.google.com/maps/documentation/geocoding/requests-reverse-geocoding + $Address = Get-NearestCity -lat $Lat -lon $Lon -GoogleApi $GoogleApiKey + + $Report = "Approximate Address: $Address`nApproximate GPS Coordinates: $Lat,$Lon" + Write-Host $Report + } + catch { + Write-Error $_ + exit 1 + } + + if (-not $CustomFieldName) { + exit 0 + } + + # Set a custom field + try { + # Set-NinjaProperty -Name $CustomFieldName -Value $Report # Removed NinjaOne dependency + } + catch { + Write-Error -Message $_.ToString() -Category InvalidOperation -Exception (New-Object System.Exception) + exit 1 + } + + exit 0 +} +end { + + + +} + diff --git a/Powershell Scripts/User Must Be Logged In Alert.ps1 b/Powershell Scripts/User Must Be Logged In Alert.ps1 index a50c6c5..1e71b5a 100644 --- a/Powershell Scripts/User Must Be Logged In Alert.ps1 +++ b/Powershell Scripts/User Must Be Logged In Alert.ps1 @@ -1,349 +1,349 @@ # Alerts if no user is logged in or if the specified user(s) are not logged in. You can optionally retrieve a comma-separated list from a custom field you specify. - -<# -.SYNOPSIS - Alerts if no user is logged in or if the specified user(s) are not logged in. You can optionally retrieve a comma-separated list from a custom field you specify. -.DESCRIPTION - Alerts if no user is logged in or if the specified user(s) are not logged in. You can optionally retrieve a comma-separated list from a custom field you specify. -.EXAMPLE - (No Parameters) - A user was not given to look for. Alerting if no user is logged in. - A user is currently signed in! - - Username SessionName ID State IdleTime LogonTime - -------- ----------- -- ----- -------- --------- - cheart console 1 Active none 10/24/2024 6:31 PM - -PARAMETER: -UsersToCheckFor "itAdmin" - Specify a comma-separated list of users you would like to alert on if they are not currently logged in. - -PARAMETER: -CustomFieldName "ReplaceMeWithAnyTextCustomField" - The name of a text custom field from which to retrieve the UsersToCheckFor value. - -PARAMETER: -ActiveOnly - Alerts only if the user is listed as 'active' in quser.exe. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2012 R2 - Release Notes: Allows checking for multiple users, added data validation, stopped using exit codes as alert triggers, removed Write-Error, switched to the standard alert tag '[Alert]' for when an alert is triggered. Updated functions. -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$UsersToCheckFor, - [Parameter()] - [String]$CustomFieldName, - [Parameter()] - [Switch]$ActiveOnly = [System.Convert]::ToBoolean($env:userMustBeActive) -) - -begin { - # If script variables are used overwrite the existing variables. - if ($env:usersToCheckFor -and $env:usersToCheckFor -notlike "null") { $UsersToCheckFor = $env:usersToCheckFor } - if ($env:retrieveUserFromCustomFieldName -and $env:retrieveUserFromCustomFieldName -notlike "null") { $CustomFieldName = $env:retrieveUserFromCustomFieldName } - - if ($PSversionTable.PSVersion.Major -lt 3 -and $CustomFieldName) { - Write-Host -Object "[Error] PowerShell 3 or higher is required to retrieve from custom fields." - Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/en-us/articles/4405408656013-Custom-Fields-and-Documentation-CLI-and-Scripting" - exit 1 - } - - # This function is to make it easier to parse Ninja Custom Fields. - function Get-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter()] - [String]$DocumentName - ) - - # Initialize a hashtable for documentation parameters - $DocumentationParams = @{} - - # If a document name is provided, add it to the documentation parameters - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # Define types that require options to be retrieved - $NeedsOptions = "DropDown", "MultiSelect" - - # If a document name is provided, retrieve the property value from the document - if ($DocumentName) { - # Throw an error if the type is "Secure", as it's not a valid type in this context - if ($Type -Like "Secure") { throw [System.ArgumentOutOfRangeException]::New("$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality") } - - # Notify the user that the value is being retrieved from a Ninja document - Write-Host "Retrieving value from Ninja Document..." - $NinjaPropertyValue = Ninja-Property-Docs-Get -AttributeName $Name @DocumentationParams 2>&1 - - # If the property type requires options, retrieve them - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - # If no document name is provided, retrieve the property value directly - $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1 - - # If the property type requires options, retrieve them - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # Throw an exception if there was an error retrieving the property value or options - if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # Throw an error if the retrieved property value is null or empty - if (!($NinjaPropertyValue)) { - Write-Warning -Message "The Custom Field '$Name' is empty." - } - - # Handle the property value based on its type - switch ($Type) { - "Attachment" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Checkbox" { - # Convert the value to a boolean - [System.Convert]::ToBoolean([int]$NinjaPropertyValue) - } - "Date or Date Time" { - # Convert a Unix timestamp to local date and time - $UnixTimeStamp = $NinjaPropertyValue - $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp) - $TimeZone = [TimeZoneInfo]::Local - [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) - } - "Decimal" { - # Convert the value to a double (floating-point number) - [double]$NinjaPropertyValue - } - "Device Dropdown" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Device MultiSelect" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Dropdown" { - # Convert options to a CSV format and match the GUID to retrieve the display name - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name - } - "Integer" { - # Convert the value to an integer - [int]$NinjaPropertyValue - } - "MultiSelect" { - # Convert options to a CSV format, then match and return selected items - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = ($NinjaPropertyValue -split ',').trim() - - foreach ($Item in $Selection) { - $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name - } - } - "Organization Dropdown" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization Location Dropdown" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization Location MultiSelect" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization MultiSelect" { - # Convert JSON formatted property value to a PowerShell object - $NinjaPropertyValue | ConvertFrom-Json - } - "Time" { - # Convert the value from seconds to a time format in the local timezone - $Seconds = $NinjaPropertyValue - $UTC = ([timespan]::fromseconds($Seconds)).ToString("hh\:mm\:ss") - $TimeZone = [TimeZoneInfo]::Local - $ConvertedTime = [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) - - Get-Date $ConvertedTime -DisplayHint Time - } - default { - # For any other types, return the raw value - $NinjaPropertyValue - } - } - } - - function Get-QueryUser { - # Run the quser.exe command to get the list of currently logged-in users - try { - $ErrorActionPreference = "Stop" - $QuserOutput = quser.exe - $ErrorActionPreference = "Continue" - } - catch { - throw $_ - } - - $i = 0 - $QuserOutput | Where-Object { $_.Trim() } | ForEach-Object { - # Skip the first line (header) and process only the data lines - if ($i -ne 0) { - # Extract the relevant columns using fixed width positions - [PSCustomObject]@{ - Username = ($_.Substring(0, 21).Trim() -replace '^>') - SessionName = $_.Substring(21, 21).Trim() - ID = $_.Substring(40, 5).Trim() - State = $_.Substring(45, 10).Trim() - IdleTime = $_.Substring(55, 10).Trim() - LogonTime = $_.Substring(65).Trim() - } - } - - $i++ - } - } - - if (!$ExitCode) { - $ExitCode = 0 - } -}process { - # Initialize an empty list to store the usernames that will be alerted on. - $UsersToAlertOn = New-Object System.Collections.Generic.List[string] - - # Split the $UsersToCheckFor string by commas, trim each username, and add to the list. - $UsersToCheckFor -split ',' | ForEach-Object { - $User = $_.Trim() - - # Check if the username starts and ends with double quotes (") using regex matching. - if ($User -match '^"' -and $User -match '"$') { - $User = $User -replace '^"' -replace '"$' - } - - # Check if the username contains any invalid characters (special characters) using a regular expression. - if ($User -match '\[|\]|:|;|\||=|\+|\*|\?|<|>|/|\\|"|@') { - Write-Host -Object ("[Error] $_ contains one of the following invalid characters." + ' " [ ] : ; | = + * ? < > / \ @') - - # Set an exit code to indicate an error - $ExitCode = 1 - return - } - - # Only add non-empty usernames to the list. - if ($User) { - $UsersToAlertOn.Add($User) - } - } - - # If a custom field name is provided, retrieve user list from that field. - if ($CustomFieldName) { - try { - # Retrieve users from the custom field. - $UsersToCheckFor = Get-NinjaProperty -Name $CustomFieldName - - # Split, trim, and add users from the custom field to the alert list. - $UsersToCheckFor -split ',' | ForEach-Object { - $User = $_.Trim() - - # Check if the username starts and ends with double quotes (") using regex matching. - if ($User -match '^"' -and $User -match '"$') { - $User = $User -replace '^"' -replace '"$' - } - - # Check if the username contains any invalid characters (special characters) using a regular expression. - if ($User -match '\[|\]|:|;|\||=|\+|\*|\?|<|>|/|\\|"|@') { - Write-Host -Object ("[Error] $_ contains one of the following invalid characters." + ' " [ ] : ; | = + * ? < > / \ @') - - # Set an exit code to indicate an error - $ExitCode = 1 - return - } - - # Add non-empty usernames to the list. - if ($User) { - $UsersToAlertOn.Add($User) - } - } - } - catch { - # If an error occurs, output the error message and exit the script with status 1. - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - } - - # If users were added to the alert list, display them. Otherwise, notify that no specific user was given. - if ($UsersToAlertOn.Count -gt 0) { - Write-Host -Object "Checking for the following users: $($UsersToAlertOn -join ", ")" - } - else { - Write-Host -Object "A user was not given to look for. Alerting if no user is logged in." - } - - # Try to retrieve the currently logged-in users. - try { - $LoggedInUsers = Get-QueryUser - } - catch { - # Output error message and exit if the Get-QueryUser command fails. - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # If specific users are being checked, filter the logged-in users to only include those from the alert list. - if ($UsersToAlertOn.Count -gt 0) { - $LoggedInUsers = $LoggedInUsers | Where-Object { $UsersToAlertOn -contains $_.Username } - } - - # If the $ActiveOnly flag is set, further filter the logged-in users to only include those marked as active. - if ($ActiveOnly) { - $LoggedInUsers = $LoggedInUsers | Where-Object { $_.State -like "Active" } - } - - # If no users were found after filtering, alert that no users are logged in or specific users are not logged in. - if (!$LoggedInUsers) { - if ($UsersToAlertOn.Count -gt 0) { - Write-Host -Object "[Alert] The user(s) you are checking for are not currently logged in." - } - else { - Write-Host -Object "[Alert] No users are currently logged in." - } - } - - # If users were found after filtering, check if any of the users from the alert list are logged in or not. - if ($LoggedInUsers) { - if ($UsersToAlertOn.Count -gt 0) { - # Alert for users who are not logged in. - $UsersToAlertOn | Where-Object { $LoggedInUsers.Username -notcontains $_ } | ForEach-Object { - Write-Host -Object "[Alert] $_ is not currently logged in." - } - - # Notify for users who are currently logged in. - $UsersToAlertOn | Where-Object { $LoggedInUsers.Username -contains $_ } | ForEach-Object { - Write-Host -Object "$_ is currently logged in." - } - } - else { - # If no specific users were given, notify that a user is logged in. - Write-Host -Object "A user is currently signed in!" - } - } - - # Display the list of logged-in users in a table format. - $LoggedInUsers | Format-Table | Out-String | Write-Host - - exit $ExitCode -}end { - - - -} - + +<# +.SYNOPSIS + Alerts if no user is logged in or if the specified user(s) are not logged in. You can optionally retrieve a comma-separated list from a custom field you specify. +.DESCRIPTION + Alerts if no user is logged in or if the specified user(s) are not logged in. You can optionally retrieve a comma-separated list from a custom field you specify. +.EXAMPLE + (No Parameters) + A user was not given to look for. Alerting if no user is logged in. + A user is currently signed in! + + Username SessionName ID State IdleTime LogonTime + -------- ----------- -- ----- -------- --------- + cheart console 1 Active none 10/24/2024 6:31 PM + +PARAMETER: -UsersToCheckFor "itAdmin" + Specify a comma-separated list of users you would like to alert on if they are not currently logged in. + +PARAMETER: -CustomFieldName "ReplaceMeWithAnyTextCustomField" + The name of a text custom field from which to retrieve the UsersToCheckFor value. + +PARAMETER: -ActiveOnly + Alerts only if the user is listed as 'active' in quser.exe. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2012 R2 + Release Notes: Allows checking for multiple users, added data validation, stopped using exit codes as alert triggers, removed Write-Error, switched to the standard alert tag '[Alert]' for when an alert is triggered. Updated functions. +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$UsersToCheckFor, + [Parameter()] + [String]$CustomFieldName, + [Parameter()] + [Switch]$ActiveOnly = [System.Convert]::ToBoolean($env:userMustBeActive) +) + +begin { + # If script variables are used overwrite the existing variables. + if ($env:usersToCheckFor -and $env:usersToCheckFor -notlike "null") { $UsersToCheckFor = $env:usersToCheckFor } + if ($env:retrieveUserFromCustomFieldName -and $env:retrieveUserFromCustomFieldName -notlike "null") { $CustomFieldName = $env:retrieveUserFromCustomFieldName } + + if ($PSversionTable.PSVersion.Major -lt 3 -and $CustomFieldName) { + Write-Host -Object "[Error] PowerShell 3 or higher is required to retrieve from custom fields." + Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/en-us/articles/4405408656013-Custom-Fields-and-Documentation-CLI-and-Scripting" + exit 1 + } + + # This function is to make it easier to parse Ninja Custom Fields. + function Get-NinjaProperty { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [String]$Name, + [Parameter()] + [String]$Type, + [Parameter()] + [String]$DocumentName + ) + + # Initialize a hashtable for documentation parameters + $DocumentationParams = @{} + + # If a document name is provided, add it to the documentation parameters + if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } + + # Define types that require options to be retrieved + $NeedsOptions = "DropDown", "MultiSelect" + + # If a document name is provided, retrieve the property value from the document + if ($DocumentName) { + # Throw an error if the type is "Secure", as it's not a valid type in this context + if ($Type -Like "Secure") { throw [System.ArgumentOutOfRangeException]::New("$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality") } + + # Notify the user that the value is being retrieved from a Ninja document + Write-Host "Retrieving value from Ninja Document..." + # $NinjaPropertyValue = Ninja-Property-Docs-Get -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + + # If the property type requires options, retrieve them + if ($NeedsOptions -contains $Type) { + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + } + } + else { + # If no document name is provided, retrieve the property value directly + # $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1 # Removed NinjaOne dependency + + # If the property type requires options, retrieve them + if ($NeedsOptions -contains $Type) { + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + } + } + + # Throw an exception if there was an error retrieving the property value or options + if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } + if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } + + # Throw an error if the retrieved property value is null or empty + if (!($NinjaPropertyValue)) { + Write-Warning -Message "The Custom Field '$Name' is empty." + } + + # Handle the property value based on its type + switch ($Type) { + "Attachment" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Checkbox" { + # Convert the value to a boolean + [System.Convert]::ToBoolean([int]$NinjaPropertyValue) + } + "Date or Date Time" { + # Convert a Unix timestamp to local date and time + $UnixTimeStamp = $NinjaPropertyValue + $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp) + $TimeZone = [TimeZoneInfo]::Local + [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) + } + "Decimal" { + # Convert the value to a double (floating-point number) + [double]$NinjaPropertyValue + } + "Device Dropdown" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Device MultiSelect" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Dropdown" { + # Convert options to a CSV format and match the GUID to retrieve the display name + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name + } + "Integer" { + # Convert the value to an integer + [int]$NinjaPropertyValue + } + "MultiSelect" { + # Convert options to a CSV format, then match and return selected items + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Selection = ($NinjaPropertyValue -split ',').trim() + + foreach ($Item in $Selection) { + $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name + } + } + "Organization Dropdown" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization Location Dropdown" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization Location MultiSelect" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization MultiSelect" { + # Convert JSON formatted property value to a PowerShell object + $NinjaPropertyValue | ConvertFrom-Json + } + "Time" { + # Convert the value from seconds to a time format in the local timezone + $Seconds = $NinjaPropertyValue + $UTC = ([timespan]::fromseconds($Seconds)).ToString("hh\:mm\:ss") + $TimeZone = [TimeZoneInfo]::Local + $ConvertedTime = [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) + + Get-Date $ConvertedTime -DisplayHint Time + } + default { + # For any other types, return the raw value + $NinjaPropertyValue + } + } + } + + function Get-QueryUser { + # Run the quser.exe command to get the list of currently logged-in users + try { + $ErrorActionPreference = "Stop" + $QuserOutput = quser.exe + $ErrorActionPreference = "Continue" + } + catch { + throw $_ + } + + $i = 0 + $QuserOutput | Where-Object { $_.Trim() } | ForEach-Object { + # Skip the first line (header) and process only the data lines + if ($i -ne 0) { + # Extract the relevant columns using fixed width positions + [PSCustomObject]@{ + Username = ($_.Substring(0, 21).Trim() -replace '^>') + SessionName = $_.Substring(21, 21).Trim() + ID = $_.Substring(40, 5).Trim() + State = $_.Substring(45, 10).Trim() + IdleTime = $_.Substring(55, 10).Trim() + LogonTime = $_.Substring(65).Trim() + } + } + + $i++ + } + } + + if (!$ExitCode) { + $ExitCode = 0 + } +}process { + # Initialize an empty list to store the usernames that will be alerted on. + $UsersToAlertOn = New-Object System.Collections.Generic.List[string] + + # Split the $UsersToCheckFor string by commas, trim each username, and add to the list. + $UsersToCheckFor -split ',' | ForEach-Object { + $User = $_.Trim() + + # Check if the username starts and ends with double quotes (") using regex matching. + if ($User -match '^"' -and $User -match '"$') { + $User = $User -replace '^"' -replace '"$' + } + + # Check if the username contains any invalid characters (special characters) using a regular expression. + if ($User -match '\[|\]|:|;|\||=|\+|\*|\?|<|>|/|\\|"|@') { + Write-Host -Object ("[Error] $_ contains one of the following invalid characters." + ' " [ ] : ; | = + * ? < > / \ @') + + # Set an exit code to indicate an error + $ExitCode = 1 + return + } + + # Only add non-empty usernames to the list. + if ($User) { + $UsersToAlertOn.Add($User) + } + } + + # If a custom field name is provided, retrieve user list from that field. + if ($CustomFieldName) { + try { + # Retrieve users from the custom field. + $UsersToCheckFor = Get-NinjaProperty -Name $CustomFieldName + + # Split, trim, and add users from the custom field to the alert list. + $UsersToCheckFor -split ',' | ForEach-Object { + $User = $_.Trim() + + # Check if the username starts and ends with double quotes (") using regex matching. + if ($User -match '^"' -and $User -match '"$') { + $User = $User -replace '^"' -replace '"$' + } + + # Check if the username contains any invalid characters (special characters) using a regular expression. + if ($User -match '\[|\]|:|;|\||=|\+|\*|\?|<|>|/|\\|"|@') { + Write-Host -Object ("[Error] $_ contains one of the following invalid characters." + ' " [ ] : ; | = + * ? < > / \ @') + + # Set an exit code to indicate an error + $ExitCode = 1 + return + } + + # Add non-empty usernames to the list. + if ($User) { + $UsersToAlertOn.Add($User) + } + } + } + catch { + # If an error occurs, output the error message and exit the script with status 1. + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + } + + # If users were added to the alert list, display them. Otherwise, notify that no specific user was given. + if ($UsersToAlertOn.Count -gt 0) { + Write-Host -Object "Checking for the following users: $($UsersToAlertOn -join ", ")" + } + else { + Write-Host -Object "A user was not given to look for. Alerting if no user is logged in." + } + + # Try to retrieve the currently logged-in users. + try { + $LoggedInUsers = Get-QueryUser + } + catch { + # Output error message and exit if the Get-QueryUser command fails. + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # If specific users are being checked, filter the logged-in users to only include those from the alert list. + if ($UsersToAlertOn.Count -gt 0) { + $LoggedInUsers = $LoggedInUsers | Where-Object { $UsersToAlertOn -contains $_.Username } + } + + # If the $ActiveOnly flag is set, further filter the logged-in users to only include those marked as active. + if ($ActiveOnly) { + $LoggedInUsers = $LoggedInUsers | Where-Object { $_.State -like "Active" } + } + + # If no users were found after filtering, alert that no users are logged in or specific users are not logged in. + if (!$LoggedInUsers) { + if ($UsersToAlertOn.Count -gt 0) { + Write-Host -Object "[Alert] The user(s) you are checking for are not currently logged in." + } + else { + Write-Host -Object "[Alert] No users are currently logged in." + } + } + + # If users were found after filtering, check if any of the users from the alert list are logged in or not. + if ($LoggedInUsers) { + if ($UsersToAlertOn.Count -gt 0) { + # Alert for users who are not logged in. + $UsersToAlertOn | Where-Object { $LoggedInUsers.Username -notcontains $_ } | ForEach-Object { + Write-Host -Object "[Alert] $_ is not currently logged in." + } + + # Notify for users who are currently logged in. + $UsersToAlertOn | Where-Object { $LoggedInUsers.Username -contains $_ } | ForEach-Object { + Write-Host -Object "$_ is currently logged in." + } + } + else { + # If no specific users were given, notify that a user is logged in. + Write-Host -Object "A user is currently signed in!" + } + } + + # Display the list of logged-in users in a table format. + $LoggedInUsers | Format-Table | Out-String | Write-Host + + exit $ExitCode +}end { + + + +} + diff --git a/Powershell Scripts/User Profile Size Report.ps1 b/Powershell Scripts/User Profile Size Report.ps1 index 2e26846..d52f4f6 100644 --- a/Powershell Scripts/User Profile Size Report.ps1 +++ b/Powershell Scripts/User Profile Size Report.ps1 @@ -1,98 +1,98 @@ # Updates a Custom Field with the total size of all User Profiles. If the Max parameter is specified then it will return an exit code of 1 for any profile being over that Max threshold in GB. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Updates a Custom Field with the total size of all User Profiles. If the Max parameter is specified then it will return an exit code of 1 for any profile being over that Max threshold in GB. -.DESCRIPTION - Updates a Custom Field with the total size of all User Profiles. - If the Max parameter is specified then it will return an exit code of 1 - for any profile being over that Max threshold in GB. -.EXAMPLE - -Max 60 - Returns and exit code of 1 if any profile is over 60GB -.EXAMPLE - -CustomField "Something" - Specifies the name of the custom field to update. -.EXAMPLE - No Parameter needed. - Uses the default custom field name: TotalUsersProfileSize -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Renamed script and added Script Variable support -#> - -[CmdletBinding()] -param ( - [Parameter()] - [Alias("MaxSize", "Size", "ms", "m", "s")] - [Double]$Max, - [Parameter()] - [Alias("Custom", "Field", "cf", "c", "f")] - [String]$CustomField = "TotalUsersProfileSize" -) - -begin { - if ($env:sizeInGbToAlertOn -and $env:sizeInGbToAlertOn -notlike "null") { $Max = $env:sizeInGbToAlertOn } - if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - function Format-FileSize { - param($Length) - switch ($Length) { - { $_ / 1TB -gt 1 } { "$([Math]::Round(($_ / 1TB),2)) TB"; break } - { $_ / 1GB -gt 1 } { "$([Math]::Round(($_ / 1GB),2)) GB"; break } - { $_ / 1MB -gt 1 } { "$([Math]::Round(($_ / 1MB),2)) MB"; break } - { $_ / 1KB -gt 1 } { "$([Math]::Round(($_ / 1KB),2)) KB"; break } - Default { "$_ Bytes" } - } - } -} -process { - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - $Profiles = Get-ChildItem -Path "C:\Users" - $ProfileSizes = $Profiles | ForEach-Object { - [PSCustomObject]@{ - Name = $_.BaseName - Length = Get-ChildItem -Path $_.FullName -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue | Select-Object -Property Sum -ExpandProperty Sum - } - } - $Largest = $ProfileSizes | Sort-Object -Property Length -Descending | Select-Object -First 1 - - $Size = $ProfileSizes | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue | Select-Object -Property Sum -ExpandProperty Sum - - $FormattedSize = Format-FileSize -Length $Size - - $AllProfiles = $ProfileSizes | Sort-Object -Property Length -Descending | ForEach-Object { - $FormattedSizeUser = Format-FileSize -Length $_.Length - "$($_.Name) $($FormattedSizeUser)" - } - - Write-Host "All Profiles - $FormattedSize, $($AllProfiles -join ', ')" - - Ninja-Property-Set -Name $CustomField -Value "$AllProfiles" - - if ($Max -and $Max -gt 0) { - if ($Largest.Length -gt $Max * 1GB) { - Write-Host "Found profile over the max size of $Max GB." - Write-Host "$($Largest.Name) profile is $($Largest.Length / 1GB) GB" - exit 1 - } - } - exit 0 -} -end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + Updates a Custom Field with the total size of all User Profiles. If the Max parameter is specified then it will return an exit code of 1 for any profile being over that Max threshold in GB. +.DESCRIPTION + Updates a Custom Field with the total size of all User Profiles. + If the Max parameter is specified then it will return an exit code of 1 + for any profile being over that Max threshold in GB. +.EXAMPLE + -Max 60 + Returns and exit code of 1 if any profile is over 60GB +.EXAMPLE + -CustomField "Something" + Specifies the name of the custom field to update. +.EXAMPLE + No Parameter needed. + Uses the default custom field name: TotalUsersProfileSize +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Renamed script and added Script Variable support +#> + +[CmdletBinding()] +param ( + [Parameter()] + [Alias("MaxSize", "Size", "ms", "m", "s")] + [Double]$Max, + [Parameter()] + [Alias("Custom", "Field", "cf", "c", "f")] + [String]$CustomField = "TotalUsersProfileSize" +) + +begin { + if ($env:sizeInGbToAlertOn -and $env:sizeInGbToAlertOn -notlike "null") { $Max = $env:sizeInGbToAlertOn } + if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + function Format-FileSize { + param($Length) + switch ($Length) { + { $_ / 1TB -gt 1 } { "$([Math]::Round(($_ / 1TB),2)) TB"; break } + { $_ / 1GB -gt 1 } { "$([Math]::Round(($_ / 1GB),2)) GB"; break } + { $_ / 1MB -gt 1 } { "$([Math]::Round(($_ / 1MB),2)) MB"; break } + { $_ / 1KB -gt 1 } { "$([Math]::Round(($_ / 1KB),2)) KB"; break } + Default { "$_ Bytes" } + } + } +} +process { + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + $Profiles = Get-ChildItem -Path "C:\Users" + $ProfileSizes = $Profiles | ForEach-Object { + [PSCustomObject]@{ + Name = $_.BaseName + Length = Get-ChildItem -Path $_.FullName -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue | Select-Object -Property Sum -ExpandProperty Sum + } + } + $Largest = $ProfileSizes | Sort-Object -Property Length -Descending | Select-Object -First 1 + + $Size = $ProfileSizes | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue | Select-Object -Property Sum -ExpandProperty Sum + + $FormattedSize = Format-FileSize -Length $Size + + $AllProfiles = $ProfileSizes | Sort-Object -Property Length -Descending | ForEach-Object { + $FormattedSizeUser = Format-FileSize -Length $_.Length + "$($_.Name) $($FormattedSizeUser)" + } + + Write-Host "All Profiles - $FormattedSize, $($AllProfiles -join ', ')" + + # Ninja-Property-Set -Name $CustomField -Value "$AllProfiles" # Removed NinjaOne dependency + + if ($Max -and $Max -gt 0) { + if ($Largest.Length -gt $Max * 1GB) { + Write-Host "Found profile over the max size of $Max GB." + Write-Host "$($Largest.Name) profile is $($Largest.Length / 1GB) GB" + exit 1 + } + } + exit 0 +} +end { + + + +} + diff --git a/Powershell Scripts/User or Group Membership Report.ps1 b/Powershell Scripts/User or Group Membership Report.ps1 index ca97af5..45e52ee 100644 --- a/Powershell Scripts/User or Group Membership Report.ps1 +++ b/Powershell Scripts/User or Group Membership Report.ps1 @@ -1,353 +1,353 @@ # This will output the user or group membership for the specified group or user. - -<# -.SYNOPSIS - This will output the user or group membership for the specified group or user. -.DESCRIPTION - This will output the user or group membership for the specified group or user. - -PARAMETER: -Usernames "ReplaceMe","ReplaceMe2" (Quotations are not necessary if using script variables) - Grabs the group membership for each user you specified. -.EXAMPLE - -Usernames "Administrator" (Windows 10 - Domain Joined) - - A Domain Joined Computer was detected. Attempting to search Active Directory... - WARNING: The Active Directory Powershell Module was not found please install RSAT for better results. - Searching Active Directory using the ADSI Searcher... - Searching local groups... - #### User Membership #### - - Group Group Type Member - ----- ---------- ------ - Administrators Domain Administrator - Administrators Local Administrator - Domain Admins Domain Administrator - Domain Users Domain Administrator - Enterprise Admins Domain Administrator - Group Policy Creator Owners Domain Administrator - Schema Admins Domain Administrator - -PARAMETER: -Groups "ReplaceMe","ReplaceMe2" (Quotations are not necessary if using script variables) - Grabs the user membership for each group you specified. -.EXAMPLE - -Groups "Domain Admins" (Server 2008 - Domain Controller) - - A Domain Joined Computer was detected. Attempting to search Active Directory... - Searching Active Directory using the Active Directory Powershell Module... - Searching local groups... - #### Group Membership #### - - Member Group Type Group - ------ ---------- ----- - Administrator Domain Domain Admins - kbohlander Domain Domain Admins - -PARAMETER: -LastLoggedInUser - Checks the last logged in user. - -PARAMETER: -AzureAD - Adds 'AzureAD\' prefix to usernames - -PARAMETER: -CustomField "ReplaceMeWithAMultilineCustomField" - Outputs the results to a multiline customfield of your choice. -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 7, Windows Server 2008 - Release Notes: Updated Calculated Name -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String[]]$Usernames, - [Parameter()] - [String[]]$Groups, - [Parameter()] - [Switch]$AzureAD = [System.Convert]::ToBoolean($env:azureadAccounts), - [Parameter()] - [Switch]$LastLoggedInUser = [System.Convert]::ToBoolean($env:getLastLoggedInUserMembership), - [Parameter()] - [String]$CustomField -) - -begin { - # If script variables are used replace the parameters - if ($env:usernames -and $env:usernames -notlike "null") { - $Usernames = $env:usernames -split ',' | ForEach-Object { $_.trim() } - } - - if ($env:groups -and $env:groups -notlike "null") { - $Groups = $env:groups -split ',' | ForEach-Object { $_.trim() } - } - - if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } - - # Microsoft always adds AzureAD\ as a prefix to AzureAD Accounts. - if($Usernames -and $AzureAD){ - Write-Warning "Adding AzureAD\ prefix to all usernames in the list." - $AzureAccounts = $Usernames | ForEach-Object { - "AzureAD\$_" - } - $Usernames = $AzureAccounts - } - - # We'll check the last login registry key and replace the extra info we don't want / need - if($LastLoggedInUser){ - $Regkey = (Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI").LastLoggedOnSAMUser - if($Regkey){ - if($Regkey -notlike "AzureAD\*"){ - $LastLogon = $Regkey -replace ".*\\" - }else{ - $LastLogon = $Regkey - } - - Write-Host "Adding $LastLogon to the list!" - $Usernames += $LastLogon - }else{ - Write-Warning "No user has previously signed in (you may need to reboot). Skipping!" - } - } - - # Error out if we're missing information - if (-not ($Usernames) -and -not ($Groups)) { - Write-Error "You must specify at least 1 group or 1 user to get the membership for." - Exit 1 - } - - # Check if domain joined. - function Test-IsDomainJoined { - if ($PSVersionTable.PSVersion.Major -ge 5) { - return $(Get-CimInstance -Class Win32_ComputerSystem).PartOfDomain - } - else { - return $(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain - } - } - - # When executed as System net localgroup gives an error. This will grab and filter the groups using a wmi query. - function Get-AllLocalGroups { - if ($PSVersionTable.PSVersion.Major -ge 5) { - Get-CimInstance -Class Win32_Group | Where-Object { $_.LocalAccount -eq $True } | Select-Object -ExpandProperty Name - } - else { - Get-WmiObject -Class Win32_Group | Where-Object { $_.LocalAccount -eq $True } | Select-Object -ExpandProperty Name - } - } -} -process { - - if ($Usernames) { - $UserResults = New-Object System.Collections.Generic.List[Object] - if (Test-IsDomainJoined) { - # The active directory powershell module is prefered but not required - Write-Host "A Domain Joined Computer was detected. Attempting to search Active Directory..." - if ((Get-Module -Name ActiveDirectory -ListAvailable -ErrorAction SilentlyContinue)) { - Import-Module -Name ActiveDirectory - Write-Host "Searching Active Directory using the Active Directory Powershell Module..." - foreach ($User in $Usernames) { - Get-ADGroup -Filter * | ForEach-Object { - if (Get-ADGroupMember $_ | Where-Object { $_.SamAccountName -like $User }) { - $UserResults.Add( - ( - New-Object psobject -Property @{ - Member = $User - Group = ($_ | Select-Object -ExpandProperty SamAccountName) - "Group Type" = "Domain" - } - ) - ) - } - } - } - } - else { - Write-Warning "The Active Directory Powershell Module was not found switching to ADSI Searcher..." - if (Test-ComputerSecureChannel -ErrorAction SilentlyContinue) { - Write-Host "Searching Active Directory using the ADSI Searcher..." - # If for some reason this script is ran without the active directory powershell module we'll do two adsi searches to get the membership info - foreach ($User in $Usernames) { - $search = [adsisearcher]"samaccountname=$User" - # The memberof search won't include the primary group so we'll have to search separately for that - $primarygroup = $search.FindOne().Properties.primarygroupid - $primaryGroupSearch = [adsisearcher]"objectclass=group" - # When adding a property it always outputs the number of properties currently added. - $primaryGroupSearch.PropertiesToLoad.Add("SamAccountName") | Out-Null - $primaryGroupSearch.PropertiesToLoad.Add("PrimaryGroupToken") | Out-Null - $primaryGroupSearch.FindAll() | ForEach-Object { - if ($_.Properties.primarygrouptoken -like $primarygroup) { - $UserResults.Add( - ( - New-Object psobject -Property @{ - Member = $User - Group = ($_.Properties.samaccountname | Out-String).Trim() - "Group Type" = "Domain" - } - ) - ) - } - } - $search.FindOne().Properties.memberof | ForEach-Object { - $namesearch = [adsisearcher]"distinguishedname=$_" - $namesearch.PropertiesToLoad.Add("SamAccountName") | Out-Null - $UserResults.Add( - ( - New-Object psobject -Property @{ - Member = $User - Group = ($namesearch.FindOne().Properties.samaccountname | Out-String).Trim() - "Group Type" = "Domain" - } - ) - ) - } - } - } - else { - Write-Warning "A Secure connection to the domain could not be established. Unable to get Active Directory memberships." - } - } - } - - # Grabs the localgroup info using net localgroup - Write-Host "Searching local groups..." - $netlocalgroup = Get-AllLocalGroups - $netlocalgroup | ForEach-Object { - foreach ($User in $Usernames) { - if ((net.exe localgroup $_) -replace 'The command completed successfully.' | Select-Object -Skip 6 | Where-Object { $_ -and $_ -like "*$User" }) { - $UserResults.Add( - ( - New-Object psobject -Property @{ - Member = $User - Group = $_ - "Group Type" = "Local" - } - ) - ) - } - } - } - - if ($UserResults) { - Write-Host "User Membership info found!" - Write-Host "#### User Membership ####" - $UserResults | Sort-Object -Property Group | Format-Table -Property Group, "Group Type", Member -AutoSize | Out-String | Write-Host - } - } - - # All of this ia pretty similar to grabing the user membership except its searching for the group membership instead of the user - if ($Groups) { - $GroupResults = New-Object System.Collections.Generic.List[Object] - if (Test-IsDomainJoined) { - Write-Host "A Domain Joined Computer was detected. Attempting to search Active Directory..." - if ((Get-Module -Name ActiveDirectory -ListAvailable -ErrorAction SilentlyContinue)) { - Import-Module -Name ActiveDirectory - Write-Host "Searching Active Directory using the Active Directory Powershell Module..." - Get-ADGroup -Filter * | Where-Object { $Groups -contains $_.SamAccountName } | ForEach-Object { - $Group = $_.SamAccountName - Get-ADGroupMember $_ | ForEach-Object { - $GroupResults.Add( - ( - New-Object psobject -Property @{ - Member = $_.SamAccountName - Group = $Group - "Group Type" = "Domain" - } - ) - ) - } - } - } - else { - Write-Warning "The Active Directory Powershell Module was not found switching to ADSI Searcher..." - if (Test-ComputerSecureChannel -ErrorAction SilentlyContinue) { - Write-Host "Searching Active Directory using the ADSI Searcher..." - foreach ($Group in $Groups) { - $search = [adsisearcher]"samaccountname=$Group" - $search.FindOne().Properties.member | ForEach-Object { - $namesearch = [adsisearcher]"distinguishedname=$_" - $namesearch.PropertiesToLoad.Add("SamAccountName") | Out-Null - $GroupResults.Add( - ( - New-Object psobject -Property @{ - Member = ($namesearch.FindOne().Properties.samaccountname | Out-String).trim() - Group = $Group - "Group Type" = "Domain" - } - ) - ) - } - } - } - else { - Write-Warning "A Secure connection to the domain could not be established. Unable to get Active Directory memberships." - } - } - } - - Write-Host "Searching local groups..." - $netlocalgroup = Get-AllLocalGroups - foreach ($Group in $Groups) { - $netlocalgroup | Where-Object { $_ -eq $Group } | ForEach-Object { - ((net.exe localgroup $_) -replace 'The command completed successfully.' | Select-Object -Skip 6) | Where-Object { $_ } | ForEach-Object { - $GroupResults.Add( - ( - New-Object psobject -Property @{ - Member = $_ - Group = $Group - "Group Type" = "Local" - } - ) - ) - } - } - } - - if ($GroupResults) { - Write-Host "Group Membership info found!" - Write-Host "#### Group Membership ####" - $GroupResults | Sort-Object -Property Member | Format-Table -Property Member, "Group Type", Group -AutoSize | Out-String | Write-Host - } - } - - # If we're outputing to a custom field we'll need to combine our results - if ($CustomField) { - $CombinedResults = New-Object System.Collections.Generic.List[Object] - - if ($UserResults) { - $CombinedResults.Add("### User Results ###") - $CombinedResults.Add( - ($UserResults | Sort-Object -Property Group | Format-List -Property Group, "Group Type", Member | Out-String) - ) - $CombinedResults.Add("") - } - - if ($GroupResults) { - $CombinedResults.Add("### Group Results ###") - $CombinedResults.Add( - ($GroupResults | Sort-Object -Property Member | Format-List -Property Member, "Group Type", Group | Out-String) - ) - } - - if ($PSVersionTable.PSVersion.Major -gt 2) { - Ninja-Property-Set -Name $CustomField -Value ($CombinedResults | Out-String) - } - else { - Write-Warning "Powershell 1 and 2 cannot set custom fields. https://ninjarmm.zendesk.com/hc/en-us/articles/4405408656013" - } - } - - # Check if we should error out or not - if ($GroupResults -or $UserResults) { - exit 0 - } - else { - Write-Error "Failed to find User or Group Membership. Does the user or group exist?" - exit 1 - } -} -end { - - - -} - + +<# +.SYNOPSIS + This will output the user or group membership for the specified group or user. +.DESCRIPTION + This will output the user or group membership for the specified group or user. + +PARAMETER: -Usernames "ReplaceMe","ReplaceMe2" (Quotations are not necessary if using script variables) + Grabs the group membership for each user you specified. +.EXAMPLE + -Usernames "Administrator" (Windows 10 - Domain Joined) + + A Domain Joined Computer was detected. Attempting to search Active Directory... + WARNING: The Active Directory Powershell Module was not found please install RSAT for better results. + Searching Active Directory using the ADSI Searcher... + Searching local groups... + #### User Membership #### + + Group Group Type Member + ----- ---------- ------ + Administrators Domain Administrator + Administrators Local Administrator + Domain Admins Domain Administrator + Domain Users Domain Administrator + Enterprise Admins Domain Administrator + Group Policy Creator Owners Domain Administrator + Schema Admins Domain Administrator + +PARAMETER: -Groups "ReplaceMe","ReplaceMe2" (Quotations are not necessary if using script variables) + Grabs the user membership for each group you specified. +.EXAMPLE + -Groups "Domain Admins" (Server 2008 - Domain Controller) + + A Domain Joined Computer was detected. Attempting to search Active Directory... + Searching Active Directory using the Active Directory Powershell Module... + Searching local groups... + #### Group Membership #### + + Member Group Type Group + ------ ---------- ----- + Administrator Domain Domain Admins + kbohlander Domain Domain Admins + +PARAMETER: -LastLoggedInUser + Checks the last logged in user. + +PARAMETER: -AzureAD + Adds 'AzureAD\' prefix to usernames + +PARAMETER: -CustomField "ReplaceMeWithAMultilineCustomField" + Outputs the results to a multiline customfield of your choice. +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 7, Windows Server 2008 + Release Notes: Updated Calculated Name +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String[]]$Usernames, + [Parameter()] + [String[]]$Groups, + [Parameter()] + [Switch]$AzureAD = [System.Convert]::ToBoolean($env:azureadAccounts), + [Parameter()] + [Switch]$LastLoggedInUser = [System.Convert]::ToBoolean($env:getLastLoggedInUserMembership), + [Parameter()] + [String]$CustomField +) + +begin { + # If script variables are used replace the parameters + if ($env:usernames -and $env:usernames -notlike "null") { + $Usernames = $env:usernames -split ',' | ForEach-Object { $_.trim() } + } + + if ($env:groups -and $env:groups -notlike "null") { + $Groups = $env:groups -split ',' | ForEach-Object { $_.trim() } + } + + if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } + + # Microsoft always adds AzureAD\ as a prefix to AzureAD Accounts. + if($Usernames -and $AzureAD){ + Write-Warning "Adding AzureAD\ prefix to all usernames in the list." + $AzureAccounts = $Usernames | ForEach-Object { + "AzureAD\$_" + } + $Usernames = $AzureAccounts + } + + # We'll check the last login registry key and replace the extra info we don't want / need + if($LastLoggedInUser){ + $Regkey = (Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI").LastLoggedOnSAMUser + if($Regkey){ + if($Regkey -notlike "AzureAD\*"){ + $LastLogon = $Regkey -replace ".*\\" + }else{ + $LastLogon = $Regkey + } + + Write-Host "Adding $LastLogon to the list!" + $Usernames += $LastLogon + }else{ + Write-Warning "No user has previously signed in (you may need to reboot). Skipping!" + } + } + + # Error out if we're missing information + if (-not ($Usernames) -and -not ($Groups)) { + Write-Error "You must specify at least 1 group or 1 user to get the membership for." + Exit 1 + } + + # Check if domain joined. + function Test-IsDomainJoined { + if ($PSVersionTable.PSVersion.Major -ge 5) { + return $(Get-CimInstance -Class Win32_ComputerSystem).PartOfDomain + } + else { + return $(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain + } + } + + # When executed as System net localgroup gives an error. This will grab and filter the groups using a wmi query. + function Get-AllLocalGroups { + if ($PSVersionTable.PSVersion.Major -ge 5) { + Get-CimInstance -Class Win32_Group | Where-Object { $_.LocalAccount -eq $True } | Select-Object -ExpandProperty Name + } + else { + Get-WmiObject -Class Win32_Group | Where-Object { $_.LocalAccount -eq $True } | Select-Object -ExpandProperty Name + } + } +} +process { + + if ($Usernames) { + $UserResults = New-Object System.Collections.Generic.List[Object] + if (Test-IsDomainJoined) { + # The active directory powershell module is prefered but not required + Write-Host "A Domain Joined Computer was detected. Attempting to search Active Directory..." + if ((Get-Module -Name ActiveDirectory -ListAvailable -ErrorAction SilentlyContinue)) { + Import-Module -Name ActiveDirectory + Write-Host "Searching Active Directory using the Active Directory Powershell Module..." + foreach ($User in $Usernames) { + Get-ADGroup -Filter * | ForEach-Object { + if (Get-ADGroupMember $_ | Where-Object { $_.SamAccountName -like $User }) { + $UserResults.Add( + ( + New-Object psobject -Property @{ + Member = $User + Group = ($_ | Select-Object -ExpandProperty SamAccountName) + "Group Type" = "Domain" + } + ) + ) + } + } + } + } + else { + Write-Warning "The Active Directory Powershell Module was not found switching to ADSI Searcher..." + if (Test-ComputerSecureChannel -ErrorAction SilentlyContinue) { + Write-Host "Searching Active Directory using the ADSI Searcher..." + # If for some reason this script is ran without the active directory powershell module we'll do two adsi searches to get the membership info + foreach ($User in $Usernames) { + $search = [adsisearcher]"samaccountname=$User" + # The memberof search won't include the primary group so we'll have to search separately for that + $primarygroup = $search.FindOne().Properties.primarygroupid + $primaryGroupSearch = [adsisearcher]"objectclass=group" + # When adding a property it always outputs the number of properties currently added. + $primaryGroupSearch.PropertiesToLoad.Add("SamAccountName") | Out-Null + $primaryGroupSearch.PropertiesToLoad.Add("PrimaryGroupToken") | Out-Null + $primaryGroupSearch.FindAll() | ForEach-Object { + if ($_.Properties.primarygrouptoken -like $primarygroup) { + $UserResults.Add( + ( + New-Object psobject -Property @{ + Member = $User + Group = ($_.Properties.samaccountname | Out-String).Trim() + "Group Type" = "Domain" + } + ) + ) + } + } + $search.FindOne().Properties.memberof | ForEach-Object { + $namesearch = [adsisearcher]"distinguishedname=$_" + $namesearch.PropertiesToLoad.Add("SamAccountName") | Out-Null + $UserResults.Add( + ( + New-Object psobject -Property @{ + Member = $User + Group = ($namesearch.FindOne().Properties.samaccountname | Out-String).Trim() + "Group Type" = "Domain" + } + ) + ) + } + } + } + else { + Write-Warning "A Secure connection to the domain could not be established. Unable to get Active Directory memberships." + } + } + } + + # Grabs the localgroup info using net localgroup + Write-Host "Searching local groups..." + $netlocalgroup = Get-AllLocalGroups + $netlocalgroup | ForEach-Object { + foreach ($User in $Usernames) { + if ((net.exe localgroup $_) -replace 'The command completed successfully.' | Select-Object -Skip 6 | Where-Object { $_ -and $_ -like "*$User" }) { + $UserResults.Add( + ( + New-Object psobject -Property @{ + Member = $User + Group = $_ + "Group Type" = "Local" + } + ) + ) + } + } + } + + if ($UserResults) { + Write-Host "User Membership info found!" + Write-Host "#### User Membership ####" + $UserResults | Sort-Object -Property Group | Format-Table -Property Group, "Group Type", Member -AutoSize | Out-String | Write-Host + } + } + + # All of this ia pretty similar to grabing the user membership except its searching for the group membership instead of the user + if ($Groups) { + $GroupResults = New-Object System.Collections.Generic.List[Object] + if (Test-IsDomainJoined) { + Write-Host "A Domain Joined Computer was detected. Attempting to search Active Directory..." + if ((Get-Module -Name ActiveDirectory -ListAvailable -ErrorAction SilentlyContinue)) { + Import-Module -Name ActiveDirectory + Write-Host "Searching Active Directory using the Active Directory Powershell Module..." + Get-ADGroup -Filter * | Where-Object { $Groups -contains $_.SamAccountName } | ForEach-Object { + $Group = $_.SamAccountName + Get-ADGroupMember $_ | ForEach-Object { + $GroupResults.Add( + ( + New-Object psobject -Property @{ + Member = $_.SamAccountName + Group = $Group + "Group Type" = "Domain" + } + ) + ) + } + } + } + else { + Write-Warning "The Active Directory Powershell Module was not found switching to ADSI Searcher..." + if (Test-ComputerSecureChannel -ErrorAction SilentlyContinue) { + Write-Host "Searching Active Directory using the ADSI Searcher..." + foreach ($Group in $Groups) { + $search = [adsisearcher]"samaccountname=$Group" + $search.FindOne().Properties.member | ForEach-Object { + $namesearch = [adsisearcher]"distinguishedname=$_" + $namesearch.PropertiesToLoad.Add("SamAccountName") | Out-Null + $GroupResults.Add( + ( + New-Object psobject -Property @{ + Member = ($namesearch.FindOne().Properties.samaccountname | Out-String).trim() + Group = $Group + "Group Type" = "Domain" + } + ) + ) + } + } + } + else { + Write-Warning "A Secure connection to the domain could not be established. Unable to get Active Directory memberships." + } + } + } + + Write-Host "Searching local groups..." + $netlocalgroup = Get-AllLocalGroups + foreach ($Group in $Groups) { + $netlocalgroup | Where-Object { $_ -eq $Group } | ForEach-Object { + ((net.exe localgroup $_) -replace 'The command completed successfully.' | Select-Object -Skip 6) | Where-Object { $_ } | ForEach-Object { + $GroupResults.Add( + ( + New-Object psobject -Property @{ + Member = $_ + Group = $Group + "Group Type" = "Local" + } + ) + ) + } + } + } + + if ($GroupResults) { + Write-Host "Group Membership info found!" + Write-Host "#### Group Membership ####" + $GroupResults | Sort-Object -Property Member | Format-Table -Property Member, "Group Type", Group -AutoSize | Out-String | Write-Host + } + } + + # If we're outputing to a custom field we'll need to combine our results + if ($CustomField) { + $CombinedResults = New-Object System.Collections.Generic.List[Object] + + if ($UserResults) { + $CombinedResults.Add("### User Results ###") + $CombinedResults.Add( + ($UserResults | Sort-Object -Property Group | Format-List -Property Group, "Group Type", Member | Out-String) + ) + $CombinedResults.Add("") + } + + if ($GroupResults) { + $CombinedResults.Add("### Group Results ###") + $CombinedResults.Add( + ($GroupResults | Sort-Object -Property Member | Format-List -Property Member, "Group Type", Group | Out-String) + ) + } + + if ($PSVersionTable.PSVersion.Major -gt 2) { + # Ninja-Property-Set -Name $CustomField -Value ($CombinedResults | Out-String) # Removed NinjaOne dependency + } + else { + Write-Warning "Powershell 1 and 2 cannot set custom fields. https://ninjarmm.zendesk.com/hc/en-us/articles/4405408656013" + } + } + + # Check if we should error out or not + if ($GroupResults -or $UserResults) { + exit 0 + } + else { + Write-Error "Failed to find User or Group Membership. Does the user or group exist?" + exit 1 + } +} +end { + + + +} + diff --git a/Powershell Scripts/Verify running processes are signed.ps1 b/Powershell Scripts/Verify running processes are signed.ps1 index ede1376..8252a07 100644 --- a/Powershell Scripts/Verify running processes are signed.ps1 +++ b/Powershell Scripts/Verify running processes are signed.ps1 @@ -1,628 +1,629 @@ # Verify that running processes are signed and output unsigned. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Verify that running processes are signed and output unsigned. -.DESCRIPTION - Verify that running processes are signed and output unsigned. - It will exclude processes based on the process name, path, or product name. - The script will output the unsigned processes to the console and save the results to a Multi-Line custom field and a WYSIWYG custom field if specified. - -.EXAMPLE - (No Parameters) - ## EXAMPLE OUTPUT WITHOUT PARAMS ## - Unsigned Processes Found: 2 - - Name : explorer - Description : Windows Explorer - Path : C:\Windows\explorer.exe - Id : 1234 - Signed : NotSigned - - Name : notepad - Description : Notepad - Path : C:\Windows\notepad.exe - Id : 5678 - Signed : NotSigned - -PARAMETER: -ExcludeProcess "explorer.exe" - Exclude the process explorer.exe from the results. -.EXAMPLE - -ExcludeProcess "notepad" - ## EXAMPLE OUTPUT WITH ExcludeProcess ## - Unsigned Processes Found: 1 - - Name : explorer - Description : Windows Explorer - Path : C:\Windows\explorer.exe - Id : 1234 - Signed : NotSigned - -PARAMETER: -ExcludeProcessFromCustomField "ReplaceMeWithAnyTextCustomField" - Exclude the processes from the custom field specified. -.EXAMPLE - -ExcludeProcessFromCustomField "ReplaceMeWithAnyTextCustomField" - ## EXAMPLE OUTPUT WITH ExcludeProcessFromCustomField ## - Unsigned Processes Found: 2 - - Name : explorer - Description : Windows Explorer - Path : C:\Windows\explorer.exe - Id : 1234 - Signed : NotSigned - - Name : notepad - Description : Notepad - Path : C:\Windows\notepad.exe - Id : 5678 - Signed : NotSigned - -PARAMETER: -SaveResultsToMultilineCustomField "ReplaceMeWithAnyMultilineCustomField" - Save the results to a Multi-Line custom field specified. -.EXAMPLE - -SaveResultsToMultilineCustomField "ReplaceMeWithAnyMultilineCustomField" - ## EXAMPLE OUTPUT WITH ExcludeProcessFromCustomField ## - Unsigned Processes Found: 2 - - Name : explorer - Description : Windows Explorer - Path : C:\Windows\explorer.exe - Id : 1234 - Signed : NotSigned - - Name : notepad - Description : Notepad - Path : C:\Windows\notepad.exe - Id : 5678 - Signed : NotSigned - - [Info] Attempting to update Multiline Custom Field(ReplaceMeWithAnyMultilineCustomField) - [Info] Updated Multiline Custom Field(ReplaceMeWithAnyMultilineCustomField) - - -PARAMETER: -SaveResultsToWysiwygCustomField "ReplaceMeWithAnyMultilineCustomField" - Save the results to a WYSIWYG custom field specified. -.EXAMPLE - -SaveResultsToWysiwygCustomField "ReplaceMeWithAnyWysiwygCustomField" - ## EXAMPLE OUTPUT WITH ExcludeProcessFromCustomField ## - Unsigned Processes Found: 2 - - Name : explorer - Description : Windows Explorer - Path : C:\Windows\explorer.exe - Id : 1234 - Signed : NotSigned - - Name : notepad - Description : Notepad - Path : C:\Windows\notepad.exe - Id : 5678 - Signed : NotSigned - - [Info] Attempting to update Wysiwyg Custom Field(ReplaceMeWithAnyWysiwygCustomField) - [Info] Updated Wysiwyg Custom Field(ReplaceMeWithAnyWysiwygCustomField) - -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2012 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [String[]]$ExcludeProcess, - [String]$ExcludeProcessFromCustomField, - [String]$SaveResultsToMultilineCustomField, - [String]$SaveResultsToWysiwygCustomField -) - -begin { - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - function Get-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter()] - [String]$DocumentName - ) - - if ($PSVersionTable.PSVersion.Major -lt 3) { - throw "PowerShell 3.0 or higher is required to retrieve data from custom fields. https://ninjarmm.zendesk.com/hc/en-us/articles/4405408656013" - } - - # If we're requested to get the field value from a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # These two types require more information to parse. - $NeedsOptions = "DropDown", "MultiSelect" - - # Grabbing document values requires a slightly different command. - if ($DocumentName) { - # Secure fields are only readable when they're a device custom field - if ($Type -Like "Secure") { throw "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - Write-Host "Retrieving value from Ninja Document..." - $NinjaPropertyValue = Ninja-Property-Docs-Get -AttributeName $Name @DocumentationParams 2>&1 - - # Certain fields require more information to parse. - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - # We'll redirect error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1 - - # Certain fields require more information to parse. - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If we received some sort of error it should have an exception property and we'll exit the function with that error information. - if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # This switch will compare the type given with the quoted string. If it matches, it'll parse it further; otherwise, the default option will be selected. - switch ($Type) { - "Attachment" { - # Attachments come in a JSON format this will convert it into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Checkbox" { - # Checkbox's come in as a string representing an integer. We'll need to cast that string into an integer and then convert it to a more traditional boolean. - [System.Convert]::ToBoolean([int]$NinjaPropertyValue) - } - "Date or Date Time" { - # In Ninja Date and Date/Time fields are in Unix Epoch time in the UTC timezone the below should convert it into local time as a date time object. - $UnixTimeStamp = $NinjaPropertyValue - $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp) - $TimeZone = [TimeZoneInfo]::Local - [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) - } - "Decimal" { - # In ninja decimals are strings that represent a decimal this will cast it into a double data type. - [double]$NinjaPropertyValue - } - "Device Dropdown" { - # Device Drop-Downs Fields come in a JSON format this will convert it into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Device MultiSelect" { - # Device Multi-Select Fields come in a JSON format this will convert it into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Dropdown" { - # Drop-Down custom fields come in as a comma-separated list of GUIDs; we'll compare these with all the options and return just the option values selected instead of a GUID. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name - } - "Integer" { - # Cast's the Ninja provided string into an integer. - [int]$NinjaPropertyValue - } - "MultiSelect" { - # Multi-Select custom fields come in as a comma-separated list of GUID's we'll compare these with all the options and return just the option values selected instead of a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = ($NinjaPropertyValue -split ',').trim() - - foreach ($Item in $Selection) { - $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name - } - } - "Organization Dropdown" { - # Turns the Ninja provided JSON into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization Location Dropdown" { - # Turns the Ninja provided JSON into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization Location MultiSelect" { - # Turns the Ninja provided JSON into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Organization MultiSelect" { - # Turns the Ninja provided JSON into a PowerShell Object. - $NinjaPropertyValue | ConvertFrom-Json - } - "Time" { - # Time fields are given as a number of seconds starting from midnight. This will convert it into a date time object. - $Seconds = $NinjaPropertyValue - $UTC = ([TimeSpan]::FromSeconds($Seconds)).ToString("hh\:mm\:ss") - $TimeZone = [TimeZoneInfo]::Local - $ConvertedTime = [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) - - Get-Date $ConvertedTime -DisplayHint Time - } - default { - # If no type was given or not one that matches the above types just output what we retrieved. - $NinjaPropertyValue - } - } - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 10000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") - } - - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to be set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received it will have an exception property, the function will exit with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the Date-Time to be in Unix Epoch time so we'll convert it here. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - - function Set-WysiwygCustomField { - param ( - [string]$Name, - [Parameter(ValueFromPipeline = $True)] - [string]$Value - ) - end { - - # Set the Custom Field - # If the value is greater than 10,000 characters, use the Ninja-Property-Set-Piped function - # Otherwise, use the Ninja-Property-Set function - $CustomField = $Value | Ninja-Property-Set-Piped -Name $Name 2>&1 - - # Check for errors - if ($CustomField -or $CustomField.Exception) { - # If the Custom Field was not found, throw an error - if ($CustomField -like "Unable to find the specified field." -or $CustomField.Exception -like "Unable to find the specified field.") { - throw "The Custom field ($Name) was not found" - } - # If the Custom Field is read-only, throw an error - if ($CustomField -like "Unable to update read-only attribute" -or $CustomField.Exception -like "Unable to update read-only attribute") { - throw "The Custom field ($Name) is read-only" - } - # Catch all other errors and throw the error - throw $CustomField - } - } - } - - # Predefined values for Success, Danger, and Other - $ConvertToWysiwygHtmlSuccess = @("Signed", "Valid") - $ConvertToWysiwygHtmlDanger = @("SignedAndNotTrusted", "NotSigned", "NotTrusted", "HashMismatch") - $ConvertToWysiwygHtmlOther = @("UnknownError", "Incompatible") - # Function to convert the output to a WYSIWYG HTML format - function ConvertTo-WysiwygHtml { - param( - [string]$Title, - [PSObject[]]$Value, - [string[]]$Success = $ConvertToWysiwygHtmlSuccess, - [string[]]$Danger = $ConvertToWysiwygHtmlDanger, - [string[]]$Other = $ConvertToWysiwygHtmlOther - ) - begin { - $htmlReport = New-Object System.Collections.Generic.List[String] - # If used add the Title to the report - if ($Title) { - $htmlReport.Add("

$Title

") - } - } - process { - # Convert the value to HTML - $htmlTable = $Value | ConvertTo-Html -Fragment - # Set the class for each row based on the Success, Danger, and Other values - if ($Success) { - # For each Success value, find the row in the table and add a class of 'success' - $Success | ForEach-Object { - $Status = $_ - # Split the table into lines and find the lines that contain the Status - $htmlTable -split "`r`n" | Where-Object { - # Select only lines that have
", "") - } - } - } - } - if ($Danger) { - # For each Danger value, find the row in the table and add a class of 'danger' - $Danger | ForEach-Object { - $Status = $_ - # Split the table into lines and find the lines that contain the Status - $htmlTable -split "`r`n" | Where-Object { - # Select only lines that have ", "") - } - } - } - } - if ($Other) { - # For each Other value, find the row in the table and add a class of 'other' - $Other | ForEach-Object { - $Status = $_ - # Split the table into lines and find the lines that contain the Status - $htmlTable -split "`r`n" | Where-Object { - # Select only lines that have ", "") - } - } - } - } - # Add the Table to the report - $htmlTable | ForEach-Object { $htmlReport.Add($_) } - } - end { - # Return the HTML report - $htmlReport | Out-String - } - } - - # Update the script variables with the Script Variables if they are not null - if ($env:excludeProcess -and $env:excludeProcess -notlike "null") { - $ExcludeProcess = $env:excludeProcess - } - if ($env:excludeProcessFromCustomField -and $env:excludeProcessFromCustomField -notlike "null") { - $ExcludeProcessFromCustomField = $env:excludeProcessFromCustomField - } - if ($env:saveResultsToMultilineCustomField -and $env:saveResultsToMultilineCustomField -notlike "null") { - $SaveResultsToMultilineCustomField = $env:saveResultsToMultilineCustomField - } - if ($env:saveResultsToWysiwygCustomField -and $env:saveResultsToWysiwygCustomField -notlike "null") { - $SaveResultsToWysiwygCustomField = $env:saveResultsToWysiwygCustomField - } - - # If ExcludeProcess is a comma-separated list, split it into an array - if ($ExcludeProcess -like '*,*') { - $ExcludeProcess = $ExcludeProcess -split ',' | ForEach-Object { - if ($_ -like '*,*') { - $_ -split ',' | ForEach-Object { "$_".Trim() } - } - else { "$_".Trim() } - } - } - # If ExcludeProcessFromCustomField is not null, get a list of processes to exclude from the Custom Field - if ($ExcludeProcessFromCustomField -and $ExcludeProcessFromCustomField -notlike "null") { - try { - # Get the processes to exclude from the Custom Field - $TempString = $(Get-NinjaProperty -Name $ExcludeProcessFromCustomField) - # If the Custom Field is empty, throw an error - if ([string]::IsNullOrWhiteSpace($TempString)) { - throw "Empty" - } - # If the Custom Field is a comma-separated list, split it into an array - $ExcludeProcess = $TempString -split ',' | ForEach-Object { "$_".Trim() } - } - catch { - # If the Custom Field is empty, output a warning - if ($_.Exception.Message -like "Empty") { - Write-Host "[Warn] The Custom Field($ExcludeProcessFromCustomField) is empty" - } - else { - # If the Custom Field is Like empty, output an error - Write-Host "[Warn] Failed to get processes to exclude from Custom Field($ExcludeProcessFromCustomField)" - } - } - } -} -process { - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Get processes and if excluding, look at Name, Path/FileName, and ProductName - $Processes = $( - if ($ExcludeProcess) { - # Output excluded processes - Write-Host "Excluding Processes:" - $ExcludeProcess | Out-String | Write-Host - # Get processes and exclude based on Name, Path/FileName, and ProductName - Get-Process | Where-Object { - $( - $_.Name -notin $ExcludeProcess -and - $( - if ($_.Path) { - Split-Path $_.Path -Leaf - } - else { $_.FileName } - ) -notin $ExcludeProcess -and - $_.ProductName -notin $ExcludeProcess - ) - } - } - else { - # Get all processes if no exclusion is specified - Get-Process - } - ) - - # Reduce list to just the paths and get signed status - $ProcessesWithSigned = $Processes | Sort-Object -Unique -Property Path | ForEach-Object { - if ($_.Path) { - # Get the signer certificate - $Signature = Get-AuthenticodeSignature -FilePath $_.Path - - # Check if the signer certificate is trusted - $Status = if ($Signature.Status -eq "Valid") { - "Signed" - } - else { - $Signature.Status - } - - # Output the process name, description, path, id, and signed status - [PSCustomObject]@{ - Name = $_.Name - Description = $_.Description - Path = $_.Path - Id = $_.Id - Signed = $Status - } - $Status = $null - } - } - - # Get unsigned processes - $Unsigned = $ProcessesWithSigned | Where-Object { $_.Signed -notlike "Signed" } - if ($Unsigned -and $Unsigned.Count) { - # Output number of processes - Write-Host "Unsigned Processes Found: $($Unsigned.Count)" - } - elseif ($Unsigned) { - # Handle edge case where $Unsigned isn't an array of items, but is an object alone - Write-Host "Unsigned Processes Found: 1" - } - else { - # If $Unsigned doesn't have a count and isn't a object, assume there are 0 unsigned processes found - Write-Host "Unsigned Processes Found: 0" - } - - # Output unsigned processes for Activity Feed - $Unsigned | Out-String | Write-Host - - $HasErrors = $false - # Save results to a Multi-Line custom field - if ($SaveResultsToMultilineCustomField -and $SaveResultsToMultilineCustomField -notlike "null") { - try { - $Unsigned | Out-String | Set-NinjaProperty -Name $SaveResultsToMultilineCustomField - Write-Host "[Info] Updated Multiline Custom Field($SaveResultsToMultilineCustomField)" - } - catch { - if ($_.Exception.Message -like "*Unable to find the specified field*") { - Write-Host "[Error] Unable to find and save to the Custom Field ($SaveResultsToMultilineCustomField)" - } - else { - Write-Host "[Error] ninjarmm-cli returned error: $($_.Exception.Message)" - } - $HasErrors = $true - } - } - else { - Write-Host "[Info] Not updating Multiline Custom Field($SaveResultsToWysiwygCustomField) due to not being specified or inaccessible." - } - - # Save results to a WYSIWYG custom field - if ($SaveResultsToWysiwygCustomField -and $SaveResultsToWysiwygCustomField -notlike "null") { - try { - ConvertTo-WysiwygHtml -Value $Unsigned | Set-WysiwygCustomField -Name $SaveResultsToWysiwygCustomField - Write-Host "[Info] Updated Wysiwyg Custom Field($SaveResultsToWysiwygCustomField)" - } - catch { - if ($_.Exception.Message -like "*Unable to find the specified field*") { - Write-Host "[Error] Unable to find and save to the Custom Field ($SaveResultsToWysiwygCustomField)" - } - else { - Write-Host "[Error] ninjarmm-cli returned error: $($_.Exception.Message)" - } - $HasErrors = $true - } - } - else { - Write-Host "[Info] Not updating Wysiwyg Custom Field($SaveResultsToWysiwygCustomField) due to not being specified or inaccessible." - } - if ($HasErrors) { - exit 1 - } -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Verify that running processes are signed and output unsigned. +.DESCRIPTION + Verify that running processes are signed and output unsigned. + It will exclude processes based on the process name, path, or product name. + The script will output the unsigned processes to the console and save the results to a Multi-Line custom field and a WYSIWYG custom field if specified. + +.EXAMPLE + (No Parameters) + ## EXAMPLE OUTPUT WITHOUT PARAMS ## + Unsigned Processes Found: 2 + + Name : explorer + Description : Windows Explorer + Path : C:\Windows\explorer.exe + Id : 1234 + Signed : NotSigned + + Name : notepad + Description : Notepad + Path : C:\Windows\notepad.exe + Id : 5678 + Signed : NotSigned + +PARAMETER: -ExcludeProcess "explorer.exe" + Exclude the process explorer.exe from the results. +.EXAMPLE + -ExcludeProcess "notepad" + ## EXAMPLE OUTPUT WITH ExcludeProcess ## + Unsigned Processes Found: 1 + + Name : explorer + Description : Windows Explorer + Path : C:\Windows\explorer.exe + Id : 1234 + Signed : NotSigned + +PARAMETER: -ExcludeProcessFromCustomField "ReplaceMeWithAnyTextCustomField" + Exclude the processes from the custom field specified. +.EXAMPLE + -ExcludeProcessFromCustomField "ReplaceMeWithAnyTextCustomField" + ## EXAMPLE OUTPUT WITH ExcludeProcessFromCustomField ## + Unsigned Processes Found: 2 + + Name : explorer + Description : Windows Explorer + Path : C:\Windows\explorer.exe + Id : 1234 + Signed : NotSigned + + Name : notepad + Description : Notepad + Path : C:\Windows\notepad.exe + Id : 5678 + Signed : NotSigned + +PARAMETER: -SaveResultsToMultilineCustomField "ReplaceMeWithAnyMultilineCustomField" + Save the results to a Multi-Line custom field specified. +.EXAMPLE + -SaveResultsToMultilineCustomField "ReplaceMeWithAnyMultilineCustomField" + ## EXAMPLE OUTPUT WITH ExcludeProcessFromCustomField ## + Unsigned Processes Found: 2 + + Name : explorer + Description : Windows Explorer + Path : C:\Windows\explorer.exe + Id : 1234 + Signed : NotSigned + + Name : notepad + Description : Notepad + Path : C:\Windows\notepad.exe + Id : 5678 + Signed : NotSigned + + [Info] Attempting to update Multiline Custom Field(ReplaceMeWithAnyMultilineCustomField) + [Info] Updated Multiline Custom Field(ReplaceMeWithAnyMultilineCustomField) + + +PARAMETER: -SaveResultsToWysiwygCustomField "ReplaceMeWithAnyMultilineCustomField" + Save the results to a WYSIWYG custom field specified. +.EXAMPLE + -SaveResultsToWysiwygCustomField "ReplaceMeWithAnyWysiwygCustomField" + ## EXAMPLE OUTPUT WITH ExcludeProcessFromCustomField ## + Unsigned Processes Found: 2 + + Name : explorer + Description : Windows Explorer + Path : C:\Windows\explorer.exe + Id : 1234 + Signed : NotSigned + + Name : notepad + Description : Notepad + Path : C:\Windows\notepad.exe + Id : 5678 + Signed : NotSigned + + [Info] Attempting to update Wysiwyg Custom Field(ReplaceMeWithAnyWysiwygCustomField) + [Info] Updated Wysiwyg Custom Field(ReplaceMeWithAnyWysiwygCustomField) + +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2012 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [String[]]$ExcludeProcess, + [String]$ExcludeProcessFromCustomField, + [String]$SaveResultsToMultilineCustomField, + [String]$SaveResultsToWysiwygCustomField +) + +begin { + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + function Get-NinjaProperty { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [String]$Name, + [Parameter()] + [String]$Type, + [Parameter()] + [String]$DocumentName + ) + + if ($PSVersionTable.PSVersion.Major -lt 3) { + throw "PowerShell 3.0 or higher is required to retrieve data from custom fields. https://ninjarmm.zendesk.com/hc/en-us/articles/4405408656013" + } + + # If we're requested to get the field value from a Ninja document we'll specify it here. + $DocumentationParams = @{} + if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } + + # These two types require more information to parse. + $NeedsOptions = "DropDown", "MultiSelect" + + # Grabbing document values requires a slightly different command. + if ($DocumentName) { + # Secure fields are only readable when they're a device custom field + if ($Type -Like "Secure") { throw "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } + + # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. + Write-Host "Retrieving value from Ninja Document..." + # $NinjaPropertyValue = Ninja-Property-Docs-Get -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + + # Certain fields require more information to parse. + if ($NeedsOptions -contains $Type) { + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + } + } + else { + # We'll redirect error output to the success stream to make it easier to error out if nothing was found or something else went wrong. + # $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1 # Removed NinjaOne dependency + + # Certain fields require more information to parse. + if ($NeedsOptions -contains $Type) { + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + } + } + + # If we received some sort of error it should have an exception property and we'll exit the function with that error information. + if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue } + if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } + + # This switch will compare the type given with the quoted string. If it matches, it'll parse it further; otherwise, the default option will be selected. + switch ($Type) { + "Attachment" { + # Attachments come in a JSON format this will convert it into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Checkbox" { + # Checkbox's come in as a string representing an integer. We'll need to cast that string into an integer and then convert it to a more traditional boolean. + [System.Convert]::ToBoolean([int]$NinjaPropertyValue) + } + "Date or Date Time" { + # In Ninja Date and Date/Time fields are in Unix Epoch time in the UTC timezone the below should convert it into local time as a date time object. + $UnixTimeStamp = $NinjaPropertyValue + $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp) + $TimeZone = [TimeZoneInfo]::Local + [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) + } + "Decimal" { + # In ninja decimals are strings that represent a decimal this will cast it into a double data type. + [double]$NinjaPropertyValue + } + "Device Dropdown" { + # Device Drop-Downs Fields come in a JSON format this will convert it into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Device MultiSelect" { + # Device Multi-Select Fields come in a JSON format this will convert it into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Dropdown" { + # Drop-Down custom fields come in as a comma-separated list of GUIDs; we'll compare these with all the options and return just the option values selected instead of a GUID. + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name + } + "Integer" { + # Cast's the Ninja provided string into an integer. + [int]$NinjaPropertyValue + } + "MultiSelect" { + # Multi-Select custom fields come in as a comma-separated list of GUID's we'll compare these with all the options and return just the option values selected instead of a guid. + $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" + $Selection = ($NinjaPropertyValue -split ',').trim() + + foreach ($Item in $Selection) { + $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name + } + } + "Organization Dropdown" { + # Turns the Ninja provided JSON into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization Location Dropdown" { + # Turns the Ninja provided JSON into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization Location MultiSelect" { + # Turns the Ninja provided JSON into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Organization MultiSelect" { + # Turns the Ninja provided JSON into a PowerShell Object. + $NinjaPropertyValue | ConvertFrom-Json + } + "Time" { + # Time fields are given as a number of seconds starting from midnight. This will convert it into a date time object. + $Seconds = $NinjaPropertyValue + $UTC = ([TimeSpan]::FromSeconds($Seconds)).ToString("hh\:mm\:ss") + $TimeZone = [TimeZoneInfo]::Local + $ConvertedTime = [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone) + + Get-Date $ConvertedTime -DisplayHint Time + } + default { + # If no type was given or not one that matches the above types just output what we retrieved. + $NinjaPropertyValue + } + } + } + + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + # if ($Characters -ge 10000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If we're requested to set the field value for a Ninja document we'll specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # The field below requires additional information to be set # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If an error is received it will have an exception property, the function will exit with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the Date-Time to be in Unix Epoch time so we'll convert it here. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # if (-not $Selection) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # We'll need to set the field differently depending on if its a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + function Set-WysiwygCustomField { + param ( + [string]$Name, + [Parameter(ValueFromPipeline = $True)] + [string]$Value + ) + end { + + # NinjaOne integration removed - Custom field setting skipped + # Set the Custom Field + # If the value is greater than 10,000 characters, use the Ninja-Property-Set-Piped function + # Otherwise, use the Ninja-Property-Set function + # $CustomField = $Value | Ninja-Property-Set-Piped -Name $Name 2>&1 + + # Check for errors + # if ($CustomField -or $CustomField.Exception) { + # If the Custom Field was not found, throw an error + if ($CustomField -like "Unable to find the specified field." -or $CustomField.Exception -like "Unable to find the specified field.") { + throw "The Custom field ($Name) was not found" + } + # If the Custom Field is read-only, throw an error + if ($CustomField -like "Unable to update read-only attribute" -or $CustomField.Exception -like "Unable to update read-only attribute") { + throw "The Custom field ($Name) is read-only" + } + # Catch all other errors and throw the error + throw $CustomField + } + } + } + + # Predefined values for Success, Danger, and Other + $ConvertToWysiwygHtmlSuccess = @("Signed", "Valid") + $ConvertToWysiwygHtmlDanger = @("SignedAndNotTrusted", "NotSigned", "NotTrusted", "HashMismatch") + $ConvertToWysiwygHtmlOther = @("UnknownError", "Incompatible") + # Function to convert the output to a WYSIWYG HTML format + function ConvertTo-WysiwygHtml { + param( + [string]$Title, + [PSObject[]]$Value, + [string[]]$Success = $ConvertToWysiwygHtmlSuccess, + [string[]]$Danger = $ConvertToWysiwygHtmlDanger, + [string[]]$Other = $ConvertToWysiwygHtmlOther + ) + begin { + $htmlReport = New-Object System.Collections.Generic.List[String] + # If used add the Title to the report + if ($Title) { + $htmlReport.Add("

$Title

") + } + } + process { + # Convert the value to HTML + $htmlTable = $Value | ConvertTo-Html -Fragment + # Set the class for each row based on the Success, Danger, and Other values + if ($Success) { + # For each Success value, find the row in the table and add a class of 'success' + $Success | ForEach-Object { + $Status = $_ + # Split the table into lines and find the lines that contain the Status + $htmlTable -split "`r`n" | Where-Object { + # Select only lines that have ", "") + } + } + } + } + if ($Danger) { + # For each Danger value, find the row in the table and add a class of 'danger' + $Danger | ForEach-Object { + $Status = $_ + # Split the table into lines and find the lines that contain the Status + $htmlTable -split "`r`n" | Where-Object { + # Select only lines that have ", "") + } + } + } + } + if ($Other) { + # For each Other value, find the row in the table and add a class of 'other' + $Other | ForEach-Object { + $Status = $_ + # Split the table into lines and find the lines that contain the Status + $htmlTable -split "`r`n" | Where-Object { + # Select only lines that have ", "") + } + } + } + } + # Add the Table to the report + $htmlTable | ForEach-Object { $htmlReport.Add($_) } + } + end { + # Return the HTML report + $htmlReport | Out-String + } + } + + # Update the script variables with the Script Variables if they are not null + if ($env:excludeProcess -and $env:excludeProcess -notlike "null") { + $ExcludeProcess = $env:excludeProcess + } + if ($env:excludeProcessFromCustomField -and $env:excludeProcessFromCustomField -notlike "null") { + $ExcludeProcessFromCustomField = $env:excludeProcessFromCustomField + } + if ($env:saveResultsToMultilineCustomField -and $env:saveResultsToMultilineCustomField -notlike "null") { + $SaveResultsToMultilineCustomField = $env:saveResultsToMultilineCustomField + } + if ($env:saveResultsToWysiwygCustomField -and $env:saveResultsToWysiwygCustomField -notlike "null") { + $SaveResultsToWysiwygCustomField = $env:saveResultsToWysiwygCustomField + } + + # If ExcludeProcess is a comma-separated list, split it into an array + if ($ExcludeProcess -like '*,*') { + $ExcludeProcess = $ExcludeProcess -split ',' | ForEach-Object { + if ($_ -like '*,*') { + $_ -split ',' | ForEach-Object { "$_".Trim() } + } + else { "$_".Trim() } + } + } + # If ExcludeProcessFromCustomField is not null, get a list of processes to exclude from the Custom Field + if ($ExcludeProcessFromCustomField -and $ExcludeProcessFromCustomField -notlike "null") { + try { + # Get the processes to exclude from the Custom Field + $TempString = $(Get-NinjaProperty -Name $ExcludeProcessFromCustomField) + # If the Custom Field is empty, throw an error + if ([string]::IsNullOrWhiteSpace($TempString)) { + throw "Empty" + } + # If the Custom Field is a comma-separated list, split it into an array + $ExcludeProcess = $TempString -split ',' | ForEach-Object { "$_".Trim() } + } + catch { + # If the Custom Field is empty, output a warning + if ($_.Exception.Message -like "Empty") { + Write-Host "[Warn] The Custom Field($ExcludeProcessFromCustomField) is empty" + } + else { + # If the Custom Field is Like empty, output an error + Write-Host "[Warn] Failed to get processes to exclude from Custom Field($ExcludeProcessFromCustomField)" + } + } + } +} +process { + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Get processes and if excluding, look at Name, Path/FileName, and ProductName + $Processes = $( + if ($ExcludeProcess) { + # Output excluded processes + Write-Host "Excluding Processes:" + $ExcludeProcess | Out-String | Write-Host + # Get processes and exclude based on Name, Path/FileName, and ProductName + Get-Process | Where-Object { + $( + $_.Name -notin $ExcludeProcess -and + $( + if ($_.Path) { + Split-Path $_.Path -Leaf + } + else { $_.FileName } + ) -notin $ExcludeProcess -and + $_.ProductName -notin $ExcludeProcess + ) + } + } + else { + # Get all processes if no exclusion is specified + Get-Process + } + ) + + # Reduce list to just the paths and get signed status + $ProcessesWithSigned = $Processes | Sort-Object -Unique -Property Path | ForEach-Object { + if ($_.Path) { + # Get the signer certificate + $Signature = Get-AuthenticodeSignature -FilePath $_.Path + + # Check if the signer certificate is trusted + $Status = if ($Signature.Status -eq "Valid") { + "Signed" + } + else { + $Signature.Status + } + + # Output the process name, description, path, id, and signed status + [PSCustomObject]@{ + Name = $_.Name + Description = $_.Description + Path = $_.Path + Id = $_.Id + Signed = $Status + } + $Status = $null + } + } + + # Get unsigned processes + $Unsigned = $ProcessesWithSigned | Where-Object { $_.Signed -notlike "Signed" } + if ($Unsigned -and $Unsigned.Count) { + # Output number of processes + Write-Host "Unsigned Processes Found: $($Unsigned.Count)" + } + elseif ($Unsigned) { + # Handle edge case where $Unsigned isn't an array of items, but is an object alone + Write-Host "Unsigned Processes Found: 1" + } + else { + # If $Unsigned doesn't have a count and isn't a object, assume there are 0 unsigned processes found + Write-Host "Unsigned Processes Found: 0" + } + + # Output unsigned processes for Activity Feed + $Unsigned | Out-String | Write-Host + + $HasErrors = $false + # Save results to a Multi-Line custom field + if ($SaveResultsToMultilineCustomField -and $SaveResultsToMultilineCustomField -notlike "null") { + try { + # $Unsigned | Out-String | Set-NinjaProperty -Name $SaveResultsToMultilineCustomField # Removed NinjaOne dependency + Write-Host "[Info] Updated Multiline Custom Field($SaveResultsToMultilineCustomField)" + } + catch { + if ($_.Exception.Message -like "*Unable to find the specified field*") { + Write-Host "[Error] Unable to find and save to the Custom Field ($SaveResultsToMultilineCustomField)" + } + else { + Write-Host "[Error] ninjarmm-cli returned error: $($_.Exception.Message)" + } + $HasErrors = $true + } + } + else { + Write-Host "[Info] Not updating Multiline Custom Field($SaveResultsToWysiwygCustomField) due to not being specified or inaccessible." + } + + # Save results to a WYSIWYG custom field + if ($SaveResultsToWysiwygCustomField -and $SaveResultsToWysiwygCustomField -notlike "null") { + try { + ConvertTo-WysiwygHtml -Value $Unsigned | Set-WysiwygCustomField -Name $SaveResultsToWysiwygCustomField + Write-Host "[Info] Updated Wysiwyg Custom Field($SaveResultsToWysiwygCustomField)" + } + catch { + if ($_.Exception.Message -like "*Unable to find the specified field*") { + Write-Host "[Error] Unable to find and save to the Custom Field ($SaveResultsToWysiwygCustomField)" + } + else { + Write-Host "[Error] ninjarmm-cli returned error: $($_.Exception.Message)" + } + $HasErrors = $true + } + } + else { + Write-Host "[Info] Not updating Wysiwyg Custom Field($SaveResultsToWysiwygCustomField) due to not being specified or inaccessible." + } + if ($HasErrors) { + exit 1 + } +} +end { + + + +} diff --git a/Powershell Scripts/WAN IP Blocklist Check.ps1 b/Powershell Scripts/WAN IP Blocklist Check.ps1 index 4e75677..fa05c10 100644 --- a/Powershell Scripts/WAN IP Blocklist Check.ps1 +++ b/Powershell Scripts/WAN IP Blocklist Check.ps1 @@ -1,416 +1,416 @@ # Checks several common blacklists to see if the devices WAN IP is currently being blacklisted. A private recursive DNS server is recommended, as it is not uncommon for DNS blocklists to block public DNS servers such as 1.1.1.1. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Checks several common blacklists to see if the device's WAN IP is currently being blacklisted. A private recursive DNS server is recommended, as it is not uncommon for DNS blocklists to block public DNS servers such as 1.1.1.1. -.DESCRIPTION - Checks several common blacklists to see if the device's WAN IP is currently being blacklisted. A private recursive DNS server is recommended, as it is not uncommon for DNS blocklists to block public DNS servers such as 1.1.1.1. -.EXAMPLE - (No Parameters) - When found on blacklist - - [Alert] The WAN IP '127.0.0.1' was found on 9 blacklist(s). - You may want to validate these results with 'https://mxtoolbox.com/SuperTool.aspx?action=blacklist%3a127.0.0.1'. - Name TTL ResponseCode - ---- --- ------------ - Blocklist.de 1269 127.0.0.14 - Interserver RBL 903 127.0.0.2 - Interserver Spam Assassin RBL 903 127.0.0.2 - Mailspike Z 120 127.0.0.2 - Mailspike BL 120 127.0.0.2 - S5H 5, 86400, 86400, 86400, 30, 300, 30, 300, 300, 300 127.0.0.2, 85.119.82.99, 2001:ba8:1... - UCE Protect - L1 902 127.0.0.2 - UCE Protect - L2 902 127.0.0.2 - UCE Protect - L3 902 127.0.0.2 - - Blacklists Checked: 0Spam, 0Spam RBL, Anonmails DNSBL, Backscatterer, Blocklist.de, Cymru Bogons, Dan Tor, Dan Tor Exit, Drone BL, Fabel Sources, Host Karma, ImproWare (IMP) DNS RBL, ImproWare (IMP) Spam RBL, Interserver RBL, Interserver Spam Assassin RBL, JIPPG's Relay Blackhole List Project, Kempt.net DNS Black List, Mailspike Z, Mailspike BL, Nordspam BL, PSBL, S5H, Schulte, Spam Eating Monkey - Backscatter, Spam Eating Monkey - Black, SpamCop, Suomispam, Truncate, UCE Protect - L1, UCE Protect - L2, UCE Protect - L3, ZapBL - -.EXAMPLE - (No Parameters) - When not found on blacklist - - The WAN IP '127.0.0.1' was not found on any blacklists. - You may want to validate these results with 'https://mxtoolbox.com/SuperTool.aspx?action=blacklist%3a127.0.0.1'. - - Blacklists Checked: 0Spam, 0Spam RBL, Anonmails DNSBL, Backscatterer, Blocklist.de, Cymru Bogons, Dan Tor, Dan Tor Exit, Drone BL, Fabel Sources, Host Karma, ImproWare (IMP) DNS RBL, ImproWare (IMP) Spam RBL, Interserver RBL, Interserver Spam Assassin RBL, JIPPG's Relay Blackhole List Project, Kempt.net DNS Black List, Mailspike Z, Mailspike BL, Nordspam BL, PSBL, S5H, Schulte, Spam Eating Monkey - Backscatter, Spam Eating Monkey - Black, SpamCop, Suomispam, Truncate, UCE Protect - L1, UCE Protect - L2, UCE Protect - L3, ZapBL - -PARAMETER: -CustomField "ReplaceMeWithYourDesiredMultilineCustomField" - Optionally specify the name of a multiline custom field to store the results in. - -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String]$CustomField -) - -begin { - # If using script form variables, replace command line parameters with them. - if($env:multilineCustomFieldName -and $env:multilineCustomFieldName -notlike "null") { $CustomField = $env:multilineCustomFieldName } - - # Local administrator privileges are required to set custom fields. - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - - $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 200000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") - } - - # If requested to set the field value for a Ninja document, specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - - # This is a list of valid fields that can be set. If no type is specified, assume that the input does not need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - - # The field below requires additional information to set. - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # Redirect error output to the success stream to handle errors more easily if nothing is found or something else goes wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - - # If an error is received with an exception property, exit the function with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - - # The types below require values not typically given to be set. The code below will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # Although it's highly likely we were given a value like "True" or a boolean data type, it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, match the given value with a GUID. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli expects the GUID of the option we're trying to select, so match the value we were given with a GUID. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") - } - - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - - # Set the field differently depending on whether it's a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 - } - - if ($CustomField.Exception) { - throw $CustomField - } - } - - # Blacklists we are going to check. - $BlackLists = @( - [PSCustomObject]@{ - DisplayName = "0Spam" - DNSBLDomainName = "bl.0spam.org" - } - [PSCustomObject]@{ - DisplayName = "0Spam RBL" - DNSBLDomainName = "rbl.0spam.org" - } - [PSCustomObject]@{ - DisplayName = "Anonmails DNSBL" - DNSBLDomainName = "spam.dnsbl.anonmails.de" - } - [PSCustomObject]@{ - DisplayName = "Backscatterer" - DNSBLDomainName = "ips.backscatterer.org" - } - [PSCustomObject]@{ - DisplayName = "Blocklist.de" - DNSBLDomainName = "bl.blocklist.de" - } - [PSCustomObject]@{ - DisplayName = "Cymru Bogons" - DNSBLDomainName = "bogons.cymru.com" - } - [PSCustomObject]@{ - DisplayName = "Dan Tor" - DNSBLDomainName = "tor.dan.me.uk" - } - [PSCustomObject]@{ - DisplayName = "Dan Tor Exit" - DNSBLDomainName = "torexit.dan.me.uk" - } - [PSCustomObject]@{ - DisplayName = "Drone BL" - DNSBLDomainName = "dnsbl.dronebl.org" - } - [PSCustomObject]@{ - DisplayName = "Fabel Sources" - DNSBLDomainName = "spamsources.fabel.dk" - } - [PSCustomObject]@{ - DisplayName = "Host Karma" - DNSBLDomainName = "hostkarma.junkemailfilter.com" - } - [PSCustomObject]@{ - DisplayName = "ImproWare (IMP) DNS RBL" - DNSBLDomainName = "dnsrbl.swinog.ch" - } - [PSCustomObject]@{ - DisplayName = "ImproWare (IMP) Spam RBL" - DNSBLDomainName = "spamrbl.swinog.ch" - } - [PSCustomObject]@{ - DisplayName = "Interserver RBL" - DNSBLDomainName = "rbl.interserver.net" - } - [PSCustomObject]@{ - DisplayName = "Interserver Spam Assassin RBL" - DNSBLDomainName = "rblspamassassin.interserver.net" - } - [PSCustomObject]@{ - DisplayName = "JIPPG's Relay Blackhole List Project" - DNSBLDomainName = "mail-abuse.blacklist.jippg.org" - } - [PSCustomObject]@{ - DisplayName = "Kempt.net DNS Black List" - DNSBLDomainName = "dnsbl.kempt.net" - } - [PSCustomObject]@{ - DisplayName = "Mailspike Z" - DNSBLDomainName = "z.mailspike.net" - } - [PSCustomObject]@{ - DisplayName = "Mailspike BL" - DNSBLDomainName = "bl.mailspike.net" - } - [PSCustomObject]@{ - DisplayName = "Nordspam BL" - DNSBLDomainName = "bl.nordspam.com" - } - [PSCustomObject]@{ - DisplayName = "PSBL" - DNSBLDomainName = "psbl.surriel.com" - } - [PSCustomObject]@{ - DisplayName = "S5H" - DNSBLDomainName = "all.s5h.net" - } - [PSCustomObject]@{ - DisplayName = "Schulte" - DNSBLDomainName = "rbl.schulte.org" - } - [PSCustomObject]@{ - DisplayName = "Spam Eating Monkey - Backscatter" - DNSBLDomainName = "backscatter.spameatingmonkey.net" - } - [PSCustomObject]@{ - DisplayName = "Spam Eating Monkey - Black" - DNSBLDomainName = "bl.spameatingmonkey.net" - } - [PSCustomObject]@{ - DisplayName = "SpamCop" - DNSBLDomainName = "bl.spamcop.net" - } - [PSCustomObject]@{ - DisplayName = "Suomispam" - DNSBLDomainName = "bl.suomispam.net" - } - [PSCustomObject]@{ - DisplayName = "Truncate" - DNSBLDomainName = "truncate.gbudb.net" - } - [PSCustomObject]@{ - DisplayName = "UCE Protect - L1" - DNSBLDomainName = "dnsbl-1.uceprotect.net" - } - [PSCustomObject]@{ - DisplayName = "UCE Protect - L2" - DNSBLDomainName = "dnsbl-2.uceprotect.net" - } - [PSCustomObject]@{ - DisplayName = "UCE Protect - L3" - DNSBLDomainName = "dnsbl-3.uceprotect.net" - } - [PSCustomObject]@{ - DisplayName = "ZapBL" - DNSBLDomainName = "dnsbl.zapbl.net" - } - ) - - if (!$ExitCode) { - $ExitCode = 0 - } -} -process { - # Check if the script is running with elevated privileges (Administrator) - if (!(Test-IsElevated)) { - Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." - exit 1 - } - - # Try to retrieve the WAN IP using the ipify.org service - try { - $WanIP = (Invoke-WebRequest -Uri "api.ipify.org" -UseBasicParsing).Content - } - catch { - Write-Host -Object "[Error] Failed to retrieve WAN IP." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Validate the retrieved WAN IP format - if ($WanIP -notmatch '\d+\.\d+\.\d+\.\d+') { - Write-Host -Object "[Error] The service ipify.org returned '$WanIp' which is not a valid IP." - exit 1 - } - - # Further validate the WAN IP by attempting to cast it as an IP address object - try { - [IPAddress]$WanIP | Out-Null - } - catch { - Write-Host -Object "[Error] The service ipify.org returned '$WanIp' which is not a valid IP." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Reverse the IP address octets for DNSBL query - $IPOctets = $WanIP -split '\.' - [array]::Reverse($IPOctets) - $ReversedIp = $IPOctets -join '.' - - # Validate the reversed IP format - if ($ReversedIp -notmatch '\d+\.\d+\.\d+\.\d+') { - Write-Host -Object "[Error] '$ReversedIp' is not a valid reversed IP of '$WanIP'." - exit 1 - } - - # Further validate the reversed IP by attempting to cast it as an IP address object - try { - [IPAddress]$ReversedIp | Out-Null - } - catch { - Write-Host -Object "[Error] '$ReversedIp' is not a valid reversed IP of '$WanIP'." - Write-Host -Object "[Error] $($_.Exception.Message)" - exit 1 - } - - # Initialize a list to store blacklisted services - $BlackListedServices = New-Object System.Collections.Generic.List[object] - - # Loop through each DNSBL to check if the IP is listed - $BlackLists | ForEach-Object { - try { - $Result = Resolve-DnsName -Name "$ReversedIp.$($_.DNSBLDomainName)" -NoHostsFile -DnsOnly -QuickTimeout -ErrorAction Stop - - $BlockListIP = Resolve-DnsName -Name $($_.DNSBLDomainName) -NoHostsFile -DnsOnly -QuickTimeout -ErrorAction SilentlyContinue | Select-Object -ExpandProperty IPAddress -ErrorAction SilentlyContinue - - foreach($IPAddress in $Result.IPAddress){ - if($IPaddress -notmatch '^127\.0\.' -and $BlockListIP -and $BlockListIP -notcontains $IPAddress){ - Write-Host -Object "[Error] A Response Code of '$IPaddress' was given by $($_.DisplayName)." - Write-Host -Object "[Error] Typically response codes start with '127.0.' you may want to use different DNS servers." - $ExitCode = 1 - return - } - } - - # If the result does not contain an IP address skip to next entry - if(!$($Result.IPAddress)){ - return - } - - $BlackListedServices.Add( - [PSCustomObject]@{ - Name = $($_.DisplayName) - TTL = $($Result.TTL -join ', ') - ResponseCode = $($Result.IPAddress -join ', ') - } - ) - } - catch { - return - } - } - - # Create a custom field value to store the results - $CustomFieldValue = New-Object System.Collections.Generic.List[string] - $MXToolboxLink = "https://mxtoolbox.com/SuperTool.aspx?action=blacklist%3a$WanIP" - - # Check if any blacklists contain the WAN IP and output the results - if($BlackListedServices.Count -gt 0){ - Write-Host -Object "[Alert] The WAN IP '$WanIp' was found on $($BlackListedServices.Count) blacklist(s)." - Write-Host -Object "You may want to validate these results with '$MXToolboxLink'." - $CustomFieldValue.Add("[Alert] The WAN IP '$WanIp' was found on $($BlackListedServices.Count) blacklist(s).") - $CustomFieldValue.Add($MXToolboxLink) - - ($BlackListedServices | Format-Table | Out-String).Trim() | Write-Host - $CustomFieldValue.Add($($BlackListedServices | Format-List | Out-String)) - }else{ - Write-Host -Object "The WAN IP '$WanIp' was not found on any blacklists." - Write-Host -Object "You may want to validate these results with '$MXToolboxLink'." - - $CustomFieldValue.Add("The WAN IP '$WanIp' was not found on any blacklists.") - $CustomFieldValue.Add($MXToolboxLink) - } - - # Output the list of blacklists checked - Write-Host -Object "`nBlacklists Checked: $($BlackLists.DisplayName -join ', ')" - - # Optionally set a custom field with the results - if($CustomField){ - try { - Write-Host "`nAttempting to set Custom Field '$CustomField'." - Set-NinjaProperty -Name $CustomField -Value $CustomFieldValue - Write-Host "Successfully set Custom Field '$CustomField'!" - } - catch { - Write-Host "[Error] $($_.Exception.Message)" - exit 1 - } - } - - exit $ExitCode -} -end { - - - -} +#Requires -Version 5.1 + +<# +.SYNOPSIS + Checks several common blacklists to see if the device's WAN IP is currently being blacklisted. A private recursive DNS server is recommended, as it is not uncommon for DNS blocklists to block public DNS servers such as 1.1.1.1. +.DESCRIPTION + Checks several common blacklists to see if the device's WAN IP is currently being blacklisted. A private recursive DNS server is recommended, as it is not uncommon for DNS blocklists to block public DNS servers such as 1.1.1.1. +.EXAMPLE + (No Parameters) - When found on blacklist + + [Alert] The WAN IP '127.0.0.1' was found on 9 blacklist(s). + You may want to validate these results with 'https://mxtoolbox.com/SuperTool.aspx?action=blacklist%3a127.0.0.1'. + Name TTL ResponseCode + ---- --- ------------ + Blocklist.de 1269 127.0.0.14 + Interserver RBL 903 127.0.0.2 + Interserver Spam Assassin RBL 903 127.0.0.2 + Mailspike Z 120 127.0.0.2 + Mailspike BL 120 127.0.0.2 + S5H 5, 86400, 86400, 86400, 30, 300, 30, 300, 300, 300 127.0.0.2, 85.119.82.99, 2001:ba8:1... + UCE Protect - L1 902 127.0.0.2 + UCE Protect - L2 902 127.0.0.2 + UCE Protect - L3 902 127.0.0.2 + + Blacklists Checked: 0Spam, 0Spam RBL, Anonmails DNSBL, Backscatterer, Blocklist.de, Cymru Bogons, Dan Tor, Dan Tor Exit, Drone BL, Fabel Sources, Host Karma, ImproWare (IMP) DNS RBL, ImproWare (IMP) Spam RBL, Interserver RBL, Interserver Spam Assassin RBL, JIPPG's Relay Blackhole List Project, Kempt.net DNS Black List, Mailspike Z, Mailspike BL, Nordspam BL, PSBL, S5H, Schulte, Spam Eating Monkey - Backscatter, Spam Eating Monkey - Black, SpamCop, Suomispam, Truncate, UCE Protect - L1, UCE Protect - L2, UCE Protect - L3, ZapBL + +.EXAMPLE + (No Parameters) - When not found on blacklist + + The WAN IP '127.0.0.1' was not found on any blacklists. + You may want to validate these results with 'https://mxtoolbox.com/SuperTool.aspx?action=blacklist%3a127.0.0.1'. + + Blacklists Checked: 0Spam, 0Spam RBL, Anonmails DNSBL, Backscatterer, Blocklist.de, Cymru Bogons, Dan Tor, Dan Tor Exit, Drone BL, Fabel Sources, Host Karma, ImproWare (IMP) DNS RBL, ImproWare (IMP) Spam RBL, Interserver RBL, Interserver Spam Assassin RBL, JIPPG's Relay Blackhole List Project, Kempt.net DNS Black List, Mailspike Z, Mailspike BL, Nordspam BL, PSBL, S5H, Schulte, Spam Eating Monkey - Backscatter, Spam Eating Monkey - Black, SpamCop, Suomispam, Truncate, UCE Protect - L1, UCE Protect - L2, UCE Protect - L3, ZapBL + +PARAMETER: -CustomField "ReplaceMeWithYourDesiredMultilineCustomField" + Optionally specify the name of a multiline custom field to store the results in. + +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String]$CustomField +) + +begin { + # If using script form variables, replace command line parameters with them. + if($env:multilineCustomFieldName -and $env:multilineCustomFieldName -notlike "null") { $CustomField = $env:multilineCustomFieldName } + + # Local administrator privileges are required to set custom fields. + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + + # $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + # if ($Characters -ge 200000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If requested to set the field value for a Ninja document, specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + + # # This is a list of valid fields that can be set. If no type is specified, assume that the input does not need to be changed. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + + # # The field below requires additional information to set. # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # Redirect error output to the success stream to handle errors more easily if nothing is found or something else goes wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # If an error is received with an exception property, exit the function with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + + # # The types below require values not typically given to be set. The code below will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # Although it's highly likely we were given a value like "True" or a boolean data type, it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, match the given value with a GUID. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the GUID of the option we're trying to select, so match the value we were given with a GUID. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + + # if (-not $Selection) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # # Set the field differently depending on whether it's a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + + # Blacklists we are going to check. + $BlackLists = @( + [PSCustomObject]@{ + DisplayName = "0Spam" + DNSBLDomainName = "bl.0spam.org" + } + [PSCustomObject]@{ + DisplayName = "0Spam RBL" + DNSBLDomainName = "rbl.0spam.org" + } + [PSCustomObject]@{ + DisplayName = "Anonmails DNSBL" + DNSBLDomainName = "spam.dnsbl.anonmails.de" + } + [PSCustomObject]@{ + DisplayName = "Backscatterer" + DNSBLDomainName = "ips.backscatterer.org" + } + [PSCustomObject]@{ + DisplayName = "Blocklist.de" + DNSBLDomainName = "bl.blocklist.de" + } + [PSCustomObject]@{ + DisplayName = "Cymru Bogons" + DNSBLDomainName = "bogons.cymru.com" + } + [PSCustomObject]@{ + DisplayName = "Dan Tor" + DNSBLDomainName = "tor.dan.me.uk" + } + [PSCustomObject]@{ + DisplayName = "Dan Tor Exit" + DNSBLDomainName = "torexit.dan.me.uk" + } + [PSCustomObject]@{ + DisplayName = "Drone BL" + DNSBLDomainName = "dnsbl.dronebl.org" + } + [PSCustomObject]@{ + DisplayName = "Fabel Sources" + DNSBLDomainName = "spamsources.fabel.dk" + } + [PSCustomObject]@{ + DisplayName = "Host Karma" + DNSBLDomainName = "hostkarma.junkemailfilter.com" + } + [PSCustomObject]@{ + DisplayName = "ImproWare (IMP) DNS RBL" + DNSBLDomainName = "dnsrbl.swinog.ch" + } + [PSCustomObject]@{ + DisplayName = "ImproWare (IMP) Spam RBL" + DNSBLDomainName = "spamrbl.swinog.ch" + } + [PSCustomObject]@{ + DisplayName = "Interserver RBL" + DNSBLDomainName = "rbl.interserver.net" + } + [PSCustomObject]@{ + DisplayName = "Interserver Spam Assassin RBL" + DNSBLDomainName = "rblspamassassin.interserver.net" + } + [PSCustomObject]@{ + DisplayName = "JIPPG's Relay Blackhole List Project" + DNSBLDomainName = "mail-abuse.blacklist.jippg.org" + } + [PSCustomObject]@{ + DisplayName = "Kempt.net DNS Black List" + DNSBLDomainName = "dnsbl.kempt.net" + } + [PSCustomObject]@{ + DisplayName = "Mailspike Z" + DNSBLDomainName = "z.mailspike.net" + } + [PSCustomObject]@{ + DisplayName = "Mailspike BL" + DNSBLDomainName = "bl.mailspike.net" + } + [PSCustomObject]@{ + DisplayName = "Nordspam BL" + DNSBLDomainName = "bl.nordspam.com" + } + [PSCustomObject]@{ + DisplayName = "PSBL" + DNSBLDomainName = "psbl.surriel.com" + } + [PSCustomObject]@{ + DisplayName = "S5H" + DNSBLDomainName = "all.s5h.net" + } + [PSCustomObject]@{ + DisplayName = "Schulte" + DNSBLDomainName = "rbl.schulte.org" + } + [PSCustomObject]@{ + DisplayName = "Spam Eating Monkey - Backscatter" + DNSBLDomainName = "backscatter.spameatingmonkey.net" + } + [PSCustomObject]@{ + DisplayName = "Spam Eating Monkey - Black" + DNSBLDomainName = "bl.spameatingmonkey.net" + } + [PSCustomObject]@{ + DisplayName = "SpamCop" + DNSBLDomainName = "bl.spamcop.net" + } + [PSCustomObject]@{ + DisplayName = "Suomispam" + DNSBLDomainName = "bl.suomispam.net" + } + [PSCustomObject]@{ + DisplayName = "Truncate" + DNSBLDomainName = "truncate.gbudb.net" + } + [PSCustomObject]@{ + DisplayName = "UCE Protect - L1" + DNSBLDomainName = "dnsbl-1.uceprotect.net" + } + [PSCustomObject]@{ + DisplayName = "UCE Protect - L2" + DNSBLDomainName = "dnsbl-2.uceprotect.net" + } + [PSCustomObject]@{ + DisplayName = "UCE Protect - L3" + DNSBLDomainName = "dnsbl-3.uceprotect.net" + } + [PSCustomObject]@{ + DisplayName = "ZapBL" + DNSBLDomainName = "dnsbl.zapbl.net" + } + ) + + if (!$ExitCode) { + $ExitCode = 0 + } +} +process { + # Check if the script is running with elevated privileges (Administrator) + if (!(Test-IsElevated)) { + Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." + exit 1 + } + + # Try to retrieve the WAN IP using the ipify.org service + try { + $WanIP = (Invoke-WebRequest -Uri "api.ipify.org" -UseBasicParsing).Content + } + catch { + Write-Host -Object "[Error] Failed to retrieve WAN IP." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Validate the retrieved WAN IP format + if ($WanIP -notmatch '\d+\.\d+\.\d+\.\d+') { + Write-Host -Object "[Error] The service ipify.org returned '$WanIp' which is not a valid IP." + exit 1 + } + + # Further validate the WAN IP by attempting to cast it as an IP address object + try { + [IPAddress]$WanIP | Out-Null + } + catch { + Write-Host -Object "[Error] The service ipify.org returned '$WanIp' which is not a valid IP." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Reverse the IP address octets for DNSBL query + $IPOctets = $WanIP -split '\.' + [array]::Reverse($IPOctets) + $ReversedIp = $IPOctets -join '.' + + # Validate the reversed IP format + if ($ReversedIp -notmatch '\d+\.\d+\.\d+\.\d+') { + Write-Host -Object "[Error] '$ReversedIp' is not a valid reversed IP of '$WanIP'." + exit 1 + } + + # Further validate the reversed IP by attempting to cast it as an IP address object + try { + [IPAddress]$ReversedIp | Out-Null + } + catch { + Write-Host -Object "[Error] '$ReversedIp' is not a valid reversed IP of '$WanIP'." + Write-Host -Object "[Error] $($_.Exception.Message)" + exit 1 + } + + # Initialize a list to store blacklisted services + $BlackListedServices = New-Object System.Collections.Generic.List[object] + + # Loop through each DNSBL to check if the IP is listed + $BlackLists | ForEach-Object { + try { + $Result = Resolve-DnsName -Name "$ReversedIp.$($_.DNSBLDomainName)" -NoHostsFile -DnsOnly -QuickTimeout -ErrorAction Stop + + $BlockListIP = Resolve-DnsName -Name $($_.DNSBLDomainName) -NoHostsFile -DnsOnly -QuickTimeout -ErrorAction SilentlyContinue | Select-Object -ExpandProperty IPAddress -ErrorAction SilentlyContinue + + foreach($IPAddress in $Result.IPAddress){ + if($IPaddress -notmatch '^127\.0\.' -and $BlockListIP -and $BlockListIP -notcontains $IPAddress){ + Write-Host -Object "[Error] A Response Code of '$IPaddress' was given by $($_.DisplayName)." + Write-Host -Object "[Error] Typically response codes start with '127.0.' you may want to use different DNS servers." + $ExitCode = 1 + return + } + } + + # If the result does not contain an IP address skip to next entry + if(!$($Result.IPAddress)){ + return + } + + $BlackListedServices.Add( + [PSCustomObject]@{ + Name = $($_.DisplayName) + TTL = $($Result.TTL -join ', ') + ResponseCode = $($Result.IPAddress -join ', ') + } + ) + } + catch { + return + } + } + + # Create a custom field value to store the results + $CustomFieldValue = New-Object System.Collections.Generic.List[string] + $MXToolboxLink = "https://mxtoolbox.com/SuperTool.aspx?action=blacklist%3a$WanIP" + + # Check if any blacklists contain the WAN IP and output the results + if($BlackListedServices.Count -gt 0){ + Write-Host -Object "[Alert] The WAN IP '$WanIp' was found on $($BlackListedServices.Count) blacklist(s)." + Write-Host -Object "You may want to validate these results with '$MXToolboxLink'." + $CustomFieldValue.Add("[Alert] The WAN IP '$WanIp' was found on $($BlackListedServices.Count) blacklist(s).") + $CustomFieldValue.Add($MXToolboxLink) + + ($BlackListedServices | Format-Table | Out-String).Trim() | Write-Host + $CustomFieldValue.Add($($BlackListedServices | Format-List | Out-String)) + }else{ + Write-Host -Object "The WAN IP '$WanIp' was not found on any blacklists." + Write-Host -Object "You may want to validate these results with '$MXToolboxLink'." + + $CustomFieldValue.Add("The WAN IP '$WanIp' was not found on any blacklists.") + $CustomFieldValue.Add($MXToolboxLink) + } + + # Output the list of blacklists checked + Write-Host -Object "`nBlacklists Checked: $($BlackLists.DisplayName -join ', ')" + + # Optionally set a custom field with the results + if($CustomField){ + try { + Write-Host "`nAttempting to set Custom Field '$CustomField'." + # Set-NinjaProperty -Name $CustomField -Value $CustomFieldValue # Removed NinjaOne dependency + Write-Host "Successfully set Custom Field '$CustomField'!" + } + catch { + Write-Host "[Error] $($_.Exception.Message)" + exit 1 + } + } + + exit $ExitCode +} +end { + + + +} diff --git a/Powershell Scripts/Wi-Fi Report.ps1 b/Powershell Scripts/Wi-Fi Report.ps1 index bd38a43..cfdae9c 100644 --- a/Powershell Scripts/Wi-Fi Report.ps1 +++ b/Powershell Scripts/Wi-Fi Report.ps1 @@ -1,426 +1,426 @@ # Saves a Wireless LAN report to a WYSIWYG Custom Field. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Saves a Wireless LAN report to a WYSIWYG Custom Field. -.DESCRIPTION - Saves a Wireless LAN report to a WYSIWYG Custom Field. -.EXAMPLE - -CustomField "wlanreport" - Saves a Wireless LAN report to a WYSIWYG Custom Field. - - --- Wifi Report --- - - ### Wifi Adapters ### - - Interface SSID Authentication Band Channel Signal State RadioType - --------- ---- -------------- ---- ------- ------ ----- --------- - Wi-Fi TestAP1 WPA2-Personal 5.0GHz 157 99% connected 802.11ac - - - - ### Other Wifi Networks ### - - SSID Authentication Band Channel Signal - ---- -------------- ---- ------- ------ - WPA2-Personal 2.4GHz, 6.0GHz 1 95% - TestAP1 WPA2-Personal 5.0GHz 112 18% - TestAP2 WPA3-Personal 2.4GHz, 6.0GHz 1 91% - TestAP3 WPA2-Enterprise 2.4GHz, 6.0GHz 1 98% - TestAP4 WPA2-Personal 2.4GHz, 6.0GHz 1 94% - TestAP5 Open 5.0GHz 36 87% - TestAP6 WPA3-Personal 2.4GHz 1 93% - - [Info] Attempting to set Custom Field 'wysiwygCustomFieldName'. - [Info] Successfully set Custom Field 'wysiwygCustomFieldName'! - -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 10 - Release Notes: Renamed script and added Script Variable support -#> - -[CmdletBinding()] -param ( - [Parameter()] - [String] - $CustomField, - [Parameter()] - [switch] - $DebugHtml -) - -begin { - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - - function Get-WifiBand { - param ($RadioType, $Channel) - @( - [PSCustomObject]@{ # Wi-Fi 2.4GHz - RadioType = "802.11b", "802.11g", "802.11n", "802.11ax", "802.11be" - Band = "2.4GHz" - Channels = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 - } - [PSCustomObject]@{ - RadioType = "802.11y" - Band = "3.65GHz" - Channels = 131, 132, 133, 134, 135, 136, 137, 138 - } - [PSCustomObject]@{ - RadioType = "802.11j" - Band = "4.9-5.0GHz" - Channels = 7, 8, 9, 11, 12, 16, 183, 184, 185, 187, 188, 189, 192, 193, 194, 195, 196 - } - [PSCustomObject]@{ # Wi-Fi 5GHz - RadioType = "802.11a", "802.11h", "802.11n", "802.11ac", "802.11ax", "802.11be" - Band = "5.0GHz" - Channels = 7, 8, 9, 11, 12, 16, 34, 36, 40, 42, 44, 48, 50, 52, 54, 56, 58, 60, 62, 100, 102, - 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 132, 134, 136, 138, 140, 142, - 144, 149, 151, 153, 155, 157, 159, 161, 165, 193, 184, 185, 187, 188, 189, 192, 196 - } - [PSCustomObject]@{ # Wi‑Fi 6E - RadioType = "802.11ax", "802.11be" - Band = "6.0GHz" - Channels = 1, 2, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, - 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, - 95, 97, 99, 101, 103, 105, 107, 109, 111, 113, 115, 117, 119, 121, 123, 125, 127, 129, 131, 133, 135, - 137, 139, 141, 143, 145, 147, 149, 151, 153, 155, 157, 159, 161, 163, 165, 167, 169, 171, 173, 175, 177, - 179, 181, 183, 185, 187, 189, 191, 193, 195, 197, 199, 201, 203, 205, 209, 211, 213, 215, 217, 219, 221, - 225, 227, 229, 233 - } - [PSCustomObject]@{ - RadioType = "802.11p" - Band = "5.9GHz" - Channels = 172, 174, 176, 178, 180, 182, 184 - } - [PSCustomObject]@{ # WiGig - RadioType = "802.11ad", "802.11aj", "802.11ay" - Band = "60GHz" - Channels = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, - 19, 20, 21, 22, 25, 26, 27, 29, 28, 33, 34, 35, 36, 37, 38, 39, 40 - } - [PSCustomObject]@{ - RadioType = "802.11ah" - Band = "900MHz" - Channels = 1, 2, 3, 5, 6, 11, 13, 26 - } - ) | Where-Object { - $_.RadioType -contains $RadioType -and $_.Channels -contains $Channel - } | Select-Object -ExpandProperty Band - } - - function Get-WifiAdapters { - $NetShOutput = $(netsh.exe wlan show interfaces) - $IsNext = $false - $IsLast = $false - foreach ($Line in $NetShOutput) { - switch -regex ($Line) { - "^\s{4}Name\s{1,24}\s:\s(.*)" { - $Name = $Matches[1] - $IsNext = $true - } - default { - if ($IsNext) { - if ($Line -eq "") { - $IsNext = $false - $Band = Get-WifiBand -RadioType $RadioType -Channel $Channel - if ($null -eq $Band) { $Band = "Unknown" } - [PSCustomObject]@{ - Interface = $Name - SSID = $Ssid - Authentication = $Authentication - Band = $($Band) -join ', ' - Channel = $Channel - Signal = $Signal - State = $State - RadioType = $RadioType - } - $IsLast = $false - } - else { - switch -regex ($Line) { - "^\s{4}BSSID\s{1,24}\s:\s(.*)" { - if ($IsLast) { - $Band = Get-WifiBand -RadioType $RadioType -Channel $Channel - if ($null -eq $Band) { $Band = "Unknown" } - [PSCustomObject]@{ - Interface = $Name - SSID = $Ssid - Authentication = $Authentication - Band = $($Band) -join ', ' - Channel = $Channel - Signal = $Signal - State = $State - RadioType = $RadioType - } - $IsLast = $false - } - } - "^\s{4}Description\s{1,24}\s:\s(.*)" { $Description = $Matches[1] } - "^\s{4}SSID\s{1,24}\s:\s(.*)" { $Ssid = $Matches[1] } - "^\s{4}State\s{1,24}\s:\s(.*)" { $State = $Matches[1] } - "^\s{4}Signal\s{1,24}\s:\s(.*)" { $Signal = $Matches[1] } - "^\s{4}Radio type\s{1,24}\s:\s(.*)" { $RadioType = $Matches[1] } - "^\s{4}Channel\s{1,24}\s:\s(.*)" { $Channel = $Matches[1] } - "^\s{4}Authentication\s{1,24}\s:\s(.*)" { $Authentication = $Matches[1] } - "^\s{4}Profile\s{1,24}\s:\s(.*)" { $IsLast = $true } - } - } - } - } - } - } - } - - function Get-WifiAPs { - $SsidRegex = "^SSID\s[0-9]{1,4}\s:\s(.*)" - $NetShOutput = $(netsh.exe wlan show networks mode=bssid) - $IsNext = $false - $IsLast = $false - foreach ($Line in $NetShOutput) { - switch -regex ($Line) { - $SsidRegex { - $Name = [regex]::Match($Line, $SsidRegex).Captures.Groups[1].Value - $IsNext = $true - } - default { - if ($IsNext) { - if ($Line -eq "") { - $IsNext = $false - $Band = Get-WifiBand -RadioType $RadioType -Channel $Channel - if ($null -eq $Band) { $Band = "Unknown" } - [PSCustomObject]@{ - SSID = $Name - Authentication = $Authentication - Band = $($Band) -join ', ' - Channel = $Channel - Signal = $Signal - RadioType = $RadioType - } - $IsLast = $false - } - else { - switch -regex ($Line) { - "^\s{4}Authentication\s{1,24}\s:\s(.*)" { $Authentication = $Matches[1] } - "^\s{4}BSSID\s{1,24}\s:\s(.*)" { - if ($IsLast) { - $Band = Get-WifiBand -RadioType $RadioType -Channel $Channel - if ($null -eq $Band) { $Band = "Unknown" } - [PSCustomObject]@{ - SSID = $Name - Authentication = $Authentication - Band = $($Band) -join ', ' - Channel = $Channel - Signal = $Signal - RadioType = $RadioType - } - $IsLast = $false - } - } - "^\s{9}SSID\s{1,24}\s:\s(.*)" { $Name = $Matches[1] } - "^\s{9}Signal\s{1,24}\s:\s(.*)" { $Signal = $Matches[1] } - "^\s{9}Radio type\s{1,24}\s:\s(.*)" { $RadioType = $Matches[1] } - "^\s{9}Channel\s{1,24}\s:\s(.*)" { $Channel = $Matches[1]; $IsLast = $true } - } - } - } - } - } - } - } - - function Get-WifiRadioStatus { - $NetShOutput = $(netsh.exe wlan show interfaces) - $RadioStatus = [PSCustomObject]@{ - Hardware = "Off" - Software = "Off" - } - if ($NetShOutput -imatch " connected") { - # If we are connected to a AP then hardware and software radio status are On - $RadioStatus.Hardware = "On" - $RadioStatus.Software = "On" - return $RadioStatus - } - foreach ($Line in $NetShOutput) { - switch -regex ($Line) { - "Hardware\s(.*)" { $RadioStatus.Hardware = $Matches[1] } - "Software\s(.*)" { $RadioStatus.Software = $Matches[1] } - } - } - return $RadioStatus - } - - function Test-WifiRadioStatus { - $RadioStatus = Get-WifiRadioStatus - if ($RadioStatus.Hardware -eq "On" -and $RadioStatus.Software -eq "On") { - return $true - } - else { - return $false - } - } - function Set-NinjaProperty { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $True)] - [String]$Name, - [Parameter()] - [String]$Type, - [Parameter(Mandatory = $True, ValueFromPipeline = $True)] - $Value, - [Parameter()] - [String]$DocumentName - ) - $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters - if ($Characters -ge 10000) { - throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") - } - # If we're requested to set the field value for a Ninja document we'll specify it here. - $DocumentationParams = @{} - if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } - # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. - $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" - if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } - # The field below requires additional information to be set - $NeedsOptions = "Dropdown" - if ($DocumentName) { - if ($NeedsOptions -contains $Type) { - # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. - $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 - } - } - else { - if ($NeedsOptions -contains $Type) { - $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 - } - } - # If an error is received it will have an exception property, the function will exit with that error information. - if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } - # The below types require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. - switch ($Type) { - "Checkbox" { - # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. - $NinjaValue = [System.Convert]::ToBoolean($Value) - } - "Date or Date Time" { - # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. - $Date = (Get-Date $Value).ToUniversalTime() - $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date - $NinjaValue = $TimeSpan.TotalSeconds - } - "Dropdown" { - # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. - $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" - $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID - if (-not $Selection) { - throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") - } - $NinjaValue = $Selection - } - default { - # All the other types shouldn't require additional work on the input. - $NinjaValue = $Value - } - } - # We'll need to set the field differently depending on if its a field in a Ninja Document or not. - if ($DocumentName) { - $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 - } - else { - $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 - } - if ($CustomField.Exception) { - throw $CustomField - } - } -} - -process { - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $CustomField = $env:wysiwygCustomFieldName } - - if (Test-WifiRadioStatus) { - Write-Host "[Info] Wifi Radio is On" - } - else { - $RadioStatus = Get-WifiRadioStatus - Write-Host "[Info] Wi-Fi Radio is $($RadioStatus.Hardware) in Hardware" - Write-Host "[Info] Wi-Fi Radio is $($RadioStatus.Software) in Software" - Write-Host "[Warn] Wi-Fi Radio is Off" - } - - # Get Wifi Adapters - $WifiAdapters = Get-WifiAdapters - - # Get Wifi Access Points - $AccessPointList = Get-WifiAPs - - # Build the report - $Report = "

Wifi Report

" - - $Report += "

Wifi Adapters

" - if ($WifiAdapters) { - $Report += $WifiAdapters | ConvertTo-Html -Fragment | Out-String - } - else { - $Report += "

No Wifi Adapters Found

" - } - - $Report += "

Other Wifi Networks

" - if ($AccessPointList) { - $Report += $AccessPointList | ConvertTo-Html -Fragment | Out-String - } - else { - $Report += "

No Other Wifi Networks Found

" - } - - Write-Host "--- Wifi Report ---" - Write-Host "" - Write-Host "### Wifi Adapters ###" - $WifiAdapters | Format-Table -AutoSize | Out-String -Width 4000 | Write-Host - - Write-Host "### Other Wifi Networks ###" - $AccessPointList | - Select-Object -Property SSID, Authentication, Band, Channel, Signal | - Format-Table -AutoSize | Out-String -Width 4000 | Write-Host - - if ($DebugHtml) { - $Report | Out-String | Write-Host - } - - if ($Report) { - # Save report to multi-line custom field - if ($CustomField) { - try { - # Set the custom field with the generated report - Write-Host "[Info] Attempting to set Custom Field '$CustomField'." - Set-NinjaProperty -Name $CustomField -Value $($Report | Out-String) - Write-Host "[Info] Successfully set Custom Field '$CustomField'!" - } - catch { - Write-Host "[Warn] $($_.Exception.Message)" - } - } - } - else { - Write-Host "Could not generate wlan report." - exit 1 - } - -} -end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + Saves a Wireless LAN report to a WYSIWYG Custom Field. +.DESCRIPTION + Saves a Wireless LAN report to a WYSIWYG Custom Field. +.EXAMPLE + -CustomField "wlanreport" + Saves a Wireless LAN report to a WYSIWYG Custom Field. + + --- Wifi Report --- + + ### Wifi Adapters ### + + Interface SSID Authentication Band Channel Signal State RadioType + --------- ---- -------------- ---- ------- ------ ----- --------- + Wi-Fi TestAP1 WPA2-Personal 5.0GHz 157 99% connected 802.11ac + + + + ### Other Wifi Networks ### + + SSID Authentication Band Channel Signal + ---- -------------- ---- ------- ------ + WPA2-Personal 2.4GHz, 6.0GHz 1 95% + TestAP1 WPA2-Personal 5.0GHz 112 18% + TestAP2 WPA3-Personal 2.4GHz, 6.0GHz 1 91% + TestAP3 WPA2-Enterprise 2.4GHz, 6.0GHz 1 98% + TestAP4 WPA2-Personal 2.4GHz, 6.0GHz 1 94% + TestAP5 Open 5.0GHz 36 87% + TestAP6 WPA3-Personal 2.4GHz 1 93% + + [Info] Attempting to set Custom Field 'wysiwygCustomFieldName'. + [Info] Successfully set Custom Field 'wysiwygCustomFieldName'! + +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 10 + Release Notes: Renamed script and added Script Variable support +#> + +[CmdletBinding()] +param ( + [Parameter()] + [String] + $CustomField, + [Parameter()] + [switch] + $DebugHtml +) + +begin { + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + + function Get-WifiBand { + param ($RadioType, $Channel) + @( + [PSCustomObject]@{ # Wi-Fi 2.4GHz + RadioType = "802.11b", "802.11g", "802.11n", "802.11ax", "802.11be" + Band = "2.4GHz" + Channels = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 + } + [PSCustomObject]@{ + RadioType = "802.11y" + Band = "3.65GHz" + Channels = 131, 132, 133, 134, 135, 136, 137, 138 + } + [PSCustomObject]@{ + RadioType = "802.11j" + Band = "4.9-5.0GHz" + Channels = 7, 8, 9, 11, 12, 16, 183, 184, 185, 187, 188, 189, 192, 193, 194, 195, 196 + } + [PSCustomObject]@{ # Wi-Fi 5GHz + RadioType = "802.11a", "802.11h", "802.11n", "802.11ac", "802.11ax", "802.11be" + Band = "5.0GHz" + Channels = 7, 8, 9, 11, 12, 16, 34, 36, 40, 42, 44, 48, 50, 52, 54, 56, 58, 60, 62, 100, 102, + 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 132, 134, 136, 138, 140, 142, + 144, 149, 151, 153, 155, 157, 159, 161, 165, 193, 184, 185, 187, 188, 189, 192, 196 + } + [PSCustomObject]@{ # Wi‑Fi 6E + RadioType = "802.11ax", "802.11be" + Band = "6.0GHz" + Channels = 1, 2, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, + 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, + 95, 97, 99, 101, 103, 105, 107, 109, 111, 113, 115, 117, 119, 121, 123, 125, 127, 129, 131, 133, 135, + 137, 139, 141, 143, 145, 147, 149, 151, 153, 155, 157, 159, 161, 163, 165, 167, 169, 171, 173, 175, 177, + 179, 181, 183, 185, 187, 189, 191, 193, 195, 197, 199, 201, 203, 205, 209, 211, 213, 215, 217, 219, 221, + 225, 227, 229, 233 + } + [PSCustomObject]@{ + RadioType = "802.11p" + Band = "5.9GHz" + Channels = 172, 174, 176, 178, 180, 182, 184 + } + [PSCustomObject]@{ # WiGig + RadioType = "802.11ad", "802.11aj", "802.11ay" + Band = "60GHz" + Channels = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, + 19, 20, 21, 22, 25, 26, 27, 29, 28, 33, 34, 35, 36, 37, 38, 39, 40 + } + [PSCustomObject]@{ + RadioType = "802.11ah" + Band = "900MHz" + Channels = 1, 2, 3, 5, 6, 11, 13, 26 + } + ) | Where-Object { + $_.RadioType -contains $RadioType -and $_.Channels -contains $Channel + } | Select-Object -ExpandProperty Band + } + + function Get-WifiAdapters { + $NetShOutput = $(netsh.exe wlan show interfaces) + $IsNext = $false + $IsLast = $false + foreach ($Line in $NetShOutput) { + switch -regex ($Line) { + "^\s{4}Name\s{1,24}\s:\s(.*)" { + $Name = $Matches[1] + $IsNext = $true + } + default { + if ($IsNext) { + if ($Line -eq "") { + $IsNext = $false + $Band = Get-WifiBand -RadioType $RadioType -Channel $Channel + if ($null -eq $Band) { $Band = "Unknown" } + [PSCustomObject]@{ + Interface = $Name + SSID = $Ssid + Authentication = $Authentication + Band = $($Band) -join ', ' + Channel = $Channel + Signal = $Signal + State = $State + RadioType = $RadioType + } + $IsLast = $false + } + else { + switch -regex ($Line) { + "^\s{4}BSSID\s{1,24}\s:\s(.*)" { + if ($IsLast) { + $Band = Get-WifiBand -RadioType $RadioType -Channel $Channel + if ($null -eq $Band) { $Band = "Unknown" } + [PSCustomObject]@{ + Interface = $Name + SSID = $Ssid + Authentication = $Authentication + Band = $($Band) -join ', ' + Channel = $Channel + Signal = $Signal + State = $State + RadioType = $RadioType + } + $IsLast = $false + } + } + "^\s{4}Description\s{1,24}\s:\s(.*)" { $Description = $Matches[1] } + "^\s{4}SSID\s{1,24}\s:\s(.*)" { $Ssid = $Matches[1] } + "^\s{4}State\s{1,24}\s:\s(.*)" { $State = $Matches[1] } + "^\s{4}Signal\s{1,24}\s:\s(.*)" { $Signal = $Matches[1] } + "^\s{4}Radio type\s{1,24}\s:\s(.*)" { $RadioType = $Matches[1] } + "^\s{4}Channel\s{1,24}\s:\s(.*)" { $Channel = $Matches[1] } + "^\s{4}Authentication\s{1,24}\s:\s(.*)" { $Authentication = $Matches[1] } + "^\s{4}Profile\s{1,24}\s:\s(.*)" { $IsLast = $true } + } + } + } + } + } + } + } + + function Get-WifiAPs { + $SsidRegex = "^SSID\s[0-9]{1,4}\s:\s(.*)" + $NetShOutput = $(netsh.exe wlan show networks mode=bssid) + $IsNext = $false + $IsLast = $false + foreach ($Line in $NetShOutput) { + switch -regex ($Line) { + $SsidRegex { + $Name = [regex]::Match($Line, $SsidRegex).Captures.Groups[1].Value + $IsNext = $true + } + default { + if ($IsNext) { + if ($Line -eq "") { + $IsNext = $false + $Band = Get-WifiBand -RadioType $RadioType -Channel $Channel + if ($null -eq $Band) { $Band = "Unknown" } + [PSCustomObject]@{ + SSID = $Name + Authentication = $Authentication + Band = $($Band) -join ', ' + Channel = $Channel + Signal = $Signal + RadioType = $RadioType + } + $IsLast = $false + } + else { + switch -regex ($Line) { + "^\s{4}Authentication\s{1,24}\s:\s(.*)" { $Authentication = $Matches[1] } + "^\s{4}BSSID\s{1,24}\s:\s(.*)" { + if ($IsLast) { + $Band = Get-WifiBand -RadioType $RadioType -Channel $Channel + if ($null -eq $Band) { $Band = "Unknown" } + [PSCustomObject]@{ + SSID = $Name + Authentication = $Authentication + Band = $($Band) -join ', ' + Channel = $Channel + Signal = $Signal + RadioType = $RadioType + } + $IsLast = $false + } + } + "^\s{9}SSID\s{1,24}\s:\s(.*)" { $Name = $Matches[1] } + "^\s{9}Signal\s{1,24}\s:\s(.*)" { $Signal = $Matches[1] } + "^\s{9}Radio type\s{1,24}\s:\s(.*)" { $RadioType = $Matches[1] } + "^\s{9}Channel\s{1,24}\s:\s(.*)" { $Channel = $Matches[1]; $IsLast = $true } + } + } + } + } + } + } + } + + function Get-WifiRadioStatus { + $NetShOutput = $(netsh.exe wlan show interfaces) + $RadioStatus = [PSCustomObject]@{ + Hardware = "Off" + Software = "Off" + } + if ($NetShOutput -imatch " connected") { + # If we are connected to a AP then hardware and software radio status are On + $RadioStatus.Hardware = "On" + $RadioStatus.Software = "On" + return $RadioStatus + } + foreach ($Line in $NetShOutput) { + switch -regex ($Line) { + "Hardware\s(.*)" { $RadioStatus.Hardware = $Matches[1] } + "Software\s(.*)" { $RadioStatus.Software = $Matches[1] } + } + } + return $RadioStatus + } + + function Test-WifiRadioStatus { + $RadioStatus = Get-WifiRadioStatus + if ($RadioStatus.Hardware -eq "On" -and $RadioStatus.Software -eq "On") { + return $true + } + else { + return $false + } + } + # function Set-NinjaProperty { # Removed NinjaOne dependency + # [CmdletBinding()] # Removed NinjaOne dependency + # Param( # Removed NinjaOne dependency + # [Parameter(Mandatory = $True)] # Removed NinjaOne dependency + # [String]$Name, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$Type, # Removed NinjaOne dependency + # [Parameter(Mandatory = $True, ValueFromPipeline = $True)] # Removed NinjaOne dependency + # $Value, # Removed NinjaOne dependency + # [Parameter()] # Removed NinjaOne dependency + # [String]$DocumentName # Removed NinjaOne dependency + # ) # Removed NinjaOne dependency + # $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters # Removed NinjaOne dependency + # if ($Characters -ge 10000) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # # If we're requested to set the field value for a Ninja document we'll specify it here. # Removed NinjaOne dependency + # $DocumentationParams = @{} # Removed NinjaOne dependency + # if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Removed NinjaOne dependency + # # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. # Removed NinjaOne dependency + # $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Removed NinjaOne dependency + # if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Removed NinjaOne dependency + # # The field below requires additional information to be set # Removed NinjaOne dependency + # $NeedsOptions = "Dropdown" # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # if ($NeedsOptions -contains $Type) { # Removed NinjaOne dependency + # $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # # If an error is received it will have an exception property, the function will exit with that error information. # Removed NinjaOne dependency + # if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Removed NinjaOne dependency + # # The below types require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. # Removed NinjaOne dependency + # switch ($Type) { # Removed NinjaOne dependency + # "Checkbox" { # Removed NinjaOne dependency + # # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. # Removed NinjaOne dependency + # $NinjaValue = [System.Convert]::ToBoolean($Value) # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Date or Date Time" { # Removed NinjaOne dependency + # # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. # Removed NinjaOne dependency + # $Date = (Get-Date $Value).ToUniversalTime() # Removed NinjaOne dependency + # $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date # Removed NinjaOne dependency + # $NinjaValue = $TimeSpan.TotalSeconds # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # "Dropdown" { # Removed NinjaOne dependency + # # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. # Removed NinjaOne dependency + # $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" # Removed NinjaOne dependency + # $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Removed NinjaOne dependency + # if (-not $Selection) { # Removed NinjaOne dependency + # throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # $NinjaValue = $Selection # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # default { # Removed NinjaOne dependency + # # All the other types shouldn't require additional work on the input. # Removed NinjaOne dependency + # $NinjaValue = $Value # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # # We'll need to set the field differently depending on if its a field in a Ninja Document or not. # Removed NinjaOne dependency + # if ($DocumentName) { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # else { # Removed NinjaOne dependency + # $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # if ($CustomField.Exception) { # Removed NinjaOne dependency + # throw $CustomField # Removed NinjaOne dependency + # } # Removed NinjaOne dependency + # } # Removed NinjaOne dependency +} + +process { + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $CustomField = $env:wysiwygCustomFieldName } + + if (Test-WifiRadioStatus) { + Write-Host "[Info] Wifi Radio is On" + } + else { + $RadioStatus = Get-WifiRadioStatus + Write-Host "[Info] Wi-Fi Radio is $($RadioStatus.Hardware) in Hardware" + Write-Host "[Info] Wi-Fi Radio is $($RadioStatus.Software) in Software" + Write-Host "[Warn] Wi-Fi Radio is Off" + } + + # Get Wifi Adapters + $WifiAdapters = Get-WifiAdapters + + # Get Wifi Access Points + $AccessPointList = Get-WifiAPs + + # Build the report + $Report = "

Wifi Report

" + + $Report += "

Wifi Adapters

" + if ($WifiAdapters) { + $Report += $WifiAdapters | ConvertTo-Html -Fragment | Out-String + } + else { + $Report += "

No Wifi Adapters Found

" + } + + $Report += "

Other Wifi Networks

" + if ($AccessPointList) { + $Report += $AccessPointList | ConvertTo-Html -Fragment | Out-String + } + else { + $Report += "

No Other Wifi Networks Found

" + } + + Write-Host "--- Wifi Report ---" + Write-Host "" + Write-Host "### Wifi Adapters ###" + $WifiAdapters | Format-Table -AutoSize | Out-String -Width 4000 | Write-Host + + Write-Host "### Other Wifi Networks ###" + $AccessPointList | + Select-Object -Property SSID, Authentication, Band, Channel, Signal | + Format-Table -AutoSize | Out-String -Width 4000 | Write-Host + + if ($DebugHtml) { + $Report | Out-String | Write-Host + } + + if ($Report) { + # Save report to multi-line custom field + if ($CustomField) { + try { + # Set the custom field with the generated report + Write-Host "[Info] Attempting to set Custom Field '$CustomField'." + # Set-NinjaProperty -Name $CustomField -Value $($Report | Out-String) # Removed NinjaOne dependency + Write-Host "[Info] Successfully set Custom Field '$CustomField'!" + } + catch { + Write-Host "[Warn] $($_.Exception.Message)" + } + } + } + else { + Write-Host "Could not generate wlan report." + exit 1 + } + +} +end { + + + +} + diff --git a/Powershell Scripts/Windows Update Diagnostic.ps1 b/Powershell Scripts/Windows Update Diagnostic.ps1 index bfd15ea..006c85b 100644 --- a/Powershell Scripts/Windows Update Diagnostic.ps1 +++ b/Powershell Scripts/Windows Update Diagnostic.ps1 @@ -1,291 +1,291 @@ # Diagnose Windows Update issues. -#Requires -Version 5.1 - -<# -.SYNOPSIS - Diagnose Windows Update issues. -.DESCRIPTION - Checks that CryptSvc, and bits or running or not - Checks that wuauserv is running and the startup type is set correctly. - Checks WaaSMedic plugins doesn't have issues. (Only applies to OS Build Version is greater than 17600). - Checks if NTP is setup. - Checks Windows Update logs for any errors in the last week. - -.EXAMPLE - (No Parameters) - ## EXAMPLE OUTPUT WITHOUT PARAMS ## - [Info] Last checked for updates on 4/29/2023 - [Issue] Windows Update has not checked for updates in over 30 days. - -PARAMETER: -ResultsCustomField WindowsUpdate - Saves results to a multi-line custom field. -.EXAMPLE - -ResultsCustomField WindowsUpdate - ## EXAMPLE OUTPUT WITH ResultsCustomField ## - [Info] Last checked for updates on 4/29/2023 - [Issue] Windows Update has not checked for updates in over 90 days. -.OUTPUTS - None -.NOTES - Minimum OS Architecture Supported: Windows 10, Windows Server 2016 - Release Notes: Initial Release -#> - -[CmdletBinding()] -param ( - [int]$Days = 30, - [string]$ResultsCustomField -) - -begin { - if ($env:Days) { - $Days = $env:Days - } - if ($env:resultscustomfield -notlike "null") { - $ResultsCustomField = $env:resultscustomfield - } - function Test-IsElevated { - $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object System.Security.Principal.WindowsPrincipal($id) - $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - function Test-WaaSMedic { - [CmdletBinding()] - param() - $WaaS = 0 - Try { - $WaaS = New-Object -ComObject "Microsoft.WaaSMedic.1" - } - Catch { - Write-Host "WaaS Medic Support: No" - } - - Try { - if ($WaaS -ne 0) { - Write-Host "WaaS Medic Support: Yes" - $Plugins = $WaaS.LaunchDetectionOnly("Troubleshooter") - - if ($Plugins -eq "") { - [PSCustomObject]@{ - Id = "WaaSMedic" - Detected = $false - Parameter = @{"error" = $Plugins } - } - } - else { - [PSCustomObject]@{ - Id = "WaaSMedic" - Detected = $true - Parameter = @{"error" = $Plugins } - } - "Plugins that might have errors: " + $Plugins | Out-String | Write-Host - } - } - } - Catch { - Write-Host "WaaS Medic Detection: Failed" - } - Finally { - # Release COM Object if we aren't running test cases - if (-not $env:NinjaPesterTesting) { - [System.Runtime.Interopservices.Marshal]::ReleaseComObject($WaaS) | Out-Null - } - } - } - function Get-TimeSyncType { - [string]$result = "" - [string]$registryKey = "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Parameters" - [string]$registryKeyName = "Type" - - if ((Test-Path $registryKey -ErrorAction SilentlyContinue)) { - $registryEntry = Get-Item -Path $registryKey -ErrorAction SilentlyContinue - if ($null -ne $registryEntry) { - return Get-ItemPropertyValue -Path $registryKey -Name $registryKeyName - } - } - return $result - } - function Test-ConnectedToInternet { - $NLMType = [Type]::GetTypeFromCLSID('DCB00C01-570F-4A9B-8D69-199FDBA5723B') - $INetworkListManager = [Activator]::CreateInstance($NLMType) - return ($INetworkListManager.IsConnectedToInternet -eq $true) - } - function Get-ComponentAndErrorCode([string]$msg) { - $Codes = [regex]::matches($msg, "0x[a-f0-9a-f0-9A-F0-9A-F0-9]{6,8}") - if ($Codes.count -gt 1) { - $CodeList = "" - # there can be more than one error code can be returned for the same component at once - foreach ($Code in $Codes) { - $CodeList += "_" + $Code - } - return $CodeList - } - else { - return $Codes[0].Value - } - } - function Get-DatedEvents($EventLog) { - $DatedEvents = @() - if ($null -eq $EventLog) { - return $null - } - foreach ($Event in $EventLog) { - #$eventMsg = $event.Message - $DatedEvents += $Event.Message - } - return $DatedEvents - } - function Get-SystemEvents($EventSrc, $Time) { - $Events = Get-WinEvent -ProviderName $EventsSrc -ErrorAction 0 | Where-Object { ($_.LevelDisplayName -ne "Information") -and (($_.Id -eq 20) -or ($_.Id -eq 25)) -and ($_.TimeCreated -gt $Time) } - return $Events - } - function Get-HasWinUpdateErrorInLastWeek([switch]$AllLastWeekError) { - $Events = @() - $EventsSrc = "Microsoft-Windows-WindowsUpdateClient" - $startTime = (Get-Date) - (New-TimeSpan -Day 8) - $wuEvents = Get-SystemEvents $EventsSrc $startTime - if ($null -eq $wuEvents) { - return $null - } - $Events += Get-DatedEvents $wuEvents - $LatestError = Get-ComponentAndErrorCode $Events[0] - $ErrorList = @{} - $ErrorList.add("latest", $LatestError) - if ($AllLastWeekError) { - foreach ($str in $Events) { - $ECode = Get-ComponentAndErrorCode $str - if ($null -ne $ECode -and !$ErrorList.ContainsValue($ECode)) { - $ErrorList.add($ECode, $ECode) - } - } - } - return $ErrorList - } - Function Get-LocalTime($UTCTime) { - $strCurrentTimeZone = (Get-CimInstance -ClassName Win32_TimeZone).StandardName - # If running test cases return current date - if ($env:NinjaPesterTesting) { - return Get-Date - } - $TZ = [System.TimeZoneInfo]::FindSystemTimeZoneById($strCurrentTimeZone) - Return [System.TimeZoneInfo]::ConvertTimeFromUtc($UTCTime, $TZ) - } - $IssuesFound = $false - $Log = [System.Collections.Generic.List[String]]::new() -} -process { - if (-not (Test-IsElevated)) { - Write-Error -Message "Access Denied. Please run with Administrator privileges." - exit 1 - } - - if (-not $(Test-ConnectedToInternet)) { - Write-Host "[Issue] Windows doesn't think it is connected to Internet." - $IssuesFound = $true - } - - # Check CryptSvc amd bits services - $Service = Get-Service -Name CryptSvc - if ($Service.StartType -notlike 'Automatic') { - Write-Host "[Issue] (CryptSvc) CryptSvc service is set to $($Service.StartType) but needs to be set to Automatic" - $Log.Add("[Issue] (CryptSvc) CryptSvc service is set to $($Service.StartType) but needs to be set to Automatic") - $IssuesFound = $true - } - else { - Write-Host "[Info] (CryptSvc) CryptSvc service is set to $($Service.StartType)" - $Log.Add("[Info] (CryptSvc) CryptSvc service is set to $($Service.StartType)") - } - - $Service = Get-Service -Name bits - if ($Service.StartType -eq 'Disabled') { - Write-Host "[Issue] (bits) BITS service is set to $($Service.StartType) but needs to be set to Manual" - $Log.Add("[Issue] (bits) BITS service is set to $($Service.StartType) but needs to be set to Manual") - $IssuesFound = $true - } - else { - Write-Host "[Info] (bits) BITS service is set to $($Service.StartType)" - $Log.Add("[Info] (bits) BITS service is set to $($Service.StartType)") - } - - # Check that Windows Update service is running and isn't disabled - $wuService = Get-Service -Name wuauserv -ErrorAction SilentlyContinue - if ($wuService.Status -ne "Running") { - $Service = Get-Service -Name wuauserv - if ($Service.StartType -eq 'Disabled') { - Write-Host "[Issue] (wuauserv) Windows Update service is set to $($Service.StartType) but needs to be set to Automatic (Trigger Start) or Manual" - $Log.Add("[Issue] (wuauserv) Windows Update service is set to $($Service.StartType) but needs to be set to Automatic (Trigger Start) or Manual") - $IssuesFound = $true - } - else { - Write-Host "[Info] (wuauserv) Windows Update service is set to $($Service.StartType)" - $Log.Add("[Info] (wuauserv) Windows Update service is set to $($Service.StartType)") - } - } - - # Check WaaSMedic - $SupportWaaSMedic = [System.Environment]::OSVersion.Version.Build -gt 17600 - if ($SupportWaaSMedic) { - $Plugins = Test-WaaSMedic - $PluginIssues = $Plugins | Where-Object { $_.Parameter["error"] } | ForEach-Object { - $PluginErrors = $_.Parameter["error"] - "[Potential Issue] WaaSMedic plugin errors found with: $($PluginErrors)" - } - if ($PluginIssues.Count -gt 1) { - Write-Host "[Issue] Found more than 1 plugin errors." - $Log.Add("[Issue] Found more than 1 plugin errors.") - $PluginIssues | Write-Host - $IssuesFound = $true - } - } - - # Check if NTP is setup - if ("NoSync" -eq (Get-TimeSyncType)) { - Write-Host "[Issue] NTP not setup!" - $Log.Add("[Issue] NTP not setup!") - $IssuesFound = $true - } - - # Check Windows Update logs - $EventErrors = Get-HasWinUpdateErrorInLastWeek -AllLastWeekError - if ($EventErrors.Count -gt 0) { - if (![string]::IsNullOrEmpty($allError.Values)) { - Write-Host "[Issue] Event Log has Windows Update errors." - $Log.Add("[Issue] Event Log has Windows Update errors.") - $errorCodes = $allError.Values -join ';' - Write-Host "[Issue] Error codes found: $errorCodes" - $Log.Add("[Issue] Error codes found: $errorCodes") - $IssuesFound = $true - } - } - - # If no issues found, get number of days since the last check for updates happened - if (-not $IssuesFound) { - $LastCheck = Get-LocalTime $(New-Object -ComObject Microsoft.Update.AutoUpdate).Results.LastSearchSuccessDate - - Write-Host "[Info] Last checked for updates on $($LastCheck.ToShortDateString())" - $Log.Add("[Info] Last checked for updates on $($LastCheck.ToShortDateString())") - - $LastCheckTimeSpan = New-TimeSpan -Start $LastCheck -End $(Get-Date) - if ($LastCheckTimeSpan.TotalDays -gt $Days) { - $Days = [System.Math]::Round($LastCheckTimeSpan.TotalDays, 0) - Write-Host "[Issue] Windows Update has not checked for updates in over $Days days." - $Log.Add("[Issue] Windows Update has not checked for updates in over $Days days.") - $IssuesFound = $true - } - } - - if ($ResultsCustomField) { - Ninja-Property-Set -Name $ResultsCustomField -Value $($Log | Out-String) - } - - if ($IssuesFound) { - exit 1 - } - exit 0 -} -end { - - - -} - +#Requires -Version 5.1 + +<# +.SYNOPSIS + Diagnose Windows Update issues. +.DESCRIPTION + Checks that CryptSvc, and bits or running or not + Checks that wuauserv is running and the startup type is set correctly. + Checks WaaSMedic plugins doesn't have issues. (Only applies to OS Build Version is greater than 17600). + Checks if NTP is setup. + Checks Windows Update logs for any errors in the last week. + +.EXAMPLE + (No Parameters) + ## EXAMPLE OUTPUT WITHOUT PARAMS ## + [Info] Last checked for updates on 4/29/2023 + [Issue] Windows Update has not checked for updates in over 30 days. + +PARAMETER: -ResultsCustomField WindowsUpdate + Saves results to a multi-line custom field. +.EXAMPLE + -ResultsCustomField WindowsUpdate + ## EXAMPLE OUTPUT WITH ResultsCustomField ## + [Info] Last checked for updates on 4/29/2023 + [Issue] Windows Update has not checked for updates in over 90 days. +.OUTPUTS + None +.NOTES + Minimum OS Architecture Supported: Windows 10, Windows Server 2016 + Release Notes: Initial Release +#> + +[CmdletBinding()] +param ( + [int]$Days = 30, + [string]$ResultsCustomField +) + +begin { + if ($env:Days) { + $Days = $env:Days + } + if ($env:resultscustomfield -notlike "null") { + $ResultsCustomField = $env:resultscustomfield + } + function Test-IsElevated { + $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object System.Security.Principal.WindowsPrincipal($id) + $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } + function Test-WaaSMedic { + [CmdletBinding()] + param() + $WaaS = 0 + Try { + $WaaS = New-Object -ComObject "Microsoft.WaaSMedic.1" + } + Catch { + Write-Host "WaaS Medic Support: No" + } + + Try { + if ($WaaS -ne 0) { + Write-Host "WaaS Medic Support: Yes" + $Plugins = $WaaS.LaunchDetectionOnly("Troubleshooter") + + if ($Plugins -eq "") { + [PSCustomObject]@{ + Id = "WaaSMedic" + Detected = $false + Parameter = @{"error" = $Plugins } + } + } + else { + [PSCustomObject]@{ + Id = "WaaSMedic" + Detected = $true + Parameter = @{"error" = $Plugins } + } + "Plugins that might have errors: " + $Plugins | Out-String | Write-Host + } + } + } + Catch { + Write-Host "WaaS Medic Detection: Failed" + } + Finally { + # Release COM Object if we aren't running test cases + if (-not $env:NinjaPesterTesting) { + [System.Runtime.Interopservices.Marshal]::ReleaseComObject($WaaS) | Out-Null + } + } + } + function Get-TimeSyncType { + [string]$result = "" + [string]$registryKey = "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Parameters" + [string]$registryKeyName = "Type" + + if ((Test-Path $registryKey -ErrorAction SilentlyContinue)) { + $registryEntry = Get-Item -Path $registryKey -ErrorAction SilentlyContinue + if ($null -ne $registryEntry) { + return Get-ItemPropertyValue -Path $registryKey -Name $registryKeyName + } + } + return $result + } + function Test-ConnectedToInternet { + $NLMType = [Type]::GetTypeFromCLSID('DCB00C01-570F-4A9B-8D69-199FDBA5723B') + $INetworkListManager = [Activator]::CreateInstance($NLMType) + return ($INetworkListManager.IsConnectedToInternet -eq $true) + } + function Get-ComponentAndErrorCode([string]$msg) { + $Codes = [regex]::matches($msg, "0x[a-f0-9a-f0-9A-F0-9A-F0-9]{6,8}") + if ($Codes.count -gt 1) { + $CodeList = "" + # there can be more than one error code can be returned for the same component at once + foreach ($Code in $Codes) { + $CodeList += "_" + $Code + } + return $CodeList + } + else { + return $Codes[0].Value + } + } + function Get-DatedEvents($EventLog) { + $DatedEvents = @() + if ($null -eq $EventLog) { + return $null + } + foreach ($Event in $EventLog) { + #$eventMsg = $event.Message + $DatedEvents += $Event.Message + } + return $DatedEvents + } + function Get-SystemEvents($EventSrc, $Time) { + $Events = Get-WinEvent -ProviderName $EventsSrc -ErrorAction 0 | Where-Object { ($_.LevelDisplayName -ne "Information") -and (($_.Id -eq 20) -or ($_.Id -eq 25)) -and ($_.TimeCreated -gt $Time) } + return $Events + } + function Get-HasWinUpdateErrorInLastWeek([switch]$AllLastWeekError) { + $Events = @() + $EventsSrc = "Microsoft-Windows-WindowsUpdateClient" + $startTime = (Get-Date) - (New-TimeSpan -Day 8) + $wuEvents = Get-SystemEvents $EventsSrc $startTime + if ($null -eq $wuEvents) { + return $null + } + $Events += Get-DatedEvents $wuEvents + $LatestError = Get-ComponentAndErrorCode $Events[0] + $ErrorList = @{} + $ErrorList.add("latest", $LatestError) + if ($AllLastWeekError) { + foreach ($str in $Events) { + $ECode = Get-ComponentAndErrorCode $str + if ($null -ne $ECode -and !$ErrorList.ContainsValue($ECode)) { + $ErrorList.add($ECode, $ECode) + } + } + } + return $ErrorList + } + Function Get-LocalTime($UTCTime) { + $strCurrentTimeZone = (Get-CimInstance -ClassName Win32_TimeZone).StandardName + # If running test cases return current date + if ($env:NinjaPesterTesting) { + return Get-Date + } + $TZ = [System.TimeZoneInfo]::FindSystemTimeZoneById($strCurrentTimeZone) + Return [System.TimeZoneInfo]::ConvertTimeFromUtc($UTCTime, $TZ) + } + $IssuesFound = $false + $Log = [System.Collections.Generic.List[String]]::new() +} +process { + if (-not (Test-IsElevated)) { + Write-Error -Message "Access Denied. Please run with Administrator privileges." + exit 1 + } + + if (-not $(Test-ConnectedToInternet)) { + Write-Host "[Issue] Windows doesn't think it is connected to Internet." + $IssuesFound = $true + } + + # Check CryptSvc amd bits services + $Service = Get-Service -Name CryptSvc + if ($Service.StartType -notlike 'Automatic') { + Write-Host "[Issue] (CryptSvc) CryptSvc service is set to $($Service.StartType) but needs to be set to Automatic" + $Log.Add("[Issue] (CryptSvc) CryptSvc service is set to $($Service.StartType) but needs to be set to Automatic") + $IssuesFound = $true + } + else { + Write-Host "[Info] (CryptSvc) CryptSvc service is set to $($Service.StartType)" + $Log.Add("[Info] (CryptSvc) CryptSvc service is set to $($Service.StartType)") + } + + $Service = Get-Service -Name bits + if ($Service.StartType -eq 'Disabled') { + Write-Host "[Issue] (bits) BITS service is set to $($Service.StartType) but needs to be set to Manual" + $Log.Add("[Issue] (bits) BITS service is set to $($Service.StartType) but needs to be set to Manual") + $IssuesFound = $true + } + else { + Write-Host "[Info] (bits) BITS service is set to $($Service.StartType)" + $Log.Add("[Info] (bits) BITS service is set to $($Service.StartType)") + } + + # Check that Windows Update service is running and isn't disabled + $wuService = Get-Service -Name wuauserv -ErrorAction SilentlyContinue + if ($wuService.Status -ne "Running") { + $Service = Get-Service -Name wuauserv + if ($Service.StartType -eq 'Disabled') { + Write-Host "[Issue] (wuauserv) Windows Update service is set to $($Service.StartType) but needs to be set to Automatic (Trigger Start) or Manual" + $Log.Add("[Issue] (wuauserv) Windows Update service is set to $($Service.StartType) but needs to be set to Automatic (Trigger Start) or Manual") + $IssuesFound = $true + } + else { + Write-Host "[Info] (wuauserv) Windows Update service is set to $($Service.StartType)" + $Log.Add("[Info] (wuauserv) Windows Update service is set to $($Service.StartType)") + } + } + + # Check WaaSMedic + $SupportWaaSMedic = [System.Environment]::OSVersion.Version.Build -gt 17600 + if ($SupportWaaSMedic) { + $Plugins = Test-WaaSMedic + $PluginIssues = $Plugins | Where-Object { $_.Parameter["error"] } | ForEach-Object { + $PluginErrors = $_.Parameter["error"] + "[Potential Issue] WaaSMedic plugin errors found with: $($PluginErrors)" + } + if ($PluginIssues.Count -gt 1) { + Write-Host "[Issue] Found more than 1 plugin errors." + $Log.Add("[Issue] Found more than 1 plugin errors.") + $PluginIssues | Write-Host + $IssuesFound = $true + } + } + + # Check if NTP is setup + if ("NoSync" -eq (Get-TimeSyncType)) { + Write-Host "[Issue] NTP not setup!" + $Log.Add("[Issue] NTP not setup!") + $IssuesFound = $true + } + + # Check Windows Update logs + $EventErrors = Get-HasWinUpdateErrorInLastWeek -AllLastWeekError + if ($EventErrors.Count -gt 0) { + if (![string]::IsNullOrEmpty($allError.Values)) { + Write-Host "[Issue] Event Log has Windows Update errors." + $Log.Add("[Issue] Event Log has Windows Update errors.") + $errorCodes = $allError.Values -join ';' + Write-Host "[Issue] Error codes found: $errorCodes" + $Log.Add("[Issue] Error codes found: $errorCodes") + $IssuesFound = $true + } + } + + # If no issues found, get number of days since the last check for updates happened + if (-not $IssuesFound) { + $LastCheck = Get-LocalTime $(New-Object -ComObject Microsoft.Update.AutoUpdate).Results.LastSearchSuccessDate + + Write-Host "[Info] Last checked for updates on $($LastCheck.ToShortDateString())" + $Log.Add("[Info] Last checked for updates on $($LastCheck.ToShortDateString())") + + $LastCheckTimeSpan = New-TimeSpan -Start $LastCheck -End $(Get-Date) + if ($LastCheckTimeSpan.TotalDays -gt $Days) { + $Days = [System.Math]::Round($LastCheckTimeSpan.TotalDays, 0) + Write-Host "[Issue] Windows Update has not checked for updates in over $Days days." + $Log.Add("[Issue] Windows Update has not checked for updates in over $Days days.") + $IssuesFound = $true + } + } + + if ($ResultsCustomField) { + # Ninja-Property-Set -Name $ResultsCustomField -Value $($Log | Out-String) # Removed NinjaOne dependency + } + + if ($IssuesFound) { + exit 1 + } + exit 0 +} +end { + + + +} +
*>$($Status)<* - $_ -like "
*>$($Status)<*" - } | ForEach-Object { - # Escape the line for regex - $LineEscaped = [regex]::Escape($_) - if ($_ -like "*$Status*") { - # Replace the line with the line and add a class of 'success' - $htmlTable = $htmlTable -replace $LineEscaped, $($_ -replace "
*>$($Status)<* - $_ -like "
*>$($Status)<*" - } | ForEach-Object { - # Escape the line for regex - $LineEscaped = [regex]::Escape($_) - if ($_ -like "*$Status*") { - # Replace the line with the line and add a class of 'danger' - $htmlTable = $htmlTable -replace $LineEscaped, $($_ -replace "
*>$($Status)<* - $_ -like "
*>$($Status)<*" - } | ForEach-Object { - # Escape the line for regex - $LineEscaped = [regex]::Escape($_) - if ($_ -like "*$Status*") { - # Replace the line with the line and add a class of 'other' - $htmlTable = $htmlTable -replace $LineEscaped, $($_ -replace "
*>$($Status)<* + $_ -like "
*>$($Status)<*" + } | ForEach-Object { + # Escape the line for regex + $LineEscaped = [regex]::Escape($_) + if ($_ -like "*$Status*") { + # Replace the line with the line and add a class of 'success' + $htmlTable = $htmlTable -replace $LineEscaped, $($_ -replace "
*>$($Status)<* + $_ -like "
*>$($Status)<*" + } | ForEach-Object { + # Escape the line for regex + $LineEscaped = [regex]::Escape($_) + if ($_ -like "*$Status*") { + # Replace the line with the line and add a class of 'danger' + $htmlTable = $htmlTable -replace $LineEscaped, $($_ -replace "
*>$($Status)<* + $_ -like "
*>$($Status)<*" + } | ForEach-Object { + # Escape the line for regex + $LineEscaped = [regex]::Escape($_) + if ($_ -like "*$Status*") { + # Replace the line with the line and add a class of 'other' + $htmlTable = $htmlTable -replace $LineEscaped, $($_ -replace "