Compare commits

..
146 Commits
Author SHA1 Message Date
dyoder 819b8d72f0 added Upload-File.ps1 2026-10-01 11:16:36 -04:00
dyoder a53dcfb6a9 added boolean conversion helper and replaced code in scripts to use it 2026-10-01 11:16:23 -04:00
dyoder 2efb7116d8 removed, not necessary 2026-10-01 11:14:25 -04:00
dyoder 3b8ceeac5a added RMM shim script to repo 2026-10-01 09:39:44 -04:00
dyoder 4912bbc28e change from domain so email notifications pass through relay after successful upload 2026-10-01 09:36:23 -04:00
dyoder 0975f5d24a implement new Download-FileDirectly function to handle downloads 2026-09-30 11:42:05 -04:00
dyoder 4e06ede640 added Test-FileDownload.ps1 2026-09-30 10:34:09 -04:00
dyoder 4e6c35a8f4 stop on BITS download errors
reject 0 byte dsa-collector.exe before launching
2026-09-30 09:46:37 -04:00
dyoder 35c1904d0d add upload feature to DSA manifest creation 2026-09-30 08:12:55 -04:00
dyoder c74fe82d36 add Upload-File function to Tools.ps1 2026-09-30 07:45:01 -04:00
dyoder 745902f8c2 accept non-boolean string values for the boolean $ZIP
append hostname to output file
2026-09-30 07:20:37 -04:00
dyoder bb44f2a9ef added Get-DSAManifest.ps1 2026-09-30 07:00:20 -04:00
dyoder a47be2f91b change hosting location for ODT setup file 2026-09-21 21:59:47 -04:00
dyoder 7a25ff2d6d update tool hosting location 2026-09-21 15:05:32 -04:00
dyoder 164dc1ac58 update to version 8.7.1.177 2026-09-16 12:37:02 -04:00
dyoder 1732f23d37 update to version 8.7.0.224 2026-08-26 17:24:29 -04:00
dyoder af1ccf5fa1 added Install-SystemFont 2026-07-24 14:43:37 -04:00
dyoder 1b68d12ab3 revert test commit after gitea upgrade 2026-07-16 17:44:17 -04:00
dyoder f5e12804be test commit after gitea upgrade 2026-07-16 17:43:45 -04:00
dyoder 912bff8d81 added context for agents 2026-07-16 10:42:41 -04:00
dyoder 8a53de4430 update and harden 2026-07-16 10:42:30 -04:00
dyoder 1e5739b75d replace bottom table with list of unlocked times 2026-07-08 16:51:52 -04:00
dyoder db8fc541df more clarity 2026-07-08 16:44:12 -04:00
dyoder faddaba0ed improved clarity 2026-07-08 12:20:45 -04:00
dyoder 8ae07c0030 more fixes 2026-07-08 12:13:48 -04:00
dyoder 4a7e0642ef more fixes 2026-07-08 12:00:25 -04:00
dyoder 3470464b1d fix an error 2026-07-08 11:56:10 -04:00
dyoder cf40e50c30 behavioral changes 2026-07-08 11:36:27 -04:00
dyoder ac85a06003 add usage window 2026-07-08 11:27:51 -04:00
dyoder cce2950d10 add additional indicators 2026-07-08 11:04:22 -04:00
dyoder 41260f438b added source column 2026-07-07 17:30:11 -04:00
dyoder 1e10306b61 add power events 2026-07-07 15:39:57 -04:00
dyoder 631ce956fd fixed stability and output 2026-07-07 15:23:15 -04:00
dyoder 06b66813cf fix: check both target and subject fields, 4624 type 7 is now an unlock event, 4634 now allows type 7 for logoff/session change, added security log access check 2026-07-07 15:06:27 -04:00
dyoder 5fa2d50bd6 added default run mode when variables are missing or not defined 2026-07-07 14:56:56 -04:00
dyoder 26191c9e58 added Get-UserLogonActivity.ps1 2026-07-07 14:45:18 -04:00
dyoder 91c3bc858d added Create-NetworkDrive 2026-06-26 10:35:40 -04:00
dyoder 9d36fabf00 fix: improve MSP360 module import and installation process 2026-05-29 11:24:53 -04:00
dyoder d2824cb771 changed URL because git.io is commonly blocked 2026-05-18 16:38:50 -04:00
dyoder 30fe5e32d6 update to version 8.6.0.338 2026-05-18 16:30:38 -04:00
dyoder e7fe443894 update to version 2.3.0 2026-03-17 13:19:24 -04:00
dyoder f446086e38 upgrade to version 3.0.37 2026-03-17 11:53:30 -04:00
dyoder 30c68220f2 fix: correct path for deleting old version of ekDNSHelper 2026-03-04 23:16:04 -05:00
dyoder a17848a5eb fix: improve error handling for ekDNSHelper version check 2026-03-04 23:12:43 -05:00
dyoder 930c9eca73 fix: update required version of ekDNSHelper to 2.2.0 2026-03-04 23:00:20 -05:00
dyoder cbee4335f3 fix: improve syntax for Test-Path and Remove-Item in ekDNSHelper deletion logic 2026-03-04 22:55:16 -05:00
dyoder 751af98c6e fix: correct syntax for Remove-Item path in ekDNSHelper deletion logic 2026-03-04 22:51:45 -05:00
dyoder 7a74d6d2a9 fix: enhance logic to check and download the correct version of ekDNSHelper 2026-03-04 22:49:05 -05:00
dyoder 79290125f4 fix: add logic to delete old version of ekDNSHelper if it exists 2026-03-04 18:14:43 -05:00
dyoder ac870b4e82 fix: correct syntax for Test-Path condition in DNS helper URL selection 2026-03-04 18:11:28 -05:00
dyoder a9ff804048 fix: update DNS helper executable references and remove unused resolver logic 2026-03-04 17:59:33 -05:00
dyoder 330501c551 Update to version 8.3.0.85 2026-02-13 14:14:52 -05:00
dyoder b55d302311 fix: rename 'toc' key to 'table_of_contents' for clarity 2026-01-14 13:55:07 -05:00
dyoder 3b4532529b update to version 8.2.0.122 2025-12-16 13:06:21 -05:00
dyoder 253b714fe2 fix: update profile removal logic to use SID variable directly 2025-10-08 18:07:07 -04:00
dyoder c6d587db8e refactor removal process for provisioning admin account and profile 2025-10-08 18:05:59 -04:00
dyoder 5da50abc5a update provisioning admin username to "ek-provadmin" and add domain controller check 2025-10-08 17:44:17 -04:00
dyoder 1c50712eb0 add script to remove provisioning admin user and associated profile 2025-10-08 17:27:07 -04:00
dyoder c6bd2e7c49 update provisioning admin username to match project standards 2025-10-08 17:22:12 -04:00
dyoder 04d714cc5b fix error handling in Set-DefaultAdministratorCredentials script 2025-10-08 17:19:41 -04:00
dyoder 31312fb510 update provisioning admin username to match standard naming convention 2025-10-08 17:18:06 -04:00
dyoder f2b6a44169 add script to remove provisioning admin user and associated profile 2025-10-08 16:14:29 -04:00
dyoder 1c582e7b0d refactor New-LocalAccount function to accept plain text password and hide user from login screen 2025-10-08 16:12:56 -04:00
dyoder baa6f76e1a clean exit on successful install 2025-10-08 11:40:21 -04:00
dyoder a378cf2cc0 initial commit 2025-10-03 14:32:21 -04:00
dyoder 93f916e48a update the script to check for admin, not enforce 2025-09-24 14:57:48 -04:00
dyoder 6abcc7a729 hopefully the last minor wording update 2025-09-24 14:37:34 -04:00
dyoder 9781af8745 another minor wording update 2025-09-24 14:35:40 -04:00
dyoder d6653c4554 minor wording change 2025-09-24 14:34:36 -04:00
dyoder d4acdaae9e minor 2025-09-24 14:30:31 -04:00
dyoder 1aab6b3755 initial commit 2025-09-24 14:25:29 -04:00
dyoder 227f940ca1 remove duplicated functions from Tools.ps1 2025-08-15 16:02:45 -04:00
dyoder 821505014d cleanup recycle bin cleaner 2025-08-15 16:00:11 -04:00
dyoder 3f89290295 fix old downloaded apps cleaner 2025-08-15 15:57:59 -04:00
dyoder 432133775a stop shadow copy cleaner from showing error on 0 shadows 2025-08-15 15:55:11 -04:00
dyoder 49b0f25369 source Tools.ps1 again 2025-08-15 15:48:50 -04:00
dyoder c3a89f73bb fix cleaners for recycle bin and autodesk installers 2025-08-15 15:48:16 -04:00
dyoder 1036cca39a update to version 8.1.2.172 2025-08-13 16:15:28 -04:00
dyoder 92c75d8f22 update to version 8.1.0.630 2025-05-22 12:47:02 -04:00
dyoder 7193b6e596 update to version 8.1.0.607 2025-05-05 16:25:27 -04:00
dyoder 0fededf036 use cmdlet name instead of alias 2025-04-11 17:22:26 -04:00
dyoder 910cc65444 match 2025-03-22 08:20:13 -04:00
dyoder b3ebdd4434 temporarily remove installation of local tools 2025-03-22 08:19:55 -04:00
dyoder 61b0c676bc update to version 8.0.4.6 2025-02-07 16:53:49 -05:00
dyoder 0cbc618d05 update to version 8.0.3.36 2025-01-22 09:44:08 -05:00
dyoder af7c4af9b6 fix several places where full path was not provided to Remove-Item 2025-01-06 13:40:15 -05:00
dyoder 65f8e2c6a1 replace Get-ItemPropertyValue with Get-ItemProperty for PS 3.0 compatibility 2025-01-06 13:17:25 -05:00
dyoder 8fca4894cd replace Get-ItemPropertyValue with Get-ItemProperty for PS 3.0 compatibility 2025-01-06 13:16:48 -05:00
dyoder 4a5043ca74 added IsRunning func from Tools.ps1 2025-01-06 12:39:06 -05:00
dyoder 8ace7c6be1 added Get-UserProfiles func from Tools.ps1 2025-01-06 12:35:23 -05:00
dyoder 288a1a4d9c don't source Tools.ps1 2025-01-06 12:32:17 -05:00
dyoder fdc0e536cc update DaysWithoutModification to DaysWithNoModification 2025-01-02 17:00:29 -05:00
dyoder 39ae5a4235 use Where-Object instead of Where
add found items to output
2025-01-02 16:52:59 -05:00
dyoder ff6d6bb88c initial commit 2025-01-02 16:27:53 -05:00
dyoder bbc6cdb142 update to version 8.0.2.41 2024-12-27 11:22:00 -05:00
dyoder 01c06ad64b removed 2024-12-06 11:12:56 -05:00
dyoder 38d0e87a2e initial commit 2024-12-06 11:12:13 -05:00
dyoder 5bc1437398 initial commit 2024-12-06 11:11:51 -05:00
dyoder cf575be0c6 updated to version 8.0.2.33 2024-12-05 09:56:52 -05:00
dyoder 20408316df adjusted comments 2024-12-05 09:56:09 -05:00
dyoder b8758fec60 initial commit 2024-12-05 09:54:48 -05:00
dyoder 7a91bce87a set default TLS version to 1.2 2024-11-06 10:07:39 -05:00
dyoder 6dc5a4efce renamed script 2024-10-22 14:34:31 -04:00
dyoder ececf83389 initial commit 2024-10-22 14:32:46 -04:00
dyoder cec26755f9 delete the correct shadow copies 2024-10-14 12:57:41 -04:00
dyoder 63792a557f match os edition instead 2024-09-19 15:05:29 -04:00
dyoder 87f7a75c9c exit early if run on a server 2024-09-19 14:50:35 -04:00
dyoder 5676dbb554 initial commit 2024-09-19 14:41:19 -04:00
dyoder 8e0e3711f7 Set-OutlookProfilePrompt 2024-08-23 11:18:44 -04:00
dyoder efea29ac95 fix casing for supplied path 2024-08-08 15:36:26 -04:00
dyoder 11317ef618 try using a literal path 2024-08-08 13:13:54 -04:00
dyoder 644cd0cd14 oops 2024-08-08 12:43:09 -04:00
dyoder b0772bcb1c cleanup the path the user enters 2024-08-08 12:41:58 -04:00
dyoder b8d9b7b9d3 don't operate on unset variables! 2024-08-06 15:00:13 -04:00
dyoder 98565b7fff apparently logging is no longer a config item in odt 2024-08-06 13:58:27 -04:00
dyoder 67d291e2eb don't need to see the UI if running as admin 2024-08-06 11:38:35 -04:00
dyoder 8d2913cd5a I don't think the strings need to be escaped 2024-08-06 11:37:05 -04:00
dyoder 4b740b06e8 initial commit 2024-08-06 11:27:46 -04:00
dyoder a447ac5652 update to v7.9.7.69 2024-08-01 14:47:43 -04:00
dyoder 298b8ef050 exit early if the BSOD report is empty 2024-07-18 15:42:41 -04:00
dyoder 2885d975f4 added func Get-StringHash 2024-07-15 13:30:29 -04:00
dyoder 659f03bf3a update to version 7.9.6.150 2024-07-05 12:21:03 -04:00
dyoder 96c3e03b89 bill ticket 2024-07-03 17:09:35 -04:00
dyoder e872cdf02a do not bill ticket 2024-07-03 17:09:29 -04:00
dyoder 6904647871 improved logic for sfc output 2024-07-03 17:00:42 -04:00
dyoder 2dd6384c57 exit if specific apps are running at the time of install 2024-07-03 16:53:29 -04:00
dyoder dbb6fefa07 cleanup chocolatey cache 2024-07-03 16:37:56 -04:00
dyoder bcf8b87dc5 update to version 4.0.2.11 2024-07-03 15:56:09 -04:00
dyoder 689cfa66df added invoking script name to local script file 2024-07-01 11:26:12 -04:00
dyoder 0127474dbe try getting the date again 2024-07-01 11:24:24 -04:00
dyoder bb682a43ee get the date to run and add a new line 2024-07-01 11:22:17 -04:00
dyoder 98c1b3e214 create script and run that instead 2024-07-01 11:19:20 -04:00
dyoder f77479c7b7 lots of cleanup 2024-06-25 16:07:23 -04:00
dyoder 68114af27e removed a few lines since the logic moved to Tools.ps1 2024-06-25 16:07:04 -04:00
dyoder f4905fec68 added a bunch of functions 2024-06-25 16:06:37 -04:00
dyoder b82f741a79 just need to test the data that gets sent to this script 2024-06-25 12:04:42 -04:00
dyoder 676dd0011c fixed incorrect global var name 2024-06-25 11:47:03 -04:00
dyoder 6c27d5fa18 source external scripts that have no arguments instead of running in scriptblock 2024-06-24 17:06:45 -04:00
dyoder c753b13f1e use new funcs in Tools.ps1 2024-06-24 16:51:50 -04:00
dyoder b5bfe4b814 use new funcs in Tools.ps1 2024-06-24 16:51:40 -04:00
dyoder 907ee882bf added funcs for working with tickets 2024-06-24 16:51:19 -04:00
dyoder cbdca2b4eb handle no alert data 2024-06-21 17:04:17 -04:00
dyoder 7986d28dfc renamed 2024-06-21 17:01:38 -04:00
dyoder 4e85dee127 import the syncro module if needed 2024-06-21 14:51:19 -04:00
dyoder b738b25b9a trim 2024-06-21 14:28:50 -04:00
dyoder ebc98e979e allow running the script adhoc to update an existing ticket 2024-06-21 14:24:37 -04:00
122 changed files with 4945 additions and 165 deletions
+21
View File
@@ -0,0 +1,21 @@
# Repository Guidance
## RMM script execution model
Scripts in this repository are generally executed by a small caller script in the RMM. The caller first downloads and dot-sources `Tools.ps1` from the public repository URL:
```powershell
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
```
The caller then invokes `Run-Script`, passing the repository script's filename without the `.ps1` extension:
```powershell
Run-Script -LivePSScript Create-BatteryReport
```
For a live PowerShell script without `-Arguments`, `Run-Script` downloads the target script and dot-sources it. This keeps the core script logic in version control, and changes pushed to the repository take effect in production without updating each RMM caller.
When a repository script needs RMM-provided input, define the variable in the RMM caller before calling `Run-Script`. Because the target is dot-sourced into the caller's scope, it can read that variable directly.
When changing `Run-Script` or scripts invoked through it, preserve this shared-scope behavior unless the change explicitly includes migrating all affected RMM callers.
@@ -0,0 +1,79 @@
# $CleanupPath must be set by RMM
# $DaysWithNoModification must be set by RMM
# Exit if the source path does not exist
If ( !(Test-Path $CleanupPath) ) { Write-Output "The specified path does not exist: ""${CleanupPath}""" ; Return }
# Running total of the size of all directories and files
$TotalSize = 0
# Running total of the number of all identified directories
$TotalDirectories = 0
# Running total of the number of all identified files
$TotalFiles = 0
$StartDate = Get-Date
$StartTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Write-Output '-------------------------------------------------------------------------------'
Write-Output "Started at : ${StartTime}"
Write-Output "Run on : ${Env:COMPUTERNAME}"
Write-Output "Source : ${CleanupPath}"
If ( $DaysWithNoModification -gt 0 ) { Write-Output "Modified : ${DaysWithNoModification} day(s) ago" }
Write-Output '-------------------------------------------------------------------------------'
# Identify all files and folders that match the criteria for what we're looking for
$TEMPORARY_REVIT_OBJECTS = Get-ChildItem -Path $CleanupPath -Recurse | Where-Object {
(
# Revit model backup folder
$_.Name -match '_backup$' -or
# Revit backup models
$_.Name -match '\.\d\d\d\d\.rvt$'
# Files and folders not recently modified
) -and ($_.LastWriteTime -lt $StartDate.AddDays(-$DaysWithNoModification))
}
# Loop thru each path to get its size and
Foreach ( $obj in $TEMPORARY_REVIT_OBJECTS ) {
# Skip the object if it no longer exists or is otherwise inaccessible
If ( !(Test-Path $obj.FullName) ) { Continue } Else { $item = $obj.FullName }
Switch ( $obj.PSIsContainer ) {
# Get the size of the directory and update counter
$true {
$size = (Get-ChildItem -Path $item -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
$TotalDirectories++
}
# Get the size of the file and update counter
$false {
$size = (Get-Item $item | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
$TotalFiles++
}
}
Write-Output $item
$TotalSize += $size
}
# Record stats
$EndTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$Duration = "{0:g}" -f (New-TimeSpan -Start $StartTime -End $EndTime)
Switch ( $TotalSize ) {
{ $_ -ge 1TB } { $GrandTotal = "{0}TB" -f ([Math]::Round(($TotalSize / 1TB),2)) ; break }
{ $_ -ge 1GB } { $GrandTotal = "{0}GB" -f ([Math]::Round(($TotalSize / 1GB),2)) ; break }
{ $_ -ge 1MB } { $GrandTotal = "{0}MB" -f ([Math]::Round(($TotalSize / 1MB),2)) ; break }
{ $_ -ge 1KB } { $GrandTotal = "{0}KB" -f ([Math]::Round(($TotalSize / 1KB),2)) ; break }
{ $_ -lt 1KB } { $GrandTotal = "${TotalSize} bytes" ; break }
}
Write-Output '-------------------------------------------------------------------------------'
Write-Output "Finished at: ${EndTime}"
Write-Output "Duration : ${Duration}"
Write-Output "Directories: ${TotalDirectories}"
Write-Output "Files : ${TotalFiles}"
Write-Output "Total size : ${GrandTotal}"
Write-Output '-------------------------------------------------------------------------------'
+36 -26
View File
@@ -39,24 +39,23 @@ If ( $CBSLogs ) {
$OldTempFiles = Get-ChildItem -Path "${Env:SystemRoot}\TEMP" | Where-Object { $_.LastWriteTime -lt ((Get-Date) - $TimeDelta) }
If ( $OldTempFiles ) {
Write-Output "Deleting all temp files not modified in ${OlderThan} day(s) from ${Env:SystemRoot}\TEMP"
ForEach ( $OldTempFile in $OldTempFiles ) { Remove-Item $OldTempFile -Force -Recurse -ErrorAction SilentlyContinue }
ForEach ( $OldTempFile in $OldTempFiles ) { Remove-Item $OldTempFile.FullName -Force -Recurse -ErrorAction SilentlyContinue }
}
# Remove all old files from the Recycle Bin
$RecycleBin = (New-Object -ComObject Shell.Application).NameSpace(0xa)
ForEach($file in $RecycleBin.Items()) {
# Remove RTL and LTR marks from date string so it can be compared
$DateDeleted = $Recycler.GetDetailsOf($item,2) -replace "\u200f|\u200e",""
# Make sure the item has expired
If( (Get-Date $DateDeleted) -lt ((Get-Date) - $TimeDelta)) {
# Delete the item
$path = $file.Path
Write-Output "Deleting expired Recycle Bin item: ""${path}"""
Try { Remove-Item -Path $path -Force -Recurse }
Catch { Write-Error $_.Exception.Message }
$UserRecycleBins = Get-ChildItem -Path "${Env:SystemDrive}\`$Recycle.Bin" -Directory -ErrorAction SilentlyContinue
If ( ($UserRecycleBins | Measure-Object).Count -gt 0 ) {
Write-Output "Deleting all recycle bin items older than ${OlderThan} days from:"
ForEach ( $userRecycleBin in $UserRecycleBins ) {
$userRecycleBinFullName = $userRecycleBin.FullName
Write-Output " - $userRecycleBinFullName"
$items = Get-ChildItem -Path $userRecycleBin.FullName -Recurse -ErrorAction SilentlyContinue
ForEach ( $item in $items ) {
If ( $item.LastWriteTime -lt $TimeDelta ) {
Try { Remove-Item -Path $item.FullName -Force -ErrorAction Continue }
Catch { Write-Error $_.Exception.Message }
}
}
}
}
@@ -71,7 +70,7 @@ If ( Test-Path "${Env:SystemDrive}\hiberfil.sys" ) {
$PDCRevocationDumpFiles = Get-ChildItem -Path "${Env:SystemRoot}\LiveKernelReports\*" -File -Include "PDCRevocation-*.dmp"
If ( $PDCRevocationDumpFiles ) {
Write-Output "Deleting PDCRevocation dump files from ${Env:SystemRoot}\LiveKernelReports"
ForEach ( $PDCRevocationDump in $PDCRevocationDumpFiles ) { Remove-Item $PDCRevocationDump -Force -ErrorAction SilentlyContinue }
ForEach ( $PDCRevocationDump in $PDCRevocationDumpFiles ) { Remove-Item $PDCRevocationDump.FullName -Force -ErrorAction SilentlyContinue }
}
# Remove Genetec application crash dumps
@@ -108,25 +107,36 @@ If ( Test-Path $ESDB ) {
# Remove any Autodesk installers at their default location
$ADSKInst = "${Env:SystemDrive}\Autodesk"
If ( Test-Path $ADSKInst ) {
$foldersToDelete = Get-ChildItem -Path $ADKSInst -ErrorAction SilentlyContinue | Where-Object { ($_.PSIsContainer) -and ($_.LastWriteTime -lt ((Get-Date) - $TimeDelta)) -and ( $_.Name -ne "WI") }
$foldersToDelete = Get-ChildItem -Path $ADSKInst -ErrorAction SilentlyContinue | Where-Object { ($_.PSIsContainer) -and ($_.LastWriteTime -lt ((Get-Date) - $TimeDelta)) -and ( $_.Name -ne "WI") }
ForEach ( $f in $foldersToDelete ) {
$path = $f.FullName
Write-Output "Deleting Autodesk installer directory: ""${path}"""
Try { Remove-Item $f -Force -Recurse -ErrorAction SilentlyContinue }
Try { Remove-Item $f.FullName -Force -Recurse -ErrorAction SilentlyContinue }
Catch { Write-Error $_.Exception.Message }
}
}
# Delete all but most recent shadow copy of system drive
$SystemVolume = Get-CimInstance -ClassName Win32_Volume -ErrorAction SilentlyContinue | Where-Object { $_.Name -like "${Env:SystemDrive}\" }
$ShadowCopies = Get-CimInstance -ClassName Win32_ShadowCopy | Where-Object { $_.VolumeName -like $SystemVolume.DeviceID }
If ( $ShadowCopies.Length -gt 1 ) {
Write-Output "Deleting all of the oldest shadow copies on ${Env:SystemDrive}"
$ShadowsToDelete = $ShadowCopies.Length - 1
$ShadowCopies = Get-CimInstance -ClassName Win32_ShadowCopy -ErrorAction SilentlyContinue | Where-Object { $_.VolumeName -like $SystemVolume.DeviceID }
If ( $ShadowCopies.Count -gt 1 ) {
Write-Output "Deleting all of the oldest shadow copies of ${Env:SystemDrive}"
$ShadowsToDelete = $ShadowCopies.Count - 1
While ( $ShadowsToDelete -gt 0 ) {
$ShadowsToDelete = $ShadowsToDelete - 1
vssadmin delete shadows /For=$Env:SystemDrive /Oldest /Quiet
}
$ShadowsToDelete = $ShadowsToDelete - 1
}
}
# Delete old temp directories from Chocolatey cache
$ChocolateyCache = "${Env:WinDir}\Temp\chocolatey"
$ChocolateyTempDirs = Get-ChildItem -Path $ChocolateyCache -Directory -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt ((Get-Date) - $TimeDelta) }
If ( $ChocolateyTempDirs ) {
Write-Output "Deleting all temp directories not modified in ${OlderThan} day(s) from ""${ChocolateyCache}"""
ForEach ( $d in $ChocolateyTempDirs ) {
Try { Remove-Item $d.FullName -Force -Recurse -ErrorAction SilentlyContinue }
Catch { Write-Error $_.Exception.Message }
}
}
# Remove files from user profile directories
@@ -136,7 +146,7 @@ ForEach ( $userProfile in $(Get-UserProfiles) ) {
$OldTempFiles = Get-ChildItem -Path "${userProfile}\AppData\Local\Temp" -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt ((Get-Date) - $TimeDelta) }
If ( $OldTempFiles ) {
Write-Output "Deleting all temp files not modified in ${OlderThan} day(s) from ""${userProfile}\AppData\Local\Temp"""
ForEach ( $OldTempFile in $OldTempFiles ) { Remove-Item $OldTempFile -Force -Recurse -ErrorAction SilentlyContinue }
ForEach ( $OldTempFile in $OldTempFiles ) { Remove-Item $OldTempFile.FullName -Force -Recurse -ErrorAction SilentlyContinue }
}
# Remove AutoCAD temp files
@@ -145,7 +155,7 @@ ForEach ( $userProfile in $(Get-UserProfiles) ) {
If ( $AutoCADTempFiles ) {
Write-Output "Deleting AutoCAD temp files"
ForEach ( $tempfile in $AutoCADTempFiles ) {
Try { Remove-Item $tempfile -Force -ErrorAction SilentlyContinue }
Try { Remove-Item $tempfile.FullName -Force -ErrorAction SilentlyContinue }
Catch { Write-Error $_.Exception.Message }
}
}
+61 -4
View File
@@ -13,10 +13,67 @@ If ( Test-Path $HtmlReportPath ) { Remove-Item -Path $HtmlReportPath -Force }
# Generate the XML report
Write-Output "Generating XML battery report: ${XmlReportPath}"
Try { Start-Process "powercfg.exe" -ArgumentList "/batteryreport /output ""${XmlReportPath}"" /xml" -Wait }
Catch { Write-Error $_.Exception.Message }
Try {
$XmlProcess = Start-Process "powercfg.exe" `
-ArgumentList "/batteryreport /output ""${XmlReportPath}"" /xml" `
-Wait `
-PassThru `
-ErrorAction Stop
}
Catch {
Write-Error $_.Exception.Message
Exit 1
}
# Make sure powercfg succeeded and actually produced the report
If ( $XmlProcess.ExitCode -ne 0 ) {
Write-Error "powercfg failed to generate the XML battery report with exit code $($XmlProcess.ExitCode)"
Exit 1
}
If ( !(Test-Path -LiteralPath $XmlReportPath -PathType Leaf) ) {
Write-Error "The XML battery report was not created: ${XmlReportPath}"
Exit 1
}
# Validate the generated XML and confirm that it contains battery records
Try {
[xml]$GeneratedBatteryReport = Get-Content `
-LiteralPath $XmlReportPath `
-Raw `
-ErrorAction Stop
}
Catch {
Write-Error "The generated battery report is not valid XML: $($_.Exception.Message)"
Exit 1
}
$GeneratedBatteries = @(
$GeneratedBatteryReport.BatteryReport.Batteries.Battery
)
If ( $GeneratedBatteries.Count -eq 0 ) {
Write-Error "The generated battery report contains no battery records"
Exit 1
}
# Convert XML report to HTML
Write-Output "Generating HTML battery report: ${HtmlReportPath}"
Try { Start-Process "powercfg.exe" -ArgumentList "/batteryreport /transformxml ""${XmlReportPath}"" /output ""${HtmlReportPath}""" -Wait }
Catch { Write-Error $_.Exception.Message }
Try {
$HtmlProcess = Start-Process "powercfg.exe" `
-ArgumentList "/batteryreport /transformxml ""${XmlReportPath}"" /output ""${HtmlReportPath}""" `
-Wait `
-PassThru `
-ErrorAction Stop
}
Catch {
Write-Error $_.Exception.Message
Exit 1
}
If ( $HtmlProcess.ExitCode -ne 0 -or !(Test-Path -LiteralPath $HtmlReportPath -PathType Leaf) ) {
Write-Error "Failed to create the HTML battery report"
Exit 1
}
@@ -1,28 +0,0 @@
# Create ticket
Import-Module $env:SyncroModule -WarningAction SilentlyContinue
$TicketSubject = 'App Crash on ' + $Env:COMPUTERNAME
$TicketIssueType = 'Remote Support'
$TicketStatus = 'New'
Try {
$TicketData = Create-Syncro-Ticket -Subject $TicketSubject -IssueType $TicketIssueType -Status $TicketStatus
$TicketID = $TicketData.ticket.id
$TicketComment = "Application crash detected. Reason for crash indicates a Windows component is at fault."
Create-Syncro-Ticket-Comment -TicketIdOrNumber $TicketID -Subject "Issue" -Body $TicketComment -Hidden $true -DoNotEmail $true
}
Catch { Write-Error "Error creating ticket" ; Return }
# Update the ticket
$TicketComment = "It looks like an app crashed on your computer, and the reason for the crash indicates a Windows component is at fault.`n`nI'll repair your system now."
Create-Syncro-Ticket-Comment -TicketIdOrNumber $TicketID -Subject "Update" -Body $TicketComment -Hidden $false -DoNotEmail $false
# Bill time to the ticket
$BillableTimeInMinutes = 15
$StartAt = (Get-Date).AddMinutes(-$BillableTimeInMinutes).ToString("o")
$TimeEntryNote = "Analysis indicates crash caused by Windows component(s). Scanning and repairing corruption in the system."
Create-Syncro-Ticket-TimerEntry -TicketIdOrNumber $TicketID -StartTime $StartAt -DurationMinutes $BillableTimeInMinutes -Notes $TimeEntryNote -UserIdOrEmail $TimeAttributedToUser -ChargeTime "true"
# Fix Windows Health
$FixWindowsHealth = Download-File -URL 'https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Fix-WindowsHealth.ps1'
. $FixWindowsHealth
If ( Test-Path $FixWindowsHealth ) { Remove-Item $FixWindowsHealth -Force -ErrorAction SilentlyContinue }
+21
View File
@@ -0,0 +1,21 @@
# Eval the alert data
If ( -not $AlertData ) { Write-Error "No alert data provided" ; Return } Else { Write-Output $AlertData ; Return }
## Create ticket
#New-RMMTicket -Subject "App Crash on ${Env:COMPUTERNAME}" -InitialIssue "Application crash detected. Reason for crash indicates a Windows component is at fault."
#
## Update the ticket
#$TicketComment = "It looks like an app crashed on your computer, and the reason for the crash indicates a Windows component is at fault.`n`nI'll repair your system now."
#Create-Syncro-Ticket-Comment -TicketIdOrNumber $TicketID -Subject "Update" -Body $TicketComment -Hidden $false -DoNotEmail $false
#
## Bill time to the ticket
#$BillableTimeInMinutes = 15
#$StartAt = (Get-Date).AddMinutes(-$BillableTimeInMinutes).ToString("o")
#$TimeEntryNote = "Analysis indicates crash caused by Windows component(s). Scanning and repairing corruption in the system."
#Create-Syncro-Ticket-TimerEntry -TicketIdOrNumber $TicketID -StartTime $StartAt -DurationMinutes $BillableTimeInMinutes -Notes $TimeEntryNote -UserIdOrEmail $TimeAttributedToUser -ChargeTime "true"
#
## Fix Windows Health
#$FixWindowsHealth = Download-File -URL 'https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Fix-WindowsHealth.ps1'
#. $FixWindowsHealth
#If ( Test-Path $FixWindowsHealth ) { Remove-Item $FixWindowsHealth -Force -ErrorAction SilentlyContinue }
+17 -30
View File
@@ -1,12 +1,12 @@
If ( Is64Bit ) { $BlueScreenView = $Global:MSPToolsDirectory + '\BlueScreenView-x64.exe' } Else { $BlueScreenView = $Global:MSPToolsDirectory + '\BlueScreenView-x86.exe' }
$BSODReport = $Global:OutputDirectory + '\BSODReport.txt'
If ( Is64Bit ) { $BlueScreenView = $Global:ToolsDirectory + '\BlueScreenView-x64.exe' } Else { $BlueScreenView = $Global:ToolsDirectory + '\BlueScreenView-x86.exe' }
If ( !(Test-Path $BlueScreenView) ) { Write-Error "File does not exist: ${BlueScreenView}" ; Return }
# Remove existing BSOD report
If ( Test-Path $BSODReport ) { Remove-Item $BSODReport -Force -ErrorAction SilentlyContinue }
$BSODReport = $Global:OutputDirectory + '\BSODReport.txt'
If ( Test-Path $BSODReport ) { Write-Output "Deleting existing BSOD report: ${BSODReport}" ; Remove-Item $BSODReport -Force -ErrorAction SilentlyContinue }
# Run BlueScreenView
If ( !(Test-Path $BlueScreenView) ) { Write-Error "File does not exist: ${BlueScreenView}" ; Return }
Start-Process $BlueScreenView -ArgumentList "/stext $BSODReport /sort ~1" -Wait
Start-Process $BlueScreenView -ArgumentList "/stext ${BSODReport} /sort ~1" -Wait
# Wait for BSOD report
For ($i = 0; $i -lt 1200; $i++) {
@@ -17,22 +17,19 @@ For ($i = 0; $i -lt 1200; $i++) {
# Exit if the BSOD report was not created
If ( !(Test-Path $BSODReport) ) { Write-Output "BSOD report was not found: ${BSODReport}" ; Return }
# Create ticket
Try {
Import-Module $env:SyncroModule -WarningAction SilentlyContinue
$BSODReportContent = Get-Content -Path $BSODReport
$TicketSubject = 'System Crash on ' + $Env:COMPUTERNAME
$TicketIssueType = 'Remote Support'
$TicketStatus = 'New'
$TicketComment = $BSODReportContent -join "`n"
$TicketData = Create-Syncro-Ticket -Subject $TicketSubject -IssueType $TicketIssueType -Status $TicketStatus
$TicketID = $TicketData.ticket.id
Create-Syncro-Ticket-Comment -TicketIdOrNumber $TicketID -Subject "Issue" -Body $TicketComment -Hidden $true -DoNotEmail $true
# Exit if the BSOD report is empty
If ( (Get-Content -Path $BSODReport).Length -eq 0 ) {
Write-Output "BSOD report is empty: ${BSODReport}"
Remove-Item $BSODReport -Force
Return
}
Catch { Write-Error "Error creating ticket" ; Return }
# Create ticket
$BSODReportContent = $(Get-Content -Path $BSODReport) -join "`n"
New-RMMTicket -Subject "System Crash on ${Env:COMPUTERNAME}" -InitialIssue $BSODReportContent
# Update the ticket
Create-Syncro-Ticket-Comment -TicketIdOrNumber $TicketID -Subject "Update" -Body "It looks like your computer crashed. I'll get this resolved as soon as possible." -Hidden $false -DoNotEmail $false
New-TicketComment -Comment "It looks like your computer crashed. I'll get this resolved as soon as possible."
# Wait a bit
Start-Sleep -Seconds 120
@@ -43,20 +40,10 @@ $ProviderMicrosoft = 'Microsoft|Windows'
# Get the provider data from the most recent BSOD
$CulpritProvider = $BSODReportContent[12..13] -join '`n'
# Set the billable time
$BillableTimeInMinutes = 15
$StartAt = (Get-Date).AddMinutes(-$BillableTimeInMinutes).ToString("o")
Switch ( $CulpritProvider ) {
{ $_ -match $ProviderMicrosoft } {
$TicketComment = "I've analyzed the crash report and it indicates a problem with a Windows component. I'll repair the system now."
$TimeEntryNote = "Analysis indicates crash caused by Windows component(s). Scanning and repairing corruption in the system."
Create-Syncro-Ticket-Comment -TicketIdOrNumber $TicketID -Subject "Update" -Body $TicketComment -Hidden $false -DoNotEmail $false
Create-Syncro-Ticket-TimerEntry -TicketIdOrNumber $TicketID -StartTime $StartAt -DurationMinutes $BillableTimeInMinutes -Notes $TimeEntryNote -UserIdOrEmail $TimeAttributedToUser -ChargeTime "true"
$FixWindowsHealth = Download-File -URL 'https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Fix-WindowsHealth.ps1'
Start-Sleep -Seconds 120
. $FixWindowsHealth
If ( Test-Path $FixWindowsHealth ) { Remove-Item $FixWindowsHealth -Force -ErrorAction SilentlyContinue }
New-TicketComment -Comment "I've analyzed the crash report and it indicates a problem with a Windows component. I'll repair the system now."
Source-PSScript -ScriptName "Fix-WindowsHealth"
}
}
+17
View File
@@ -0,0 +1,17 @@
# Exit if the endpoint is a server edition
Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -Property ProductType | ForEach-Object { If ( $_.ProductType -gt 1 ) { Return } }
# Exit if the endpoint is not running at least Windows 10
If ( IsWindowsVersion -lt "10.0" ) { Return }
# Exit if the endpoint is not running the Pro/Enterprise version of Windows
$Edition = ($(systeminfo | findstr /B /C:"OS Name") -replace "OS Name:","").Trim()
If ( ($Edition -notmatch "Pro") -and ($Edition -notmatch "Enterprise") ) { Return }
# Exit if Hyper-V is already enabled
If ( (Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V).State -eq "Enabled" ) { Return }
# Endpoint is running the correct version of Windows, enabling Hyper-V features
Write-Output "Enabling Hyper-V features"
Try { Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All -NoRestart }
Catch { Write-Error $_.Exception.Message }
+23
View File
@@ -0,0 +1,23 @@
@echo off
if /I %0 EQU "%~dpnx0" (set LAUNCH=1) else (set LAUNCH=0)
set PROP=ExcludeExplicitO365Endpoint
set KEY=Microsoft\Office\16.0\Outlook\AutoDiscover
echo Disabling Office 365 AutoDiscover...
reg add HKCU\Software\%KEY% /v %PROP% /t REG_DWORD /d 1 /f /reg:32 >nul 2>nul
reg add HKCU\Software\Policies\%KEY% /v %PROP% /t REG_DWORD /d 1 /f /reg:32 >nul 2>nul
reg add HKCU\Software\%KEY% /v %PROP% /t REG_DWORD /d 1 /f /reg:64 >nul 2>nul
reg add HKCU\Software\Policies\%KEY% /v %PROP% /t REG_DWORD /d 1 /f /reg:64 >nul 2>nul
if %ERRORLEVEL% GTR 0 (
if %LAUNCH% GTR 0 color 0e
echo ERROR: This script failed to run. Please make sure to Run As Administrator
reg delete HKCU\Software\%KEY% /v %PROP% /f /reg:32 >nul 2>nul
reg delete HKCU\Software\Policies\%KEY% /v %PROP% /f /reg:32 >nul 2>nul
reg delete HKCU\Software\%KEY% /v %PROP% /f /reg:64 >nul 2>nul
reg delete HKCU\Software\Policies\%KEY% /v %PROP% /f /reg:64 >nul 2>nul
echo Changes have been reverted.
if %LAUNCH% GTR 0 pause
goto :EOF
)
echo Successfully disabled Office 365 AutoDiscover!
echo Please restart your comptuer.
if %LAUNCH% GTR 0 pause
+24
View File
@@ -0,0 +1,24 @@
@echo off
if /I %0 EQU "%~dpnx0" (set LAUNCH=1) else (set LAUNCH=0)
set PROP=HideNewOutlookToggle
set KEY=Microsoft\Office\16.0\Outlook\Options\General
set ARGS=/v %PROP% /t REG_DWORD /d 1 /f
echo Disabling New Outlook toggle...
reg add HKCU\Software\%KEY% %ARGS% /reg:32 >nul 2>nul
reg add HKCU\Software\Policies\%KEY% %ARGS% /reg:32 >nul 2>nul
reg add HKCU\Software\%KEY% %ARGS% /reg:64 >nul 2>nul
reg add HKCU\Software\Policies\%KEY% %ARGS% /reg:64 >nul 2>nul
if %ERRORLEVEL% GTR 0 (
if %LAUNCH% GTR 0 color 0e
echo ERROR: This script failed to run. Please make sure to Run As Administrator
reg delete HKCU\Software\%KEY% /v %PROP% /f /reg:32 >nul 2>nul
reg delete HKCU\Software\Policies\%KEY% /v %PROP% /f /reg:32 >nul 2>nul
reg delete HKCU\Software\%KEY% /v %PROP% /f /reg:64 >nul 2>nul
reg delete HKCU\Software\Policies\%KEY% /v %PROP% /f /reg:64 >nul 2>nul
echo Changes have been reverted.
if %LAUNCH% GTR 0 pause
goto :EOF
)
echo Successfully disabled New Outlook toggle!
echo Please restart your comptuer.
if %LAUNCH% GTR 0 pause
+52 -16
View File
@@ -1,15 +1,47 @@
$DNSHelperURL = 'https://www.emberkom.com/tools/dnshelper.exe'
$DNSHelper = $Global:ToolsDirectory + '\dnshelper.exe'
If ( !(Test-Path $DNSHelper) ) {
$DNSHelper = $Global:ToolsDirectory + '\ekdns.exe'
$DNSHelperRequiredVersion = '2.3.0'
$DownloadRequired = $false
If (Test-Path $DNSHelper) {
$versionOutput = (& $DNSHelper 2>&1 | Select-Object -First 1)
If ($versionOutput -match 'v(\d+\.\d+\.\d+)') {
If ($Matches[1] -ne $DNSHelperRequiredVersion) {
$DownloadRequired = $true
}
} Else {
$DownloadRequired = $true
}
} Else {
$DownloadRequired = $true
}
# Download the latest version of ekDNSHelper
If ($DownloadRequired) {
$DNSHelperURL = 'https://xfer.emberkom.com/shares/tools/folders/9b9988fb-c53a-4833-a4ad-68d01a3021bb/files/ekdns-x64-exe/download/ekdns-x64.exe'
If (!(Test-Path ${Env:ProgramFiles(x86)})) {
$DNSHelperURL = 'https://xfer.emberkom.com/shares/tools/folders/9b9988fb-c53a-4833-a4ad-68d01a3021bb/files/ekdns-x86-exe/download/ekdns-x86.exe'
}
If (Test-Path $DNSHelper) { Remove-Item -Path $DNSHelper -Force -ErrorAction SilentlyContinue }
Write-Output "Downloading latest version of ekDNSHelper"
Download-File -URL $DNSHelperURL -File $DNSHelper
}
$Resolvers = @(
'1.1.1.1',
'8.8.8.8',
'1.0.0.1',
'8.8.4.4'
)
# Delete the old version of ekDNSHelper if it exists
If ( Test-Path ($Global:ToolsDirectory + '\dnshelper.exe') ) {
Write-Output "Deleting old version of ekDNSHelper: ${Global:ToolsDirectory}\dnshelper.exe"
Remove-Item -Path ($Global:ToolsDirectory + '\dnshelper.exe') -Force -ErrorAction SilentlyContinue
}
$RefreshScript = $Global:ToolsDirectory + '\refresh-local-hosts.bat'
$todaysDate = Get-LongDate
$RefreshScriptHeader = @"
@echo off
REM Refresh IP to name mappings in local hosts file
REM This script was created by an automatic process and should not be edited manually
REM Author: David Yoder, Emberkom LLC (Fix-UpdateLocalHosts.ps1)
REM Updated: ${todaysDate}`n
"@
$Hosts = @(
'auth.services.mspbackups.com',
@@ -20,12 +52,6 @@ If ($BackupBucket.Length -ge 2 ) {
$Hosts += $BackupBucket
}
$ResolverString = ''
ForEach ($Resolver in $Resolvers) {
If ($ResolverString.Length -gt 0) { $ResolverString = $ResolverString + ',' + $Resolver }
Else { $ResolverString = $Resolver }
}
$HostString = ''
ForEach ($Hostname in $Hosts) {
If ($HostString.Length -gt 0) { $HostString = $HostString + ',' + $Hostname }
@@ -33,8 +59,18 @@ ForEach ($Hostname in $Hosts) {
}
If ( Test-Path $DNSHelper ) {
$commandString = """${DNSHelper}"" add -host ${HostString}"
$content = $RefreshScriptHeader + $commandString
Write-Output "Creating latest refresh script"
If ( Test-Path $RefreshScript ) { Remove-Item -Path $RefreshScript -Force }
Try {
New-Item -Path $RefreshScript -ItemType File -Force | Out-Null
Set-Content -Path $RefreshScript -Value $content
}
Catch { Write-Error $_.Exception.Message }
Write-Output "Updating local hosts file with name-to-IP mappings for the following host(s): ${HostString}"
& $DNSHelper add -host $HostString -server $ResolverString
& "${RefreshScript}"
} Else {
Write-Error "Cannot find the DNS Helper executable: ""${DNSHelper}"""
}
+27 -19
View File
@@ -7,23 +7,36 @@ $TicketComment = "SFC Output:`n"
ForEach ( $line in $SFCOutput ) {
If ( $line -notmatch 'Verification \d{1,2}% complete' ) { $TicketComment = $TicketComment + $line + "`n" }
}
If ( $TicketID ) { Create-Syncro-Ticket-Comment -TicketIdOrNumber $TicketID -Subject "Update" -Body $TicketComment -Hidden $true -DoNotEmail $true }
New-TicketComment -Comment $TicketComment -Hidden -DoNotEmail
Remove-Item $tempFile -Force -ErrorAction SilentlyContinue
# Examine $SFCOutput for integrity violations, if found make sure chkdsk runs at the next boot
If ( $SFCOutput -contains 'Windows Resource Protection did not find any integrity violations.' ) { $ScheduleCheckDisk = $false } Else { $ScheduleCheckDisk = $true }
Switch ( $SFCOutput ) {
{ $_ -match 'Windows Resource Protection found integrity violations' } { $ScheduleCheckDisk = $true }
{ $_ -match 'Windows Resource Protection did not find any integrity violations.' } { $ScheduleCheckDisk = $false }
{ $_ -match 'There is a system repair pending which requires reboot to complete.' } {
$ScheduleCheckDisk = $false
New-TicketComment -Comment "System repair is already pending. Reboot needs to occur first, then SFC should be run again." -Hidden -DoNotEmail
}
}
# Run enhanced DISM for Windows 8 and higher endpoints
$DISMRun = $false
If ( IsWindowsVersion -ge "6.2" ) {
# Record start of DISM repair
$BeginDISMRun = (Get-Date)
Write-Output "Beginning repair of Windows Component Store"
Start-Process "dism.exe" -ArgumentList "/online /cleanup-image /restorehealth" -Wait
Try {
Write-Output "Beginning repair of Windows Component Store"
Start-Process "dism.exe" -ArgumentList "/online /cleanup-image /restorehealth" -Wait
Write-Output "Beginning cleanup of superceded components in Windows Component Store"
Start-Process "dism.exe" -ArgumentList "/online /cleanup-image /startcomponentcleanup" -Wait
Write-Output "Beginning cleanup of superceded components in Windows Component Store"
Start-Process "dism.exe" -ArgumentList "/online /cleanup-image /startcomponentcleanup" -Wait
$DISMRun = $true
}
Catch { Write-Error $_.Exception.Message }
# Record end of DISM repair
$EndDISMRun = (Get-Date)
@@ -44,17 +57,12 @@ Else { Write-Output "Online image cleanup and restoration is only supported on W
If ( $ScheduleCheckDisk ) {
Write-Output "Scheduling disk repair at next boot"
Start-Process "cmd.exe" -ArgumentList '/C echo y | chkdsk /F /R /X' -Wait
If ( $TicketID ) {
$TicketComment = "Scheduled disk repair at next boot"
Create-Syncro-Ticket-Comment -TicketIdOrNumber $TicketID -Subject "Update" -Body $TicketComment -Hidden $true -DoNotEmail $true
$TicketComment = "I've made some changes that will require you to restart your computer.`n`nWhenver you're ready, please restart your computer and allow it 30min to complete the startup process."
Create-Syncro-Ticket-Comment -TicketIdOrNumber $TicketID -Subject "Update" -Body $TicketComment -Hidden $false -DoNotEmail $false
}
New-TicketComment -Comment "Scheduled disk repair at next boot" -Hidden -DoNotEmail
New-TicketComment -Comment "I've made some changes that will require you to restart your computer.`n`nWhenver you're ready, please restart your computer and allow it 30min to complete the startup process."
} Else {
If ( $TicketID ) {
$TicketComment = "Disk check was not scheduled to run, as no Windows integrity violations were found."
Create-Syncro-Ticket-Comment -TicketIdOrNumber $TicketID -Subject "Update" -Body $TicketComment -Hidden $true -DoNotEmail $true
$TicketComment = "Your computer has been repaired. Please restart as soon as you're able."
Create-Syncro-Ticket-Comment -TicketIdOrNumber $TicketID -Subject "Update" -Body $TicketComment -Hidden $false -DoNotEmail $false
}
}
New-TicketComment -Comment "Disk check was not scheduled to run, as no Windows integrity violations were found." -Hidden -DoNotEmail
New-TicketComment -Comment "Your computer has been repaired. Please restart as soon as you're able."
}
# Bill ticket if action was taken
If ( $DISMRun -or $ScheduleCheckDisk ) { New-TicketTimerEntry -Comment "Corruption was found and has been repaired." }
+104
View File
@@ -0,0 +1,104 @@
#Requires -Version 5.0
<#
.SYNOPSIS
Downloads and runs the DSA manifest collector, with optional upload and cleanup.
.DESCRIPTION
Tools.ps1 must be dot-sourced by the caller first. Set $DSAManifestSource
in the caller's scope before using Run-Script -LivePSScript Get-DSAManifest.
The output path is generated in $Global:OutputDirectory with the filename
dsa-manifest-HOSTNAME-yyyyMMddHHmmss.csv, using the computer name and current local date and time.
$Zip enables ZIP compression; $Upload sends the result to the FileDrop;
$DeleteAfterUpload removes the local result only after a confirmed upload.
All three accept 'true', '$true', 'yes', 'y', or '1' to enable, and
'false', '$false', 'no', 'n', or '0' to disable. Empty values disable the option.
Values are case-insensitive and surrounding whitespace is ignored. When
$Upload is false, the local CSV or ZIP is retained regardless of $DeleteAfterUpload.
#>
If ( [string]::IsNullOrWhiteSpace([string]$DSAManifestSource) ) {
Throw 'Set $DSAManifestSource in the caller before running Get-DSAManifest.'
}
# Normalize RMM text values without treating every nonempty string as true.
$DsaOptions = @{
Zip = ConvertTo-RmmBoolean -Value $Zip -VariableName 'Zip'
Upload = ConvertTo-RmmBoolean -Value $Upload -VariableName 'Upload'
DeleteAfterUpload = ConvertTo-RmmBoolean -Value $DeleteAfterUpload -VariableName 'DeleteAfterUpload'
}
$DsaZipEnabled = $DsaOptions.Zip
$DSAManifestOutput = Join-Path -Path $Global:OutputDirectory -ChildPath (
'dsa-manifest-{0}-{1}.csv' -f $Env:COMPUTERNAME, (Get-Date -Format 'yyyyMMddHHmmss')
)
New-Item -Path $Global:OutputDirectory -ItemType Directory -Force -ErrorAction Stop | Out-Null
# Select the executable for the OS, even when the RMM uses 32-bit PowerShell.
If ( [Environment]::Is64BitOperatingSystem ) {
$DsaDownloadUrl = 'https://xfer.emberkom.com/shares/tools/folders/9b9988fb-c53a-4833-a4ad-68d01a3021bb/files/dsa-collect-windows-amd64-exe/download/dsa-collect-windows-amd64.exe'
}
Else {
$DsaDownloadUrl = 'https://xfer.emberkom.com/shares/tools/folders/9b9988fb-c53a-4833-a4ad-68d01a3021bb/files/dsa-collect-windows-386-exe/download/dsa-collect-windows-386.exe'
}
$DsaExecutablePath = Join-Path -Path $Global:ToolsDirectory -ChildPath 'dsa-collect.exe'
New-Item -Path $Global:ToolsDirectory -ItemType Directory -Force -ErrorAction Stop | Out-Null
Write-Output "Downloading dsa-collect.exe to ${DsaExecutablePath}"
$DsaDownloadedFile = Download-FileDirectly -URL $DsaDownloadUrl -File $DsaExecutablePath -ErrorAction Stop
# Require a returned path and a nonempty download before starting the collector.
If ( [string]::IsNullOrWhiteSpace([string]$DsaDownloadedFile) -or
!(Test-Path -LiteralPath $DsaExecutablePath -PathType Leaf) ) {
Throw "Failed to download dsa-collect.exe to ${DsaExecutablePath}"
}
If ( (Get-Item -LiteralPath $DsaExecutablePath -ErrorAction Stop).Length -eq 0 ) {
Throw "The downloaded dsa-collect.exe is 0 bytes: ${DsaExecutablePath}. The collector was not started. Check the download response and client TLS/BITS settings."
}
$DsaArguments = @('--source', [string]$DSAManifestSource, '--output', [string]$DSAManifestOutput)
If ( $DsaZipEnabled ) {
$DsaArguments += '--zip'
}
# Quote native arguments explicitly, including paths ending in a backslash.
# Start-Process joins its argument list without adding the required quoting.
$DsaCommandLine = ($DsaArguments | ForEach-Object {
'"{0}"' -f ($_ -replace '(\\*)"', '$1$1\"' -replace '(\\+)$', '$1$1')
}) -join ' '
$DsaCollectionTime = Get-Date
Write-Output "Running dsa-collect.exe (ZIP compression: ${DsaZipEnabled})"
$DsaProcess = Start-Process -FilePath $DsaExecutablePath -ArgumentList $DsaCommandLine `
-Wait -PassThru -WindowStyle Hidden -ErrorAction Stop
If ( $DsaProcess.ExitCode -ne 0 ) {
Throw "dsa-collect.exe failed with exit code $($DsaProcess.ExitCode)"
}
$DsaResultPath = $DSAManifestOutput
If ( $DsaZipEnabled ) {
$DsaResultPath = [System.IO.Path]::ChangeExtension($DSAManifestOutput, '.zip')
}
If ( !(Test-Path -LiteralPath $DsaResultPath -PathType Leaf) ) {
Throw "dsa-collect.exe did not create the expected output: ${DsaResultPath}"
}
Write-Output "DSA manifest created: ${DsaResultPath}"
If ( $DsaOptions.Upload ) {
Write-Output "Uploading DSA manifest: ${DsaResultPath}"
$DsaUploadReceipt = Upload-File -Path $DsaResultPath `
-Subject "DSA manifest file uploaded from ${Env:COMPUTERNAME}" `
-Message "DSA collection performed on ${DSAManifestSource} at $($DsaCollectionTime.ToString('yyyy-MM-dd HH:mm:ss zzz'))" `
-ErrorAction Stop
If ( [string]::IsNullOrWhiteSpace([string]$DsaUploadReceipt.AttachmentId) -or
[string]::IsNullOrWhiteSpace([string]$DsaUploadReceipt.Status) ) {
Throw 'The upload did not return a confirmation receipt. The local manifest has been retained.'
}
Write-Output "DSA manifest uploaded: ${DsaResultPath}"
If ( $DsaOptions.DeleteAfterUpload ) {
Remove-Item -LiteralPath $DsaResultPath -Force -ErrorAction Stop
Write-Output "Deleted uploaded DSA manifest: ${DsaResultPath}"
}
$DsaUploadReceipt
}
File diff suppressed because it is too large Load Diff
+528
View File
@@ -0,0 +1,528 @@
# Based on the script by lwhitelock, which is based on the original script by Kelvin Tegelaar https://github.com/KelvinTegelaar/AutomaticDocumentation
#####################################################################
#
# Active Directory Details to Hudu
#
# $HuduAPIKey must be supplied by RMM script
# $HuduBaseDomain is the base domain of your Hudu instance (without a trailing /)
# and must be supplied by RMM script
# $CompanyName must exactly match the name of the company in Hudu
# and must be supplied by RMM script
# This is the name of the Hudu Asset Layout you want to use.
$HuduAssetLayoutName = "Active Directory"
#####################################################################
# Get the Hudu API Module if not installed
if (Get-Module -ListAvailable -Name HuduAPI) {
Import-Module HuduAPI
} else {
Install-Module HuduAPI -Force
Import-Module HuduAPI
}
# Set Hudu logon information
New-HuduAPIKey $HuduAPIKey
New-HuduBaseUrl $HuduBaseDomain
Function Get-RegistryValue
{
# Gets the specified registry value or $Null if it is missing
[CmdletBinding()]
Param
(
[String] $path,
[String] $name,
[String] $ComputerName
)
If($ComputerName -eq $env:computername -or $ComputerName -eq "LocalHost")
{
$key = Get-Item -LiteralPath $path -EA 0
If($key)
{
Return $key.GetValue($name, $Null)
}
Else
{
Return $Null
}
}
# Path needed here is different for remote registry access
$path1 = $path.SubString( 6 )
$path2 = $path1.Replace( '\', '\\' )
$registry = $null
try
{
## Use the Remote Registry service
$registry = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey(
[Microsoft.Win32.RegistryHive]::LocalMachine,
$ComputerName )
}
catch
{
#$e = $error[ 0 ]
#3.06, remove the verbose message as it confised some people
#wv "Could not open registry on computer $ComputerName ($e)"
}
$val = $null
If( $registry )
{
$key = $registry.OpenSubKey( $path2 )
If( $key )
{
$val = $key.GetValue( $name )
$key.Close()
}
$registry.Close()
}
Return $val
}
Function GetBasicDCInfo {
Param
(
[Parameter( Mandatory = $true )]
[String] $dn # distinguishedName of a DC
)
$DCName = $dn.SubString( 0, $dn.IndexOf( '.' ) )
$SrvName = $dn.SubString( $dn.IndexOf( '.' ) + 1 )
$Results = Get-ADDomainController -Identity $DCName -Server $SrvName -EA 0
If($? -and $Null -ne $Results)
{
$GC = $Results.IsGlobalCatalog.ToString()
$ReadOnly = $Results.IsReadOnly.ToString()
$IPv4Address = $Results.IPv4Address -join ", "
$IPv6Address = $Results.IPv6Address -join ", "
$ServerOS = $Results.OperatingSystem
$tmp = Get-RegistryValue "HKLM:\software\microsoft\windows nt\currentversion" "installationtype" $DCName
If( $null -eq $tmp ) { $ServerCore = 'Unknown' }
ElseIf( $tmp -eq 'Server Core') { $ServerCore = 'Yes' }
Else { $ServerCore = 'No' }
}
Else
{
$GC = 'Unable to retrieve status'
$ReadOnly = $GC
$ServerOS = $GC
$ServerCore = $GC
$IPv4Address = $GC
$IPv6Address = $GC
}
$obj = [PSCustomObject] @{
DCName = $DCName
GC = $GC
ReadOnly = $ReadOnly
ServerOS = $ServerOS
ServerCore = $ServerCore
IPv4Address = $IPv4Address
IPv6Address = $IPv6Address
}
Return $obj
}
Function GetTimeServerRegistryKeys {
Param
(
[String] $DCName
)
$AnnounceFlags = Get-RegistryValue "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Config" "AnnounceFlags" $DCName
If( $null -eq $AnnounceFlags )
{
## DCName can't be contacted or DCName is an appliance with no registry
$AnnounceFlags = 'n/a'
$MaxNegPhaseCorrection = 'n/a'
$MaxPosPhaseCorrection = 'n/a'
$NtpServer = 'n/a'
$NtpType = 'n/a'
$SpecialPollInterval = 'n/a'
$VMICTimeProviderEnabled = 'n/a'
$NTPSource = 'Cannot retrieve data from registry'
}
Else
{
$MaxNegPhaseCorrection = Get-RegistryValue "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Config" "MaxNegPhaseCorrection" $DCName
$MaxPosPhaseCorrection = Get-RegistryValue "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Config" "MaxPosPhaseCorrection" $DCName
$NtpServer = Get-RegistryValue "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Parameters" "NtpServer" $DCName
$NtpType = Get-RegistryValue "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Parameters" "Type" $DCName
$SpecialPollInterval = Get-RegistryValue "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpClient" "SpecialPollInterval" $DCName
$VMICTimeProviderEnabled = Get-RegistryValue "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\VMICTimeProvider" "Enabled" $DCName
$NTPSource = Invoke-Command -ComputerName $DCName {w32tm /query /computer:$DCName /source}
}
If( $VMICTimeProviderEnabled -eq 'n/a' )
{
$VMICEnabled = 'n/a'
}
ElseIf( $VMICTimeProviderEnabled -eq 0 )
{
$VMICEnabled = 'Disabled'
}
Else
{
$VMICEnabled = 'Enabled'
}
$obj = [PSCustomObject] @{
DCName = $DCName.Substring(0, $_.IndexOf( '.'))
TimeSource = $NTPSource
AnnounceFlags = $AnnounceFlags
MaxNegPhaseCorrection = $MaxNegPhaseCorrection
MaxPosPhaseCorrection = $MaxPosPhaseCorrection
NtpServer = $NtpServer
NtpType = $NtpType
SpecialPollInterval = $SpecialPollInterval
VMICTimeProvider = $VMICEnabled
}
Return $obj
}
function Get-WinADForestInformation {
$Data = @{ }
$ForestInformation = $(Get-ADForest)
$Data.Forest = $ForestInformation
$Data.RootDSE = $(Get-ADRootDSE -Properties *)
$Data.ForestName = $ForestInformation.Name
$Data.ForestNameDN = $Data.RootDSE.defaultNamingContext
$Data.Domains = $ForestInformation.Domains
$Data.ForestInformation = @{
'Forest Name' = $ForestInformation.Name
'Root Domain' = $ForestInformation.RootDomain
'Forest Functional Level' = $ForestInformation.ForestMode
'# of Domains' = ($ForestInformation.Domains).Count
'Sites Count' = ($ForestInformation.Sites).Count
'Forest Domains' = ($ForestInformation.Domains) -join ", "
'Sites' = ($ForestInformation.Sites) -join ", "
}
$Data.UPNSuffixes = Invoke-Command -ScriptBlock {
$UPNSuffixList = [PSCustomObject] @{
"Primary UPN" = $ForestInformation.RootDomain
"UPN Suffixes" = $ForestInformation.UPNSuffixes -join ","
}
return $UPNSuffixList
}
$Data.GlobalCatalogs = $ForestInformation.GlobalCatalogs
$Data.SPNSuffixes = $ForestInformation.SPNSuffixes
$Data.Sites = Invoke-Command -ScriptBlock {
$Sites = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest().Sites | Sort-Object
$SiteData = foreach ($Site in $Sites) {
[PSCustomObject] @{
"Site Name" = $site.Name
"Subnets" = ($site.Subnets | Sort-Object) -join ", "
"Servers" = ($Site.Servers) -join ", "
}
}
Return $SiteData
}
$Data.FSMO = Invoke-Command -ScriptBlock {
[PSCustomObject] @{
"Domain" = $ForestInformation.RootDomain
"Role" = 'Domain Naming Master'
"Holder" = $ForestInformation.DomainNamingMaster
}
[PSCustomObject] @{
"Domain" = $ForestInformation.RootDomain
"Role" = 'Schema Master'
"Holder" = $ForestInformation.SchemaMaster
}
foreach ($Domain in $ForestInformation.Domains) {
$DomainFSMO = Get-ADDomain $Domain | Select-Object PDCEmulator, RIDMaster, InfrastructureMaster
[PSCustomObject] @{
"Domain" = $Domain
"Role" = 'PDC Emulator'
"Holder" = $DomainFSMO.PDCEmulator
}
[PSCustomObject] @{
"Domain" = $Domain
"Role" = 'Infrastructure Master'
"Holder" = $DomainFSMO.InfrastructureMaster
}
[PSCustomObject] @{
"Domain" = $Domain
"Role" = 'RID Master'
"Holder" = $DomainFSMO.RIDMaster
}
}
Return $FSMO
}
$Data.OptionalFeatures = Invoke-Command -ScriptBlock {
$OptionalFeatures = $(Get-ADOptionalFeature -Filter * )
$Optional = @{
'Recycle Bin Enabled' = ''
'Privileged Access Management Feature Enabled' = ''
}
### Fix Optional Features
foreach ($Feature in $OptionalFeatures) {
if ($Feature.Name -eq 'Recycle Bin Feature') {
if ("$($Feature.EnabledScopes)" -eq '') {
$Optional.'Recycle Bin Enabled' = $False
}
else {
$Optional.'Recycle Bin Enabled' = $True
}
}
if ($Feature.Name -eq 'Privileged Access Management Feature') {
if ("$($Feature.EnabledScopes)" -eq '') {
$Optional.'Privileged Access Management Feature Enabled' = $False
}
else {
$Optional.'Privileged Access Management Feature Enabled' = $True
}
}
}
return $Optional
### Fix optional features
}
return $Data
}
$TableHeader = "<table style=`"width: 100%; border-collapse: collapse; border: 1px solid black;`">"
$Whitespace = "<br/>"
$TableStyling = "<th>", "<th align=`"left`" style=`"background-color:#00adef; border: 1px solid black;`">"
$RawAD = Get-WinADForestInformation
$ForestRawInfo = new-object PSCustomObject -property $RawAD.ForestInformation | convertto-html -Fragment | Select-Object -Skip 1
$ForestToc = "<div id=`"forest_summary`"></div>"
$ForestNice = $ForestToc + $TableHeader + ($ForestRawInfo -replace $TableStyling) + $Whitespace
$SiteRawInfo = $RawAD.Sites | Select-Object 'Site Name', Servers, Subnets | ConvertTo-Html -Fragment | Select-Object -Skip 1
$SiteHeader = "<p id=`"site_summary`"><i>AD Forest Physical Structure.</i></p>"
$SiteNice = $SiteHeader + $TableHeader + ($SiteRawInfo -replace $TableStyling) + $Whitespace
$DomainsRawInfo = $(Get-WinADForestInformation).Domains | ForEach-Object { Get-ADDomain $_ | Select-Object Name, NetBIOSName, DomainMode } | ConvertTo-Html -Fragment | Select-Object -Skip 1
$DomainsHeader = "<p id=`"domain_summary`"><i>AD Forest Logical Structure.</i></p>"
$DomainsNice = $DomainsHeader + $TableHeader + ($DomainsRawInfo -replace $TableStyling) + $Whitespace
$OptionalRawFeatures = new-object PSCustomObject -property $RawAD.OptionalFeatures | convertto-html -Fragment | Select-Object -Skip 1
$OptionalFeaturesToc = "<div id=`"optional_features`"></div>"
$OptionalNice = $OptionalFeaturesToc + $TableHeader + ($OptionalRawFeatures -replace $TableStyling) + $Whitespace
$UPNRawFeatures = $RawAD.UPNSuffixes | convertto-html -Fragment -as list| Select-Object -Skip 1
$UPNToc = "<div id=`"upn_suffixes`"></div>"
$UPNNice = $UPNToc + $TableHeader + ($UPNRawFeatures -replace $TableStyling) + $Whitespace
$DCRawFeatures = $RawAD.GlobalCatalogs| Sort-Object | ForEach-Object { GetBasicDCInfo $_ } | convertto-html -Fragment | Select-Object -Skip 1
$DCToc = "<div id=`"domain_controllers`"></div>"
$DCNice = $DCToc + $TableHeader + ($DCRawFeatures -replace $TableStyling) + $Whitespace
$DCRawNTPconfig = $RawAD.GlobalCatalogs | Sort-Object | ForEach-Object { (GetTimeServerRegistryKeys $_) } | convertto-html -Fragment | Select-Object -Skip 1
$NTPToc = "<div id=`"ntp_configuration`"></div>"
$DCNTPconfigNice = $NTPToc + $TableHeader + ($DCRawNTPconfig -replace $TableStyling) + $Whitespace
$FSMORawFeatures = $RawAD.FSMO | convertto-html -Fragment | Select-Object -Skip 1
$FSMOToc = "<div id=`"fsmo_roles`"></div>"
$FSMONice = $FSMOToc + $TableHeader + ($FSMORawFeatures -replace $TableStyling) + $Whitespace
#$ForestFunctionalLevel = $RawAD.RootDSE.forestFunctionality
#$DomainFunctionalLevel = $RawAD.RootDSE.domainFunctionality
#$domaincontrollerMaxLevel = $RawAD.RootDSE.domainControllerFunctionality
$passwordpolicyraw = Get-ADDefaultDomainPasswordPolicy | Select-Object ComplexityEnabled, PasswordHistoryCount, LockoutDuration, LockoutThreshold, MaxPasswordAge, MinPasswordAge | convertto-html -Fragment -As List | Select-Object -skip 1
$passwordpolicyheader = "<tr><th>Policy</th><th><b>Setting</b></th></tr>"
$passwordToc = "<div id=`"default_password_policies`"></div>"
$passwordpolicyNice = $passwordToc + $TableHeader + ($passwordpolicyheader -replace $TableStyling) + ($passwordpolicyraw -replace $TableStyling) + $Whitespace
$adminsraw = Get-ADGroupMember "Domain Admins" | Select-Object SamAccountName, Name | convertto-html -Fragment | Select-Object -Skip 1
$adminsToc = "<div id=`"domain_admins`"></div>"
$adminsnice = $adminsToc + $TableHeader + ($adminsraw -replace $TableStyling) + $Whitespace
$TotalUsers = (Get-AdUser -filter *).count
$EnabledUsers = (Get-AdUser -filter * | Where-Object { $_.enabled -eq $true }).count
$DisabledUSers = (Get-AdUser -filter * | Where-Object { $_.enabled -eq $false }).count
$DomainAdminUsers = (Get-ADGroupMember -Identity "Domain Admins").count
$EnterpriseAdminUsers = (Get-ADGroupMember -Identity "Enterprise Admins").count
$SchemaAdminUsers = (Get-ADGroupMember -Identity "Schema Admins").count
$AdminCountUsers = (Get-ADUser -LDAPFilter "(admincount=1)").count
$UsersCountObj = [PSCustomObject] @{
'Total' = $TotalUsers
'Enabled' = $EnabledUsers
'Disabled' = $DisabledUSers
'Domain Admins' = $DomainAdminUsers
'Enterprise Admins' = $EnterpriseAdminUsers
'Schema Admins' = $SchemaAdminUsers
'AdminCount users' = $AdminCountUsers
}
$userTotalsRaw = $UsersCountObj | convertto-html -Fragment | Select-Object -Skip 1
$userTotalsToc = "<div id=`"user_count`"></div>"
$userTotalsNice = $userTotalsToc + $TableHeader + ($userTotalsRaw -replace $TableStyling) + $Whitespace
$currentDate = Get-Date -Format "dddd dd/MM/yyyy HH:mm K"
$toc = '<h2><center>
<a href="#forest_summary">FOREST SUMMARY</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="#site_summary">SITE SUMMARY</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="#domain_summary">DOMAIN SUMMARY</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="#domain_controllers">DOMAIN CONTROLLERS</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="#ntp_configuration">NTP CONFIGURATION</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="#fsmo_roles">FSMO ROLES</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="#optional_features">OPTIONAL FEATURES</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="#upn_suffixes">UPN SUFFIXES</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="#default_password_policies">DEFAULT PASSWORD POLICIES</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="#user_count">USER COUNT</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="#domain_admins">DOMAIN ADMINS</a>
</center></h2><br />'
# Setup the fields for the Asset
$AssetFields = @{
'last_updated' = $currentDate
'table_of_contents' = $toc
'forest_name' = $RawAD.ForestName
'forest_summary' = $ForestNice
'site_summary' = $SiteNice
'domain_summary' = $DomainsNice
'domain_controllers' = $DCNice
'ntp_configuration' = $DCNTPconfigNice
'fsmo_roles' = $FSMONice
'optional_features' = $OptionalNice
'upn_suffixes' = $UPNNice
'default_password_policies' = $passwordpolicyNice
'domain_admins' = $adminsnice
'user_count' = $userTotalsNice
}
# Checking if the FlexibleAsset exists. If not, create a new one.
$Layout = Get-HuduAssetLayouts -name $HuduAssetLayoutName
if (!$Layout) {
$AssetLayoutFields = @(
@{
label = 'Last Updated'
field_type = 'Text'
show_in_list = 'true'
position = 1
},
@{
label = 'Table of Contents'
field_type = 'RichText'
show_in_list = 'false'
position = 2
},
@{
label = 'Forest Name'
field_type = 'Text'
show_in_list = 'true'
position = 3
},
@{
label = 'Forest Summary'
field_type = 'RichText'
show_in_list = 'false'
position = 4
},
@{
label = 'Site Summary'
field_type = 'RichText'
show_in_list = 'false'
position = 5
},
@{
label = 'Domain Summary'
field_type = 'RichText'
show_in_list = 'false'
position = 6
},
@{
label = 'Domain Controllers'
field_type = 'RichText'
show_in_list = 'false'
position = 7
},
@{
label = 'NTP Configuration'
field_type = 'RichText'
show_in_list = 'false'
position = 8
},
@{
label = 'FSMO Roles'
field_type = 'RichText'
show_in_list = 'false'
position = 9
},
@{
label = 'Optional Features'
field_type = 'RichText'
show_in_list = 'false'
position = 10
},
@{
label = 'UPN Suffixes'
field_type = 'RichText'
show_in_list = 'false'
position = 11
},
@{
label = 'Default Password Policies'
field_type = 'RichText'
show_in_list = 'false'
position = 12
},
@{
label = 'User Count'
field_type = 'RichText'
show_in_list = 'false'
position = 13
},
@{
label = 'Domain Admins'
field_type = 'RichText'
show_in_list = 'false'
position = 14
}
)
Write-Host "Creating New Asset Layout"
#$NewLayout = New-HuduAssetLayout -name $HuduAssetLayoutName -icon "fas fa-sitemap" -color "#00adef" -icon_color "#000000" -include_passwords $false -include_photos $false -include_comments $false -include_files $false -fields $AssetLayoutFields
New-HuduAssetLayout -name $HuduAssetLayoutName -icon "fas fa-sitemap" -color "#00adef" -icon_color "#000000" -include_passwords $false -include_photos $false -include_comments $false -include_files $false -fields $AssetLayoutFields
$Layout = Get-HuduAssetLayouts -name $HuduAssetLayoutName
}
$Company = Get-HuduCompanies -name $CompanyName
if ($company) {
#Upload data to Hudu
$Asset = Get-HuduAssets -name $RawAD.ForestName -companyid $company.id -assetlayoutid $layout.id
#If the Asset does not exist, we edit the body to be in the form of a new asset, if not, we just upload.
if (!$Asset) {
Write-Host "Creating new Asset"
$Asset = New-HuduAsset -name $RawAD.ForestName -company_id $company.id -asset_layout_id $layout.id -fields $AssetFields
}
else {
Write-Host "Updating Asset"
$Asset = Set-HuduAsset -asset_id $Asset.id -name $RawAD.ForestName -company_id $company.id -asset_layout_id $layout.id -fields $AssetFields
}
} else {
Write-Host "$CompanyName was not found in Hudu"
}
+2 -2
View File
@@ -1,8 +1,8 @@
# Specify URL to "Backup for Windows" agent installer
$ECB_WIN_URL = 'https://console.msp360.com/api/build/download?urlParams=OEFsMzhldm1SbjhaSDhBc2VtYkpUREM3Y2pJSzg0REU4dkVtR1pHREkxMGFFUk9qZXJnSXk5SUF0ei8wZlZOWTBzNG1zVDV0ckVMdStZVzBsam5uZklMVnZLeTBMVmxGNWt4VDhSMzNIL3N3M054OXZnVGV2ZTYzcWdodWsrUUM1VkpPUFp0dUphV2VIOCtxMHdkZVlNR0czdHJ1QXMwcFkrTnJtRSsxSzlTbmgwVmNQMmtSVUxweVhvU01TaXJGU1RVUElTSGZYUEJxNUNQU3pBMXI0dz09&brandId=fb8308e2-448a-4645-a5f6-a3632a7e57f4'
$ECB_WIN_URL = 'https://console.msp360.com/api/build/download?urlParams=OEFsMzhldm1SbjhaSDhBc2VtYkpUREM3Y2pJSzg0REU4dkVtR1pHREkxMGFFUk9qZXJnSXk5SUF0ei8wZlZOWTBzNG1zVDV0ckVMdStZVzBsam5uZklMVnZLeTBMVmxGNWt4VDhSMzNIL3VmK3VHNmxBVEZTQkxhMFhNR2ZuZzJCeE80MDJxVkRSWXdNa0dqSHdpbUN1bE01c3BWNFBVUmt2Q1c4endncjVZemFSSjEyYmFNL3hESHR2eWpVYXpBVlV2eU1kZVZFaEpvalk2Rk40Wk96UG9QdUwxYnZrRHREMmhLeTZoMW5Rbz0*&brandId=fb8308e2-448a-4645-a5f6-a3632a7e57f4'
# Specify URL to "Backup Virtual Machine Edition" agent installer
$ECB_VMM_URL = 'https://console.msp360.com/api/build/download?urlParams=OEFsMzhldm1SbjhaSDhBc2VtYkpUREM3Y2pJSzg0REU4dkVtR1pHREkxMGFFUk9qZXJnSXk5SUF0ei8wZlZOWUpYM2xDY3ErMkpOVGpuWTRISDNFcGE1KzhLRmw0MmRsUkNrSndPdGtScjliWGRNOEp0UFFHdkJ0MkVCbUM4WUwvYURYbFhyRERic0NSejhNQzNBT0xNMXdIck81bldzeU5ZbVdMcGM5dmp0VXh5RU1GVGZnMW15dkVnVjV1RkI0bi9GWWlpUm9odDZCOS9BRE5UTzZxUT09&brandId=fb8308e2-448a-4645-a5f6-a3632a7e57f4'
$ECB_VMM_URL = 'https://console.msp360.com/api/build/download?urlParams=OEFsMzhldm1SbjhaSDhBc2VtYkpUREM3Y2pJSzg0REU4dkVtR1pHREkxMGFFUk9qZXJnSXk5SUF0ei8wZlZOWUpYM2xDY3ErMkpOVGpuWTRISDNFcFRFdWRibDgvTm8vUDhpWndON3kzaHNwOTFVa3h6WUdoeVVod1JmT2lRQ1FlVTE3Y0pnUncwSEJwS3FFcDBmTWt3cEdnSy9YemRsSTRiQWJHNi96cUtWbzZsc25QVllaR1QyQ21VVS95dFFVTkNndXdPVStGQXBIT0FZb2FIUzFuZy9mMU5qZUd5emhab1hFYjZWODB4ND0*&brandId=fb8308e2-448a-4645-a5f6-a3632a7e57f4'
# Set the correct download URL and agent edition
switch ( $Edition.ToLower() ) {
+13 -2
View File
@@ -1,5 +1,16 @@
# Path to the MSI installer
$URL = 'https://enscape-installer.chaosgroup.com/installer.enscape.io/Enscape-4.0.0.579.exe'
# Exit early if apps are running
$ExitEarly = $false
$AppNames = @()
If ( IsRunning -Name 'revit' ) { $ExitEarly = $true ; $AppNames.Add('Revit') }
If ( IsRunning -Name 'sketchup' ) { $ExitEarly = $true ; $AppNames.Add('SketchUp') }
If ( IsRunning -Name 'archicad' ) { $ExitEarly = $true ; $AppNames.Add('ArchiCAD') }
If ( IsRunning -Name 'autocad' ) { $ExitEarly = $true ; $AppNames.Add('AutoCAD') }
If ( IsRunning -Name 'rhino' ) { $ExitEarly = $true ; $AppNames.Add('Rhino') }
If ( IsRunning -Name 'vectorworks' ) { $ExitEarly = $true ; $AppNames.Add('VectorWorks') }
If ( $ExitEarly ) { Write-Output "Installation cannot continue because the following apps are running: ${AppNames}." ; Return }
# Path to the MSI installer
$URL = 'https://enscape-installer.chaosgroup.com/installer.enscape.io/Enscape-4.0.2.11.exe'
# Set path to temp directory
$TEMPDIR = '{0}enscape-installer' -f (Get-TempPath)
+1 -1
View File
@@ -1,5 +1,5 @@
# Path to the MSI installer
$MSIURL = 'https://lfupdate.s3.amazonaws.com/clients/outlook/admin_msi/LiquidFiles_Admin_2.1.10.msi'
$MSIURL = 'https://lfupdate.s3.amazonaws.com/clients/outlook/admin_msi/LiquidFiles_OutlookPlugin_Admin_3.0.37.msi'
# Set installer args
If (-not [string]::IsNullOrEmpty($LFHost) ) {
+70
View File
@@ -0,0 +1,70 @@
# $COMPANY must be supplied by RMM
$PRODUCT = 'O365ProPlusRetail'
$BITNESS = '64'
$MIGARCH = 'TRUE'
$CHANNEL = 'SemiAnnual'
$UPDATES = 'TRUE'
$DISPLAY = 'None' # "None", "Full"
$ACCEULA = 'TRUE'
# Make changes if the system is 32bit
If ( !(Is64Bit) ) { $BITNESS = '32'; $MIGARCH = 'FALSE' }
$CONFIG = @"
<Configuration ID="e59fdca7-843c-4bee-8edf-33b4f8fb750f">
<Add OfficeClientEdition="${BITNESS}" Channel="${CHANNEL}" MigrateArch="${MIGARCH}">
<Product ID="${PRODUCT}">
<Language ID="en-us" />
<ExcludeApp ID="Groove" />
<ExcludeApp ID="Lync" />
</Product>
</Add>
<Property Name="SharedComputerLicensing" Value="0" />
<Property Name="FORCEAPPSHUTDOWN" Value="TRUE" />
<Property Name="DeviceBasedLicensing" Value="0" />
<Property Name="SCLCacheOverride" Value="0" />
<Updates Enabled="${UPDATES}" Channel="${CHANNEL}" />
<RemoveMSI />
<Display Level="${DISPLAY}" AcceptEULA="${ACCEULA}" />
<AppSettings>
<Setup Name="Company" Value="${COMPANY}" />
</AppSettings>
</Configuration>
"@
# Set path to temp directory
$TEMPDIR = '{0}microsoft-office-deployment' -f (Get-TempPath)
# Recursively delete $TEMPDIR if it already exists
If ( Test-Path $TEMPDIR ) {
Write-Output """${TEMPDIR}"" already exists and will be deleted"
Try { Remove-Item -Path $TEMPDIR -Recurse -Force -ErrorAction SilentlyContinue }
Catch { Write-Error $_.Exception.Message }
}
# Create $TEMPDIR
Write-Output "Creating: ${TEMPDIR}"
Try { New-Item -Path $TEMPDIR -ItemType Directory -Force -ErrorAction Stop }
Catch { Write-Error $_.Exception.Message ; Return }
# Write $CONFIG to config.xml
$CONFIGFILE = "${TEMPDIR}\config.xml"
Try { $CONFIG | WriteToFile_UTF8NoBOM -FilePath "${CONFIGFILE}" }
Catch { Write-Output "Cannot write configuration file! This script will exit." ; Write-Error $_.Exception.Message ; Return }
# Set the name of ODT setup utility
$ODT = "${TEMPDIR}\setup.exe"
# Download ODT
Write-Output "Downloading Office Deployment Tool"
Download-File -URL 'https://xfer.emberkom.com/shares/tools/folders/9b9988fb-c53a-4833-a4ad-68d01a3021bb/files/setup-exe/download/setup.exe' -File $ODT
# Install Office
Write-Output "Installing Microsoft Office"
Try { Start-Process $ODT -ArgumentList "/configure ""${CONFIGFILE}""" -Wait }
Catch { Write-Error $_.Exception.Message }
# Cleanup
Write-Output "Deleting temp directory and its contents"
Try { Remove-Item -Path $TEMPDIR -Recurse -Force -ErrorAction SilentlyContinue }
Catch { Write-Error $_.Exception.Message }
+31
View File
@@ -0,0 +1,31 @@
# This script is intended to be called from a Windows Provisioning Package.
# The provisioning package invokes this script the following way:
# powershell.exe -ExecutionPolicy Bypass -Command ". $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Install-SyncroAgent.ps1'))) ; Install-SyncroAgent -URL 'https://install.syncromsp.com/agent.msi'"
Function Install-SyncroAgent ([string]$URL) {
# Exit if $URL is null or empty
If ([string]::IsNullOrEmpty($URL)) {
Write-Output "The URL parameter cannot be null or empty"
Exit 1
}
# Exit if running without admin rights
If (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
Write-Output "This script must be run as an administrator"
Exit 1
}
# Download and install the Syncro Agent
$Installer = "${Env:TEMP}\SyncroInstaller.msi"
Try { (New-Object System.Net.WebClient).DownloadFile($URL,$Installer) }
Catch { Write-Error "The Syncro Agent installer could not be downloaded from ${URL}`n"+$_.Exception.Message ; Exit 1 }
If ( Test-Path $Installer ) {
Start-Process msiexec.exe -ArgumentList "/i ""${Installer}"" /qn /norestart" -Wait
Remove-Item $Installer -Force -ErrorAction SilentlyContinue
} Else {
Write-Error "The Syncro Agent installer was successfully downloaded but could not be found at ""${Installer}"""
Exit 1
}
Exit 0
}
+60 -2
View File
@@ -1,9 +1,67 @@
# Management Scripts
Most scripts here require the Tools.ps1 script. You can dot source that script like this:
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
```powershell
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
```
On older Windows PowerShell/.NET configurations, select TLS 1.2 **before** the first HTTPS download. The TLS setting inside `Tools.ps1` cannot help download `Tools.ps1` itself. See [Microsoft's PowerShell TLS guidance](https://learn.microsoft.com/en-us/security/engineering/solving-tls1-problem#update-windows-powershell-scripts-or-related-registry-settings).
You can then run any of the scripts using the Run-Script function:
Run-Script -LivePSScript Script-Name
If you're using an RMM tool, the scripts in the /rmm directory are all that's needed to hook the main script in this directory.
If you're using an RMM tool, the scripts in the /rmm directory are all that's needed to hook the main script in this directory.
For DSA collection, paste [rmm/Get-DSAManifest.ps1](rmm/Get-DSAManifest.ps1) into the RMM caller, preserving its configured platform variables. This caller selects TLS 1.2 before downloading, stops on bootstrap failure, and dot-sources the downloaded tools in the caller's scope. Updating the repository alone will not change an existing RMM caller.
For download failures, use the standalone [Test-FileDownload.ps1](Test-FileDownload.ps1) diagnostic. See [the RMM caller, report guide, and shared caller audit](docs/FileDownload-Diagnostics.md). It compares the existing helper, BITS, and direct GET without changing production download behavior or running downloaded files.
`Get-DSAManifest.ps1` uses `Download-FileDirectly`, which passes the original URL to BITS without preliminary `Get-AbsoluteURI` or `IsURLValid` requests. It accepts the same URL and optional `-File` arguments as `Download-File`, waits for completion, and throws on BITS errors. If `-File` is omitted, its temporary filename comes from the original URL; supply `-File` for extensionless URLs or URLs with query strings. Existing `Download-File` callers retain their previous behavior. Publish `Tools.ps1` together with `Get-DSAManifest.ps1` so the new helper is available.
## Converting RMM boolean variables
After dot-sourcing `Tools.ps1`, use `ConvertTo-RmmBoolean` with any RMM variable:
```powershell
$RestartEnabled = ConvertTo-RmmBoolean -Value $Restart -VariableName 'Restart'
If ( $RestartEnabled ) {
# Perform the requested restart.
}
# The variable name is optional; positional and pipeline input also work.
$ZipEnabled = ConvertTo-RmmBoolean $Zip
$UploadEnabled = $Upload | ConvertTo-RmmBoolean
```
Each input returns a `System.Boolean`: `true`, `$true`, `yes`, `y`, and `1` become `$true`; `false`, `$false`, `no`, `n`, `0`, blank strings, and `$null` become `$false`. Case and surrounding whitespace are ignored, and native boolean values also work. Other values throw an error; `-VariableName` identifies the input in that error. The function returns the converted value without changing the original variable; assign the result wherever needed.
`Get-DSAManifest.ps1` uses this helper for its three options. Publish it together with the updated `Tools.ps1` so the helper is available to the RMM caller.
## Uploading files
After dot-sourcing `Tools.ps1`, scripts can upload a completed file to the Emberkom FileDrop:
```powershell
Upload-File -Path 'C:\ProgramData\Emberkom\Output\report.zip'
```
`Get-DSAManifest.ps1` uploads its completed CSV or ZIP when `$Upload` is enabled. If `$DeleteAfterUpload` is also enabled, it deletes that same local file only after a confirmed successful upload. Disabled or failed uploads retain the file. `$Zip`, `$Upload`, and `$DeleteAfterUpload` all accept `true`, `$true`, `yes`, `y`, or `1`; `false`, `$false`, `no`, `n`, `0`, or an empty value disable the option. Casing and surrounding whitespace are ignored. The notification subject identifies the computer, and its message includes the source path and collection start time (local time with UTC offset).
The default destination is `https://xfer.emberkom.com/filedrop/cmd`. The sender defaults to the computer's hostname plus its primary DNS suffix, followed by `@emberkom.com` (for example, `pc01.example.com@emberkom.com`). Without a DNS suffix, it uses the hostname. Override these with `-FileDropUrl` and `-From`; `-Subject` and `-Message` are also optional.
`Upload-File` uses the [LiquidFiles FileDrop API](https://docs.liquidfiles.com/api/v4.3/filedrop/) to obtain a temporary token, upload the file, and submit the message that triggers FileDrop delivery and its configured email notification. It returns a receipt with the path, destination, sender, attachment ID, byte count, and server status. It retains the local file and throws on failure.
Uploads use [binary chunks](https://docs.liquidfiles.com/api/v4.3/attachments/chunks.html) with a default 10 MiB buffer, so large files do not need to fit in memory. The FileDrop's size and extension restrictions are checked before upload. `-ChunkSizeMB`, `-RetryCount`, and `-TimeoutSeconds` control chunk size, retries, and each request's timeout; `-Verbose` displays transfer details.
Allow the RMM job to run for the entire upload. The function waits for completion and retries failed chunks within that run; it does not resume across process restarts. LiquidFiles tokens expire after 24 hours. Final submission is attempted once: if its response is lost, check the FileDrop before rerunning to avoid duplicate notifications.
Offline tests (no uploads or notifications):
```powershell
powershell.exe -NoProfile -File .\tests\Upload-File.Tests.ps1
powershell.exe -NoProfile -File .\tests\Get-DSAManifest.Tests.ps1
powershell.exe -NoProfile -File .\tests\Download-File.Tests.ps1
powershell.exe -NoProfile -File .\tests\ConvertTo-RmmBoolean.Tests.ps1
```
+39
View File
@@ -0,0 +1,39 @@
# Define the username of the provisioning admin account
$ProvisioningAdminUsername = "ek-provadmin"
# Exit if this is a domain controller
If ( $(Get-CimInstance -ClassName Win32_OperatingSystem -Property ProductType | Select-Object -ExpandProperty ProductType) -eq "2" ) {
Write-Output "Cannot create or modify local accounts on a domain controller"
Exit 0
}
# Get the provisioning admin user account
$ProvisioningAdmin = Get-LocalUser -Name $ProvisioningAdminUsername -ErrorAction SilentlyContinue
# Exit if the provisioning admin account does not exist
If ( $ProvisioningAdmin -eq $false ) { Write-Output "Provisioning Admin account does not exist" ; Exit 0 }
# Exit if the user is logged in
If ( $(Get-CimInstance -Class Win32_Process -Filter 'name = "explorer.exe"' | Invoke-CimMethod -MethodName getowner).User -contains $ProvisioningAdminUsername ) {
Write-Output "User is logged in, cannot complete removal of provisioning admin account"
Exit 1
}
# Remove the provisioning admin account
$SID = $ProvisioningAdmin.SID.Value
Write-Output "Removing provisioning admin account`nUsername: ${ProvisioningAdminUsername}`nSID: $SID"
Try { Remove-LocalUser -SID $SID -ErrorAction Stop }
Catch { Write-Error "The provisioning admin account could not be removed`n"+$_.Exception.Message ; Exit 1 }
# Fully remove the provisioning admin profile
Write-Output "Removing provisioning admin CIM instance"
Try { Get-CimInstance -Class Win32_UserProfile | Where-Object { $_.SID -eq $SID } -ErrorAction SilentlyContinue | Remove-CimInstance }
Catch { Write-Error "The provisioning admin profile could not be removed`n"+$_.Exception.Message ; Exit 1 }
# Delete any remaining provisioning admin directories
$ProfilePath = "${Env:SystemDrive}\Users\$ProvisioningAdminUsername"
If ( Test-Path $ProfilePath ) {
Write-Output "Deleting provisioning admin profile directory: ${ProfilePath}"
Try { Remove-Item $ProfilePath -Recurse -Force }
Catch { Write-Error "The provisioning admin profile directory could not be removed`n"+$_.Exception.Message ; Exit 1 }
}
+2 -2
View File
@@ -1,7 +1,7 @@
If ( $(Get-CimInstance -ClassName Win32_OperatingSystem -Property ProductType | Select-Object -ExpandProperty ProductType) -eq "2" ) {
Microsoft.Powershell.Utility\Write-Output "Cannot create or modify local accounts on a domain controller"
Write-Output "Cannot create or modify local accounts on a domain controller"
Return
}
Write-Output "Setting local Administrator password"
Try { net user Administrator $Password /times:all /active:yes }
Catch { Write-Error $_.Exception.Message }
Catch { Write-Error "Could not set local Administrator password`n"+$_.Exception.Message ; Exit 1 }
+91
View File
@@ -0,0 +1,91 @@
# Exit if not running as administrator
If ( -not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator") ) { Write-Output "This script must be run as an administrator" ; Exit 1 }
# Get the operating system CIM instance
Try { $OS = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop }
Catch { Write-Error "Could not get Win32_OperatingSystem CIM instance`n"+$_.Exception.Message ; Exit 1 }
# Exit if the endpoint is not running Windows 10
If ( ($OS.Caption -notmatch "Windows 10") ) { Write-Output "This endpoint is not running Windows 10" ; Exit 1 }
Else { Write-Output "This endpoint is running Windows 10" }
# Exit if the endpoint is not running a Pro/Enterprise edition of Windows
If ( ($OS.Caption -notmatch "Pro") -and ($OS.Caption -notmatch "Enterprise") ) { Write-Output "Only Pro/Enterprise versions of Windows can be upgraded" ; Exit 1 }
Else { Write-Output "This endpoint is running a Pro/Enterprise version of Windows" }
# Exit if the endpoint is a server edition
Try
{
If ( $OS.ProductType -gt 1 ) { Write-Output "Server operating system will not be upgraded" ; Exit 1 }
Else { Write-Output "This endpoint is not a server operating system" }
}
Catch { Write-Error "The operating system type could not be determined`n"+$_.Exception.Message ; Exit 1 }
# Exit if the endpoint does not have at least 4GB of RAM
Try
{
If ( (Get-CimInstance -ClassName Win32_PhysicalMemory -ErrorAction Stop | Measure-Object -Property Capacity -Sum).Sum -lt 4GB ) { Write-Output "This endpoint does not have at least 4GB of RAM" ; Exit 1 }
Else { Write-Output "This endpoint has at least 4GB of RAM" }
}
Catch { Write-Error "The amount of RAM could not be determined`n"+$_.Exception.Message ; Exit 1 }
# Exit if the endpoint does not have at least 64GB of storage on the system drive
Try
{
$SystemDrive = $Env:SystemDrive.TrimEnd('\')
If ( (Get-CimInstance -ClassName Win32_Volume -Filter "DriveLetter = '$SystemDrive'" -ErrorAction Stop).FreeSpace -lt 64GB ) { Write-Output "This endpoint does not have at least 64GB of storage on the system drive" ; Exit 1 }
Else { Write-Output "This endpoint has at least 64GB of free space on the system drive" }
}
Catch { Write-Error "The free space on the system drive could not be determined`n"+$_.Exception.Message ; Exit 1 }
# Exit if TPM does not exist or is not version 2.0
Try
{
$TPM2Present = $false
ForEach ( $specVersion in (Get-CimInstance -Namespace "root\CIMV2\Security\MicrosoftTpm" -Class Win32_Tpm -ErrorAction Stop).SpecVersion )
{
If ( $specVersion -ge 2.0 ) { $TPM2Present = $true ; Break }
}
If ( $TPM2Present -eq $false ) { Write-Output "TPM does not support version 2.0 specification" ; Exit 1 }
Else { Write-Output "This endpoint has a TPM compliant with the version 2.0 specification" }
}
Catch { Write-Error "TPM is not present or its version cannot be determined`n"+$_.Exception.Message ; Exit 1 }
# Exit if Secure Boot is not enabled
Try
{
If ( (Confirm-SecureBootUEFI -ErrorAction Stop) -eq $false ) { Write-Output "Secure Boot is not enabled" ; Exit 1 }
Else { Write-Output "This endpoint has Secure Boot enabled" }
}
Catch { Write-Output "Secure Boot state cannot be determined.`n"+$_.Exception.Message ; Exit 1 }
# Uninstall previous Windows 11 Update Assistant
$WIAPath = "${Env:ProgramFiles(x86)}\WindowsInstallationAssistant\Windows10UpgraderApp.exe"
If ( Test-Path $WIAPath )
{
Write-Output "Uninstalling existing Windows Installation Assistant"
Get-Process -Name "Windows10UpgraderApp" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
Try { Start-Process $WIAPath -ArgumentList '/ForceUninstall' -Wait }
Catch { Write-Error $_.Exception.Message ; Exit 1}
} Else { Write-Output "This endpoint does not have the Windows Installation Assistant installed" }
# URL to the Windows Installation Assistant
$URL = 'https://go.microsoft.com/fwlink/?linkid=2171764'
# Download Windows Installation Assistant
$WIAInstaller = "${Env:TEMP}\Windows11InstallationAssistant.exe"
If ( Test-Path $WIAInstaller )
{
Write-Output "Removing existing Windows Installation Assistant installer: ${WIAInstaller}"
Remove-Item $WIAInstaller -Force -ErrorAction SilentlyContinue
}
Write-Output "Downloading ${URL} to ""${WIAInstaller}"""
Try { (New-Object System.Net.WebClient).DownloadFile($URL,$WIAInstaller) }
Catch { Write-Error "The Windows Installation Assistant could not be downloaded`n"+$_.Exception.Message ; Exit 1 }
# Install and run the Windows Installation Assistant
If ( -not (Test-Path $WIAInstaller) ) { Write-Error "The Windows Installation Assistant could not be found after successfully downloading" ; Exit 1 }
Write-Output "Launching upgrade"
Try { Start-Process -FilePath $WIAInstaller -ArgumentList "/QuietInstall /SkipEULA /Auto Upgrade /SkipSelfUpdate /ShowProgressInTaskBarIcon /SkipCompatCheck" }
Catch { Write-Output $_.Exception.Message ; Exit 1 }
Exit 0
+540
View File
@@ -0,0 +1,540 @@
#Requires -Version 2.0
<#
.SYNOPSIS
Compares download paths without changing the production downloader or TLS settings.
.DESCRIPTION
Run from the failing RMM caller, after its normal Tools.ps1 bootstrap, using the
same account and PowerShell executable. A standalone run skips the helper probe
when Download-File is not loaded. Never dot-sources a downloaded payload.
Each probe runs in a bounded child process. Only that run's BITS jobs and scratch
files are removed. Reports remain in a new subdirectory of OutputDirectory.
The source deliberately uses PowerShell 2 syntax and .NET 2-era APIs. Actual
PowerShell 2, 3, and 4 runtime validation is still required on legacy machines.
.PARAMETER URL
HTTP or HTTPS file URL. Downloads are never executed.
.PARAMETER OutputDirectory
Report parent directory; defaults to the account's temporary directory.
.PARAMETER TimeoutSeconds
Wall-clock limit for each probe, including download and hashing. Defaults to 120.
.EXAMPLE
& .\Test-FileDownload.ps1 -URL 'https://example.com/tool.exe'
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, Position=0)][string]$URL,
[string]$OutputDirectory = [IO.Path]::GetTempPath(),
[ValidateRange(1,3600)][int]$TimeoutSeconds = 120
)
# All diagnostic state is local even when the caller dot-sources this script.
& {
param($FdUrlText, $FdOutputParent, $FdTimeout)
$ErrorActionPreference = 'Stop'
$FdUri = New-Object Uri $FdUrlText
if (!$FdUri.IsAbsoluteUri -or @('http','https') -notcontains $FdUri.Scheme -or $FdUri.UserInfo) {
throw 'URL must be an absolute HTTP(S) URL without embedded credentials.'
}
if ($PSVersionTable.PSVersion.Major -gt 5) { throw 'Run this diagnostic in Windows PowerShell 2-5.1, using the same shell as the RMM.' }
$FdRunId = [guid]::NewGuid().ToString('N')
$FdParent = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FdOutputParent)
$FdRoot = Join-Path $FdParent ('file-download-diagnostic-' + $FdRunId)
$null = New-Item -Path $FdRoot -ItemType Directory -ErrorAction Stop
$FdWork = Join-Path $FdRoot 'work'
$null = New-Item -Path $FdWork -ItemType Directory -ErrorAction Stop
$FdExe = [Diagnostics.Process]::GetCurrentProcess().MainModule.FileName
if ([IO.Path]::GetFileName($FdExe) -ine 'powershell.exe') {
throw 'Run from powershell.exe rather than ISE or an embedded host so child probes can use the same executable safely.'
}
$FdExtension = [IO.Path]::GetExtension($FdUri.AbsolutePath)
if ($FdExtension -notmatch '^\.[A-Za-z0-9]{1,16}$') { $FdExtension = '.download' }
function Get-FdTextHash($Text) {
$FdHash = [Security.Cryptography.SHA256]::Create()
try { return [BitConverter]::ToString($FdHash.ComputeHash([Text.Encoding]::UTF8.GetBytes($Text))).Replace('-','') }
finally { $FdHash.Clear() }
}
function Export-FdXml($Value,$Path,$Depth) {
# PowerShell 2 lacks some LiteralPath parameter sets. A scoped drive keeps
# wildcard characters in the parent directory out of Export-Clixml's path.
$Drive = 'FdXml' + [guid]::NewGuid().ToString('N')
$null = New-PSDrive -Name $Drive -PSProvider FileSystem -Root ([IO.Path]::GetDirectoryName($Path)) -Scope Local
try { $Value | Export-Clixml -Path ($Drive + ':\' + [IO.Path]::GetFileName($Path)) -Depth $Depth }
finally { Remove-PSDrive -Name $Drive -Scope Local }
}
$FdCommands = @()
foreach ($FdName in @('Download-File','Get-AbsoluteURI','IsURLValid','Get-TempPath','Write-Error','Write-Output','Start-BitsTransfer')) {
$FdCommand = Get-Command -Name $FdName -ErrorAction SilentlyContinue | Select-Object -First 1
$FdDefinition = $null
if ($FdCommand -and @('Function','Filter') -contains [string]$FdCommand.CommandType) { $FdDefinition = $FdCommand.Definition }
$FdCommands += New-Object PSObject -Property @{
Name=$FdName; CommandType=[string]$FdCommand.CommandType; ModuleName=$FdCommand.ModuleName
ModuleVersion=[string]$FdCommand.Module.Version; Definition=$FdDefinition
DefinitionSHA256=$(if ($null -ne $FdDefinition) { Get-FdTextHash $FdDefinition } else { $null })
ResolvedDefinition=$FdCommand.Definition
}
}
$FdContext = New-Object PSObject -Property @{
RunId=$FdRunId; URL=$FdUri.AbsoluteUri; Commands=$FdCommands
SecurityProtocol=[int][Net.ServicePointManager]::SecurityProtocol
CertificateCallbackPresent=($null -ne [Net.ServicePointManager]::ServerCertificateValidationCallback)
ProxyType=$(if ([Net.WebRequest]::DefaultWebProxy) { [Net.WebRequest]::DefaultWebProxy.GetType().FullName } else { 'None' })
ProxyAddress=[string]([Net.WebRequest]::DefaultWebProxy.Address)
PowerShellVersion=[string]$PSVersionTable.PSVersion; CLRVersion=[string][Environment]::Version
ProcessBits=([IntPtr]::Size * 8); Executable=$FdExe
Identity=[Security.Principal.WindowsIdentity]::GetCurrent().Name
WorkingDirectory=(Get-Location).Path; TimeoutSeconds=$FdTimeout; DestinationExtension=$FdExtension
}
$FdContextPath = Join-Path $FdWork 'context.clixml'
Export-FdXml $FdContext $FdContextPath 12
# This is diagnostic code, not code fetched from the URL. It is copied into each
# fresh child so leaked connections and preference changes cannot cross probes.
$FdWorker = {
param($InputPath)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$InputData = Import-Clixml -Path ([Management.Automation.WildcardPattern]::Escape($InputPath))
$Context = Import-Clixml -Path ([Management.Automation.WildcardPattern]::Escape($InputData.ContextPath))
$Result = New-Object PSObject -Property @{
Name=$InputData.Name; Method=$InputData.Method; ExistingEmpty=$InputData.ExistingEmpty
Status='Running'; StartedUtc=[DateTime]::UtcNow.ToString('o'); ElapsedSeconds=0
Errors=@(); Notes=@(); Http=@(); Bits=@(); Snapshots=@(); HelperOutput=@(); HelperBitsSource=@(); Environment=$null
CleanupErrors=@(); ChildPowerShell=[string]$PSVersionTable.PSVersion
ChildCLR=[string][Environment]::Version; ChildIdentity=[Security.Principal.WindowsIdentity]::GetCurrent().Name
ChildProcessBits=([IntPtr]::Size * 8)
}
$Watch = [Diagnostics.Stopwatch]::StartNew()
function Export-FdXml($Value,$Path,$Depth) {
$Drive = 'FdXml' + [guid]::NewGuid().ToString('N')
$null = New-PSDrive -Name $Drive -PSProvider FileSystem -Root ([IO.Path]::GetDirectoryName($Path)) -Scope Local
try { $Value | Export-Clixml -Path ($Drive + ':\' + [IO.Path]::GetFileName($Path)) -Depth $Depth }
finally { Remove-PSDrive -Name $Drive -Scope Local }
}
function Save-Checkpoint {
$Result.ElapsedSeconds = [Math]::Round($Watch.Elapsed.TotalSeconds,3)
# Replace only this probe's checkpoint; the parent reads it after child exit.
Export-FdXml $Result $InputData.ResultPath 16
}
function Get-ErrorDetail($Record) {
return New-Object PSObject -Property @{
Message=$Record.Exception.Message; Exception=$Record.Exception.ToString()
FullyQualifiedErrorId=$Record.FullyQualifiedErrorId
Category=[string]$Record.CategoryInfo; Position=$Record.InvocationInfo.PositionMessage
Record=($Record | Format-List * -Force | Out-String -Width 240)
}
}
function Get-FileSnapshot($Label) {
$Snapshot = New-Object PSObject -Property @{Label=$Label; TimeUtc=[DateTime]::UtcNow.ToString('o'); Exists=$false; Length=$null; LengthAfterHash=$null; SHA256=$null; FirstBytes=$null; Error=$null}
$Stream = $null; $Hash = $null
try {
if (Test-Path -LiteralPath $InputData.Destination -PathType Leaf) {
$Snapshot.Exists = $true
$Snapshot.Length = (Get-Item -LiteralPath $InputData.Destination).Length
$Stream = [IO.File]::Open($InputData.Destination,[IO.FileMode]::Open,[IO.FileAccess]::Read,([IO.FileShare]::ReadWrite -bor [IO.FileShare]::Delete))
$Prefix = New-Object byte[] 8
$Count = $Stream.Read($Prefix,0,$Prefix.Length)
if ($Count -gt 0) { $Snapshot.FirstBytes = [BitConverter]::ToString($Prefix,0,$Count) }
$Stream.Position = 0
$Hash = [Security.Cryptography.SHA256]::Create()
$Snapshot.SHA256 = [BitConverter]::ToString($Hash.ComputeHash($Stream)).Replace('-','')
$Snapshot.LengthAfterHash = (Get-Item -LiteralPath $InputData.Destination).Length
}
} catch { $Snapshot.Error = Get-ErrorDetail $_ }
finally { if ($Stream) { $Stream.Dispose() }; if ($Hash) { $Hash.Clear() } }
return $Snapshot
}
function Add-BitsSnapshot($Job, $Stage) {
$Result.Bits += New-Object PSObject -Property @{
Stage=$Stage; TimeUtc=[DateTime]::UtcNow.ToString('o'); JobId=[string]$Job.JobId
JobState=[string]$Job.JobState; BytesTotal=$Job.BytesTotal; BytesTransferred=$Job.BytesTransferred
FilesTotal=$Job.FilesTotal; FilesTransferred=$Job.FilesTransferred
ErrorCode=$Job.ErrorCode; ErrorContext=[string]$Job.ErrorContext; ErrorDescription=$Job.ErrorDescription
ProxyUsage=[string]$Job.ProxyUsage; OwnerAccount=$Job.OwnerAccount
}
Save-Checkpoint
}
function Remove-OwnedBitsJobs {
try {
Import-Module BitsTransfer -ErrorAction Stop
$Jobs = @(BitsTransfer\Get-BitsTransfer -ErrorAction Stop | Where-Object {
$_.DisplayName -eq $InputData.Tag -and $_.Description -eq $Context.RunId
})
foreach ($Job in $Jobs) {
Add-BitsSnapshot $Job 'Before cleanup'
BitsTransfer\Remove-BitsTransfer -BitsJob $Job -ErrorAction Stop
}
} catch { $Result.CleanupErrors += Get-ErrorDetail $_ }
}
function Add-HttpSnapshot($Response, $Method, $RequestUri) {
$Headers = @{}
foreach ($Header in @('Content-Length','Content-Range','Content-Type','Transfer-Encoding','Content-Encoding','Accept-Ranges','ETag','Last-Modified','Location','Via','Age','Server')) {
$Headers[$Header] = $Response.Headers[$Header]
}
$Result.Http += New-Object PSObject -Property @{
Method=$Method; RequestUri=$RequestUri; ResponseUri=$Response.ResponseUri.AbsoluteUri
Status=[int]$Response.StatusCode; ContentLength=$Response.ContentLength; Headers=$Headers
}
Save-Checkpoint
}
function Open-HttpResponse($Method, $UseRange) {
$CurrentUri = New-Object Uri $Context.URL
for ($Redirect = 0; $Redirect -le 10; $Redirect++) {
$Request = [Net.HttpWebRequest]::Create($CurrentUri)
$Request.Method = $Method
$Request.AllowAutoRedirect = $false
$Request.Timeout = $Context.TimeoutSeconds * 1000
$Request.ReadWriteTimeout = $Context.TimeoutSeconds * 1000
if ($UseRange) { $Request.AddRange(0,15) }
try { $Response = $Request.GetResponse() }
catch {
if ($_.Exception.Response) {
try { Add-HttpSnapshot $_.Exception.Response $Method $CurrentUri.AbsoluteUri }
finally { $_.Exception.Response.Close() }
}
throw
}
Add-HttpSnapshot $Response $Method $CurrentUri.AbsoluteUri
if (@(301,302,303,307,308) -contains [int]$Response.StatusCode) {
try { $NextUri = New-Object Uri $CurrentUri, $Response.Headers['Location'] }
finally { $Response.Close() }
if (@('http','https') -notcontains $NextUri.Scheme -or $NextUri.UserInfo) { throw 'Unsupported redirect target.' }
if ($CurrentUri.Scheme -eq 'https' -and $NextUri.Scheme -eq 'http') { throw 'HTTPS-to-HTTP redirect recorded but not followed.' }
$CurrentUri = $NextUri
} else { return $Response }
}
throw 'HTTP redirect limit exceeded.'
}
try {
Save-Checkpoint
# A child does not inherit this process-local .NET setting. Reproduce the
# caller's exact value, rather than selecting a new protocol or changing Windows.
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]$Context.SecurityProtocol
if ($InputData.Method -eq 'Cleanup') {
Remove-OwnedBitsJobs
$Result.Status = 'Complete'
} elseif ($InputData.Method -eq 'Environment') {
$Info = @{}
$Result.Environment = $Info
try { $Info['OS'] = Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop | Select-Object Caption,Version,BuildNumber,OSArchitecture,ServicePackMajorVersion }
catch { $Info['OSError'] = Get-ErrorDetail $_ }
Save-Checkpoint
$Info['BitsService'] = Get-Service -Name BITS -ErrorAction SilentlyContinue | Select-Object Name,Status,StartType
$Info['BitsModules'] = @(Get-Module -ListAvailable -Name BitsTransfer | Select-Object Name,Version,Path)
$BitsBinary = Join-Path $Env:WINDIR 'System32\qmgr.dll'
$NativeBitsBinary = Join-Path $Env:WINDIR 'Sysnative\qmgr.dll'
if (Test-Path -LiteralPath $NativeBitsBinary) { $BitsBinary = $NativeBitsBinary }
$Info['BitsBinaryPath'] = $BitsBinary
try { $Info['BitsBinaryVersion'] = [Diagnostics.FileVersionInfo]::GetVersionInfo($BitsBinary).FileVersion }
catch { $Info['BitsBinaryVersionError'] = Get-ErrorDetail $_ }
$Info['DotNetProxyType'] = $Context.ProxyType
$Info['CallerProxyAddress'] = $Context.ProxyAddress
$Info['DotNetSecurityProtocol'] = [string][Net.ServicePointManager]::SecurityProtocol
$Info['CertificateCallbackPresent'] = $Context.CertificateCallbackPresent
Save-Checkpoint
$Info['WinHttpProxy'] = (& "$Env:WINDIR\System32\netsh.exe" winhttp show proxy 2>&1 | Out-String)
$Info['WinHttpRegistryViews'] = @{}
foreach ($View in @('32','64')) {
# reg.exe supports explicit views without requiring the .NET 4 RegistryView API.
try { $Info['WinHttpRegistryViews'][$View] = (& "$Env:WINDIR\System32\reg.exe" query 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\WinHttp' /v DefaultSecureProtocols ("/reg:" + $View) 2>&1 | Out-String) }
catch { $Info['WinHttpRegistryViews'][$View] = $_.Exception.Message }
}
$Info['Registry'] = @()
foreach ($Key in @(
'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings',
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\WinHttp',
'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Internet Settings\WinHttp',
'HKLM:\SOFTWARE\Microsoft\.NETFramework\v2.0.50727',
'HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319',
'HKLM:\SOFTWARE\Wow6432Node\Microsoft\.NETFramework\v2.0.50727',
'HKLM:\SOFTWARE\Wow6432Node\Microsoft\.NETFramework\v4.0.30319',
'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client',
'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client',
'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client'
)) {
$Values = Get-ItemProperty -LiteralPath $Key -ErrorAction SilentlyContinue
$Info['Registry'] += New-Object PSObject -Property @{
Key=$Key; Present=($null -ne $Values); Enabled=$Values.Enabled; DisabledByDefault=$Values.DisabledByDefault
DefaultSecureProtocols=$Values.DefaultSecureProtocols; SchUseStrongCrypto=$Values.SchUseStrongCrypto
SystemDefaultTlsVersions=$Values.SystemDefaultTlsVersions
ProxyEnable=$Values.ProxyEnable; ProxyServer=$Values.ProxyServer; AutoConfigURL=$Values.AutoConfigURL
}
}
Save-Checkpoint
$Info['DotNetResolvedProxy'] = $(if ([Net.WebRequest]::DefaultWebProxy) { [Net.WebRequest]::DefaultWebProxy.GetProxy((New-Object Uri $Context.URL)).AbsoluteUri } else { 'None' })
$Result.Status = 'Complete'
} elseif ($InputData.Method -eq 'Headers') {
foreach ($HttpMethod in @('HEAD','Range')) {
$Response = $null
try {
if ($HttpMethod -eq 'HEAD') { $Response = Open-HttpResponse 'HEAD' $false }
else { $Response = Open-HttpResponse 'GET' $true }
} catch { $Result.Errors += Get-ErrorDetail $_ }
finally { if ($Response) { $Response.Close() } }
}
$Result.Status = $(if ($Result.Errors.Count) { 'Failed' } else { 'Complete' })
} else {
if ($InputData.ExistingEmpty) { [IO.File]::WriteAllBytes($InputData.Destination,(New-Object byte[] 0)) }
$Result.Snapshots += Get-FileSnapshot 'Before transfer'
Save-Checkpoint
if ($InputData.Method -eq 'Helper') {
$Helper = @($Context.Commands | Where-Object { $_.Name -eq 'Download-File' -and $_.Definition })
$BitsResolution = $Context.Commands | Where-Object { $_.Name -eq 'Start-BitsTransfer' }
if ($Helper.Count -eq 0) {
$Result.Status = 'Skipped'
$Result.Notes += 'Download-File was not a loaded function; no Tools.ps1 was fetched or sourced.'
} elseif ($BitsResolution.CommandType -ne 'Cmdlet' -or $BitsResolution.ModuleName -ne 'BitsTransfer') {
$Result.Status = 'Skipped'
$Result.Notes += 'Start-BitsTransfer is shadowed or unavailable. Definition recorded; helper not run because its jobs cannot be safely tagged.'
} else {
Import-Module BitsTransfer -ErrorAction Stop
foreach ($Command in $Context.Commands) {
if ($Command.Definition -and $Command.Name -ne 'Start-BitsTransfer') {
. ([scriptblock]::Create(('function global:{0} {{ {1} }}' -f $Command.Name,$Command.Definition)))
}
}
$Global:LogFile = $InputData.LogPath
# Preserve all native BITS parameters; add only ownership tags so
# a timed-out synchronous job can be identified without touching others.
$Global:FdNativeBits = Get-Command BitsTransfer\Start-BitsTransfer
$Global:FdBitsTag = $InputData.Tag
$Global:FdBitsRun = $Context.RunId
$Global:FdWorkerResult = $Result
function global:Start-BitsTransfer {
[CmdletBinding()] param()
dynamicparam {
$Dictionary = New-Object Management.Automation.RuntimeDefinedParameterDictionary
foreach ($Parameter in $Global:FdNativeBits.Parameters.Values) {
if (@('Verbose','Debug','ErrorAction','WarningAction','InformationAction','ErrorVariable','WarningVariable','InformationVariable','OutVariable','OutBuffer','PipelineVariable','ProgressAction') -notcontains $Parameter.Name) {
$Attributes = New-Object 'Collections.ObjectModel.Collection[Attribute]'
foreach ($Attribute in $Parameter.Attributes) { $Attributes.Add($Attribute) }
$Dynamic = New-Object Management.Automation.RuntimeDefinedParameter $Parameter.Name,$Parameter.ParameterType,$Attributes
$Dictionary.Add($Parameter.Name,$Dynamic)
}
}
return $Dictionary
}
process {
$Global:FdWorkerResult.HelperBitsSource += @($PSBoundParameters['Source'])
$PSBoundParameters['DisplayName'] = $Global:FdBitsTag
$PSBoundParameters['Description'] = $Global:FdBitsRun
& $Global:FdNativeBits @PSBoundParameters
}
}
$Result.Notes += 'Loaded helper functions copied into a fresh process; BITS display name/description tagged for cleanup; helper logs redirected to diagnostic scratch.'
$Result.HelperOutput = @(Download-File -URL $Context.URL -File $InputData.Destination -ErrorAction Stop 2>&1 | ForEach-Object { [string]$_ })
}
} elseif ($InputData.Method -eq 'BitsSync') {
Import-Module BitsTransfer -ErrorAction Stop
BitsTransfer\Start-BitsTransfer -Source $Context.URL -Destination $InputData.Destination -DisplayName $InputData.Tag -Description $Context.RunId -ErrorAction Stop
} elseif ($InputData.Method -eq 'BitsJob') {
Import-Module BitsTransfer -ErrorAction Stop
$Job = BitsTransfer\Start-BitsTransfer -Source $Context.URL -Destination $InputData.Destination -DisplayName $InputData.Tag -Description $Context.RunId -Asynchronous -ErrorAction Stop
$PreviousState = ''
while ($true) {
$Job = BitsTransfer\Get-BitsTransfer -JobId $Job.JobId -ErrorAction Stop
if ([string]$Job.JobState -ne $PreviousState) { Add-BitsSnapshot $Job 'State change'; $PreviousState = [string]$Job.JobState }
if (@('Transferred','Error','TransientError','Suspended','Cancelled','Acknowledged') -contains [string]$Job.JobState) { break }
Start-Sleep -Milliseconds 250
}
if ([string]$Job.JobState -ne 'Transferred') { throw ('BITS job ended probe in {0}: {1}' -f $Job.JobState,$Job.ErrorDescription) }
Add-BitsSnapshot $Job 'Before completion'
BitsTransfer\Complete-BitsTransfer -BitsJob $Job -ErrorAction Stop
} elseif ($InputData.Method -eq 'DotNetGet') {
$Response = $null; $InputStream = $null; $OutputStream = $null
try {
$Response = Open-HttpResponse 'GET' $false
$InputStream = $Response.GetResponseStream()
$OutputStream = [IO.File]::Open($InputData.Destination,[IO.FileMode]::Create,[IO.FileAccess]::Write,[IO.FileShare]::Read)
$Buffer = New-Object byte[] 65536
$Received = [long]0
while (($Count = $InputStream.Read($Buffer,0,$Buffer.Length)) -gt 0) { $OutputStream.Write($Buffer,0,$Count); $Received += $Count }
if ($Response.ContentLength -ge 0 -and $Received -ne $Response.ContentLength) { throw "HTTP body length mismatch: expected $($Response.ContentLength), received $Received." }
} finally {
if ($OutputStream) { $OutputStream.Dispose() }
if ($InputStream) { $InputStream.Dispose() }
if ($Response) { $Response.Close() }
}
}
if ($Result.Status -ne 'Skipped') {
$Result.Snapshots += Get-FileSnapshot 'After transfer'
Save-Checkpoint
Start-Sleep -Milliseconds 1000
$Result.Snapshots += Get-FileSnapshot 'One second later'
$After = $Result.Snapshots[$Result.Snapshots.Count-1]
$Result.Status = $(if ($After.Error) { 'Failed' } elseif (!$After.Exists) { 'MissingFile' } elseif ($After.Length -eq 0) { 'EmptyFile' } else { 'Complete' })
}
}
} catch {
$Result.Status = 'Failed'
$Result.Errors += Get-ErrorDetail $_
if ($InputData.Destination) { $Result.Snapshots += Get-FileSnapshot 'After error' }
} finally {
Save-Checkpoint
if (@('Helper','BitsSync','BitsJob') -contains $InputData.Method) { Remove-OwnedBitsJobs }
Save-Checkpoint
}
}
$FdWorkerPath = Join-Path $FdWork 'worker.ps1'
[IO.File]::WriteAllText($FdWorkerPath,$FdWorker.ToString(),[Text.Encoding]::UTF8)
# Capture both child pipes concurrently without PowerShell callbacks on foreign
# threads. .NET file APIs also avoid Start-Process's wildcard path expansion.
if (!('EmberkomDownloadDiagnostic.ChildRunner' -as [type])) {
Add-Type -TypeDefinition @'
using System;
using System.Diagnostics;
using System.IO;
namespace EmberkomDownloadDiagnostic {
public class ChildResult { public bool TimedOut; public int ExitCode; }
public static class ChildRunner {
public static ChildResult Run(string exe,string args,string directory,string stdout,string stderr,int milliseconds) {
using (StreamWriter output=new StreamWriter(stdout))
using (StreamWriter error=new StreamWriter(stderr))
using (Process child=new Process()) {
child.StartInfo.FileName=exe; child.StartInfo.Arguments=args;
child.StartInfo.WorkingDirectory=directory;
child.StartInfo.UseShellExecute=false; child.StartInfo.CreateNoWindow=true;
child.StartInfo.WindowStyle=ProcessWindowStyle.Hidden;
child.StartInfo.RedirectStandardOutput=true; child.StartInfo.RedirectStandardError=true;
child.OutputDataReceived+=delegate(object sender,DataReceivedEventArgs e) { if(e.Data!=null) lock(output) { output.WriteLine(e.Data); } };
child.ErrorDataReceived+=delegate(object sender,DataReceivedEventArgs e) { if(e.Data!=null) lock(error) { error.WriteLine(e.Data); } };
child.Start(); child.BeginOutputReadLine(); child.BeginErrorReadLine();
ChildResult result=new ChildResult();
result.TimedOut=!child.WaitForExit(milliseconds);
if(result.TimedOut) {
try { child.Kill(); } catch(InvalidOperationException) { }
if(!child.WaitForExit(5000)) throw new TimeoutException("Diagnostic child did not exit after termination.");
}
child.WaitForExit(); // Process has exited; drain the asynchronous pipe callbacks.
result.ExitCode=child.ExitCode;
return result;
}
}
}
}
'@
}
function Invoke-FdWorker($Spec, $Limit) {
$InputFile = Join-Path $FdWork ($Spec.Name + '-input.clixml')
Export-FdXml $Spec $InputFile 8
$Code = '& ([scriptblock]::Create([IO.File]::ReadAllText(''' + $FdWorkerPath.Replace("'","''") + '''))) ''' + $InputFile.Replace("'","''") + ''''
$Encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($Code))
$Arguments = '-NoProfile -NonInteractive '
if ($PSVersionTable.PSVersion.Major -eq 2) { $Arguments += '-Version 2.0 ' }
$Arguments += '-EncodedCommand ' + $Encoded
$Child = [EmberkomDownloadDiagnostic.ChildRunner]::Run($FdExe,$Arguments,$FdContext.WorkingDirectory,(Join-Path $FdWork ($Spec.Name + '-stdout.txt')),(Join-Path $FdWork ($Spec.Name + '-stderr.txt')),($Limit * 1000))
$TimedOut = $Child.TimedOut
$ExitCode = $Child.ExitCode
$Item = $null
if (Test-Path -LiteralPath $Spec.ResultPath) {
try { $Item = Import-Clixml -Path ([Management.Automation.WildcardPattern]::Escape($Spec.ResultPath)) } catch { $Item = $null }
}
if (!$Item) { $Item = New-Object PSObject -Property @{Name=$Spec.Name; Method=$Spec.Method; ExistingEmpty=$Spec.ExistingEmpty; Status='Failed'; Notes=@('Child did not produce a readable result.'); Errors=@(); Snapshots=@(); CleanupErrors=@(); ElapsedSeconds=$Limit} }
if ($TimedOut) { $Item.Status = 'TimedOut'; $Item.Notes += "Stopped after $Limit seconds; partial checkpoint retained." }
elseif (($null -ne $ExitCode -and $ExitCode -ne 0) -or $Item.Status -eq 'Running') { $Item.Status = 'Failed' }
foreach ($StreamName in @('stdout','stderr')) {
$StreamPath = Join-Path $FdWork ($Spec.Name + '-' + $StreamName + '.txt')
$StreamText = $(if (Test-Path -LiteralPath $StreamPath) { [IO.File]::ReadAllText($StreamPath) } else { '' })
Add-Member -InputObject $Item -MemberType NoteProperty -Name $StreamName -Value $StreamText
}
Add-Member -InputObject $Item -MemberType NoteProperty -Name ChildExitCode -Value $ExitCode
return $Item
}
$FdReport = New-Object PSObject -Property @{
SchemaVersion=1; RunId=$FdRunId; StartedUtc=[DateTime]::UtcNow.ToString('o'); Context=$FdContext
Probes=@(); Cleanup=@(); Findings=@(); ReportDirectory=$FdRoot
Limitations=@(
'Fresh child processes reproduce the caller TLS selection but not existing pooled connections, custom certificate callbacks, or custom in-memory proxy objects.',
'Registry paths reflect this process registry view; a 32-bit process may not expose the native 64-bit view.',
'Helper dependencies outside the captured functions/global LogFile are not reproduced.',
'Scratch files preserve the original URL extension when usable, but not the production path; path-specific security behavior may differ.',
'PowerShell 2-4 compatibility requires actual runtime validation; newer features elsewhere in Tools.ps1 are separate from this diagnostic.'
)
}
$FdReportXml = Join-Path $FdRoot 'report.clixml'
$FdReportText = Join-Path $FdRoot 'report.txt'
try {
$FdCases = @('Environment','Headers','Helper','BitsSync','BitsJob','DotNetGet')
foreach ($FdMethod in $FdCases) {
$FdVariants = @($false)
if (@('Helper','BitsSync','BitsJob','DotNetGet') -contains $FdMethod) { $FdVariants = @($false,$true) }
foreach ($FdExisting in $FdVariants) {
$FdName = $FdMethod + $(if ($FdExisting) { '-existing-empty' } else { '-new' })
$FdSpec = New-Object PSObject -Property @{
Name=$FdName; Method=$FdMethod; ExistingEmpty=$FdExisting; ContextPath=$FdContextPath
ResultPath=(Join-Path $FdWork ($FdName + '-result.clixml'))
Destination=(Join-Path $FdWork ($FdName + $FdExtension))
LogPath=(Join-Path $FdWork ($FdName + '-helper.log'))
Tag=('Emberkom-DownloadDiagnostic-' + $FdRunId + '-' + $FdName)
}
Write-Host ("Testing {0} (limit {1}s)..." -f $FdName,$FdTimeout)
$FdResult = Invoke-FdWorker $FdSpec $FdTimeout
if (Test-Path -LiteralPath $FdSpec.LogPath) { Add-Member -InputObject $FdResult -MemberType NoteProperty -Name HelperLog -Value ([IO.File]::ReadAllText($FdSpec.LogPath)) }
$FdReport.Probes += $FdResult
Export-FdXml $FdReport $FdReportXml 20
# A killed child cannot run finally. Always use a separate, bounded
# cleanup worker; filter by BOTH unpredictable run ID and exact tag.
if (@('Helper','BitsSync','BitsJob') -contains $FdMethod) {
$FdSpec.Method = 'Cleanup'
$FdSpec.Name += '-cleanup'
$FdSpec.ResultPath = Join-Path $FdWork ($FdSpec.Name + '-result.clixml')
$FdReport.Cleanup += Invoke-FdWorker $FdSpec 15
}
}
}
$FdDownloads = @($FdReport.Probes | Where-Object { @('Helper','BitsSync','BitsJob','DotNetGet') -contains $_.Method -and $_.Status -ne 'Skipped' })
$FdBad = @($FdDownloads | Where-Object { $_.Status -ne 'Complete' })
$FdHashes = @($FdDownloads | Where-Object { $_.Status -eq 'Complete' } | ForEach-Object { $_.Snapshots[$_.Snapshots.Count-1].SHA256 } | Sort-Object -Unique)
if ($FdBad.Count -eq 0 -and $FdHashes.Count -eq 1) {
$FdReport.Findings += 'All tested download paths produced the same nonempty file. Failure not reproduced; no transport replacement is justified by this run.'
} else {
$FdReport.Findings += 'A failure or content difference was observed. Compare per-probe errors, HTTP responses, BITS byte counts, and file snapshots before choosing a shared fix.'
}
foreach ($FdProbe in $FdDownloads) {
$FdAfter = @($FdProbe.Snapshots | Where-Object { $_.Label -eq 'After transfer' -or $_.Label -eq 'One second later' })
if ($FdAfter.Count -eq 2 -and ($FdAfter[0].SHA256 -ne $FdAfter[1].SHA256 -or $FdAfter[0].Length -ne $FdAfter[1].Length)) {
$FdReport.Findings += "File changed after transfer in $($FdProbe.Name); investigate post-download modification."
}
}
} finally {
# This directory is created exclusively by this invocation. Delete only its
# immediate scratch files; never recurse or touch the production destination.
$FdResolvedWork = (Resolve-Path -LiteralPath $FdWork).Path
$FdExpectedWork = [IO.Path]::GetFullPath((Join-Path $FdRoot 'work'))
if ($FdResolvedWork -ne $FdExpectedWork -or (Get-Item -LiteralPath $FdResolvedWork).Attributes -band [IO.FileAttributes]::ReparsePoint) {
$FdReport.Findings += 'Scratch path validation failed; cleanup skipped.'
} else {
foreach ($FdScratch in @(Get-ChildItem -LiteralPath $FdResolvedWork -Force)) {
if (!$FdScratch.PSIsContainer) {
try { Remove-Item -LiteralPath $FdScratch.FullName -Force -ErrorAction Stop }
catch { $FdReport.Findings += ('Could not remove scratch file: ' + $FdScratch.FullName + ': ' + $_.Exception.Message) }
}
}
if (@(Get-ChildItem -LiteralPath $FdResolvedWork -Force).Count -eq 0) { Remove-Item -LiteralPath $FdResolvedWork -Force }
}
Export-FdXml $FdReport $FdReportXml 20
# A readable recursive rendering retains nested exception and HTTP details.
function Format-FdReport($Value, $Indent) {
if ($null -eq $Value) { return ($Indent + '<null>') }
if ($Value -is [string] -or $Value -is [ValueType]) { return ($Indent + [string]$Value) }
if ($Value -is [Collections.IDictionary]) {
foreach ($Key in @($Value.Keys | Sort-Object)) { $Indent + [string]$Key + ':'; Format-FdReport $Value[$Key] ($Indent + ' ') }
} elseif ($Value -is [Collections.IEnumerable]) {
foreach ($Entry in $Value) { Format-FdReport $Entry ($Indent + ' ') }
} else {
foreach ($Property in $Value.PSObject.Properties) {
if (@('NoteProperty','Property') -contains [string]$Property.MemberType) { $Indent + $Property.Name + ':'; Format-FdReport $Property.Value ($Indent + ' ') }
}
}
}
$FdText = @(Format-FdReport $FdReport '') -join [Environment]::NewLine
[IO.File]::WriteAllText($FdReportText,$FdText,[Text.Encoding]::UTF8)
}
Write-Host ("Diagnostic reports: {0}" -f $FdRoot)
New-Object PSObject -Property @{TextReport=$FdReportText; XmlReport=$FdReportXml; Findings=$FdReport.Findings}
} $URL $OutputDirectory $TimeoutSeconds
+691 -32
View File
@@ -12,6 +12,10 @@
# Define the root path for the MSP in the Windows Registry
[string]$Global:MSPRegistryRoot = "HKEY_LOCAL_MACHINE\SOFTWARE\${MSPName}"
# Create a global variable to keep a ticket and billing related variables if they are needed later
$Global:TicketNumber = $null
$Global:TimeAttributedToUser = ""
# Setup the MSP management directories
Function Setup-MSPDirectories {
If ( Test-Path ${Env:ProgramData} ) { $Global:RootDirectory = "${Env:ProgramData}\${MSPName}" } Else { $Global:RootDirectory = "${Env:SystemDrive}\${MSPName}" }
@@ -135,13 +139,18 @@ Function Run-Script {
switch ($PSCmdlet.ParameterSetName) {
'live-ps' {
Write-Output "Retrieving : ${LivePSScript}.ps1"
Try { $Script = (New-Object Net.WebClient).DownloadString($URL) }
Catch { Write-Error $_.Exception.Message ; Exit }
Try { $ScriptBlock = [Scriptblock]::Create($Script) }
Catch { Write-Error $_.Exception.Message ; Exit }
Write-Output "Executing: ${LivePSScript}.ps1 ${Arguments}"
Try { Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList $Arguments }
Catch { Write-Error $_.Exception.Message ; Exit }
If ( $null -eq $Arguments ) {
$ScriptFile = Download-File -URL $URL
Write-Output "Sourcing: ${LivePSScript}.ps1"
. $ScriptFile
If ( Test-Path $ScriptFile ) { Remove-Item $ScriptFile -Force -ErrorAction SilentlyContinue }
} Else {
Try { $ScriptBlock = [Scriptblock]::Create((New-Object Net.WebClient).DownloadString($URL)) }
Catch { Write-Error $_.Exception.Message ; Exit }
Write-Output "Executing: ${LivePSScript}.ps1 ${Arguments}"
Try { Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList $Arguments }
Catch { Write-Error $_.Exception.Message ; Exit }
}
}
'local' {
@@ -171,6 +180,45 @@ Function Get-Version {
Return [version]$Return.Trim('.')
}
# Convert an RMM text variable into an actual Boolean value.
Function ConvertTo-RmmBoolean {
<#
.SYNOPSIS
Converts an RMM text value to a System.Boolean.
.DESCRIPTION
Accepts true, $true, yes, y, or 1; false, $false, no, n, or 0.
Matching ignores case and surrounding whitespace. Null and empty values
return false. Unrecognized values throw instead of silently enabling an option.
.PARAMETER Value
The value to convert. Native Boolean values are also accepted.
.PARAMETER VariableName
Optional RMM variable name, without the leading $, for error messages.
.EXAMPLE
$ZipEnabled = ConvertTo-RmmBoolean -Value $Zip -VariableName 'Zip'
.EXAMPLE
ConvertTo-RmmBoolean ' No '
#>
[CmdletBinding()]
[OutputType([bool])]
param(
[Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)]
[AllowNull()]
[AllowEmptyString()]
[string]$Value,
[string]$VariableName = 'Value'
)
process {
$NormalizedValue = ([string]$Value).Trim().ToLowerInvariant()
If ( $NormalizedValue -in @('true', '$true', 'yes', 'y', '1') ) {
Return $true
}
If ( $NormalizedValue -in @('', 'false', '$false', 'no', 'n', '0') ) {
Return $false
}
Throw ('Invalid ${0} value. Use true, $true, yes, y, 1, false, $false, no, n, or 0.' -f $VariableName)
}
}
# Determines whether a URL is valid
Function IsURLValid {
param([Parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$URL)
@@ -194,12 +242,244 @@ Function Download-File {
$URI = Get-AbsoluteURI -URL $URL
If ( $null -eq $URI ) { Return }
If ( [string]::IsNullOrEmpty($File) ) { $File = '{0}{1}' -f (Get-TempPath), (Split-Path $URI -Leaf) }
#Try { (New-Object System.Net.WebClient).DownloadFile($URI,$File) }
Try { Start-BitsTransfer -Source $URI -Destination $File }
Catch { Write-Error $_.Exception.Message ; Return }
# BITS errors must stop the caller instead of returning a failed download's path.
Try { Start-BitsTransfer -Source $URI -Destination $File -ErrorAction Stop }
Catch { Throw "BITS download failed from ${URI} to ${File}: $($_.Exception.Message)" }
Return $File
}
# Downloads from the original URL; BITS handles redirects without preliminary web requests.
# Without -File, the temporary filename comes from the original URL, not its redirect target.
Function Download-FileDirectly {
param(
[Parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$URL,
[Parameter(Mandatory=$false,ValueFromPipeline=$false)][string]$File
)
If ( [string]::IsNullOrEmpty($File) ) { $File = '{0}{1}' -f (Get-TempPath), (Split-Path $URL -Leaf) }
Try { Start-BitsTransfer -Source $URL -Destination $File -ErrorAction Stop }
Catch { Throw "BITS download failed from ${URL} to ${File}: $($_.Exception.Message)" }
Return $File
}
# Upload a file to a LiquidFiles Filedrop and submit its notification message.
Function Upload-File {
<#
.SYNOPSIS
Uploads a file to the Emberkom LiquidFiles Filedrop.
.DESCRIPTION
Uses the LiquidFiles 3.7+ JSON API with bounded binary chunks, including in
32-bit Windows PowerShell 5. The local file is retained. Returns a receipt
only after the Filedrop accepts the final message for delivery.
Uploads run synchronously. Allow enough time in the RMM for the entire
transfer; the temporary Filedrop API key expires after 24 hours. Failed
chunks are retried within this call, but restarting the script starts a
new transfer. Final message submission is not retried automatically,
because a lost response could otherwise cause duplicate notifications.
.PARAMETER Path
Literal path of one completed file. Also accepts -File or a pipeline path.
.PARAMETER From
Optional sender override. Defaults to the computer's fully qualified host
name at emberkom.com (or its host name when no DNS suffix is configured).
.PARAMETER ChunkSizeMB
Maximum upload buffer size in MiB; defaults to 10 regardless of file size.
.PARAMETER TimeoutSeconds
Timeout for each HTTP request, not for the entire transfer.
.EXAMPLE
Upload-File -Path $DsaResultPath
.EXAMPLE
Upload-File -File 'C:\Reports\report.zip' -Subject 'Diagnostic report' -Verbose
.LINK
https://docs.liquidfiles.com/api/v4.3/filedrop/
.LINK
https://docs.liquidfiles.com/api/v4.3/attachments/chunks.html
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[Alias('File','FullName')][ValidateNotNullOrEmpty()][string]$Path,
[ValidateNotNullOrEmpty()][string]$FileDropUrl = 'https://xfer.emberkom.com/filedrop/cmd',
[string]$From,
[string]$Subject,
[string]$Message,
[ValidateRange(1,100)][int]$ChunkSizeMB = 10,
[ValidateRange(1,3600)][int]$TimeoutSeconds = 900,
[ValidateRange(0,10)][int]$RetryCount = 3
)
PROCESS {
$UploadUri = [uri]$FileDropUrl
If ( !$UploadUri.IsAbsoluteUri -or $UploadUri.Scheme -ne 'https' -or
$UploadUri.UserInfo -or $UploadUri.Query -or $UploadUri.Fragment ) {
Throw 'FileDropUrl must be an absolute HTTPS Filedrop URL without credentials, query, or fragment.'
}
$UploadBaseUrl = $UploadUri.AbsoluteUri.TrimEnd('/')
$UploadFile = Get-Item -LiteralPath $Path -Force -ErrorAction Stop
If ( $UploadFile -isnot [System.IO.FileInfo] ) { Throw 'Upload-File requires a file, not a directory.' }
$UploadHostInfo = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()
$UploadHostName = $UploadHostInfo.HostName
If ( [string]::IsNullOrWhiteSpace($UploadHostName) ) { $UploadHostName = [Environment]::MachineName }
$UploadDnsSuffix = $UploadHostInfo.DomainName.Trim('.')
If ( $UploadDnsSuffix -and !$UploadHostName.EndsWith(".${UploadDnsSuffix}", [StringComparison]::OrdinalIgnoreCase) ) {
$UploadHostName = "${UploadHostName}.${UploadDnsSuffix}"
}
$UploadSender = $From
If ( [string]::IsNullOrWhiteSpace($UploadSender) ) { $UploadSender = $UploadHostName.ToLowerInvariant() + '@emberkom.com' }
Try { $UploadSender = ([System.Net.Mail.MailAddress]::new($UploadSender)).Address }
Catch { Throw 'From must be a valid sender email address.' }
$UploadSubject = $Subject
If ( [string]::IsNullOrWhiteSpace($UploadSubject) ) { $UploadSubject = "File upload from ${UploadHostName}: $($UploadFile.Name)" }
$UploadMessage = $Message
If ( [string]::IsNullOrWhiteSpace($UploadMessage) ) { $UploadMessage = "Uploaded by ${UploadHostName}." }
Add-Type -AssemblyName System.Net.Http -ErrorAction Stop
$UploadClient = $null
$UploadHandler = $null
$UploadStream = $null
$UploadApiKey = $null
# Keep the same HTTP client (and cookies) for every chunk in this session.
Function Invoke-FileDropRequest {
param(
[string]$Method,
[string]$Uri,
[byte[]]$Data,
[int]$Count = 0,
[string]$ContentType,
[int]$Retries = $RetryCount
)
For ( $UploadAttempt = 0; $UploadAttempt -le $Retries; $UploadAttempt++ ) {
$UploadRequest = $null
$UploadResponse = $null
$UploadStatus = 0
Try {
$UploadRequest = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::new($Method), $Uri)
If ( $Method -eq 'POST' ) {
$UploadRequest.Content = [System.Net.Http.ByteArrayContent]::new($Data, 0, $Count)
If ( $ContentType ) {
$UploadRequest.Content.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse($ContentType)
}
}
$UploadResponse = $UploadClient.SendAsync($UploadRequest).GetAwaiter().GetResult()
$UploadStatus = [int]$UploadResponse.StatusCode
$UploadResponseText = $UploadResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult()
If ( !$UploadResponse.IsSuccessStatusCode ) {
# Do not include authentication tokens in errors or RMM logs.
If ( $UploadApiKey ) { $UploadResponseText = $UploadResponseText.Replace($UploadApiKey, '[redacted]') }
If ( $UploadResponseText.Length -gt 1000 ) { $UploadResponseText = $UploadResponseText.Substring(0, 1000) }
Throw "Filedrop returned HTTP ${UploadStatus}: ${UploadResponseText}"
}
If ( [string]::IsNullOrWhiteSpace($UploadResponseText) ) { Return $null }
$UploadResponseData = ConvertFrom-Json -InputObject $UploadResponseText -ErrorAction Stop
If ( $UploadResponseData.errors ) { Throw ('Filedrop rejected the request: ' + ($UploadResponseData.errors -join '; ')) }
Return $UploadResponseData
}
Catch {
$UploadCanRetry = $UploadStatus -eq 0 -or $UploadStatus -in @(408,429,500,502,503,504)
If ( !$UploadCanRetry -or $UploadAttempt -ge $Retries ) { Throw }
Write-Verbose "Retrying Filedrop request after a connection error or HTTP ${UploadStatus}."
}
Finally {
If ( $null -ne $UploadResponse ) { $UploadResponse.Dispose() }
If ( $null -ne $UploadRequest ) { $UploadRequest.Dispose() }
}
Start-Sleep -Seconds ([Math]::Min(30, [Math]::Pow(2, $UploadAttempt + 1)))
}
}
Try {
# Deny writes while reading so retried chunks always contain the same bytes.
$UploadStream = [System.IO.File]::Open($UploadFile.FullName, [System.IO.FileMode]::Open,
[System.IO.FileAccess]::Read, [System.IO.FileShare]::Read)
[long]$UploadLength = $UploadStream.Length
$UploadHandler = [System.Net.Http.HttpClientHandler]::new()
$UploadHandler.AllowAutoRedirect = $false
$UploadClient = New-Object -TypeName System.Net.Http.HttpClient -ArgumentList $UploadHandler
$UploadClient.Timeout = [TimeSpan]::FromSeconds($TimeoutSeconds)
$UploadClient.DefaultRequestHeaders.Accept.ParseAdd('application/json')
$UploadInfo = (Invoke-FileDropRequest -Method GET -Uri $UploadBaseUrl).filedrop
$UploadApiKey = [string]$UploadInfo.api_key
If ( [string]::IsNullOrWhiteSpace($UploadApiKey) ) {
Throw 'The Filedrop did not provide an API key. Check its URL, password, and email-validation requirements.'
}
If ( $UploadInfo.max_upload_size -gt 0 -and $UploadLength -gt ([long]$UploadInfo.max_upload_size * 1MB) ) {
Throw "The file exceeds the Filedrop limit of $($UploadInfo.max_upload_size) MiB."
}
$UploadExtension = $UploadFile.Extension.TrimStart('.').ToLowerInvariant()
$UploadPermitted = $UploadInfo.permitted_extensions
If ( !$UploadPermitted ) { $UploadPermitted = $UploadInfo.limited_extensions }
$UploadAllowedExtensions = @(([string]$UploadPermitted -split '[,\s]+') | ForEach-Object { $_.TrimStart('.') } | Where-Object { $_ })
$UploadBlockedExtensions = @(([string]$UploadInfo.blocked_extensions -split '[,\s]+') | ForEach-Object { $_.TrimStart('.') } | Where-Object { $_ })
If ( ($UploadAllowedExtensions.Count -gt 0 -and $UploadExtension -notin $UploadAllowedExtensions) -or
$UploadExtension -in $UploadBlockedExtensions ) {
Throw "The Filedrop does not permit the file extension '$UploadExtension'."
}
$UploadAuth = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($UploadApiKey + ':x'))
$UploadClient.DefaultRequestHeaders.Authorization = [System.Net.Http.Headers.AuthenticationHeaderValue]::new('Basic', $UploadAuth)
[int]$UploadChunkSize = $ChunkSizeMB * 1MB
[long]$UploadChunks = [Math]::Max(1, [Math]::Ceiling($UploadLength / [double]$UploadChunkSize))
$UploadBuffer = [byte[]]::new([int][Math]::Min($UploadChunkSize, [Math]::Max(1, $UploadLength)))
$UploadEncodedName = [uri]::EscapeDataString($UploadFile.Name)
$UploadAttachment = $null
Write-Verbose "Uploading $($UploadFile.Name) ($UploadLength bytes) in $UploadChunks chunk(s) as ${UploadSender}."
For ( [long]$UploadChunk = 0; $UploadChunk -lt $UploadChunks; $UploadChunk++ ) {
[int]$UploadBytesNeeded = [Math]::Min($UploadChunkSize, $UploadLength - $UploadStream.Position)
[int]$UploadBytesRead = 0
While ( $UploadBytesRead -lt $UploadBytesNeeded ) {
$UploadRead = $UploadStream.Read($UploadBuffer, $UploadBytesRead, $UploadBytesNeeded - $UploadBytesRead)
If ( $UploadRead -eq 0 ) { Throw 'The local file ended before all expected bytes were read.' }
$UploadBytesRead += $UploadRead
}
$UploadChunkUrl = "${UploadBaseUrl}/attachments/upload?filename=${UploadEncodedName}&chunk=${UploadChunk}&chunks=${UploadChunks}"
$UploadChunkResult = Invoke-FileDropRequest -Method POST -Uri $UploadChunkUrl -Data $UploadBuffer -Count $UploadBytesRead
If ( $UploadChunkResult.attachment.id ) { $UploadAttachment = $UploadChunkResult.attachment }
Write-Progress -Activity "Uploading $($UploadFile.Name)" -Status "Chunk $($UploadChunk + 1) of ${UploadChunks}" `
-PercentComplete ([int](100 * ($UploadChunk + 1) / $UploadChunks))
}
If ( !$UploadAttachment.id ) { Throw 'The upload did not return an attachment ID; the Filedrop message was not submitted.' }
If ( $null -ne $UploadAttachment.size -and [long]$UploadAttachment.size -ne $UploadLength ) {
Throw 'The uploaded attachment size does not match the local file; the Filedrop message was not submitted.'
}
If ( $UploadAttachment.content_blocked ) { Throw 'LiquidFiles blocked this attachment; the Filedrop message was not submitted.' }
$UploadBody = @{ message = @{
from = $UploadSender
subject = $UploadSubject
message = $UploadMessage
attachments = @([string]$UploadAttachment.id)
} } | ConvertTo-Json -Depth 4 -Compress
$UploadBodyBytes = [Text.Encoding]::UTF8.GetBytes($UploadBody)
Try {
$UploadReceipt = Invoke-FileDropRequest -Method POST -Uri $UploadBaseUrl -Data $UploadBodyBytes `
-Count $UploadBodyBytes.Length -ContentType 'application/json; charset=utf-8' -Retries 0
If ( [string]::IsNullOrWhiteSpace([string]$UploadReceipt.message.status) ) {
Throw 'The server did not return a Filedrop submission confirmation.'
}
}
Catch {
Throw "Filedrop submission was not confirmed. Check the Filedrop before retrying to avoid duplicate notifications. $($_.Exception.Message)"
}
[pscustomobject]@{
Path = $UploadFile.FullName
FileDropUrl = $UploadBaseUrl
From = $UploadSender
AttachmentId = [string]$UploadAttachment.id
Size = $UploadLength
Status = [string]$UploadReceipt.message.status
}
}
Finally {
Write-Progress -Activity "Uploading $($UploadFile.Name)" -Completed
If ( $null -ne $UploadStream ) { $UploadStream.Dispose() }
If ( $null -ne $UploadClient ) { $UploadClient.Dispose() }
ElseIf ( $null -ne $UploadHandler ) { $UploadHandler.Dispose() }
$UploadApiKey = $null
}
}
}
# Install a program from a provided path
Function Install-Program {
param(
@@ -576,15 +856,17 @@ Function Disable-Log {
# Function to install the MSP360 Powershell module
Function Import-MSP360Module {
Try {
$Module = Get-Module -ListAvailable -Name MSP360,msp360 | Sort-Object Version -Descending | Select-Object -First 1
If ( $Module ) { Import-Module -Name $Module.Name -Force -ErrorAction Stop ; Return }
If ( -not (IsAdmin) ) { Write-Output "The MSP360 Powershell module needs to be installed, but the current account is not an administrative user" ; Return }
Write-Output "Installing MSP360 Powershell module"
Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process
Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process -Force
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://git.io/JUSAA')
Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/msp360/MSP360-PSModule/master/private/Install-MSP360Module.ps1')
Install-MSP360Module
Import-Module -Name MSP360
Import-Module -Name MSP360 -Force -ErrorAction Stop
}
Catch { Write-Error $_.Exception.Message }
Catch { Write-Error "Could not import or install MSP360 PowerShell module: $($_.Exception.Message)" }
}
# Function to ZIP a file or directory, returns the newly created zip file
@@ -699,12 +981,14 @@ Function IsRunning ([Parameter(ValueFromPipeline=$true)][string]$Name) {
Function New-LocalAccount {
param(
[Parameter(Mandatory=$true)][string]$Username,
[Parameter(Mandatory=$true)][SecureString]$Password,
[Parameter(Mandatory=$true)][string]$Password,
[Parameter(Mandatory=$true)][string]$FullName,
[Parameter(Mandatory=$true)][string]$Description,
[Parameter(Mandatory=$false)][switch]$MakeAdmin=$false,
[Parameter(Mandatory=$false)][switch]$Hide=$false
)
$SecurePassword = ConvertTo-SecureString -String $Password -AsPlainText -Force
$Password = $null
If ( -not (IsAdmin) ) {
Write-Output "Cannot create or modify a local account without administrative privileges"
Return
@@ -715,22 +999,35 @@ Function New-LocalAccount {
}
If ( $(Get-LocalUser -Name $Username -ErrorAction SilentlyContinue) ) {
Write-Output "Updating user: ${Username}"
Try { Set-LocalUser -Name "${Username}" -Password $SecurePassword -FullName "${FullName}" -Description "${Description}" -AccountNeverExpires -PasswordNeverExpires }
Catch { Write-Error $_.Exception.Message ; Exit }
Try { Set-LocalUser -Name "${Username}" -Password $SecurePassword -FullName "${FullName}" -Description "${Description}" -AccountNeverExpires -PasswordNeverExpires | Out-Null }
Catch { Write-Error "Could not update user`n"+$_.Exception.Message ; Exit }
}
Else {
Write-Output "Creating user: ${Username}"
Try { New-LocalUser -Name "${Username}" -Password $SecurePassword -FullName "${FullName}" -Description "${Description}" -AccountNeverExpires -PasswordNeverExpires }
Catch { Write-Error $_.Exception.Message ; Exit }
Try { New-LocalUser -Name "${Username}" -Password $SecurePassword -FullName "${FullName}" -Description "${Description}" -AccountNeverExpires -PasswordNeverExpires | Out-Null }
Catch { Write-Error "Could not create user`n"+$_.Exception.Message ; Exit }
}
If ( (-not($(Get-LocalGroupMember -Group "Administrators" -Member $Username -ErrorAction SilentlyContinue))) -and $MakeAdmin ) {
Write-Output "Adding ""${Username}"" to local Administrators group"
Try { Add-LocalGroupMember -Name "Administrators" -Member $(Get-LocalUser -Name "${Username}") }
Catch { Write-Error $_.Exception.Message ; Exit }
Catch { Write-Error "Could not add user to Administrators group`n"+$_.Exception.Message ; Exit }
}
If ( $Hide ) {
# TODO: Hide account from logon screen
#Get-ChildItem "HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList"
$RegPath = "HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList"
$HideUser = $true
If ( -not (Test-Path $RegPath) ) {
New-Item -Path $RegPath -Force | Out-Null
} Else {
$UserList = Get-ItemProperty -Path $RegPath
ForEach ( $User in $UserList.PSObject.Properties ) {
If ( $User.Name -ieq $Username ) {
Write-Output "The user ""${Username}"" is already hidden from the login screen"
$HideUser = $false
Break
}
}
}
If ( $HideUser ) { New-ItemProperty -Path $RegPath -Name $Username -Value 0 -PropertyType DWord -Force | Out-Null }
}
}
@@ -761,14 +1058,34 @@ Function Get-FileSizes {
[string]$Units,
[int]$Precision=2
)
# Fix drive case so output looks nicer
Switch ( $Path.Length ) {
1 { $Path = $Path.ToUpper() }
{ $_ -gt 1 } {
$split = $Path.Split(":")
$Path = $split[0].ToUpper() + ":"
If ( $split[1] ) { $Path += $split[1] }
}
}
# If no path is specified, set $Path to the directory containing user profiles
If (-not $Path ) {
$Path = (Get-ItemPropertyValue -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -Name ProfilesDirectory)
$Path = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -Name ProfilesDirectory).ProfilesDirectory
$Recurse = $true
$IncludeHidden = $false
}
# If the path is not an absolute path, make it so
ElseIf ( -not (Test-Path $Path) ) {
$NewPath = $Path[0] + ":\"
If ( -not (Test-Path $NewPath) ) {
Write-Error "Could not find path: ${Path}"
Return
}
$Path = $NewPath
}
# Change the sort order depending on which parameter set was specified
Switch ( $PSCmdlet.ParameterSetName ) {
"largest" { $SortOrder = $true ; $Quantity = $Largest }
@@ -785,9 +1102,10 @@ Function Get-FileSizes {
}
# Get the file paths and sizes as specified
$FileSizes = Get-ChildItem $Path -Recurse:$Recurse -Force:$IncludeHidden -ErrorAction SilentlyContinue | `
$FileSizes = Get-ChildItem -LiteralPath "\\?\${Path}" -Recurse:$Recurse -Force:$IncludeHidden -ErrorAction SilentlyContinue | `
Where-Object { ! $_.PSIsContainer -and ($_.FullName.Length -lt 260)} | `
Sort-Object -Descending:$SortOrder -Property Length | `
Select-Object -First $Quantity @{Name="File Path";Expression={$_.DirectoryName + '\' + $_.Name}}, @{Name=$UnitLabel;Expression={[Math]::Round($_.Length / $Divisor, $Precision)}}
Select-Object -First $Quantity @{Name="File Path";Expression={[IO.Path]::Combine($_.DirectoryName, $_.Name)}}, @{Name=$UnitLabel;Expression={[Math]::Round($_.Length / $Divisor, $Precision)}}
# Set output prefix
$prefix = "The ${Quantity} " + $PSCmdlet.ParameterSetName + " files found at ""${Path}"""
@@ -814,7 +1132,7 @@ Function WriteToFile_UTF8NoBOM {
# Function to enumerate all user profile folders
Function Get-UserProfiles {
$ProfilePath = (Get-ItemPropertyValue -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -Name ProfilesDirectory)
$ProfilePath = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -Name ProfilesDirectory).ProfilesDirectory
$UserProfilePaths = @()
ForEach ( $userProfile in (Get-ChildItem -Path $ProfilePath | Where-Object { $_.PSIsContainer }) ) {
$UserProfilePaths += ,@($userProfile.FullName)
@@ -1009,16 +1327,16 @@ Function Restart-EndpointOnUptime ([timespan]$MaxUptime) {
Catch { Write-Error $_.Exception.Message }
}
# Function to download and install local tools used by scripts
Function Install-LocalTools () {
param(
[Parameter(ValueFromPipeline=$false,Mandatory=$false)][switch]$Force
[Parameter(ValueFromPipeline=$false,Mandatory=$false)][switch]$Force=$false
)
$ToolURLs = @(
'https://www.emberkom.com/tools/BlueScreenView-x64.exe'
'https://www.emberkom.com/tools/BlueScreenView-x86.exe'
'https://www.emberkom.com/tools/dnshelper.exe'
'https://www.emberkom.com/tools/etl2pcapng.exe'
'https://xfer.emberkom.com/shares/tools/folders/9b9988fb-c53a-4833-a4ad-68d01a3021bb/files/bluescreenview-x64-exe/download/BlueScreenView-x64.exe'
'https://xfer.emberkom.com/shares/tools/folders/9b9988fb-c53a-4833-a4ad-68d01a3021bb/files/bluescreenview-x86-exe/download/BlueScreenView-x86.exe'
'https://xfer.emberkom.com/shares/tools/folders/9b9988fb-c53a-4833-a4ad-68d01a3021bb/files/etl2pcapng-exe/download/etl2pcapng.exe'
)
Foreach ( $ToolURL in $ToolURLs ) {
@@ -1032,6 +1350,347 @@ Function Install-LocalTools () {
}
}
Function Source-PSScript ([string]$ScriptName) {
If ( $ScriptName.StartsWith('http://') -or $ScriptName.StartsWith('https://') ) {
$URL = $ScriptName
} Else {
$URL = "https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/${ScriptName}.ps1"
}
$ScriptFile = Download-File -URL $URL
Write-Output "Sourcing ${ScriptFile}"
. $ScriptFile
If ( Test-Path $ScriptFile ) { Write-Output "Removing ${ScriptFile}" ; Remove-Item $ScriptFile -Force -ErrorAction SilentlyContinue }
}
# Forcefully maps a network drive but requires user's credentials to already exist in Credential Manager
Function Create-NetworkDrive {
param (
[Parameter(Mandatory)]
[ValidatePattern("^[A-Z]:$")]
[string]$DriveLetter,
[Parameter(Mandatory)]
[string]$Path,
[Parameter(Mandatory)]
[string]$Label
)
$existingDrive = Get-CimInstance Win32_LogicalDisk | Where-Object { $_.DeviceID -eq $DriveLetter }
if (-not $existingDrive) {
Write-Output "Mapping ${Path} to drive letter ${DriveLetter}"
cmd.exe /c "net use $DriveLetter $Path /persistent:yes" | Out-Null
}
elseif ($existingDrive.ProviderName -ne $Path) {
Write-Output "Drive exists but points to a different path. Remapping..."
cmd.exe /c "net use $DriveLetter /delete /y" | Out-Null
cmd.exe /c "net use $DriveLetter $Path /persistent:yes" | Out-Null
}
else { Write-Verbose "Drive already mapped correctly." }
Start-Sleep -Seconds 2
try {
$dl = $DriveLetter.TrimEnd(':')
$vol = Get-Volume -DriveLetter $dl -ErrorAction Stop
if ($vol.FileSystemLabel -ne $Label) {
Write-Output "Setting ${DriveLetter} drive label to ""${Label}"""
Set-Volume -DriveLetter $dl -NewFileSystemLabel $Label
}
}
catch { Write-Verbose "Could not set volume label (possibly unsupported context)." }
}
function Install-SystemFont {
[CmdletBinding(SupportsShouldProcess=$true)]
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]$FontPath
)
try {
$fontFile = Get-Item -LiteralPath $FontPath -ErrorAction Stop
}
catch {
Write-Error "Font file '$FontPath' could not be found or accessed: $($_.Exception.Message)"
return
}
if ($fontFile.PSIsContainer) {
Write-Error "Font path '$FontPath' points to a directory. Specify a .otf or .ttf file."
return
}
$extension = $fontFile.Extension.ToLowerInvariant()
if ($extension -notin @('.otf', '.ttf')) {
Write-Error "Only .otf and .ttf font files are supported."
return
}
# Read the font's embedded family and face names instead of relying on
# language- and Windows-version-dependent Shell property column numbers.
try {
Add-Type -AssemblyName PresentationCore -ErrorAction Stop
$glyphTypeface = New-Object System.Windows.Media.GlyphTypeface ([Uri]$fontFile.FullName)
$englishLanguage = [System.Globalization.CultureInfo]::GetCultureInfo('en-US')
$fontFamily = $null
$fontFace = $null
if (-not $glyphTypeface.FamilyNames.TryGetValue($englishLanguage, [ref]$fontFamily)) {
$fontFamily = $glyphTypeface.FamilyNames.Values | Select-Object -First 1
}
if (-not $glyphTypeface.FaceNames.TryGetValue($englishLanguage, [ref]$fontFace)) {
$fontFace = $glyphTypeface.FaceNames.Values | Select-Object -First 1
}
if ([string]::IsNullOrWhiteSpace($fontFamily)) {
throw "The font does not contain a readable family name."
}
if ([string]::IsNullOrWhiteSpace($fontFace) -or $fontFace -in @('Normal', 'Regular')) {
$fontName = $fontFamily
}
else {
$fontName = "$fontFamily $fontFace"
}
}
catch {
Write-Error "Font metadata could not be read from '$($fontFile.FullName)': $($_.Exception.Message)"
return
}
$fileName = $fontFile.Name
$fontsDirectory = Join-Path $env:SystemRoot 'Fonts'
$destinationPath = Join-Path $fontsDirectory $fileName
$registryValueName = if ($extension -eq '.otf') {
"$fontName (OpenType)"
}
else {
"$fontName (TrueType)"
}
try {
if (-not ('Emberkom.NativeFontMethods' -as [type])) {
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
namespace Emberkom
{
public static class NativeFontMethods
{
[DllImport("gdi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern int AddFontResource(string fileName);
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr SendMessageTimeout(
IntPtr windowHandle,
uint message,
UIntPtr wParam,
IntPtr lParam,
uint flags,
uint timeout,
out UIntPtr result
);
}
}
'@ -ErrorAction Stop
}
}
catch {
Write-Error "Windows font installation support could not be initialized: $($_.Exception.Message)"
return
}
if (-not $PSCmdlet.ShouldProcess($destinationPath, "Install system font '$fontName'")) {
return
}
try {
$copyRequired = $true
if (Test-Path -LiteralPath $destinationPath) {
$sourceHash = (Get-FileHash -LiteralPath $fontFile.FullName -Algorithm SHA256 -ErrorAction Stop).Hash
$destinationHash = (Get-FileHash -LiteralPath $destinationPath -Algorithm SHA256 -ErrorAction Stop).Hash
$copyRequired = $sourceHash -ne $destinationHash
if (-not $copyRequired) {
Write-Output "Font file '$fileName' is already present in '$fontsDirectory'."
}
}
if ($copyRequired) {
Copy-Item -LiteralPath $fontFile.FullName -Destination $destinationPath -Force -ErrorAction Stop
Write-Output "Copied font file '$fileName' to '$fontsDirectory'."
}
$fontRegistryKey = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey(
'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts',
$true
)
if ($null -eq $fontRegistryKey) {
throw "The system Fonts registry key could not be opened for writing."
}
try {
$fontRegistryKey.SetValue(
$registryValueName,
$fileName,
[Microsoft.Win32.RegistryValueKind]::String
)
}
finally {
$fontRegistryKey.Dispose()
}
Write-Output "Registered '$registryValueName' as a system font."
$fontsAdded = [Emberkom.NativeFontMethods]::AddFontResource($destinationPath)
if ($fontsAdded -eq 0) {
throw "Windows did not load '$destinationPath' into the current session."
}
Write-Output "Loaded '$fontName' into the current Windows session."
# Notify applications in this session without allowing a hung window to
# block an unattended RMM script indefinitely.
$broadcastHandle = [IntPtr]0xffff
$fontChangeMessage = 0x001D
$abortIfHung = 0x0002
$messageResult = [UIntPtr]::Zero
$notificationSent = [Emberkom.NativeFontMethods]::SendMessageTimeout(
$broadcastHandle,
$fontChangeMessage,
[UIntPtr]::Zero,
[IntPtr]::Zero,
$abortIfHung,
1000,
[ref]$messageResult
)
if ($notificationSent -eq [IntPtr]::Zero) {
Write-Output "Installed system font '$fontName', but no application acknowledged the font-change notification."
return
}
Write-Output "Installed system font '$fontName' successfully."
}
catch {
Write-Error "Failed to install font '$($fontFile.FullName)': $($_.Exception.Message)"
}
}
# Usage Example:
# Install-SystemFont -FontPath "C:\Path\To\YourFont.otf"
# Function to import the Syncro Module
Function Import-RMMModule () {
Try { Import-Module $env:SyncroModule -WarningAction SilentlyContinue -ErrorAction Stop }
Catch { Write-Error $_.Exception.Message ; Return }
}
# Function to create a new ticket
Function New-RMMTicket () {
param(
[Parameter(ValueFromPipeline=$false,Mandatory=$true)][string]$Subject,
[Parameter(ValueFromPipeline=$false,Mandatory=$false)][string]$IssueType="Remote Support",
[Parameter(ValueFromPipeline=$false,Mandatory=$true)][string]$InitialIssue
)
If ( !(Get-Command Create-Syncro-Ticket -ErrorAction SilentlyContinue) ) { Import-RMMModule}
Try { $Ticket = Create-Syncro-Ticket -Subject $Subject -IssueType $IssueType -Status "New" }
Catch { Write-Error $_.Exception.Message ; Return }
$Global:TicketNumber = $Ticket.ticket.id
New-TicketComment -TicketID $Global:TicketNumber -Subject "Issue" -Comment $InitialIssue -Hidden -DoNotEmail
}
# Function to create a new comment on a ticket
Function New-TicketComment () {
param(
[Parameter(ValueFromPipeline=$false,Mandatory=$false)]$TicketID=$null,
[Parameter(ValueFromPipeline=$false,Mandatory=$false)][string]$Subject="Update",
[Parameter(ValueFromPipeline=$true,Mandatory=$true)][string]$Comment,
[Parameter(ValueFromPipeline=$false,Mandatory=$false)][switch]$Hidden=$false,
[Parameter(ValueFromPipeline=$false,Mandatory=$false)][switch]$DoNotEmail=$false
)
If ( !(Get-Command Create-Syncro-Ticket-Comment -ErrorAction SilentlyContinue) ) { Import-RMMModule}
If ( ($null -eq $TicketID) -and ($null -eq $Global:TicketNumber) ) { Return }
If ( ($null -eq $TicketID) -and ($null -ne $Global:TicketNumber) ) { $TicketID = $Global:TicketNumber }
Try { Create-Syncro-Ticket-Comment -TicketIdOrNumber $TicketID -Subject $Subject -Body $Comment -Hidden $Hidden -DoNotEmail $DoNotEmail }
Catch { Write-Error $_.Exception.Message ; Return }
}
# Function to create a new billable time entry on a ticket
Function New-TicketTimerEntry () {
param(
[Parameter(ValueFromPipeline=$false,Mandatory=$false)]$TicketID=$null,
[Parameter(ValueFromPipeline=$false,Mandatory=$false)][int]$BillableMinutes=15,
[Parameter(ValueFromPipeline=$true,Mandatory=$true)][string]$Comment,
[Parameter(ValueFromPipeline=$false,Mandatory=$false)][string]$BillableUser=$Global:TimeAttributedToUser,
[Parameter(ValueFromPipeline=$false,Mandatory=$false)][switch]$ChargeLater=$false
)
If ( $BillableUser -eq "" ) { Write-Error "Billable user not specified" ; Return }
If ( !(Get-Command Create-Syncro-Ticket-TimerEntry -ErrorAction SilentlyContinue) ) { Import-RMMModule}
If ( $ChargeLater -eq $true ) { $ChargeTime = "false" } Else { $ChargeTime = "true" }
$StartAt = (Get-Date).AddMinutes(-$BillableMinutes).ToString("o")
Create-Syncro-Ticket-TimerEntry -TicketIdOrNumber $TicketID -StartTime $StartAt -DurationMinutes $BillableMinutes -Notes $Comment -UserIdOrEmail $BillableUser -ChargeTime $ChargeTime
}
# Function to get the SHA256 hash of a string
Function Get-StringHash() {
param([Parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$String)
$stream = [IO.MemoryStream]::new([byte[]][char[]]$String)
$hashObj = Get-FileHash -InputStream $stream -Algorithm SHA256
$hashObj.Hash
}
# Function to enable or disable the profile prompt when launching Outlook
Function Set-OutlookProfilePrompt () {
param(
[Parameter(ValueFromPipeline=$false,Mandatory=$true,ParameterSetName='enable')][switch]$Enable,
[Parameter(ValueFromPipeline=$false,Mandatory=$true,ParameterSetName='disable')][switch]$Disable
)
If ( $Enable ) { $val = '1' ; $action = "Enabling"}
If ( $Disable ) { $val = '0' ; $action = "Disabling"}
Write-Output "${action} profile picker on Outlook launch"
[Microsoft.Win32.Registry]::SetValue('HKEY_CURRENT_USER\SOFTWARE\Microsoft\Exchange\Client\Options','PickLogonProfile',$val,[Microsoft.Win32.RegistryValueKind]::String)
$Profiles = (Get-ChildItem -Path 'HKCU:\SOFTWARE\Microsoft\Office\16.0\Outlook\Profiles' | Select-Object -ExpandProperty Name | Split-Path -Leaf | Sort-Object) -join "`n"
Write-Output "Available Outlook profiles:`n${Profiles}"
}
# Function to install and run script using Powershell 7
# Provided by j.mcbride from https://community.syncromsp.com/t/powershell-7-2/7425/5
Function Start-ScriptInPowershell7 () {
# Check for required PowerShell version (7+)
If (!($PSVersionTable.PSVersion.Major -ge 7)) {
Try {
# Install PowerShell 7 if missing
If (!(Test-Path "$env:SystemDrive\Program Files\PowerShell\7")) {
Write-Output 'Installing PowerShell version 7...'
Invoke-Expression "& { $(Invoke-RestMethod https://aka.ms/install-powershell.ps1) } -UseMSI -Quiet"
}
# Refresh PATH
$Env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'User')
# Restart script in PowerShell 7
pwsh -File "`"$PSCommandPath`"" @PSBoundParameters
}
Catch { Write-Error $_.Exception.Message }
Finally { Exit $LASTEXITCODE }
}
# Input the actual script below this line
}
# Upgrade default TLS version to 1.2
Try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 }
Catch { Write-Error $_.Exception.Message }
# Initial setup of the MSP management directories and log file
Setup-MSPDirectories
Setup-LogFile
Install-LocalTools
# Install the local tools
#Install-LocalTools
# Check the RMM runtime variable to see if a billable user was specified, if so then set the $TimeAttributedToUser
If ( $TimeAttributedToUser ) {
If ( $TimeAttributedToUser.Trim().Length -ge 6 ) { $Global:TimeAttributedToUser = $TimeAttributedToUser.Trim().ToLower() }
}
# Check the RMM runtime variable to see if a ticket number was specified, if so then set the $TicketID
If ( $TicketNumber ) {
If ( $TicketNumber.Trim().Length -gt 1 ) { $Global:TicketNumber = $TicketNumber.Trim() }
}
+1 -1
View File
@@ -35,7 +35,7 @@ Else
}
# Kill all running instances of TeamViewer
Get-Process | Where { $_.Name -like "*TeamViewer*" } | Stop-Process -Force
Get-Process | Where-Object { $_.Name -like "*TeamViewer*" } | Stop-Process -Force
If ( Test-Path "${INSTALLDIR}\uninstall.exe" )
{ Start-Process "${INSTALLDIR}\uninstall.exe" -ArgumentList "/S" -Wait }
+1
View File
@@ -0,0 +1 @@
"wireguard" | Uninstall-MSI -Match
+85
View File
@@ -0,0 +1,85 @@
# File download diagnosis
`Test-FileDownload.ps1` compares the loaded `Download-File` helper, synchronous BITS, an inspectable BITS job, and a direct .NET GET. It makes no changes to `Tools.ps1`, collector behavior, Windows TLS configuration, or proxy configuration. Downloaded files are never executed.
## Run on the affected host
Use the same RMM account, PowerShell executable, and bootstrap as the failing job. Keep the existing Tools bootstrap unchanged and replace the final collector invocation with this block after publishing the diagnostic script to the repository:
```powershell
$DiagnosticClient = New-Object Net.WebClient
try {
$DiagnosticText = $DiagnosticClient.DownloadString(
'https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Test-FileDownload.ps1'
)
}
finally { $DiagnosticClient.Dispose() }
& ([scriptblock]::Create($DiagnosticText)) `
-URL 'https://xfer.emberkom.com/shares/tools/folders/9b9988fb-c53a-4833-a4ad-68d01a3021bb/files/dsa-collect-windows-amd64-exe/download/dsa-collect-windows-amd64.exe' `
-OutputDirectory "$Env:ProgramData\Emberkom\Output" `
-TimeoutSeconds 120
```
Do not invoke it through `Download-File`, since that is the function being investigated. Alternatively, save the diagnostic on the affected host and call it directly with `& 'C:\path\Test-FileDownload.ps1' -URL '<download URL>'`. If Tools cannot load on a legacy engine, a standalone run still tests the other methods and records that the helper was unavailable.
Use `powershell.exe`, not ISE or an embedded PowerShell host. The diagnostic rejects other executables instead of guessing how to launch equivalent child processes.
The mandatory `-URL` can be any HTTP(S) file URL without embedded credentials. `-OutputDirectory` defaults to the account's temporary directory. Each invocation creates its own `file-download-diagnostic-<GUID>` subdirectory, containing `report.txt` and `report.clixml`. No reports are uploaded automatically. Retrieve `report.txt` from the affected host for analysis; the CLIXML file preserves structured details.
There are ten sequential probes: environment, HTTP headers/ranges, and two destination variants for each of four download paths. Eight probes download the payload, so use a small representative file. Each probe has a 120-second default wall-clock limit, followed by a separate cleanup attempt of at most 15 seconds for BITS-related probes. Allow about 22 minutes plus process startup/reporting overhead for the worst case, or specify a shorter timeout. Large downloads and hashing count toward the timeout.
## What the report captures
- OS, CLR and PowerShell versions, process bitness, executable, identity, BITS service/module information, and proxy/TLS settings. Missing registry values are recorded rather than invented as defaults.
- Definitions and SHA-256 hashes of the loaded helper functions, plus command resolution. The full Tools script is not executed by the diagnostic.
- HTTP redirect chains, status, content length, range support, and selected response headers. Cookies and authorization headers are not collected.
- Independent downloads to new destinations and existing empty destinations, byte counts, hashes, first bytes, timing, and full exceptions.
- BITS job state changes and byte counts before completion, and file snapshots immediately after transfer and one second later.
Child processes use the caller's PowerShell executable and account, with its .NET TLS selection reproduced in the child only. Existing pooled connections, custom in-memory proxy objects, and certificate callbacks are not copied. The helper probe copies the captured functions, redirects helper logging into diagnostic scratch, and adds ownership tags to BITS jobs. A shadowed/non-native `Start-BitsTransfer` is recorded and the helper probe is skipped rather than risking cleanup of unidentified jobs.
Scratch filenames retain the original URL's extension when usable, but the production destination is never touched. Path-specific security rules may therefore behave differently. The loaded helper's logging overrides are copied only into its child process.
Only jobs matching both the diagnostic's unique run ID and exact probe tag are removed. Scratch cleanup does not recurse or touch the production executable. Cleanup errors and unfinished probes remain visible in the report. Reports include local machine/account details and loaded source definitions; review them before sharing.
The code uses PowerShell 2 syntax and APIs available to legacy .NET, but local validation on PowerShell 5.1 does not prove execution on 2, 3, or 4. Run the diagnostic on actual legacy runtimes before declaring them supported. Full `Tools.ps1` loadability is a separate issue: it already contains newer syntax and commands, including `-in`/`-notin`, `::new()`, `ConvertFrom-Json`, and `Get-FileHash` outside the downloader.
## Interpret the comparison
| Observation | Next investigation |
|---|---|
| Only the helper fails | Compare `HelperBitsSource` with the original URL, the redirect chain, and URL-resolution requests. |
| BITS fails and direct GET succeeds | Compare service identity, WinHTTP/.NET proxy and TLS settings, HTTP behavior, and BITS error codes. |
| Both transports fail | Investigate endpoint responses, certificate trust, connectivity, and the affected machine's configuration. |
| BITS reports bytes transferred but the resulting file differs | Inspect completion errors, destination access, and post-download changes. |
| All paths produce the same nonempty hash | Failure was not reproduced. This run does not justify changing the shared transport. |
Neither a positive byte count nor matching hashes authenticates a publisher's executable. These are diagnostic comparisons, not a replacement for publisher-supplied integrity information.
## Shared caller audit
The audit found 24 active call sites across 20 files: 19 scripts plus five wrapper call sites in `Tools.ps1`. Commented-out calls and tests are excluded.
| Callers | Required behavior |
|---|---|
| `Run-Script`, `Source-PSScript`, `Uninstall-MicrosoftOffice` | Return one completed script path with its extension; allow immediate sourcing/execution. |
| `Install-MSI`, `Install-4KVideoDownloader`, `Install-LiquidFilesOutlookAgent`, `Install-SimpleInOut`, `Install-SpecsIntact`, `Install-Wireguard` | Return a completed installer path for immediate MSI invocation and later cleanup. |
| `Install-Nextcloud`, `Install-OpenVPN`, `Install-VLC` | Work inside a subexpression or via positional URL; return only the downloaded path. |
| `Install-AutodeskDesktopConnector`, `Install-Enscape`, `Install-ESETManagementAgent`, `Install-MicrosoftOffice365`, `Install-SketchUp` | Honor an explicit destination; handle redirects and potentially large installers; finish before installation. |
| `Fix-AutodeskProductLicensing`, `Test-InternetBandwidth`, `Get-LiquidFilesCLI` | Retain a usable ZIP filename and extension for extraction and derived working-directory names. |
| `Get-DSAManifest`, `Fix-UpdateLocalHosts`, `Remove-AllESETProducts`, `Install-LocalTools` | Honor fixed executable paths and complete replacement before execution. Caller-level caching remains separate. |
All concrete download URLs in this audit use HTTPS. The public wrappers also accept caller-provided HTTP(S) URLs. No audited caller consumes a BITS job object or explicitly controls background transfers/resume.
`Download-File` currently calls `Get-AbsoluteURI`, which first calls `IsURLValid`; both issue GET requests without disposing their responses. The final response URI is then passed to BITS. This is an observed implementation detail to investigate, not proof that BITS should be replaced.
Any subsequent shared fix must preserve URL binding, explicit/automatic filenames, synchronous completion, one success path on the output stream, redirects, large-file streaming, shell architectures, and `Run-Script` shared scope. It must also address validation and failure propagation before callers install or extract content. Production implementation and rollout follow the diagnostic findings; this change adds no fallback or collector-specific transport option.
## Local verification
Run `tests\Test-FileDownload.Tests.ps1` in both System32 and SysWOW64 Windows PowerShell. The suite serves synthetic payloads over loopback, tests both destination variants, redirects, chunked/empty/truncated/error responses, timeouts, hashing, and preservation of an unrelated BITS job. It requires access to the BITS service but performs no external downloads, installations, or uploads.
The affected RMM host and actual PowerShell 2-4 engines must still be tested separately. No local result substitutes for those environments.
Local verification on 2026-09-30 passed under both 32-bit and 64-bit PowerShell 5.1. A live 32-bit run against the AMD64 collector URL returned 3,455,488 bytes with SHA-256 `CC693EF2FEEEF05C99410B6F7A05AE2621EA0B89BA6D3E12C50B7F059D557F99` for every download method and destination variant. This is an observed comparison hash, not a publisher-provided trust anchor. The failure was not reproduced, so the current recommendation is to retain the shared transport until the affected RMM run supplies evidence.
@@ -0,0 +1,5 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
# $CleanupPath and $DaysWithNoModification must be set by caller
Run-Script -LivePSScript Cleanup-RevitTempFilesOnFileServerCalcOnly
+1
View File
@@ -0,0 +1 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Cleanup-SystemTempFiles.ps1')))
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Create-BatteryReport
+9
View File
@@ -0,0 +1,9 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
# Create the Enberkom Administrator user
New-LocalAccount -Username 'ek-admin' -Password 'C0d3R3d@!!' -FullName 'Emberkom Administrator' -Description 'Used by Emberkom to administer this endpoint' -MakeAdmin -Hide
# Delete the provisioning user that may have been created via PPKG
If ( $(Get-LocalUser -Name "ek-admin") ) {
Run-Script -LivePSScript Remove-ProvisioningAdminUser
}
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Create-NewTicketFromAppCrash-KERNELBASEdll
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Create-NewTicketFromBSODAnalysis
@@ -0,0 +1,3 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
$VisionURL = 'https://dbia.deltekfirst.com/DBIAClient/DeltekVision.application'
Run-Script -LivePSScript Install-DeltekVision -Arguments $VisionURL
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Enable-HyperVFeatures
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-AutodeskProductLicensing
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-ClearOutlookAutoCompleteCache
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-ClearQuickAccess
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-DellOptimizer
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-DisableAllOneDriveNotifications
+6
View File
@@ -0,0 +1,6 @@
@echo off
title Disable Sleep While Plugged-In
powercfg /x -hibernate-timeout-ac 0
powercfg /x -disk-timeout-ac 0
powercfg /x -monitor-timeout-ac 0
powercfg /x -standby-timeout-ac 0
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-DisableWindowsTelemetry
@@ -0,0 +1,12 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
# Set the edition of the ECB agent.
# Valid options are 'desktop' 'server' or 'vm'
$Edition = '{[Edition]}'
# Set the MSP360 username and password
$MSP360Username = '{[MSP360_Username]}'
$MSP360Password = '{[MSP360_Password]}'
# Run the installation script
Run-Script -LivePSScript Fix-EmberkomCloudBackupProductEdition
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-EnableGPUScheduling
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-EnableSystemRestore
+11
View File
@@ -0,0 +1,11 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
# Set the TDR level
# Note: 1 = Disable, 3 = Recover (default)
[int]$TdrLevel = '{[TDR_Level]}'
# Set TDR delay
# Note: 2 = Default
[int]$TdrDelay = '{[TDR_Delay]}'
Run-Script -LivePSScript Fix-GPUTimeoutDetectionResponse
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-GroupPolicyUpdate
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-HighCPUUsage
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-HighMemoryUsage
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-MicrosoftTeams
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-OfficeModernAuth
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-OutlookUnblockAttachmentTypes
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-RebootIntoSafeMode
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-ReinstallAllAppXPackages
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-Service-QuickBooksDB31
+9
View File
@@ -0,0 +1,9 @@
@echo off
rem Remove registry properties that add the Splashtop PDF Printer to the system
REG DELETE "HKEY_LOCAL_MACHINE\SOFTWARE\Splashtop Inc.\Splashtop Remote Server" /f /v PrinterINFa
REG DELETE "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Splashtop Inc.\Splashtop Remote Server" /f /v PrinterINFa
rem Restart the SplashtopRemoteService
net stop SplashtopRemoteService
net start SplashtopRemoteService
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-SplashtopService
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-SystemPrintQueue
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-UpdateLocalHosts
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-WindowsHealth
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-WindowsManagementInsturmentationWMI
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Fix-WindowsUpdate
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Get-BackupCapacityPlanningData
+23
View File
@@ -0,0 +1,23 @@
#Requires -Version 5.0
# Syncro supplies DSAManifestSource, Zip, Upload, and DeleteAfterUpload.
# Preserve those caller variables for the repository script.
# This must run before downloading Tools.ps1, including on Server 2016.
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$RmmToolsUrl = 'https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1'
$RmmToolsClient = New-Object Net.WebClient
Try {
$RmmToolsText = $RmmToolsClient.DownloadString($RmmToolsUrl)
$RmmToolsScript = [Scriptblock]::Create($RmmToolsText)
}
Catch {
Throw "Unable to download or parse Tools.ps1 from ${RmmToolsUrl}: $($_.Exception.Message)"
}
Finally {
$RmmToolsClient.Dispose()
}
# Dot-source in this scope so the RMM input variables remain accessible.
. $RmmToolsScript
Run-Script -LivePSScript Get-DSAManifest
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Get-DisplayInformation
+3
View File
@@ -0,0 +1,3 @@
. ([ScriptBlock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
$DriveLetter = '{[DriveLetter]}'
Get-FileSizes -Path "${DriveLetter}:\" -Recurse -IncludeHidden -Largest 50 -Units 'GB'
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Get-InstalledSoftware
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Get-UserLogonActivity
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Get-WindowsAppXPackages
@@ -0,0 +1,4 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Create-NetworkDrive -DriveLetter O: -Path "\\nexus\Office Drive" -Label "Office Drive"
Create-NetworkDrive -DriveLetter P: -Path "\\nexus\CWolfe Drive" -Label "Design Drive"
Create-NetworkDrive -DriveLetter Z: -Path "\\nexus\Accounting Drive" -Label "Accounting Drive"
+32
View File
@@ -0,0 +1,32 @@
# The following section is by j.mcbride from https://community.syncromsp.com/t/powershell-7-2/7425/5
# Check for required PowerShell version (7+)
if (!($PSVersionTable.PSVersion.Major -ge 7)) {
try {
# Install PowerShell 7 if missing
if (!(Test-Path "$env:SystemDrive\Program Files\PowerShell\7")) {
Write-Output 'Installing PowerShell version 7...'
Invoke-Expression "& { $(Invoke-RestMethod https://aka.ms/install-powershell.ps1) } -UseMSI -Quiet"
}
# Refresh PATH
$env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'User')
# Restart script in PowerShell 7
pwsh -File "`"$PSCommandPath`"" @PSBoundParameters
}
catch {
Write-Output 'PowerShell 7 was not installed. Update PowerShell and try again.'
throw $Error
}
finally { exit $LASTEXITCODE }
}
# Input the actual script below this line
# The rest of this script is by Emberkom
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
$HuduAPIKey = ''
$HuduBaseDomain = 'https://emberkom.huducloud.com'
$CompanyName = $CName
Run-Script -LivePSScript Hudu-DocumentADDS
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Install-4KVideoDownloader
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Install-AdobeConnect
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Install-AmazonCorretto
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Install-AutodeskDesktopConnector
+3
View File
@@ -0,0 +1,3 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
# $VisionURL = ''
Run-Script -LivePSScript Install-DeltekVision -Arguments $VisionURL
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Install-DisplayLink
+12
View File
@@ -0,0 +1,12 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
# Set the edition of the ECB agent.
# Valid options are 'desktop' 'server' or 'vm'
$Edition = '{[Edition]}'
# Set the MSP360 username and password
$MSP360Username = '{[MSP360_Username]}'
$MSP360Password = '{[MSP360_Password]}'
# Run the installation script
Run-Script -LivePSScript Install-EmberkomCloudBackup
+3
View File
@@ -0,0 +1,3 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Install-Enscape
+6
View File
@@ -0,0 +1,6 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
# Set the host to connect to
$LFHost = '{[LF_Base_URL]}'
Run-Script -LivePSScript Install-LiquidFilesOutlookAgent
+5
View File
@@ -0,0 +1,5 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
# RMM provider must supply the value for $COMPANY variable
Run-Script -LivePSScript Install-MicrosoftOffice365
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Install-Nextcloud
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Install-OpenVPN
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Install-SimpleInOut
+6
View File
@@ -0,0 +1,6 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
## Version of SketchUp to install
$InstallVersion = '{[Version]}'
Run-Script -LivePSScript Install-SketchUp
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Install-SpecsIntact
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Install-Wireguard
@@ -0,0 +1,247 @@
# Settings
$UserPrincipalName = "admin@constructtechservices.com"
$EnableExternalTagging = $true
$ExternalTaggingAllowedDomains = @{Add="emberkom.com", "ef-capital.com", "elysionconstruction.com", "elysioncreations.com", "tesseractrentals.com", "arcadiapropertyservices.com"}
$BypassSenderDomains = @(
"emberkom.com",
"notify.emberkom.com",
"ef-capital.com",
"elysionconstruction.com",
"elysioncreations.com",
#"constructtechservices.com",
"tesseractrentals.com",
"arcadiapropertyservices.com"
)
$SetImpersonationRule = $true
$SetSuspiciousEmailRule = $true
$SetGeneralExternalEmailRule = $true
# Connect to EOL
Connect-ExchangeOnline -UserPrincipalName $UserPrincipalName
# Enable/disable external tagging for Outlook clients
If ( (Get-ExternalInOutlook).Enabled -ne $EnableExternalTagging ) {
If ( $EnableExternalTagging ) {
Write-Output "Enabling external email tagging"
} Else {
Write-Output "Disabling external email tagging"
}
If ( $EnableExternalTagging -and $ExternalTaggingAllowedDomains ) {
Set-ExternalInOutlook -Enabled $EnableExternalTagging -AllowList $ExternalTaggingAllowedDomains
} Else {
Set-ExternalInOutlook -Enabled $EnableExternalTagging
}
}
# Impersonation Rule
If ( $SetImpersonationRule ) {
$ImpersonationEmailDisclaimerTitle = "Warning:"
$ImpersonationEmailDisclaimerText = "This appears to be a fradulent email attempting to impersonate someone inside your organization. Do not click on links or open attachments unless you are certain this email is safe."
$ImpersonationRuleName = "Impersonation Warning"
$ImpersonationDisclaimer = '<table border=0 cellspacing=0 cellpadding=0 align="left" width="100%">
<tr>
<td style="background:#dc3232;padding:5pt 2pt 5pt 2pt"></td>
<td width="100%" cellpadding="7px 6px 7px 15px" style="background:#fff8e5;padding:5pt 4pt 5pt 12pt;word-wrap:break-word">
<div style="color:#222222;">
<span style="color:#222; font-weight:bold;">' + $ImpersonationEmailDisclaimerTitle + ' </span>' + $ImpersonationEmailDisclaimerText + '
</div>
</td>
</tr>
</table>
<br/>'
# Set the transport rule for user impersonation
$DisplayNames = (Get-EXOMailbox -ResultSize Unlimited -RecipientTypeDetails UserMailbox).DisplayName
$ImpersonationTransportRule = Get-TransportRule | Where-Object { $_.Name -eq $ImpersonationRuleName }
If ( $ImpersonationTransportRule ) {
Write-Output "Updating transport rule: ${ImpersonationRuleName}"
Set-TransportRule -Identity $ImpersonationRuleName `
-FromScope NotInOrganization `
-SentToScope InOrganization `
-HeaderMatchesMessageHeader From `
-HeaderMatchesPatterns $DisplayNames `
-ExceptIfSenderDomainIs $BypassSenderDomains `
-ApplyHtmlDisclaimerLocation Prepend `
-ApplyHtmlDisclaimerText $ImpersonationDisclaimer `
-ApplyHtmlDisclaimerFallbackAction Wrap
Write-Output "Transport rule updated"
} Else {
Write-Output "Creating transport rule: ${ImpersonationRuleName}"
New-TransportRule -Name $ImpersonationRuleName `
-FromScope NotInOrganization `
-SentToScope InOrganization `
-HeaderMatchesMessageHeader From `
-HeaderMatchesPatterns $DisplayNames `
-ExceptIfSenderDomainIs $BypassSenderDomains `
-ApplyHtmlDisclaimerLocation Prepend `
-ApplyHtmlDisclaimerText $ImpersonationDisclaimer `
-ApplyHtmlDisclaimerFallbackAction Wrap
Write-Output "Transport rule created"
}
}
# Suspicious Email Rule
If ( $SetSuspiciousEmailRule ) {
$SuspiciousEmailDisclaimerTitle = "Caution:"
$SuspiciousEmailDisclaimerText = "This is a suspicious email from outside your organization. Please be cautious when clicking links or opening attachments."
$SuspiciousEmailRuleName = "Suspicious Email Warning"
$SuspiciousEmailDisclaimer = '<table border=0 cellspacing=0 cellpadding=0 align="left" width="100%">
<tr>
<td style="background:#ffb900;padding:5pt 2pt 5pt 2pt"></td>
<td width="100%" cellpadding="7px 6px 7px 15px" style="background:#fff8e5;padding:5pt 4pt 5pt 12pt;word-wrap:break-word">
<div style="color:#222222;">
<span style="color:#222; font-weight:bold;">' + $SuspiciousEmailDisclaimerTitle + ' </span>' + $SuspiciousEmailDisclaimerText + '
</div>
</td>
</tr>
</table>
<br />'
# This list is copied from: https://github.com/SwiftOnSecurity/PhishingRegex/blob/master/PhishingRegex.txt
$SuspiciousEmailPatterns = @(
'blocked\ your?\ online',
'suspicious\ activit',
'updated?\ your\ account\ record',
'Securely\ \S{3,4}\ one(\ )?drive',
'Securely\ \S{3,4}\ drop(\ )?box',
'Securely\ \S{3,4}\ Google\ Drive',
'sign\ in\S{0,7}(with\ )?\ your\ email\ address',
'Verify\ your\ ID\s',
'dear\ \w{3,8}(\ banking)?\ user',
'chase\S{0,10}\.html"',
'\b(?<=https?://)(www\.)?icloud(?!\.com)',
'(?<![\x00\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5A])appie\W',
'/GoogleDrive/',
'/googledocs?/',
'/Dropfile/',
'limit\ (and\ suspend\ )?your\ account',
'\b(?<=https?://)(?!www\.paypal\.com/)\S{0,40}pa?y\S{0,2}al(?!\S*\.com/)',
'sitey\.me',
'myfreesites\.net',
'/uploadfile/',
'/\S{0,3}outloo\S{0,2}k\S{1,3}\W',
'\b(?<=https?://webmail\.)\S{0,40}webmail\w{0,3}(?!/[0-9])(?!\S{0,40}\.com/)',
'owaportal',
'outlook\W365',
'/office\S{0,3}365/',
'-icloud\Wcom',
'pyapal',
'/docu\S{0,3}sign\S{1,4}/',
'/helpdesk/',
'pay\Sa\S{0,2}login',
'/natwest/',
'/dro?pbo?x/',
'%20paypal',
'\.invoice\.php',
'security-?err',
'/newdropbox/',
'/www/amazon',
'simplefileupload',
'security-?warning',
'-(un)?b?locked',
'//helpdesk(?!\.)',
'\.my-free\.website',
'mail-?update',
'\.yolasite\.com',
'//webmail(?!\.)',
'\.freetemplate\.site',
'\.sitey\.me',
'\.ezweb123\.com',
'\.tripod\.com',
'\.myfreesites\.net',
'mailowa',
'-icloud',
'icloud-',
'contabo\.net',
'\.xyz/',
'ownership\ validation\ (has\ )?expired',
'icloudcom',
'\w\.jar(?=\b)',
'/https?/www/',
'\.000webhost(app)?\.com',
'is\.gd/',
'\.weebly\.com',
'\.wix\.com',
'tiny\.cc/',
'\.joburg',
'\.top/'
)
# Set the transport rule for suspicious emails
$SuspiciousEmailRule = Get-TransportRule | Where-Object { $_.Name -eq $SuspiciousEmailRuleName }
If ( $SuspiciousEmailRule ) {
Write-Output "Updating transport rule: ${SuspiciousEmailRuleName}"
Set-TransportRule -Identity $SuspiciousEmailRuleName `
-FromScope NotInOrganization `
-SentToScope InOrganization `
-SubjectOrBodyMatchesPatterns $SuspiciousEmailPatterns `
-ExceptIfSenderDomainIs $BypassSenderDomains `
-ApplyHtmlDisclaimerLocation Prepend `
-ApplyHtmlDisclaimerText $SuspiciousEmailDisclaimer `
-ApplyHtmlDisclaimerFallbackAction Wrap
Write-Output "Transport rule updated"
} Else {
Write-Output "Creating transport rule: ${SuspiciousEmailRuleName}"
New-TransportRule -Name $SuspiciousEmailRuleName `
-FromScope NotInOrganization `
-SentToScope InOrganization `
-SubjectOrBodyMatchesPatterns $SuspiciousEmailPatterns `
-ExceptIfSenderDomainIs $BypassSenderDomains `
-ApplyHtmlDisclaimerLocation Prepend `
-ApplyHtmlDisclaimerText $SuspiciousEmailDisclaimer `
-ApplyHtmlDisclaimerFallbackAction Wrap
Write-Output "Transport rule created"
}
}
# General External Email Rule
If ( $SetGeneralExternalEmailRule ) {
$GeneralExternalEmailDisclaimerTitle = "Notice:"
$GeneralExternalEmailDisclaimerText = "This email came from outside your organization. Please be sure you know the recipient or were expecting this email before clicking on links or opening attachments."
$GeneralExternalEmailRuleName = "External Email Warning"
$GeneralExternalEmailDisclaimer = '<table border=0 cellspacing=0 cellpadding=0 align="left" width="100%">
<tr>
<td style="background:#00A0d2;padding:5pt 2pt 5pt 2pt"></td>
<td width="100%" cellpadding="7px 6px 7px 15px" style="background:#e5f5fa;padding:5pt 4pt 5pt 12pt;word-wrap:break-word">
<div style="color:#222222;">
<span style="color:#222; font-weight:bold;">' + $GeneralExternalEmailDisclaimerTitle + ' </span>' + $GeneralExternalEmailDisclaimerText + '
</div>
</td>
</tr>
</table>
<br/>'
# Set the transport rule for all external emails
$GeneralExternalEmailRule = Get-TransportRule | Where-Object { $_.Name -eq $GeneralExternalEmailRuleName }
If ( $GeneralExternalEmailRule ) {
Write-Output "Updating transport rule: ${GeneralExternalEmailRuleName}"
Set-TransportRule -Identity $GeneralExternalEmailRuleName `
-FromScope NotInOrganization `
-SentToScope InOrganization `
-ExceptIfSenderDomainIs $BypassSenderDomains `
-ApplyHtmlDisclaimerLocation Prepend `
-ApplyHtmlDisclaimerText $GeneralExternalEmailDisclaimer `
-ApplyHtmlDisclaimerFallbackAction Wrap
Write-Output "Transport rule updated"
} Else {
Write-Output "Creating transport rule: ${GeneralExternalEmailRuleName}"
New-TransportRule -Name $GeneralExternalEmailRuleName `
-FromScope NotInOrganization `
-SentToScope InOrganization `
-ExceptIfSenderDomainIs $BypassSenderDomains `
-ApplyHtmlDisclaimerLocation Prepend `
-ApplyHtmlDisclaimerText $GeneralExternalEmailDisclaimer `
-ApplyHtmlDisclaimerFallbackAction Wrap
Write-Output "Transport rule created"
}
}
# Close connection to EOL
Disconnect-ExchangeOnline -Confirm:$false | Out-Null
+8
View File
@@ -0,0 +1,8 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
$Username = '{[Username]}'
$Password = '{[Password]}'
$FullName = '{[FullName]}'
$Description = '{[Description]}'
If ( '{[MakeAdmin]}' -eq 'true' ) { $MakeAdmin = $true } Else { $MakeAdmin = $false }
If ( '{[Hide]}' -eq 'true' ) { $Hide = $true } Else { $Hide = $false }
New-LocalUser -Username "${Username}" -Password "${Password}" -FullName "${FullName}" -Description "${Description}" -MakeAdmin:$MakeAdmin -Hide:$Hide
+6
View File
@@ -0,0 +1,6 @@
@ECHO OFF
REM Atera agent reinstallation script
powershell -ExecutionPolicy Bypass -InputFormat None -NonInteractive -Command "((New-Object System.Net.WebClient).DownloadFile('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Install-AteraAgent.ps1','Install-AteraAgent.ps1')); ./Install-AteraAgent.ps1 "-FORCE""
DEL /F /Q Install-AteraAgent.ps1
:END
EXIT /B
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Remove-AllESETProducts
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Remove-DeltekVision
+9
View File
@@ -0,0 +1,9 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
## These are RMM variables
$Path = '{[Path]}'
$OlderThanDays = '{[OlderThanDays]}'
Try { $OlderThanDays = [int]$OlderThanDays }
Catch { LogErr $_.Exception.Message }
Run-Script -LivePSScript Remove-OldFiles
+2
View File
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Remove-OldRevitLocals
+4
View File
@@ -0,0 +1,4 @@
# Note: this script came from https://adamtheautomator.com/powershell-delete-user-profile/
$UserToDelete = '{[UserToDelete]}'
Get-CimInstance -Class Win32_UserProfile | Where-Object { $_.LocalPath.split('\')[-1] -eq "${UserToDelete}" } | Remove-CimInstance
+5
View File
@@ -0,0 +1,5 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
$MaxUptimeHours = '{[MaxUptimeHours]}'
Restart-EndpointOnUptime -MaxUptime $(New-TimeSpan -Hours [int]$MaxUptimeHours)
@@ -0,0 +1,2 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Set-DefaultAdministratorCredentials

Some files were not shown because too many files have changed in this diff Show More