Powershell workflows, installing software locally after disabling UAC? - powershell

So I'm bored and trying to learn about WorkFlows in Powershell but somethings just not clicking. So I've got a script that prompts for admin creds to open a new POSH window, then makes the registry changes to disable UAC (only for the duration of the script), then called some exe and MSI installers located on a mapped drive.
import-module ActiveDirectory
$erroractionpreference = 'SilentlyContinue'
#Elevate running of script to Administrative
param([switch]$Elevated)
function Check-Admin {
$currentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent())
$currentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}
if ((Check-Admin) -eq $false) {
if ($elevated)
{
# could not elevate, quit
}
else {
Start-Process powershell.exe -Verb RunAs -ArgumentList ('-noprofile -noexit -file "{0}" -elevated' -f ($myinvocation.MyCommand.Definition))
}
exit
}
#Workflow to restart the pc
Workflow reboot_and_continue {
#The following section allows for changing the UAC settings for the duration of the script
{
New-Variable -Name Key
New-Variable -Name PromptOnSecureDesktop_Name
New-Variable -Name ConsentPromptBehaviorAdmin_Name
Function Set-RegistryValue($key, $name, $value, $type="Dword") {
If ((Test-Path -Path $key) -Eq $false) { New-Item -ItemType Directory -Path $key | Out-Null }
Set-ItemProperty -Path $key -Name $name -Value $value -Type $type
}
Function Get-RegistryValue($key, $value) {
(Get-ItemProperty $key $value).$value
}
$Key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"
$ConsentPromptBehaviorAdmin_Name = "ConsentPromptBehaviorAdmin"
$PromptOnSecureDesktop_Name = "PromptOnSecureDesktop"
Function Get-UACLevel(){
$ConsentPromptBehaviorAdmin_Value = Get-RegistryValue $Key $ConsentPromptBehaviorAdmin_Name
$PromptOnSecureDesktop_Value = Get-RegistryValue $Key $PromptOnSecureDesktop_Name
If($ConsentPromptBehaviorAdmin_Value -Eq 0 -And $PromptOnSecureDesktop_Value -Eq 0){
"Never notIfy"
}
ElseIf($ConsentPromptBehaviorAdmin_Value -Eq 5 -And $PromptOnSecureDesktop_Value -Eq 0){
"NotIfy me only when apps try to make changes to my computer(do not dim my desktop)"
}
ElseIf($ConsentPromptBehaviorAdmin_Value -Eq 5 -And $PromptOnSecureDesktop_Value -Eq 1){
"NotIfy me only when apps try to make changes to my computer(default)"
}
ElseIf($ConsentPromptBehaviorAdmin_Value -Eq 2 -And $PromptOnSecureDesktop_Value -Eq 1){
"Always notIfy"
}
Else{
"Unknown"
}
}
Function Set-UACLevel() {
Param([int]$Level= 2)
New-Variable -Name PromptOnSecureDesktop_Value
New-Variable -Name ConsentPromptBehaviorAdmin_Value
If($Level -In 0, 1, 2, 3) {
$ConsentPromptBehaviorAdmin_Value = 5
$PromptOnSecureDesktop_Value = 1
Switch ($Level)
{
0 {
$ConsentPromptBehaviorAdmin_Value = 0
$PromptOnSecureDesktop_Value = 0
}
1 {
$ConsentPromptBehaviorAdmin_Value = 5
$PromptOnSecureDesktop_Value = 0
}
2 {
$ConsentPromptBehaviorAdmin_Value = 5
$PromptOnSecureDesktop_Value = 1
}
3 {
$ConsentPromptBehaviorAdmin_Value = 2
$PromptOnSecureDesktop_Value = 1
}
}
Set-RegistryValue -Key $Key -Name $ConsentPromptBehaviorAdmin_Name -Value $ConsentPromptBehaviorAdmin_Value
Set-RegistryValue -Key $Key -Name $PromptOnSecureDesktop_Name -Value $PromptOnSecureDesktop_Value
Get-UACLevel
}
Else{
"No supported level"
}
}
Export-ModuleMember -Function Get-UACLevel
Export-ModuleMember -Function Set-UACLevel
}
set-uaclevel 0
#This section calls the installers from a network share and runs them locally
Invoke-Command -ComputerName $env:COMPUTERNAME -ScriptBlock { Start-Process -FilePath "g:\IT\software\ChromeSetup.exe" -ArgumentList "/s" -Wait }
if ($setup.exitcode -eq 0)
Invoke-Command -ComputerName $env:COMPUTERNAME -ScriptBlock { Start-Process -FilePath "g:\IT\support\NSG_Vipre_agent_ssi-mi.msi" -ArgumentList "-silent" -Wait }
if ($setup.exitcode -eq 0)
Invoke-Command -ComputerName $env:COMPUTERNAME -ScriptBlock { Start-Process -FilePath "G:\IT\software\spiceworks\install_collection.bat" -Wait }
if ($setup.exitcode -eq 0)
Line 25 is where I'm thinking of trying to introduce the work flow.. but on second thought it might be better to encompass the entire thing in the workflow... I just don't know.
I've tried searching but I just can't find any examples of how to install software using a workflow. the idea is to run a powershell window with admin creds, disabled UAC then reboot. Once started back up call the various installers then re-enable UAC and reboot again.
Thanks!

Related

PowerShell Script Issues with Variable Values

I am trying to write this script to restart computers only if they are Offline. The script for getting user infomration works but I cannot get the variable values for the restart portion at the bottom of the script. Does anyone have a suggestion? I am somewhat new to Powershell, but writing code. Example of my script follows:
Function Get-LoggedOnUser
{
Param
(
$ComputerName = $env:COMPUTERNAME,
$Credential
)
Function Test-RemoteRegistry
{
Param
(
[Parameter(Mandatory = $False)]
[switch]$Enable
,
[Parameter(Mandatory = $False)]
[switch]$Disable
,
[Parameter(ValueFromPipeline=$True)]
[String[]]$ComputerName = $env:COMPUTERNAME
)
Begin
{
$PipelineInput = (-not $PSBOUNDPARAMETERS.ContainsKey("ComputerName")) -and (-not $ComputerName)
Function Test ($Computer)
{
Try
{
[Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $Computer) | Out-Null
#20ms faster than Get-Service per computer! Not sure how to handle/check things like the firewall though...
#If we hit here without error Remote Reg is enabled.
If ($Disable)
{
Try
{
Get-Service -Name RemoteRegistry -ComputerName $Computer | Set-Service -Status Stopped -ErrorAction Stop
Return $False
#If we hit here without error Remote Reg is now disabled.
}
Catch
{
Return $True
#If we hit here, we couldn't stop remote registry.
}
}
Else
{
Return $True
}
}
Catch
{
If ($Enable)
{
Try
{
Get-Service -Name RemoteRegistry -ComputerName $Computer | Set-Service -Status Running -ErrorAction Stop
Return $True
#If we hit here without error Remote Reg is now enabled.
}
Catch
{
Return $False
#If we hit here, we couldn't start remote registry.
}
}
Else
{
Return $False
#If we hit here remote registry is disabled.
}
}
}
}
Process
{
If ($PipelineInput)
{
Test $_
}
Else
{
$ComputerName | ForEach-Object {
Test $_
}
}
}
}
Foreach ($Computer in $Computername)
{
$Online = $False
$User = $False
$Locked = $False
If (Test-Connection $Computer -Count 2 -Quiet)
{
$Online = $True
If ($Credential)
{
$User = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $Computer -Credential $Credential | Select-Object -ExpandProperty UserName -ErrorAction Stop
}
Else
{
$User = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $Computer | Select-Object -ExpandProperty UserName -ErrorAction Stop
}
If (Test-RemoteRegistry -Enable -ComputerName $Computer)
{
If ((Get-Process logonui -ComputerName $Computer -ErrorAction SilentlyContinue) -and ($user))
{
$Locked = $True
}
}
}
$Output = New-Object PSObject
$Output | Add-Member noteproperty ComputerName $Computer
$Output | Add-Member noteproperty Online $Online
$Output | Add-Member noteproperty Username $User
$Output | Add-Member noteproperty Locked $Locked
$Output
}
}
Get-LoggedOnUser
If (($Online) -eq $False)
{Shutdown /r t 0 /m \\$Computername}
ELSE
{Write-host 'HELLO $Online $Computername'}
I just want this for a single user as I am using PDQ Inventory to roll out the script. The variables at the end of the script are $null?
Variables defined in a child scope - in which functions run by default - are never seen by the calling scope. See the conceptual about_Scopes help topic
It's best for functions to communicate values to the caller via their output ("return value"), which you're function is already doing: it outputs objects whose properties contain the values of interest.
Therefore:
Get-LoggedOnUser |
ForEach-Object { # Loop over all output objects
# Refer to the object at hand via the automatic $_ variable.
# Note the use of "..." (expandable strings) so as to support
# expansion (string interpolation).
if (-not $_.Online) { Shutdown /r t 0 /m "\\$($_.ComputerName)" }
else { "HELLO $($_.Online) $($_.ComputerName)" }
}

Running a Powershell command to another ws that requires administrator privileges

I'm required to run a TPM command (requires admin access).
Lets use these for the legend:
Angelo - Standard user
AngeloAdmin - Admin user
Windows7 - Computer that has a standard user in it
How can I use my standard account to run the script as an Administrator to execute a script to another computer with a standard account remotely?
Heres the partial code I will run:
Set-Variable -Name BuildLog -Scope Global -Force
Set-Variable -Name Errors -Value $null -Scope Global -Force
Set-Variable -Name LogFile -Scope Global -Force
Set-Variable -Name Phase -Scope Global -Force
Set-Variable -Name RelativePath -Scope Global -Force
Set-Variable -Name Sequence -Scope Global -Force
Set-Variable -Name Title -Scope Global -Force
Function ConsoleTitle ($Title){
$host.ui.RawUI.WindowTitle = $Title
}
Function DeclareGlobalVariables {
$Global:BuildLog = $Env:windir+"\Logs\BuildLogs\Build.csv"
$Global:LogFile = $Env:windir+"\Logs\BuildLogs\TPM_On.log"
$Global:Phase = "Final Build"
$Global:Sequence = ""
$Global:Title = "TPM Clear Ownership"
}
Function GetRelativePath {
$Global:RelativePath = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent)+"\"
}
Function ClearTPM {
#Declare Local Memory
Set-Variable -Name ClassName -Value "Win32_Tpm" -Scope Local -Force
Set-Variable -Name Computer -Value $env:COMPUTERNAME -Scope Local -Force
Set-Variable -Name NameSpace -Value "ROOT\CIMV2\Security\MicrosoftTpm" -Scope Local -Force
Set-Variable -Name oTPM -Scope Local -Force
$oTPM = Get-WmiObject -Class $ClassName -ComputerName $Computer -Namespace $NameSpace
$Output = "Clearing TPM Ownership....."
Write-Host "Clearing TPM Ownership....." -NoNewline
$Temp = $oTPM.SetPhysicalPresenceRequest(5)
If ($Temp.ReturnValue -eq 0) {
$Output = "Success"
Write-Host "Success" -ForegroundColor Yellow
} else {
$Output = "Failure"
Write-Host "Failure" -ForegroundColor Red
$Global:Errors++
}
Out-File -FilePath $Global:LogFile -InputObject $Output -Append -Force
#Cleanup Local Memory
Remove-Variable -Name oTPM -Scope Local -Force
}
Function ProcessLogFile {
If ((Test-Path $Env:windir"\Logs") -eq $false) {
New-Item -ItemType Directory -Path $Env:windir"\Logs"
}
If ((Test-Path $Env:windir"\Logs\ApplicationLogs") -eq $false) {
New-Item -ItemType Directory -Path $Env:windir"\Logs\ApplicationLogs"
}
If ((Test-Path $Env:windir"\Logs\BuildLogs") -eq $false) {
New-Item -ItemType Directory -Path $Env:windir"\Logs\BuildLogs"
}
If ($Global:Errors -eq $null) {
If (Test-Path $Global:LogFile) {
Remove-Item $Global:LogFile -Force
}
$File1 = $Global:LogFile.Split(".")
$Filename1 = $File1[0]+"_ERROR"+"."+$File1[1]
If (Test-Path $Filename1) {
Remove-Item $Filename1 -Force
}
$Global:Errors = 0
} elseIf ($Global:Errors -ne 0) {
If (Test-Path $Global:LogFile) {
$Global:LogFile.ToString()
$File1 = $Global:LogFile.Split(".")
$Filename1 = $File1[0]+"_ERROR"+"."+$File1[1]
Rename-Item $Global:LogFile -NewName $Filename1 -Force
}
} else {
$date = get-date
$LogTitle = $Global:Phase+[char]9+$Global:Sequence+[char]9+$Global:Title+[char]9+$date.month+"/"+$date.day+"/"+$date.year+" "+$date.hour+":"+$date.minute
Out-File -FilePath $Global:BuildLog -InputObject $LogTitle -Append -Force
}
}
Function ExitPowerShell {
If (($Global:Errors -ne $null) -and ($Global:Errors -ne 0)) {
Exit 1
}
}
cls
GetRelativePath
DeclareGlobalVariables
ConsoleTitle $Global:Title
ProcessLogFile
ClearTPM
ProcessLogFile
Start-Sleep -Seconds 5
ExitPowerShell
If you want to run the script as a different user you can 'SHIFT+Right click > Run as a different user' but that works only on applications, so you have to run Powershell as a different user and then the script, you can make a batch file to do that. Here is an example of what you will need in the batch file.
runas /user:yourdomain.com\administrator powershell
If you dont have a domain use the computer name
runas /noprofile /user:computername\administrator powershell

