Implementing validation and checks in Powershell script - validation

So I currently have a Powershell script that installs various software silently
What I have at the moment are all of the commands which work perfectly:
Start-Process 'C:\Files\Install files\Firefox' -ArgumentList "/S" -Wait
Start-Process 'C:\Files\Install files\Office' -ArgumentList "/s" -Wait
Start-Process 'C:\Files\Install files\Zeb' -ArgumentList "/S" -Wait
What I now want to implement is 'checking if the installation is succeeded'.
I start the program by checking the install files:
Write-Host "Checking if install files are present..."
if(Test-Path 'C:\Files\Install files\')
{
Write-Host "Files located. Installation will begin"
Write-Host ""
} else
{
Write-Host "Files not located, please check the directory"
Write-Host ""
break
}
And since the code of above is working I thought that I could maybe to the same for installing my software? But using the software directory as a test-path function:
Write-Host "Installing Firefox"
Start-Process 'C:\Files\Install files\Firefox' -ArgumentList "/S" -Wait
if (Test-Path C:\Program Files(x86)\Firefox)
{
Write-Host "Firefox succesfully installed!"
}else
{
Write-Host "Error, FireFox hasn't been installed"
}
.... And continue like this for the other programs
I thought 'why not?' since all of the PC's are the same and I must admit that the code also works.
But how would someone would judge this method? And what are alternatives?
Thanks

Just a note, I think this is more a question for Server Fault or Super User.
There are some high end tools like Desired State Configuration that handle all this but it really depends on the environment. We build standalone servers in an offsite datacentre and it's so much more convenient and less setup to just have a script that installs and configures everything that can't be imaged. Sure it takes two hours to complete but it's unattended, and since the OS is installed from a template we guarantee they're all exactly the same.
Perhaps I wouldn't recommend this method if the hardware were drastically different or if not imaging the OS to run the script on, I used to have a batch file to deploy customer desktops (back in WinXP days) but eventually cut it down to just detect the model and install drivers because it was too much hassle to maintain. I would also not recommend this method if it will be maintained by more than one person, or if the person maintaining it will be maybe/definitely not long term staff.
Overall it's up to the company. DSC is certainly a more professional and complete approach and much more easily maintained by multiple people, so put it to them and if they won't devote resources for a deployment server and training then you don't have much choice either way.

I would recommend to have a look at something more professionell like "PowerShell App Deployment Toolkit". (http://psappdeploytoolkit.com/)
It will take some time for you to start, but it is not that complex.
It outputs logs and so on.

Related

How can I get the corresponding Update_ID for windows patch installation file?

I am writing a script in Power-shell that does basically the same thing that WSUS servers do,
Following the steps below:
WSUS server Downloads and installs the required patches on the client systems.
The PS script if patch is not successfully installed retries the installation on the client systems, by getting & executing the patch installation file (.cab,.msu,.exe) from the client systems.
The patches are selected by a independent process and send to a 3rd party location from where the PS script gets it.
The issue I am facing is the data in the 3rd party location for the patches contains UPDATE_ID as a patch identifier and I need a way to recognise whats is the UPDATE_ID for the existing patch installation file is.
NOTE: I can work through Hotfix_ID as the directory patches are stored in naming standards similar to "$Hotfix_ID". But single Hotfix_ID can have multiple patches.
The affected code is as follows:
param(
$KbFailed
)
foreach($KbNumber IN $KbFailed){
[array]$files_att=Get-ChildItem "C:\Windows\SoftwareDistribution\Download\*\*$KbNumber*"
Write-Host "Installing $KbNumber ..."
if ($null -eq $files_att){
Write-Error "Required file not found for $KbNumber"
}else{
foreach($file_att IN $files_att){
$file_extension=($file_att |Select-Object Extension).Extension
if ($file_extension -eq ".cab"){
dism.exe /online /add-package /PackagePath:$file_att /NoRestart
}
elseif($file_extension -eq ".exe"){
Start-Process "$file_att" -ArgumentList "/q" -Wait
}
elseif($file_extension -eq ".msu"){
Start-Process -FilePath "wusa.exe" -ArgumentList "$file_att /quiet /norestart" -Wait
}
else{
Write-Error "File type $file_extension not supported"
}
}
}
}
As mentioned I am managing it right now with HotFix_ID(Kb_number), but would like to replace it with UPDATE_ID

Poweshell Start-Job from serviceAccount using SailPoint IQService

So there are several factors in play with this question, so here they are:
SailPoint 8.2 and IQService 8.2
Windows Server 2016
A service Account(Domain Admin)
An interactive User account (Domain admin)
Powershell 5.1 build 14393 revision 4583
So what we have is SailPoint is executing a rule on its end, sending over some information to IQService, and IQService is executing the PowerShell scripts as the service account. In one of the PowerShell scripts, we have the following command:
LogToFile("calling start job")
$j = Start-Job -ScriptBlock { C:/SailPoint/Scripts/PowershellContainerAfterCreateRetry.ps1 -sAMAccountName $args[0] -company $args[1] } -ArgumentList $sAMAccountName, $company -Name 'PowershellContainerAfterCreateRetry'
LogToFile($j | Select-Object -Property *)
LogToFile("finished start-job")
and this is where things get interesting because this command, as you can note, we can log to file to see what its output is, which is as follows:
calling start job
#{
State=Running; HasMoreData=True;
StatusMessage=;
Location=localhost;
Command= C:/SailPoint/Scripts/PowershellContainerAfterCreateRetry.ps1 -sAMAccountName $args[0] -company $args[1] ;
JobStateInfo=Running;
Finished=System.Threading.ManualResetEvent;
InstanceId=aa889c06-7a8a-402e-807a-880d02465bdd; Id=1;
Name=PowershellContainerAfterCreateRetry;
ChildJobs=System.Collections.Generic.List`1[System.Management.Automation.Job];
PSBeginTime=10/15/2021 21:14:22; PSEndTime=;
PSJobTypeName=BackgroundJob;
Output=System.Management.Automation.PSDataCollection`1[System.Management.Automation.PSObject];
Error=System.Management.Automation.PSDataCollection`1[System.Management.Automation.ErrorRecord];
Progress=System.Management.Automation.PSDataCollection`1[System.Management.Automation.ProgressRecord];
Verbose=System.Management.Automation.PSDataCollection`1[System.Management.Automation.VerboseRecord];
Debug=System.Management.Automation.PSDataCollection`1[System.Management.Automation.DebugRecord];
Warning=System.Management.Automation.PSDataCollection`1[System.Management.Automation.WarningRecord];
Information=System.Management.Automation.PSDataCollection`1[System.Management.Automation.InformationRecord]}
finished start-job
When I execute this command either by itself OR within this script using Windows PowerShell ISE, it completes with no issue and calls the script in question, and everything works perfectly! (whether I am using my interactive account OR the service account)
When this script executes using the IQService, something "else" is happening - I say something "else" because I don't have any log files or errors; it just seems to disappear into the ether. (I have a log write out five lines into the PowerShell script, so one would think I would at least get SOMETHING!?!? I am out of ideas...thoughts?
As a minor note, I ran an experiment that showed me that there is something strange about the setup which should have succeeded without issue - like the above it appears to execute (because I can see the same information above, that shows that the job has started). Still, just like the above, it never actually "appears" to complete or error out. The only thing I can think of is that somehow the primary script closing out is causing this to close out as well - but I would think it would be able to get a couple of log files written to if that was the case? Anyway...thanks for reading!
$doit = {
"test" | Out-File -filepath ("c:\test.txt") -append
}
Start-job -ScriptBlock $doit
i think Start-Job is the problem here, as iqservice will launch a powershell script process and that may not support the background job aspect you are trying to use.
if you need to have something retry or wait and loop, you'll need to use another identityiq/iqservice mechanism (a workflow in iiq perhaps that calls down to AD when conditions are, timer is hit, etc.) beyond start-job inside of an iqservice powershell script.

uninstall google chrome using powershell script

I am using the following code to uninstall google chrome software from my remote machines, But this script is executed with no output. And when i check the control panel, still the google chrome program exists. Can someone check this code?
foreach($computer in (Get-Content \path\to\the\file))
{
$temp1 = Get-WmiObject -Class Win32_Product -ComputerName $computer | where { $_.name -eq "Google Chrome"}
$temp1.Uninstall()
}
You shouldn't use the Win32_Product WMI class, one of the side effects of enumeration operations is it checks the integrity of each installed program and performs a repair installation if the integrity check fails.
It is safer to query the registry for this information instead, which also happens to contain the uninstall string for removing the product with msiexec. The uninstall string here will be formatted like MsiExec.exe /X{PRODUCT_CODE_GUID}, with PRODUCT_CODE_GUID replaced with the actual product code for that software.
Note: This approach will only work for products installed with an MSI installer (or setup executables which extract and install MSIs). For pure executable installers which make no use of MSI installation, you'll need to consult the product documentation for how to uninstall that software, and find another method (such as a well-known installation location) of identifying whether that software is installed or not.
Note 2: I'm not sure when this changed but ChromeSetup.exe no longer wraps an MSI as it used to. I have modified the code below to handle the removal of both the MSI-installed and EXE-installed versions of Chrome.
# We need to check both 32 and 64 bit registry paths
$regPaths =
"HKLM:\SOFTWARE\Wow6432node\Microsoft\Windows\CurrentVersion\Uninstall",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
# Since technically you could have 32 and 64 bit versions of the same
# software, force $uninstallStrings to be an array to cover that case
# if this is reused elsewhere. Chrome usually should only have one or the
# other, however.
$productCodes = #( $regPaths | Foreach-Object {
Get-ItemProperty "${_}\*" | Where-Object {
$_.DisplayName -eq 'Google Chrome'
}
} ).PSPath
# Run the uninstall string (formatted like
$productCodes | ForEach-Object {
$keyName = ( Get-ItemProperty $_ ).PSChildName
# GUID check (if the previous key was not a product code we'll need a different removal strategy)
if ( $keyName -match '^{[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}}$' ) {
# Use Start-Process with -Wait to wait for PowerShell to finish
# /qn suppresses the prompts for automation
$p = Start-Process -Wait msiexec -ArgumentList "/l*v ""$($pwd.FullName)/chromeuninst.log"" /X${keyName} /qn" -PassThru
# 0 means success, but 1638 means uninstallation is pending a reboot to complete
# This should still be considered a successful execution
$acceptableExitCodes = 0, 1638
}
else {
Write-Host "Stopping all running instances of chrome.exe, if any are running"
Get-Process chrome.exe -EA Ignore | Stop-Process -Force
# The registry key still has an uninstall string
# But cannot be silently removed
# So we will have to get creating and control the uninstall window with PowerShell
# We need to use the undocumented --force-uninstall parameter, added to below command
$uninstallString = "$(( Get-ItemProperty $_).UninstallString )) --force-uninstall"
# Break up the string into the executable and arguments so we can wait on it properly with Start-Process
$firstQuoteIdx = $uninstallString.IndexOf('"')
$secondQuoteIdx = $uninstallString.IndexOf('"', $firstQuoteIdx + 1)
$setupExe = $uninstallString[$firstQuoteIdx..$secondQuoteIdx] -join ''
$setupArgs = $uninstallString[( $secondQuoteIdx + 1 )..$uninstallString.Length] -join ''
Write-Host "Uninstallation command: ${setupExe} ${setupArgs}"
$p = Start-Process -Wait -FilePath $setupExe -ArgumentList $setupArgs -PassThru
# My testing shows this exits on exit code 19 for success. However, this is undocumented
# behavior so you may need to tweak the list of acceptable exit codes or remove this check
# entirely.
#
$acceptableExitCodes = 0, 19
}
if ( $p.ExitCode -notin $acceptableExitCodes ) {
Write-Error "Program exited with $($p.ExitCode)"
$p.Dispose()
exit $p.ExitCode
}
exit 0
}
Incidentally, if you already know the MSI ProductCode of a given program you don't have to find the uninstall string this way. You can simply execute msiexec /X{PRODUCT_CODE_GUID}.
If you have further problems which aren't caused by the syntax of the above this would be an operational issue and would be better troubleshot over at the https://superuser.com site.
Edit
As discovered via our chat conversation, you are installing per user and with the 79.0.3945.130 version. You can remove per user Chrome with the following command if installed per user (if the version is different you will need the correct version path):
&"C:\Users\username\AppData\Local\Google\Chrome\Application\79.0.3945.130\Installer\setup.exe" --uninstall --channel=stable --verbose-logging --force-uninstall
In the future, it is not recommended to use ChromeSetup.exe or ChromeStandaloneSetup64.exe to install and manage Chrome in an enterprise environment, you should instead use the Enterprise MSI and install system-wide so you can manage Chrome more efficiently. This is the supported way to deploy Chrome in an enterprise environment, and the script I provided will work to uninstall Chrome via msiexec and searching the registry for the {PRODUCT_CODE} as provided.
Assuming it's an msi install and remote powershell is enabled:
invoke-command -computername comp001 { uninstall-package 'google chrome' }
For the programs provider (all users), it's something like:
get-package *chrome* | % { $_.metadata['uninstallstring'] }
"C:\Program Files\Google\Chrome\Application\95.0.4638.54\Installer\setup.exe" --uninstall --channel=stable --system-level --verbose-logging
And then run that uninstallstring, but you'd have to figure out the silent uninstall option (--force-uninstall). It also runs in the background.

