I had downloaded a powershell module called SecurityFever. In this lib there is a code part:
# Get the global impersonation context
$globalImpersonationContext = Get-Variable -Name 'ImpersonationContext' -Scope 'Global'
# Global variable to hold the impersonation context
if ($null -eq $globalImpersonationContext) {
$stack = New-Object -TypeName 'System.Collections.Generic.Stack[System.Security.Principal.WindowsImpersonationContext]'
New-Variable -Name 'ImpersonationContext' -Value $stack -Option ReadOnly -Scope Global -Force
}
it is absolutely clear what it is and what it wants to do, but when I execute this I got red lines in the console window:
Get-Variable : Cannot find a variable with the name
'ImpersonationContext'. At SecurityFever.psm1:2427 char:35
+ ... onContext = Get-Variable -Name 'ImpersonationContext' -Scope 'Global' ...
I think it is because the get-variable does not found this global variable for the 1st time. I am wondering
how to suppress this error without modifying the external lib source code,
or how to do a thing like this - checking if a variable exists or not.
I tried to create the expected global variable before the function call
ImpersonationContext = New-Object -TypeName 'System.Collections.Generic.Stack[System.Security.Principal.WindowsImpersonationContext]'
or
$global:ImpersonationContext = New-Object -TypeName 'System.Collections.Generic.Stack[System.Security.Principal.WindowsImpersonationContext]'
but somehow then it becomes a kind of PSVariable, and the code says
Method invocation failed because [System.Management.Automation.PSVariable] does not contain a method named 'Pop'.
I am not an expert in PowerShell, and I can say I totally can't understand why. :(
From here, you can use Test-Path with a special syntax to check for your variable. You can do something like this -
if (Test-Path variable:global:ImpersonationContext)
{
write-host "The variable ImpersonationContext exists in the global scope"
}
else
{
$stack = New-Object -TypeName 'System.Collections.Generic.Stack[System.Security.Principal.WindowsImpersonationContext]'
New-Variable -Name 'ImpersonationContext' -Value $stack -Option ReadOnly -Scope Global -Force
}
Related
There is a script for users to log in, it calls other scripts in turn, depending on the conditions.
In order to call scripts separately manually, the [switch]$Silent parameter has been added. Question - how to pass this parameter inside Start-Job? I tried to add to the list of arguments in different ways - the value always falls into the neighboring parameter, regardless of the order.
Main script example
Param(
[string]$location = 'C:\Users',
[switch]$Silent
)
Start-Job -FilePath ".\Fonts_Install.ps1" -ArgumentList ($Silent,$location) | Wait-Job
Fonts_Install.ps1
Param(
[switch]$Silent = $false,
[string]$location = '.'
)
$path_fonts = "$env:LOCALAPPDATA\Microsoft\Windows\Fonts"
$Registry = "HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"
function WriteLog {
Param ([string]$LogString)
$Stamp = (Get-Date).toString("yyyy/MM/dd HH:mm:ss")
$LogMessage = "$Stamp $LogString"
Add-content $LogFile -value $LogMessage
}
$Logfile = "$env:LOCALAPPDATA\Temp\fonts_install.log"
WriteLog "Silent $Silent"
WriteLog "location $location"
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName PresentationCore
$SourceFolder = "$location\Fonts_Install"
$WindowsFonts = [System.Drawing.Text.PrivateFontCollection]::new()
$Fonts = Get-ChildItem -Path $SourceFolder -Include *.ttf, *.otf -Recurse -File
ForEach ($Font in $Fonts) {
$Font_Name = $Font.Name
$font_fullname = $Font.fullname
if (Test-Path -PathType Leaf -Path "$path_fonts\$Font_Name") {
WriteLog "Previously installed $Font_Name"
}
else {
Copy-Item $Font -Destination "$path_fonts" -Force -Confirm:$false -PassThru
$WindowsFonts.AddFontFile("$font_fullname")
$ValueFont = "$path_fonts" + "\" + "$Font_Name"
$Typeface = New-Object -TypeName Windows.Media.GlyphTypeface -ArgumentList "$font_fullname"
[string]$FamilyFaceNames = $Typeface.FamilyNames.Values + $Typeface.FaceNames.Values
$RegistryValue = #{
Path = $Registry
Name = $FamilyFaceNames
Value = $ValueFont
}
if (Test-Path $Registry\$FamilyFaceNames) {
Remove-ItemProperty -name $FamilyFaceNames -path $Registry
}
New-ItemProperty #RegistryValue
WriteLog "New fonts installed $Font_Name"
}
}
switch ($Silent) {
$false {
if ($Error.Count -gt 0) {
for ($i = 0; $i -le ($Error.Items.Count + 1); $i++) {
$errMSG = "$Error"
}
[System.Windows.Forms.MessageBox]::Show("$errMSG", "Error", "OK", "Error")
}
else {
[System.Windows.Forms.MessageBox]::Show("ок", "Fonts", "OK", "Asterisk") | out-null
}
}
}
Unfortunately, specifying pass-through arguments via Start-Job's -ArgumentList (-Args) is limited to positional arguments, which prevents binding [switch] parameters, whose arguments must by definition be named.
As a workaround, instead of using -FilePath, invoke your script via the -ScriptBlock parameter. Inside of a script block ({ ... }, named arguments may be used in script calls, as usual:
Start-Job -ScriptBlock {
# Set the current location to the same location as the caller.
# Note: Only needed in *Windows PowerShell*.
Set-Location -LiteralPath ($using:PWD).ProviderPath
.\Fonts_Install.ps1 -Silent:$using:Silent $using:Location
} | Receive-Job -Wait -AutoRemoveJob
Note the use of the $using: scope in order to embed variable values from the caller's scope in the script block that will execute in the background.
You still need to refer to the -Silent parameter by name, and the whether the switch is on or off can be communicated by appending :$true or :$false to it, which is what :$using:Silent does.
In Windows PowerShell, background jobs execute in a fixed location (working directory), namely the user's Documents folder, hence the Set-Location call to explicitly use the same location as the caller, so that the script file can be referenced by a relative path (.\). This is no longer necessary in PowerShell (Core) 7+, which now thankfully uses the same location as the calller.
Here is a different alternative to mklement0's helpful answer, this answer does not use Start-Job and uses a PowerShell instance instead, using this method we can leverage the automatic variable $PSBoundParameters.
Do note, that for this to work properly, both .ps1 scripts must share the same parameter names or Alias Attribute Declarations that matches the same parameter from the caller. See this answer for more details.
You can use these snippets below as a example for you to test how it works.
caller.ps1
param(
[string] $Path = 'C:\Users',
[switch] $Silent
)
try {
if(-not $PSBoundParameters.ContainsKey('Path')) {
$PSBoundParameters['Path'] = $Path
}
$ps = [powershell]::Create().
AddCommand('path\to\myScript.ps1').
AddParameters($PSBoundParameters)
$iasync = $ps.BeginInvoke()
# Do something else here while the .ps1 runs
# ...
# Get async result from the PS Instance
$ps.EndInvoke($iasync)
}
finally {
if($ps -is [IDisposable]) {
$ps.Dispose()
}
}
myScript.ps1
# Note, since we're bounding this parameters from the caller.ps1,
# We don't want to assign Default Values here!
param(
[string] $Path,
[switch] $Silent
)
foreach($param in $MyInvocation.MyCommand.Parameters.Keys) {
[pscustomobject]#{
Parameter = $param
Value = Get-Variable $param -ValueOnly
}
}
A few examples:
PS /> .\caller.ps1
Parameter Value
--------- -----
Path C:\Users
Silent False
PS /> .\caller.ps1 -Path hello
Parameter Value
--------- -----
Path hello
Silent False
PS /> .\caller.ps1 -Path world -Silent
Parameter Value
--------- -----
Path world
Silent True
I have a below powerShell script that creates homedrive for user,
Import-Module ActiveDirectory 2>&1 | Write-Host;
if($?)
{
$homeDir = "\\CORP.com\HOME\Jdoe";
$user = "jdoe";
$domain = "Corp";
New-Item "$homeDir" -type directory;
$acl = Get-Acl "$homeDir";
$permission = "$domain\$user","FullControl", "ContainerInherit, ObjectInherit", "None", "Allow";
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule $permission;
$acl.SetAccessRule($accessRule);
$acl | Set-Acl "$homeDir";
}
Values within $homeDir and $User will be passed on runtime basis.
How to execute above script along with pass runtime values in $homeDir and $User attribute.
I have tried to execute,
. 'C:\hd.ps1' $homeDir = "\\CORP.com\HOME\test" $user = "test" ; without success.
Can anyone guide, what i am doing incorrect.
Put
param(
$homeDir,
$user
)
At the top of the script and call using
Powershell -File "C:\hd.ps1" -homeDir "\\CORP.com\HOME\test" -user "test"
Why are you doing this?
Import-Module ActiveDirectory 2>&1 | Write-Host;
If you are on the DC doing this or if you have the RSAT tools on your workstation, if you are on PowerShell v3+ or higher, this gets auto loaded the moment you use an AD cmdlet.
Also never user Write-Host for anything that you plan to need later. It empties / clears the buffer. Write-Host is only good for text coloring or other formatting needs in s
Make this a collection from a file for example and just read it in. I'm just using a list here:
$UserFile = #'
Property,Value
homeDir,\\CORP.com\HOME\Jdoe
user,jdoe
Targetdomain,Corp
'# | ConvertFrom-Csv
# Results
Property Value
-------- -----
homeDir \\CORP.com\HOME\Jdoe
user jdoe
Targetdomain Corp
If you are doing this from a remote machine, then you cannot use local varibles in a remote session unless you set its scope.
Get-Help about_remote_variables -Full
About Remote Variables
LONG DESCRIPTION
You can use variables in commands that you run on remote
computers.Simply assign a value to the variable and then use the
variable inplace of the value.
By default, the variables in remote commands are assumed to be
definedin the session in which the command runs. You can also use
variablesthat are defined in the local session, but you must identify
them aslocal variables in the command.
USING LOCAL VARIABLES
You can also use local variables in remote commands, but you
mustindicate that the variable is defined in the local session.
Beginning in Windows PowerShell 3.0, you can use the Using
scopemodifier to identify a local variable in a remote command.
The semi-colons are not needed in PowerShell, unless the items are on the same line.
You cannot call this code this way...
'C:\hd.ps1' $homeDir = "\\CORP.com\HOME\test" $user = "test"
... since you did not specify any params in your code.
So, something like this...
Note: I am not in a position to test this... please do only in a test environment
So this is off the cuff...
ForEach($UserLine in $UserFile)
{
New-Item $UserLine.homeDir -type directory
$acl = Get-Acl $UserLine.homeDir
$permission = ($Using:UserLine.Targetdomain + '\' + $Using:UserLine.user),'FullControl', 'ContainerInherit, ObjectInherit', 'None', 'Allow'
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule $permission
$acl.SetAccessRule($accessRule)
$acl | Set-Acl $Using:UserLine.homeDir
}
If you want this to be a parameterized function, then this.,.
Function New-ADUserHomeDirSettings
{
[cmdletbinding()]
Param
(
[string]$homeDir,
[string]$user,
[string]$Targetdomain
)
$acl = Get-Acl $UserLine.homeDir
$permission = ($Using:UserLine.Targetdomain + '\' + $Using:UserLine.user),'FullControl', 'ContainerInherit, ObjectInherit', 'None', 'Allow'
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule $permission
$acl.SetAccessRule($accessRule)
$acl | Set-Acl $Using:UserLine.homeDir
}
New-ADUserHomeDirSettings -homeDir '' -user '' -Targetdomain ''
I have hit a problem I haven’t been able to solve despite trying quite hard.
Basically I have created a PowerShell script to alter\change values in the HKU hive for a specific user on a remote Windows 10 Amazon WorkSpace. The script loads the hive and makes the changes perfectly but I am getting an error when trying to unload the hive. I have tried various methods as suggested on different forums but to no avail. Here is the part of the script I’m having trouble with:
$WorkSpace = "blahComputerName"
$PSS = New-PSSession -ComputerName $WorkSpace
$UserAcc = "XXXXX"
$SID = (Get-ADUser -server MyDomain.com -Identity $UserAcc).SID.Value
Invoke-Command -Session $PSS -ArgumentList $SID, $UserAcc -ScriptBlock {
New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS
reg load "HKU\$($args[0])" "D:\Users\$($args[1])\NTUser.Dat"
Clear-ItemProperty -Path
"HKU:\$($args[0])\SOFTWARE\Microsoft\Office\Common\UserInfo" -Name
"UserInitials"
[gc]::collect()
Start-Sleep -Seconds 5
reg unload "HKU\$($args[0])"
Remove-PSDrive -Name HKU
}
Remove-PSSession -Id $PSS.Id
I have also read that using $SomeThing.Handle.Close() will close any open handles PowerShell might still have with the provider which might be causing the error but I can’t see how to use it in this context.
Here is the exact error:
ERROR: Access is denied.
+ CategoryInfo : NotSpecified: (ERROR: Access is denied.:String) [], RemoteException
+ FullyQualifiedErrorId : NativeCommandError
+ PSComputerName : blahComputerName
I have manually observed the remote registry hive being loaded and then apparently unloaded but this error worries me and would like to solve it. I have proved that its reg unload "HKU\$($args[0])" that is causing the error but cant find the correct solution.
The script runs with the required elevated privileges, so it’s not that. The remote WorkSpace is in a logged off state.
Any advice would be greatly appreciated.
Thank You
Though Garbage collection is nice, the problem stems from how this command is handled;
Clear-ItemProperty -Path
"HKU:\$($args[0])\SOFTWARE\Microsoft\Office\Common\UserInfo" -Name
"UserInitials"
I would recommend capturing it in a variable, like so;
$n = Clear-ItemProperty -Path "HKU:\$($args[0])\SOFTWARE\Microsoft\Office\Common\UserInfo" -Name "UserInitials"
Which should allow you to properly clean it up, like so;
$n.dispose()
$n.close()
Note the above is likely redundant (Dispose should close, and close should call dispose).
With the help of other examples I figured it out. Here's what needs to be done:
$tempHive = 'HKLM\TEMP_hive'
$ntUserFile = 'C:\Users\SOME_USER\NTUSER.DAT'
# Load hive
$startParams = #{
FilePath = 'reg.exe'
ArgumentList = "load `"$tempHive`" `"$ntUserFile`""
WindowStyle = 'Hidden'
Wait = $true
PassThru = $true
}
$process = Start-Process #startParams
if ($process.ExitCode) {
throw "Failed to load the temp hive '$tempHive' for '$ntUserFile': exit code $($process.ExitCode)"
}
# make registry hive drive mapping if needed
# New-Psdrive -name <blah> -PSProvider Registry -root <blih>
# close open handles for 'New-Item'
$result = New-Item -Path "HKLM:\TEMP_hive\newkey"
$result.Handle.Close()
# no need to close open handles from 'New-ItemProperty'
# $null = New-ItemProperty #newParams
# wait for garbage clean up
[gc]::Collect()
[gc]::WaitForPendingFinalizers()
# if you did drive mapping with the mapped registry hive remove it before unload
# Remove-PSDrive <blah>
# unload the hive
$startParams = #{
FilePath = 'reg.exe'
ArgumentList = "unload `"$tempHive`""
WindowStyle = 'Hidden'
Wait = $true
PassThru = $true
}
$process = Start-Process #startParams
if ($process.ExitCode) {
throw "Failed to unload the temp hive '$tempHive' for '$ntUserFile': exit code $($process.ExitCode)"
}
I've got a collection of powershell scripts, some of which call others. Some of these subscripts can also be called on their own as needed. How can I quickly add logging to all of the scripts so that any script invocation results in a log file for later examination?
There are a number of questions dealing with logging with some great answers, like this one. But I wanted to see what we could come up with that:
required minimal touching of the existing powershell files
automatically dealt with script A.ps1 calling script B.ps1. If you call
A.ps1, A.ps1 needs to start and finish the logging. But if you call B.ps1
directly, B.ps1 does.
I came up with my answer below, and wanted to share and see if there were other ideas on how to approach this, or suggestions for improvement on my answer.
The support code I write (further down) allows for just adding the following to each ps1 file. It automatically gives me logging regardless of if a script is called at top-level or by another script:
#any params for script
. "$PSScriptRoot\ps_support.ps1"
StartTranscriptIfAppropriate
try
{
#all of the original script
}
finally
{
ConditionalStopTranscript
}
The code that powers this is in ps_support.ps1, sitting next to my collection of powershell files that need logging. It uses Get-Variable and Set-Variable to manipulate a couple variables at the caller's scope level:
Logging__TranscriptStarted is normal so sub-scopes can see that
logging is already happening and not try to start it again.
Logging__TranscriptStartedPrivate is private so a scope can know if
it is responsible for stopping the logging.
Here is ps_support.ps1:
Set-Variable -name TranscriptStartedPropertyName -opt ReadOnly -value 'Logging__TranscriptStarted'
Set-Variable -name TranscriptStartedPrivatePropertyName -opt ReadOnly -value 'Logging__TranscriptStartedPrivate'
function StartTranscriptIfAppropriate
{
$transcriptStarted = [bool](Get-Variable -name $TranscriptStartedPropertyName -ErrorAction Ignore)
if (-not $transcriptStarted)
{
$callstack = get-pscallstack
$fullPath = $callstack[$callstack.count-2].ScriptName
$name = Split-Path -Path $fullPath -Leaf
$directory = Split-Path -Path $fullPath
$logDirectory = [IO.Path]::GetFullPath("$directory\..\scripts_logs")
md -force $logDirectory | out-null
$logFinalPath = "$logDirectory\$(Get-Date -Format o | foreach {$_ -replace ":", "."})_$name.log"
Set-Variable -scope 1 -name $TranscriptStartedPropertyName -value $True
Set-Variable -scope 1 -option private -name $TranscriptStartedPrivatePropertyName -value $True
Start-Transcript $logFinalPath | Write-Host
}
$immediateCallerPath = Get-Variable -scope 1 -name PSCommandPath -ValueOnly
Write-Host "Starting script at $immediateCallerPath"
}
function ConditionalStopTranscript
{
$immediateCallerPath = Get-Variable -scope 1 -name PSCommandPath -ValueOnly
Write-Host "Stopping script at $immediateCallerPath"
$transcriptStartedByMe = [bool](Get-Variable -scope 1 -name $TranscriptStartedPrivatePropertyName -ErrorAction Ignore)
if ($transcriptStartedByMe)
{
Stop-Transcript | Write-Host
}
}
I've written a PowerShell scrip that iterates through a large number of IIS W3C logfiles and inserts the values into a MSSQL database.
Set-Variable -Name "UnprocessedDir" -Value "X:\files" -Description "Folder for unprocessed log files" Scope Script
Set-Variable -Name "InputObject" -Value (New-Object -comObject MSUtil.LogQuery.IISW3CInputFormat) -Description "Log Parser input COM object" -Scope Script
Set-Variable -Name "OutputObject" -Value (New-Object -comObject MSUtil.LogQuery.SQLOutputFormat) -Description "Log Parser output COM object" -Scope Script
$OutputObject.clearTable = $false
$OutputObject.createTable = $false
$OutputObject.database = "Database_Name"
$OutputObject.driver = "SQL Server"
$OutputObject.dsn = "DSN_Name"
$OutputObject.fixColNames = $true
$OutputObject.ignoreIdCols = $true
$OutputObject.ignoreMinWarns = $true
$OutputObject.maxStrFieldLen = 511
$OutputObject.oConnString = $null
$OutputObject.password = $null
$OutputObject.server = "sqlserver.domain.com\INSTANCENAME"
$OutputObject.transactionRowCount = 5000
$OutputObject.username = $null
Set-Variable -Name "IISLogs" -Value #(Get-ChildItem -Path $UnprocessedDir -Recurse -File) -Description "Array of files to be imported into SQL" -Scope Script
Set-Variable -Name "LPComObj" -Value (New-Object -com MSUtil.LogQuery) -Description "COM Object used to import Log Parser records into MSSQL" -Scope Script
Write-Output "$(Get-ISOTimeStamp) Beginning SQL import. $($IISLogs.Count) Files to be imported"
$IISLogs | ForEach-Object { $loop = 0 } {
Set-Variable -Name "SubDir" -Value $(($_.FullName).Split('\')[-2]) -Description "Subdirectory where log file is located" -Scope Script
Set-Variable -Name "LogType" -Value $(($_.FullName).Split('\')[-3]) -Description "Type of log being imported" -Scope Script
Set-Variable -Name "ServerName" -Value $(($_.FullName).Split('\')[-4]) -Description "ServerName of file being imported" -Scope Script
Set-Variable -Name "LPQuery" -Description "Query to use in Log Parser" -Scope Script -Value #"
SELECT
-- FIELDS LogFilename,LogRow,date,time,c-ip,cs-username,s-sitename,s-computername,s-ip,s-port,cs-method,cs-uri-stem,cs-uri-query,sc-status,sc-substatus,sc-win32-status,sc-bytes,cs-bytes,time-taken,cs-version,cs-host,cs(User-Agent),cs(Cookie),cs(Referer),s-event,s-process-type,s-user-time,s-kernel-time,s-page-faults,s-total-procs,s-active-procs,s-stopped-procs
-- STANDARD FIELDS date,time,s-ip,cs-method,cs-uri-stem,cs-uri-query,s-port,cs-username,c-ip,cs(User-Agent),cs(Referer),sc-status,sc-substatus,sc-win32-status,time-taken
'$($ServerName)' as [servername],
'$($_.Name)' as [filename],
LogRow AS [row],
'$($LogType)' as [logtype],
TO_TIMESTAMP(date,time) AS [timestamp],
[s-ip],
[cs-method],
[cs-uri-stem],
[cs-uri-query],
TO_INT([s-port]),
[cs-username],
[c-ip],
[cs(User-Agent)],
[cs(Referer)],
TO_INT([sc-status]),
TO_INT([sc-substatus]),
TO_INT([sc-win32-status]),
TO_INT([time-taken]),
0 AS lock
INTO IIS_W3C
FROM '$($_.FullName)'
"#
Set-Variable -Name "LPResult" -Value ($LPComObj.ExecuteBatch($LPQuery, $InputObject, $OutputObject)) -Description "IIS Log File imported into SQL" -Scope Script
If ($LPResult -eq $false)
{
Write-Output "$(Get-ISOTimeStamp) Data imported from `"$($_.FullName)`""
Set-Variable -Name "loop" -Value ($loop + 1) -Description "Increase loop iteration Count" -Scope Script
}
Else
{
Write-Output "$(Get-ISOTimeStamp) Log Parser returned errors importing `"$($_.FullName)`""
Throw "$(Get-ISOTimeStamp) Log Parser returned errors importing `"$($_.FullName)`""
}
}
The number of logs I'm importing is tens of thousands; the code above works spectacularly for a few hundred files, but after a few hours, it crashes. From what I can tell, it looks like every iteration of the ForEach-Object loop creates a new SQL TCP connection which is not terminated at the end of the loop.
I've tried creating the $LPComObj both within and outside of the loop. I've tried Remove-Variable. I've tried some generic commands like $LPComObj.Close(), .Remove(), .Quit(), etc. The MSUtil.LogQuery method itself does not seem to contain any methods to close the SQL TCP connection, and as the script is running, I can see more and more TCP connections piling up. I tried a few things using [System.Runtime.Interopservices.Marshal]:: to release/remove the COM object, but none of them closed the TCP connection. Even closing the PowerShell session doesn't kill the connection.
The only way I was able to do this is by hunting down and finding the dllhost.exe" process that was using the ports and killing it. But from within the script, there isn't a clean way to get the PID of the offending dllhost.exe process. (Trying to kludge some variant of Get-Process | Stop-Process might work, but would add a lot of time to the execution of the script.)
What other ways might I be able to work around this problem?