PowerShell Start-Job with a scriptblock calling another script with parameters - powershell

I'm using the Nmap-Scan.ps1 script and calling it using another script. The nmap-scan script is just a wrapper for the Nmap.exe program.
What I am required to do is provide a list of devices (by IP or DNS), run a NMAP scan, and have the results of each dropped into their own respective files.
Running the nmap-scan.ps1 directly works as expected:
.\Nmap-Scan.PS1 "localhost" -Arguments "-sS -sU -Pn -v" -OutDir "C:\Scripts\PowerShell\Get-NMAPData\Test" -Location "C:\Program Files (x86)\nmap\nmap.exe"
And I get the 'localhost.xml' file in the expected OutDir.
However, when I run the following code PowerShell shows that it is starting the jobs but I don't get any resulting XML files. So I must be doing something wrong or maybe the 'Start-Job' doesn't have the permissions to write the file out? I'm not sure either way and I'm stuck at this point. I would appreciate any help.
$MaxThreads = 2
$ServerList = Get-Content -Path "serverlist.txt"
foreach($server in $ServerList) {
#Create hashtable with parameters and their values
$Arg = #{
InputObject=$server;
Arguments="-sS -sU -Pn -v";
OutDir="C:\Scripts\PowerShell\Get-NMAPData\Test"
}
Start-Job -ScriptBlock{PARAM($Arg); & C:\Scripts\PowerShell\Get-NMAPData\Nmap-Scan.PS1 #Arg} -ArgumentList $Arg
while (#(Get-Job | Where-Object {$_.State -eq "Running"}).Count -ge $MaxThreads){
Write-Verbose "Waiting for open thread...($MaxThreads Maximum)"
Start-Sleep 3
}
}
foreach($job in Get-Job){
Receive-Job -Job $job -OutVariable temp
Write-Host($temp)
}
Job Output example:
StartTime : 9/7/2018 2:33:31 PM
OutFile : C:\Scripts\PowerShell\Get-NMAPData\Test\localhost.xml
Arguments : "-sS -sU -Pn -v"
Duration : 00:00:01.0990367
Hash :
Target : localhost
FinishTime : 9/7/2018 2:33:33 PM
RunspaceId : c68a80f5-198a-4af5-ac0d-c28667b3305c
#{StartTime=9/7/2018 2:33:31 PM; OutFile=C:\Scripts\PowerShell\Get-NMAPData\Test\localhost.xml; Arguments="-sS -sU -Pn -v"; Duration=00:00:01.0990367; Hash=; Target=localhost; FinishTime=9/7/2018 2:33:33 PM; PSComputerName=localhost; RunspaceId=c68a80f5-198a-4af5-ac0d-c28667b3305c; PSShowComputerName=False}

Related

Is there a way to find every single modules which will be needed in script?

I'd like to use a kinda analyzer which will install/import all the needed modules by the script before I run it on distant machine (which could not have it) ......
any idea ?
EDIT
Here's the case :
I'm on my dev machine, I'ved already installed lots of modules of all kind (dhcp, ntfs, remoting, register, etc.)
When I finally got my script (which is a function) to work, I can't be sure of what modules are used....
What I want is to write down, in the 'begin' section, the correct imports before I send my script on remote PCs; to be sure it's gonna run perfectly, you follow ?...
Is there a kinda a third party appplication which can scan my script and give me all needed modules ?
You could do something like this to get help in finding commands used and their source/module names. It's very unpolished, just trying to give the idea.
$scriptblock = {
Write-Host "Nothing here"
$files = Get-ChildItem c:\temp
Get-ADUser someuser
Test-NetConnection www.google.com
}
# Uncomment following lines and enter the path to your script file
# $scriptFile = "Path\to\some\scriptfile"
# $scriptblock = [scriptblock]::Create((Get-Content -raw -Path $scriptFile))
$ast = $scriptblock.Ast
$commands = $ast.FindAll( { $args[0] -is [System.Management.Automation.Language.CommandAst] }, $true)
$commandText = foreach ($command in $commands) {
$command.CommandElements[0].Extent.Text
}
$commandText |
Select-Object -Unique |
Sort-Object |
Select-Object #{
Label = "CommandName"
Expression = { $_ }
},
#{
Label = "Source"
Expression = {
(Get-Command $_).Source
}
}
Output
CommandName Source
----------- ------
Get-ADUser ActiveDirectory
Get-ChildItem Microsoft.PowerShell.Management
Test-NetConnection NetTCPIP
Write-Host Microsoft.PowerShell.Utility
Yeah, you could for example test if the module exists on that particular machine by trying to import it as follows
Try {
Import-Module dbaclone -ErrorAction stop
#ErrorAction required as failing to import is not a terminating action
} Catch {
Write-Verbose -Verbose "Failed to find dbaclone module - installing"
Install-Module dbaclone -AllowClobber -Force
Write-Verbose -Verbose "Installed!"
Import-Module dbaclone
}