Check if a reboot is needed and continue Powershell script after reboot?

This question was asked a few times before but I could not find a solution to my scenario in any of them.
Basically I need the script to continue after it reboots if needed. It will check a few registry keys and will determine if the machine needs to be rebooted.
I tried using 'workflow' in various ways but could not get it to work successfully.
Here is the rough description of my code:
function (check-if-computer-needs-a-reboot){
if(this and that)
try{
return $true
}
catch{}
return $false
}
if(check-if-computer-needs-a-reboot = $true){
Write-Host "The machine is rebooting..."
Restart-Computer -Wait -Force
Write-Host "The machine was successfully rebooted"
}
else
{
Write-Host "No pending reboot"
}
Hoping the wizards of Stack-overflow can help.
Any help will be greatly appreciated!!!
To continue doing something after a reboot, you need to add a value to the Runonce registry key to specify what to do after the reboot.
Break break your script into two parts (Preboot and Postboot). Put the following at the end of Preboot.ps1:
if(check-if-computer-needs-a-reboot){
REG ADD "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Runonce" /V "Postboot" /d "\"%~dpPostboot.ps1\" MyParameters" /t REG_SZ /f
Write-Host "The machine is rebooting..."
Restart-Computer -Wait -Force
}
# Fall through if reboot was not needed, so continue with part 2
Write-Host "No pending reboot... continuing"
"%~dpPostboot.ps1"
Notes:
I copied this from a .bat file. In a .bat file "%~dp0" means "the drive and path that the current script is running from". The syntax in Powershell might be a little different.
While Powershell can run other programs such as REG.EXE, Powershell has its own built-in HKLM: PSdrive. That should be more efficient than using REG.EXE. I leave it to you to do the conversion.
In your code, you test if a Boolean value "equals" $True. This comparison is never necessary unless the value isn't really Boolean and could be something other than True or False. Just test the Boolean value itself as I've shown above.