Powershell script to install software

I am trying to make a powershell script that I can use with an RMM tool. So, basically this powershell script would be executed on the local machine. It would need to check to see if the version of the application is installed and at least version number xx. If not installed, or version is less, it would then download executable and silently install it.
I found an example online for Adobe Reader that does work, but it doesn't do the check before hand. So, this script would install Adobe Reader every time it is ran.
$tempFolder=$Env:TEMP
function runProcess ($cmd, $params) {
$p = new-object System.Diagnostics.Process
$p.StartInfo = new-object System.Diagnostics.ProcessStartInfo
$exitcode = $false
$p.StartInfo.FileName = $cmd
$p.StartInfo.Arguments = $params
$p.StartInfo.UseShellExecute = $False
$p.StartInfo.RedirectStandardError = $True
$p.StartInfo.RedirectStandardOutput = $True
$p.StartInfo.WindowStyle = 1;
$null = $p.Start()
$p.WaitForExit()
$output = $p.StandardOutput.ReadToEnd()
$exitcode = $p.ExitCode
$p.Dispose()
$exitcode
$output
}
#download installer
invoke-webrequest "ftp://ftp.adobe.com/pub/adobe/reader/win/AcrobatDC/1500720033/AcroRdrDC1500720033_en_US.msi" -OutFile "$tempFolder\AcroRdrDC1500720033_en_US.msi" -ErrorAction Stop
#run installer
$res = runProcess msiexec "/i $tempFolder\AcroRdrDC1500720033_en_US.msi /qb"
#check if return code is 0
if(0 -ne $res[0]){
return "Failed to install Adobe Reader: $($res[0]) $($res[1])"
}
#download the patch
invoke-webrequest "ftp://ftp.adobe.com/pub/adobe/reader/win/AcrobatDC/1502320070/AcroRdrDCUpd1502320070.msp" -OutFile "$tempFolder\AcroRdrDCUpd1502320070.msp" -ErrorAction Stop
#install it
$res = runProcess msiexec "/p $tempFolder\AcroRdrDCUpd1502320070.msp /qb"
#check if return code is 0
if(0 -ne $res[0]){
return "Failed to install Adobe Reader DC Patch: $($res[0]) $($res[1])"
}else{
#Attempt to silently disable the auto updater if set in this script
new-itemproperty "HKLM:\Software\Policies\Adobe\Acrobat Reader\DC\FeatureLockDown" -name bUpdater -value 0 -ErrorAction SilentlyContinue
}
I think there are some things that may not be needed in this script. It also doesn't have the check for version.
Found this on another site, but not sure how to implement it. Also, it doesn't look like it checks for version number.
function Is-Installed( $program ) {
$x86 = ((Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall") |
Where-Object { $_.GetValue( "DisplayName" ) -like "*$program*" } ).Length -gt 0;
$x64 = ((Get-ChildItem "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall") |
Where-Object { $_.GetValue( "DisplayName" ) -like "*$program*" } ).Length -gt 0;
return $x86 -or $x64;
}
Ideally, I would like to set the parameters at the top so I could use a template for other executables. For instance
$app_name
$app_version
$app_url
$app_filename
$app_executable
$app_arguments
Any help would be greatly appreciated.
I did some more digging and found this post: How to check if a program is installed and install it if it is not?
Which allows for matching Name and Version:
$tempdir = Get-Location
$tempdir = $tempdir.tostring()
$appName = '*AirParrot*'
$appVersion = "2.6.8"
$msiFile = $tempdir+"\microsoft.interopformsredist.msi"
$msiArgs = "-qb"
function Get-InstalledApps
{
if ([IntPtr]::Size -eq 4) {
$regpath = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
}
else {
$regpath = #(
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
}
Get-ItemProperty $regpath | .{process{if($_.DisplayName -and $_.UninstallString) { $_ } }} | Select DisplayName, Publisher, InstallDate, DisplayVersion, UninstallString |Sort DisplayName
}
$result = Get-InstalledApps | where {$_.DisplayName -like $appName -and $_.DisplayVersion -ge $appVersion}
If ($result -eq $null) {
(Start-Process -FilePath $msiFile -ArgumentList $msiArgs -Wait -Passthru).ExitCode
}

Delete OST file and run outlook process

I have a form that im using to delete a ost file in powershell then starts outlook. I think the process to start outlook is opening quicker than the ost can be deleted. Here's what i have.
taskkill.exe /im outlook.exe /f
$OstPath = $Env:LocalAppData + "\Microsoft" + "\Outlook"
$ost = get-ChildItem $OstPath | where { $_.Extension -eq ".ost"}
$ost | remove-Item
Wait-Job $ost
Start-Process -FilePath "C:\Program Files\Microsoft Office 15\root\office15\outlook.exe"
This is the function i came up with and works really well with Office 2013 and Office 2016:
Function RebuildEmail
{
#COMMENT: Stops all Outlook process' and deletes .ost file then prompts to run setup.
$a = new-object -comobject wscript.shell
$intAnswer = $a.popup("This will stop Outlook and Skype for Business,`nRemoves the local copy of your email and will resynchronize from the exchange server. Be sure you have a steady internet connection! . `nDO YOU WANT TO CONTINUE?", `
0,"Service Desk",4)
If ($intAnswer -ne 6) {
$a.popup("Please call the Service Desk for support.")
} else {
$a.popup("Rebuilding will begin...")
Stop-Process -Name ("Outlook", "Lync")
$n = 0
Do
{
If(Get-Process -Name ("outlook", "Lync") -ErrorAction silentlycontinue)
{
Stop-Process -Name ("Outlook", "Lync") -ErrorAction SilentlyContinue
If ($n -lt 5)
{
$ExitLoop = $false
$n++
Start-Sleep -Seconds 3
If($n -eq 5){$ExitLoop = $true}
}
}
else {$ExitLoop = $true}
}
While ($ExitLoop -eq $false)
if ($n -ne 5)
{
$OstPath = $Env:LocalAppData + "\Microsoft" + "\Outlook"
$OSTs = Get-ChildItem $OstPath | where { $_.Extension -eq ".ost"}
$OSTs | Remove-Item
Start-Sleep -s 3
foreach ($ost in $OSTs)
{
$FilePath = $OstPath + "\" + $ost
$n = 0
Do
{
if(Test-Path $FilePath)
{
Remove-Item $FilePath
Start-Sleep -Seconds 5
$ExitLoop = $false
$n++
if($n -eq 5){$ExitLoop = $true}
}
else {$ExitLoop = $true}
}
While ($ExitLoop -eq $false)
if($n -ne 5)
{
$a = new-object -comobject wscript.shell
$intAnswer = $a.popup("The local copy of your email was successfully removed.", `
0,"Service Desk",0)
}
else
{
$a = new-object -comobject wscript.shell
$intAnswer = $a.popup("Unfortunatley, your local copy couldn't be removed. Please call the Service Desk.", `
0,"Service Desk",0)
}
}
}
else
{
$a = new-object -comobject wscript.shell
$intAnswer = $a.popup("Outlook was unable to close. Please call the Service Desk.", `
0,"Service Desk",0)
}
Start-Process -FilePath "C:\Program Files\Microsoft Office 15\root\office15\outlook.exe" -ErrorAction SilentlyContinue
Start-Process -FilePath "C:\Program Files (x86)\Microsoft Office\root\Office16\outlook.exe" -ErrorAction SilentlyContinue
Start-Process -FilePath "C:\Program Files\Microsoft Office 15\root\office15\lync.exe" -ErrorAction SilentlyContinue
Start-Process -FilePath "C:\Program Files (x86)\Microsoft Office\root\Office16\lync.exe" -ErrorAction SilentlyContinue
Start-Sleep -s 30
$a = new-object -comobject wscript.shell
$intAnswer = $a.popup("Please allow a few minutes to update folders. Call the Service Desk if issue persist.", `
0,"Service Desk",0)
}
}

Getting output from background jobs

I created the following script to reset the password of the local admin account on all machines in a specific host file. This script functions properly and provides a useful output, but it is slow as it only does one machine at a time.
# function to convert a secure string to a standard string
function ConvertTo-String {
param(
[System.Security.SecureString] $secureString
)
$marshal = [System.Runtime.InteropServices.Marshal]
try {
$intPtr = $marshal::SecureStringToBSTR($secureString)
$string = $marshal::PtrToStringAuto($intPtr)
}
finally {
if($intPtr) {
$marshal::ZeroFreeBSTR($intPtr)
}
}
$string
}
$clients = Get-Content -Path C:\scripts\utilities\hostnames_online.txt
$adminUser = "Administrator"
# prompt for password and confirm
do {
$ss1 = Read-Host "Enter new password" -AsSecureString
$ss2 = Read-Host "Enter again to confirm" -AsSecureString
# compare strings - proceed if same - prompt again if different
$ok = (ConvertTo-String $ss1) -ceq (ConvertTo-String $ss2)
Write-Host "Passwords match"
if(-not $ok) {
Write-Host "Passwords do not match"
}
}
until($ok)
# set password variable to string value
$adminPassword = ConvertTo-String $ss1
# setup job to reset password on each client
foreach($client in $clients) {
$status = "OFFLINE"
$isOnline = "OFFLINE"
if((Test-Connection -ComputerName $client -Quiet -Count 1 -Delay 1) -eq $true) {
$isOnline = "ONLINE"
}
# change the password
try {
$localAdminAccount = [adsi]"WinNT://$client/$adminuser,user"
$localAdminAccount.SetPassword($adminPassword)
$localAdminAccount.SetInfo()
Write-Verbose "Password change completed successfully"
}
catch {
$status = "FAILED"
Write-Verbose "Failed to change password"
}
# create psobject with system info
$obj = New-Object -TypeName PSObject -Property #{
ComputerName = $client
Online = $isOnline
ChangeStatus = $status
}
$obj | Select computerName, Online, changeStatus | Out-File -FilePath C:\test.txt -Append
if($status -eq "FAILED" -or $isOnline -eq "OFFLINE") {
$stream.writeline("$client -t $status")
}
}
$adminPassword = " "
Write-Verbose "Complete"
Invoke-Item C:\test.txt
To make the script run faster, I set it up to use background jobs so it could run on multiple clients at once. However, now I get no output in my text files. The new script is below. Lines 43 and 79-89 are the changes. What changes do I need to make to this script provide the output it does in the first version? I've tried a number of other ways to get the output than what I currently have on line 89.
# function to convert a secure string to a standard string
function ConvertTo-String {
param(
[System.Security.SecureString] $secureString
)
$marshal = [System.Runtime.InteropServices.Marshal]
try {
$intPtr = $marshal::SecureStringToBSTR($secureString)
$string = $marshal::PtrToStringAuto($intPtr)
}
finally {
if($intPtr) {
$marshal::ZeroFreeBSTR($intPtr)
}
}
$string
}
$clients = Get-Content -Path C:\scripts\utilities\hostnames_online.txt
$adminUser = "Administrator"
# prompt for password and confirm
do {
$ss1 = Read-Host "Enter new password" -AsSecureString
$ss2 = Read-Host "Enter again to confirm" -AsSecureString
# compare strings - proceed if same - prompt again if different
$ok = (ConvertTo-String $ss1) -ceq (ConvertTo-String $ss2)
Write-Host "Passwords match"
if(-not $ok) {
Write-Host "Passwords do not match"
}
}
until($ok)
# set password variable to string value
$adminPassword = ConvertTo-String $ss1
# setup job to reset password on each client
#-----------------------------------------------------------------------------------------
$scriptBlock = {
#-----------------------------------------------------------------------------------------
foreach($client in $clients) {
$status = "OFFLINE"
$isOnline = "OFFLINE"
if((Test-Connection -ComputerName $client -Quiet -Count 1 -Delay 1) -eq $true) {
$isOnline = "ONLINE"
}
# change the password
try {
$localAdminAccount = [adsi]"WinNT://$client/$adminuser,user"
$localAdminAccount.SetPassword($adminPassword)
$localAdminAccount.SetInfo()
Write-Verbose "Password change completed successfully"
}
catch {
$status = "FAILED"
Write-Verbose "Failed to change password"
}
# create psobject with system info
$obj = New-Object -TypeName PSObject -Property #{
ComputerName = $client
Online = $isOnline
ChangeStatus = $status
}
$obj | Select computerName, Online, changeStatus | Out-File -FilePath C:\test.txt -Append
if($status -eq "FAILED" -or $isOnline -eq "OFFLINE") {
$stream.writeline("$client -t $status")
}
}
}
#-----------------------------------------------------------------------------------------
Get-Job | Remove-Job -Force
Start-Job $scriptBlock -ArgumentList $_ -Name AdminPWReset
Get-Job
While(Get-Job -State "Running") {
Start-Sleep -m 10
}
Receive-Job -name AdminPWReset | Out-File C:\test2.txt
#-----------------------------------------------------------------------------------------
$adminPassword = " "
Write-Verbose "Complete"
Invoke-Item C:\test.txt
Invoke-Item C:\test2.txt
You don't receive output, because you moved your entire loop into the scriptblock:
$scriptBlock = {
foreach ($client in $clients) {
...
}
}
but initialized your client list ($clients) outside the scriptblock, so that the variable $clients inside the scriptblock is a different (empty) variable since it's in a different scope. Because of that your job is iterating over an empty list, and is thus not producing any output.
To be able to use the client list inside the scriptblock you'd have to use the using: scope modifier:
$scriptBlock = {
foreach ($client in $using:clients) {
...
}
}
or pass the client list into the scriptblock as an argument:
$scriptBlock = {
foreach ($client in $args[0]) {
...
}
}
Start-Job -ScriptBlock $scriptBlock -ArgumentList $clients
As I can see from your code you're trying to do the latter:
Start-Job $scriptBlock -ArgumentList $_ -Name AdminPWReset
However, that doesn't work for two reasons:
In the context where you run Start-Job there is no current object, so $_ is empty and nothing is passed into the scriptblock.
The scriptblock neither has a Param() block, nor does it use the automatic variable $args, so even if you passed an argument into the scriptblock it would never be used.
With that said, you don't want to pass $clients in the first place, because even if it worked, it wouldn't speed up anything as the entire client list would still be processed sequentially. What you actually want to do is to process the list in parallel. For that you must put just the tests into the scriptblock and then start one job for each client in a loop:
$scriptBlock = {
Param($client)
$status = "OFFLINE"
$isOnline = "OFFLINE"
if (Test-Connection -Computer $client -Quiet -Count 1 -Delay 1) {
$isOnline = "ONLINE"
}
...
}
foreach ($client in $clients) {
Start-Job -Name AdminPWReset -ScriptBlock $scriptBlock -ArgumentList $client
}
while (Get-Job -State "Running") {
Start-Sleep -Milliseconds 100
}
Receive-Job -Name AdminPWReset | Out-File C:\test2.txt
Remove-Job -Name AdminPWReset
Note that if you have a great number of target hosts you may want to use a job queue, so that you don't have hundreds of jobs running in parallel.