"echo on" in powershell or how do I make Powershell output the command lines of all the commands, INCLUDING the native ones invoked by the script?

My question may seem duplicate of PowerShell "echo on", but it is not.
I am not interested in capturing the command output, but in the command line itself of every command executed by the script, including the native commands.
This is what "echo on" in cmd does and this is what I am looking for. Set-PSDebug -Trace 1 does not do it and neither passing the -Verbose flag.
So far I have not see a way except outputing them myself, which is a huge pain in itself.
So, can Powershell do what "echo on" does in cmd?
EDIT 1
Not ideal, but I would accept an answer suggesting to use a wrapper function which would receive a command (native or powershell) with parameters and run the command while faithfully logging the respective command line. Of course, the wrapper function code should be part of the answer.
EDIT 2
The following trivial example demonstrates why Set-PSDebug -Trace 1 does not do it:
tasklist `
/fi "status eq running" | Select-Object -First 4
Please, observe:
C:\> cat C:\temp\1.ps1
tasklist `
/fi "status eq running" | Select-Object -First 4
C:\> Set-PSDebug -Trace 1
C:\> C:\temp\1.ps1
DEBUG: 1+ >>>> C:\temp\1.ps1
DEBUG: 1+ >>>> tasklist `
Image Name PID Session Name Session# Mem Usage
========================= ======== ================ =========== ============
csrss.exe 756 Console 1 2,816 K
C:\>
EDIT 3
For comparison, observe an equivalent script in cmd with echo on:
C:\>type c:\temp\1.cmd
#echo on
tasklist ^
/fi "status eq running" |findstr/n ^^|findstr "^[1-4]:"
C:\>c:\temp\1.cmd
C:\>tasklist /fi "status eq running" | findstr/n ^ | findstr "^[1-4]:"
1:
2:Image Name PID Session Name Session# Mem Usage
3:========================= ======== ================ =========== ============
4:csrss.exe 756 Console 1 2,328 K
C:\>
EDIT 4
start-transcript does not do it either:
C:\WINDOWS\system32> cat c:\temp\1.ps1
tasklist `
/fi "status eq running" | Select-Object -First 4 | Out-Default
C:\WINDOWS\system32> Start-Transcript
Transcript started, output file is ~\Documents\PowerShell_transcript.L-PF0TBKV7.Sr1ntThx.20190611143800.txt
C:\WINDOWS\system32> c:\temp\1.ps1
Image Name PID Session Name Session# Mem Usage
========================= ======== ================ =========== ============
csrss.exe 756 Console 1 2,936 K
C:\WINDOWS\system32> Stop-Transcript
Transcript stopped, output file is ~\Documents\PowerShell_transcript.L-PF0TBKV7.Sr1ntThx.20190611143800.txt
C:\WINDOWS\system32> cat ~\Documents\PowerShell_transcript.L-PF0TBKV7.Sr1ntThx.20190611143800.txt
**********************
Windows PowerShell transcript start
Start time: 20190611143800
Username: xyz\me
RunAs User: xyz\me
Configuration Name:
Machine: L-PF0TBKV7 (Microsoft Windows NT 10.0.16299.0)
Host Application: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
Process ID: 25508
PSVersion: 5.1.16299.1004
PSEdition: Desktop
PSCompatibleVersions: 1.0, 2.0, 3.0, 4.0, 5.0, 5.1.16299.1004
BuildVersion: 10.0.16299.1004
CLRVersion: 4.0.30319.42000
WSManStackVersion: 3.0
PSRemotingProtocolVersion: 2.3
SerializationVersion: 1.1.0.1
**********************
Transcript started, output file is ~\Documents\PowerShell_transcript.L-PF0TBKV7.Sr1ntThx.20190611143800.txt
C:\WINDOWS\system32
>
PS>c:\temp\1.ps1
Image Name PID Session Name Session# Mem Usage
========================= ======== ================ =========== ============
csrss.exe 756 Console 1 2,936 K
C:\WINDOWS\system32
>
PS>Stop-Transcript
**********************
Windows PowerShell transcript end
End time: 20190611143810
**********************
C:\WINDOWS\system32>
As you can see it does not contain the command line.
Firstly, the reason you're dissatisfied with the built-in options is because you're going against the grain; your requirement is like asking how to put sacks of gravel in the back of a Porsche. Powershell comes with Verbose and Debug output streams and a fantastic debugger.
If you have any ability to influence coding standards, look at splatting as an alternative to backtick-continuations.
If you can count on versions of Windows that are not years past EoL, consider Get-ScheduledTask | Where-Object State -eq 'Ready' instead of tasklist.
That said, yes, what you want is possible. Here's a script that will echo across line continuations:
# Echo.ps1
function Disable-Echo
{
param
(
[Parameter(Mandatory)]
[string]$Path
)
$Path = ($Path | Resolve-Path -ErrorAction Stop).Path
Get-PSBreakpoint -Script $Path | Remove-PSBreakpoint
}
function Enable-Echo
{
param
(
[Parameter(Mandatory)]
[string]$Path
)
$Path = ($Path | Resolve-Path -ErrorAction Stop).Path
Disable-Echo $Path
$Ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$null, [ref]$null)
$Statements = $Ast.BeginBlock, $Ast.ProcessBlock, $Ast.EndBlock |
Select-Object -ExpandProperty Statements |
Write-Output |
Where-Object {$_.Extent}
foreach ($Statement in $Statements)
{
$Action = {
$Text = $Statement.Extent.Text
$Text = $Text -replace '`\r?\n' # Concatenate lines that are escaped with backtick
# Alternative to remove superfluous whitespace:
# $Text = $Text -replace '\s+`\r?\n\s*', ' '
Write-Host "ECHO: $Text" -ForegroundColor Yellow
continue # or 'break' to stop on the line
}.GetNewClosure() # Create a closure, to capture the value of $Statement
$Params = #{
Script = $Path
Line = $Statement.Extent.StartLineNumber
Column = $Statement.Extent.StartColumnNumber
Action = $Action
}
$null = Set-PSBreakpoint #Params
}
}
Sample script:
# foo.ps1
gci `
-Name `
-File `
-Filter Victor.*
gci -File -Name *.md; gci -File -Name *.psd1
Usage:
# Without echo
❯ .\foo.ps1
Victor.build.ps1
Victor.psd1
Victor.psm1
README.md
Victor.psd1
❯ . .\Echo.ps1
❯ Enable-Echo .\foo.ps1
❯ .\foo.ps1
ECHO: gci -Name -File -Filter Victor.*
Victor.build.ps1
Victor.psd1
Victor.psm1
ECHO: gci -File -Name *.md
README.md
ECHO: gci -File -Name *.psd1
Victor.psd1
Tested on PSv5 and PSv7. Should work on PSv2, although the sample foo.ps1 is PSv3+ (IIRC).
This will not echo calls to other scripts. For that, you'd probably want to do more AST inspection, identify CommandAsts that call scripts, and recursively enable echo on those scripts too. Alternatively, there might be joy in Set-PSBreakpoint -Variable - the $$ variable might be a good place to start - but this would likely be a PITA to work with as it would invoke while you're trying to debug the echo function. You could inspect Get-PSCallStack to skip the action while you're at the command prompt.
I expect four answers and you have already mentioned three that do not work for you (Set-PSDebug, Start-Transaction, -Verbose). As much as they may be viable but not in the format you are looking for, I will not talk more of them.
For the third option, try using Get-History. Now, this will not print out each command as you execute it (only when you call it) like I assume you want. It will also likely not print out each of the lines inside another script (you would want a trace but you did not like that because it prints out more than just the execution).
You can try asking around the PowerShell repository but I do not expect you to find what you are seeking.
If Event logs is an option, start tracing by enabling this Group Policy.
Administrative Templates -> Windows Components -> Windows PowerShell
See Microsoft Docs - Script Tracing and Logging
Then you would of course need to parse the Event logs accordingly...

Error in PowerShell one liner for browser process management

I'm trying to run a code in PowerShell in one line. This code is a loop that's used for surveillance. If Microsoft Edge is opened the process has to close Chrome.
My code it works well if Edge is not opened, it goes right by the if condition, but if Edge is opened it returns me an error in the else condition.
System is Windows 10 with PowerShell ISE.
$a = 1 ;DO { 'Starting Loop' ; $vischk = get-process | where-object {$_.mainwindowhandle -ne 0 -and $_.MainWindowTitle -eq 'Start - Microsoft Edge'} | select-object name, mainwindowtitle ; if (!($vischk)) {Write-Warning 'Microsoft Edge is off'}else{Write-Warning 'Closing Chrome' Stop-Process -name chrome} ; Write-Warning 'Active surveillance' ; Start-Sleep -s 15} While ($a -le 2)
I need to run the surveillance window and when Microsoft Edge is opened, close another browser like chrome or another process name.
This is also not a One-Liner, it's a long script all put on one line. ;-},
hence your use of the semicolon. Semicolon means what is before it and after it
are independent code blocks.
I get that use case in an interactive consolehost thing, but in a script, well,
that's an entirely different thing.
Yet, doing this. the way you have it is a choice. Dev in the ISE, save the file,
run from the console directly or shell out to it from the ISE.
Also the way you are checking for the MS Edge instance is not correct. The
MainWindowTitle is only 'Microsoft Edge'
Get-Process -Name MicrosoftEdge | select *
<#
Name : MicrosoftEdge
Id : 6388
PriorityClass : Normal
...
ProductVersion : 11.00.17763.529
Description : Microsoft Edge
Product : Microsoft Edge
__NounName : Process
...
SafeHandle : Microsoft.Win32.SafeHandles.SafeProcessHandle
MachineName : .
MainWindowHandle : 132100
MainWindowTitle : Microsoft Edge
MainModule : System.Diagnostics.ProcessModule (MicrosoftEdge.exe)
MaxWorkingSet : 1413120
MinWorkingSet : 204800
Modules : {System....
#>
$Code = #'
$a = 1
DO {
'Starting Loop'
$vischk = get-process |
where-object {
$_.mainwindowhandle -ne 0 -and
$_.MainWindowTitle -eq 'Microsoft Edge'
} | select-object name, mainwindowtitle
if (!($vischk))
{Write-Warning 'Microsoft Edge is off'}
else{
Write-Warning 'Closing Chrome'
Stop-Process -name chrome
}
Write-Warning 'Active surveillance'
Start-Sleep -s 3}
While ($a -le 2)
'#
You can stay in the ISE to test code, but as Olaf points out, your user may not be the same. So, you need to validate both environments. You can stay in the ISE and test your code there as well as in the consolehost.
So, to test code using a consolehost instance from the ISE/VSCode without typing in the console.
Start-Process powershell -ArgumentList "-NoExit","-Command &{ $Code }" -Wait
Or
Start-Process pwsh -ArgumentList "-NoExit","-Command &{ $Code }" -Wait
Or just open the consolehost and run the script
The above works, as designed based on your defined case.
I agree with Olaf here as well, you need to add more error checking for what is and is not running for this to be more operationally sound. Don't run code you don't need to run if a target does not exist.

How do you pass values to another script under a foreach statement?

I want to run 1 script multiple times for each port select via a range and pass through the port details in which it needs to use to connect, I was trying to use the following:
$availableports = 7000..7050
while ($availableports -notcontains $SPort) {
[string]$SPort= Read-Host -Prompt 'S Ports'
}
while ($availableports -notcontains $FPort) {
[string]$FPort= Read-Host -Prompt 'F Ports'
}
$massport = ($SPort)..($FPort)
foreach ($Port in $massport) {
C:\PShell-Projects\Firmware\SCP-FMUPv2.ps1 -Port "$Port"
}
This works but will not move on to the next port until the referenced script has finished.
I would like to run them all in parallel.
I tried
$arg = #("-Port", $portm)
Start-Job -FilePath C:\PShell-Projects\Firmware\SCP-FMUPv2.ps1 -ArgumentList $arg
but the job becomes blocked and when I used Receive-Job it asks for the port to open a connection to, which should have been sent as part of the loop.
I seem to be missing some key information, but don't know where to start and well when looking up the information nothing seems to be standing out.
This works for me:
$jobs = #()
foreach ($Port in $massport) {
$jobs += start-job -FilePath "job.ps1" -ArgumentList #($port)
}
Receive-Job $jobs -Wait
(no named arguments in ArgumentList).
You can also take a look at Invoke-Parallel function, which simplifies running parallel tasks.
You could probably use -asjob after your C:\PShell-Projects\Firmware\SCP-FMUPv2.ps1 -Port "$Port command to have it just do them all simultaneously.
If the ps1 won't take -asjob, you might be able to wrap it with invoke-command {}

Sending each line from text file to remote computer?

This is my script to whitelist IP in a remote system. I want my script to read data from a text file on my local system and then foreach line I want to execute the scriptblock on the remote server.
Text file looks like this:
url1
url2
url3
Here is my code:
Invoke-Command -ComputerName $($server.text) -Credential ciqdev\riteshthakur {
param($a, $b, $c, $url)
Set-Location "C:\Windows\System32\inetsrv"
$url | foreach {
.\appcmd.exe set config "$_" -section:system.webServer/security/ipSecurity /+"[ipAddress='$($a)',allowed='$($c)',subnetMask='$($b)']" /commit:apphost
}
} -ArgumentList $ip.text, $mask.text, $allowed, (get-content "File location")
This adds provided ip to all the pages in all the websites in IIS. Please help.
EDIT: Improved efficiency by generating the command dynamically, and invoking it once.
I'd suggest using a technique similar to the following, where you read in the text file as an array of lines, and then iterate over each line, generating the commands that you want to run on the remote system.
Once you've generated the command as a string, you simply call the static [ScriptBlock]::Create() method to create a ScriptBlock object, based on the command string, and pass that into Invoke-Command.
I'd suggest you get familiar with the concept of PowerShell Splatting, which I talk about in this YouTube video: https://www.youtube.com/watch?v=CkbSFXjTLOA. It's a really powerful concept, and helps make your code easier to read. The example code below uses PowerShell Splatting (available in PowerShell 3.0 and later).
### Read the text file on the local system
$Whitelist = Get-Content -Path IPwhitelist.txt;
### Generate the stub for the remote command
$RemoteCommand = #'
param($a, $b, $c)
Set-Location -Path C:\Windows\System32\inetsrv
'#
### Add more commands to the remote command
foreach ($Line in $Whitelist) {
$RemoteCommand += '{1}.\appcmd.exe set config "{0}" -section:system.webServer/security/ipSecurity /+"[ipAddress=''$($a)'',allowed=''$($c)'',subnetMask=''$($b)'']" /commit:apphost' -f $Line, "`n";
}
### Invoke the entire remote command (once)
$Command = #{
ComputerName = $Server.Text
Credential = Get-Credential -Credential ciqdev\riteshthakur
ScriptBlock = [ScriptBlock]::Create($RemoteCommand);
ArgumentList = #($ip.text, $mask.text, $allowed)
}
Invoke-Command #Command;
Just read the file using the Get-Content cmdlet and iterate over each item using the Foreach-Object cmdlet:
Invoke-Command -ComputerName $($server.text) -Credential ciqdev\riteshthakur {
param($a, $b, $c, $urls)
Set-Location "C:\Windows\System32\inetsrv"
$urls | Foreach {
.\appcmd.exe set config $_ -section:system.webServer/security/ipSecurity /+"[ipAddress='$($a)',allowed='$($c)',subnetMask='$($b)']" /commit:apphost
}
} -ArgumentList $ip.text, $mask.text, $allowed, (Get-Content 'Path_to_your_file')