Running SFC.EXE from within Powershell script deployed via SCCM

I'm trying to create a Powershell script that will be deployed to any node that is showing bad update health to automate some of the simple tasks without having to interrupt users during their workday. The Powershell script works perfectly if ran from an elevated PS prompt. It also runs fine when the same script is deployed to a test machine via SCCM with one exception: it won't call SFC.EXE /SCANNOW.
I've tried using:
Start-Process -FilePath "${env:Windir}\System32\SFC.EXE" -ArgumentList '/scannow' -Wait -NoNewWindow
Start-Process -FilePath "sfc.exe" -ArgumentList '/scannow' -Wait -NoNewWindow
Start-Process -FilePath "${env:Windir}\System32\SFC.EXE" -ArgumentList '/scannow' -RedirectStandardOutput "C:\SFC-Out.log" -RedirectStandardError "C:\SFC-Err.log" -Wait -NoNewWindow
& "sfc.exe" "/scannow"
Invoke-Command -ScriptBlock { sfc.exe /scannow }
Again, all of these examples work exactly as intended when run from an elevated PS prompt, but fail when run from the deployed PowerShell script. When I used the -RedirectStandardOutput, I checked the file SFC-Out.log and it read:
"Windows Resource Protection could not start the repair service"
I think this is because SCCM runs programs/scripts in the SYSTEM context instead of a user context (or even an elevated user context, but SYSTEM is supposed to be higher than an elevated session).
Is there a way to accomplish this? Sorry for the bad formatting, this is my first post on this site.
A bit late but I encountered the same issue. Not sure if this is the case for you but the cause was configuring the deployment of the script with SCCM to run as a 32 bit process. The script was being deployed to 64 bit systems. When I unchecked "run as 32 bit process" in the deployment configuration SFC worked without an issue under the context of a System account.
I created a package (not an application) in SCCM and had to use the redirect using the elusive sysnative folder for x64 machines:
https://www.thewindowsclub.com/sysnative-folder-in-windows-64-bit
So it would be:
C:\Windows\Sysnative\SFC.EXE /SCANNOW
What you have will work, just missing "-Verb RunAs" to elevate permissions. So your cmdlet should read:-
Start-Process -FilePath "${env:Windir}\System32\SFC.EXE" -ArgumentList '/scannow' -Wait -Verb RunAs
I've been reading and searching online for this, the only answer so far is that It can't be run due to sccm using the system account. It's also the same behavior when trying to run winmgt.
Fast forward to SCCM Current Branch 2109 and I was able to solve this problem by using the new Scripts feature built into SCCM. Using & 'sfc.exe' '/scannow' works, and I can manually run this script against any device collection showing devices in error. Start-Process -FilePath "sfc.exe" -ArgumentList "/scannow" -NoNewWindow -Wait works too.

Resources