Execute Powershell cmdlet that have user prompts within C# WPF application - c#

I am attempting to create a WPF application that will execute some powershell commands using a 3rd party module (ShareGate). After extensive research and banging my head on the keyboard, I have gotten the application to at least execute the cmdlets I have asked for. The cmdlet in question, if run in powershell, will prompt the user to log into a web service using edge I believe. When running the cmdlet from the application, it throws an error which is misleading "during the last update edge was not able to be installed...."
I think that this error is coming up because this implementation isn't allowing powershell to pop open the browser like it does within a powershell window.
My question is this: "How can I redirect the user prompt to come up within the wpf application? or can I?"
here is my method:
public Task StartSGMigrations(IProgress<string> progress)
{
var sharegatePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\Apps\\ShareGate\\Sharegate.Automation.dll";
if (client != null)
{
try
{
InitialSessionState iss = InitialSessionState.CreateDefault();
iss.ImportPSModule(new string[] { sharegatePath });
using (Runspace myRunSpace = RunspaceFactory.CreateRunspace(iss))
{
myRunSpace.Open();
using (PowerShell powershell = PowerShell.Create())
{
Dictionary<string, string> parameters = new Dictionary<string, string>();
powershell.AddScript("New-CopySettings -OnContentItemExists IncrementalUpdate");
powershell.AddScript("Connect-box -email " + _admin + " -admin");
powershell.AddScript("Connect-Site -Url \"https://xxxx-admin.sharepoint.com\" -Browser");
powershell.Runspace = myRunSpace;
var results = powershell.Invoke();
var errors = myRunSpace.SessionStateProxy.PSVariable.GetValue("Error");
foreach (var result in results)
{
progress.Report(result.ToString() + Environment.NewLine);
}
}
myRunSpace.Close();
}
}
catch (Exception ex)
{
progress.Report(ex.Message);
}
}
else
{
progress.Report("not connected to Box");
}
return Task.CompletedTask;
}
}

This will be fairly tricky to do if the commands you are calling do not support noninteractive execution.
What's going on:
Your application using the PowerShell api to call your scripts. This part's good. The problem is your scripts are using functionality of the PowerShell host (possibly prompting for credentials). Because the runspace is not associated with a host, any interactive capabilities will simply fail.
So you'd need to attach a host in order for it to work as expected (and that host would need to work the same as PowerShell.exe/pwsh.exe for whatever purposes your underlying cmdlets need).
If there were lots of implementations of a PowerShell host in the wild, I'd link you to them. Unfortunately, there are not. So unless you want to go down a deep rabbit hole, I'd suggest these alternatives:
If the cmdlet supports providing credentials directly, try this
If it does not, see if it "persists" credentials for a given user. (That is, open up a shell, login, close the shell, open another shell, and see if you can use the module without providing credentials).
If credentials do persist (and you can't do option 1), you should be able to call PowerShell.exe/pwsh.exe once to log in, and then load code normally.
If the credentials do not persist, you're stuck in a much more unfortunate situation, leaving you with paths 3,4, or 5:
Call powershell.exe/pwsh.exe (hopefully in in a minimized window) and send the output back via JSON or CLIXML.
Go down the rabbit hole and build yourself a host.
Beg the cmdlet authors to better support noninteractive scenarios.
Between those options, I'd start with the last one.
Best of luck.

Related

What is the best way to check PowerShell Execution Policy in C#?

When you run Get-ExecutionPolicy in PowerShell, it gets the effective execution policy. I need to know the best way to get that information in C#. I don't need to know how to change it like many other questions about PowerShell Execution Policy, I just need to know how to get it in C#.
Note:
PowerShell execution policies apply only on Windows.
With respect to Windows, the answer below covers both PowerShell editions.
It can be inferred from the docs that boxdog pointed to in a comment, but to spell it out:
using System;
using System.Management.Automation;
namespace demo
{
class ConsoleApp {
static void Main() {
using (var ps = PowerShell.Create()) {
var effectivePolicy = ps.AddCommand("Get-ExecutionPolicy").Invoke()[0].ToString();
ps.Commands.Clear();
Console.WriteLine("Effective execution policy: " + effectivePolicy);
}
}
}
}
Note:
The above assumes that you're using the PowerShell SDK - see this answer for the appropriate NuGet package to add to your project.
If you're using a PowerShell (Core) 7+ SDK, additional considerations apply:
On Unix-like platforms, execution policies fundamentally do not apply (Unrestricted is reported, though in effect it is Bypass), so the following applies to Windows only:
The LocalMachine scope of any - by definition install-on-demand - PowerShell (Core) 7+ version does not apply; only - if defined - the CurrentUser and GPO-based policies (which preempt the former) do.
On Windows:
In the absence of a relevant execution policy being defined, Restricted is the default, which categorically prevents execution of script files (.ps1).
If your application needs to execute .ps1 files when hosting the PowerShell SDK, for predictable behavior it is best to set the execution policy, for the current process only, as shown in this answer.
The most elegant solution would probably be to get the ExecutionPolicy registry key in HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell. For this solution to work, your program needs to be running on the same architecture (x64 or x86) as the operating system it's running on or it won't be able to see the registry key. Code to do this would look something like this:
using Microsoft.Win32
...
string executionPolicy = Registry.GetValue(#"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell", "ExecutionPolicy", null)?.ToString();
If for any reason you can't do the first solution, the second way I would recommend is by using the System.Management.Automation.PowerShell NuGet package. This method would look something like this:
using(var ps = PowerShell.Create()){
ps.AddScript("Get-ExecutionPolicy");
Collection<PSObject> output = ps.Invoke();
Console.WriteLine($"Execution policy is {output[0]}")
}
If you really don't want to add an extra NuGet package to your project, there is another, but quite a bit messier way of doing this using System.Diagnostics.Process and it's output stream. It would look something like this:
var procInfo = new ProcessStartInfo("powershell.exe", "-Command \"Get-ExecutionPolicy\"")
{
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true
};
var proc = new Process
{
StartInfo = procInfo
};
proc.OutputDataReceived += Proc_OutputDataReceived;
proc.Start();
proc.BeginOutputReadLine();
Console.ReadLine();
...
private static void Proc_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(e.Data))
Console.WriteLine($"Execution policy is {e.Data}");
}

C# Process "Persistence"

I've been working on a System.Diagnostics.Process project (interractive powershell through network).
I want to be able to send a command from one host to another, so that the second runs it and sends the stdout + stderr to the first host.
The problem is, for example if i want to connect to a FTP server, list all files and get something for example, this should be done in one liner, because every time i send a command to another host, the Process start a new and no ftp connection "in this session of powershell" is present. Therefore "no persistence".
I'd like to run a powershell process on a user's instance once, redirect stdin, stdout and stderr to a variable, and from those be able to recieve command strings and send the output from that to the "main host".
P.S. All solutions i've found were just to run for every command a new instance of a Process and lose the "persistence" of that.
Unless your goal is to re-invent PowerShell Remoting, I'd strongly suggest just using a remote runspace instead, see this (oversimplified) example:
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Linq;
public class PersistentCommandRunner
{
private Runspace remoteRunspace;
public PersistentCommandRunner(string remoteMachine)
{
# Connect to remote machine using a runspace, save the runspace reference
var connectionInfo = new WSManConnectionInfo(new Uri("http://remoteMachine:5985/WSMan"));
remoteRunspace = RunspaceFactory.CreateRunspace(connectionInfo);
}
public IEnumerable<PSObject> RunScript(string script)
{
# Make sure runspace is ready to execute remote commands
EnsureConnection();
using(var shell = PowerShell.Create())
{
# Make sure our script runs in the remote runspace
shell.Runspace = remoteRunspace;
shell.AddScript(script);
foreach(var result in shell.Invoke())
{
yield return result;
}
}
}
private void EnsureConnection()
{
if(remoteRunspace.RunspaceStateInfo.State != RunspaceState.Open)
{
if(remoteRunspace.RunspaceStateInfo.State == RunspaceState.BeforeOpen)
{
remoteRunspace.Open();
return;
}
throw new Exception("Runspace is in an unusable state...");
}
}
}
Re-using the same runspace for subsequent scripts allows you to maintain state on the remote endpoint, ie.:
var serverRunner = new PersistentCommandRunner();
# Stage variable in the remote session
serverRunner.InvokeScript(#"$remoteVariable = 123");
# Retrieve it again, should print "123"
Console.WriteLine(serverRunner.InvokeScript("$remoteVariable").First()?.Base as int)

Run powershell from c# most efficient way to create multiple mailcontacts in Exchange Online

I wish to create multiple mailcontacts (external Contacts) in the GAL in Microsoft Online by running Powershell command from C#. The code below works, but is very slow and takes about 15-20 min to run for 400 mailcontacts.
foreach(EmailAdressVM emailAddressVM in emailList.emailAddresses1)
{
//Create New MailContact.
Pipeline pplNewMailContact = runspace.CreatePipeline();
Command cmdNewMailContact = new Command("New-MailContact");
cmdNewMailContact.Parameters.Add("Name", emailAddressVM.sExternalEmailAddress);
cmdNewMailContact.Parameters.Add("Displayname", emailAddressVM.sFullName.Trim());
cmdNewMailContact.Parameters.Add("Lastname", emailAddressVM.sLastName.Trim());
cmdNewMailContact.Parameters.Add("Firstname", emailAddressVM.sFirstName.Trim());
cmdNewMailContact.Parameters.Add("ExternalEmailAddress", emailAddressVM.sExternalEmailAddress.Trim());
pplNewMailContact.Commands.Add(cmdNewMailContact);
pplNewMailContact.Invoke();
pplNewMailContact.Stop();
pplNewMailContact.Dispose();
}
I am guessing that this is slow since I create a new Pipeline for every new mailcontact that is added and there has to be a more eficient way of doing this since running...
import-csv <filename> | ForEach {
new-mailcontact -name $_.emailaddress -displayname $_.FullName -lastname $_.lastname -firstname $_.firstname -externalemailaddress $_.emailaddress -alias $_.alias
}
...is much faster.
I have found some references after many hours of searching the web that you can do something similar to using a CSV when running Powershell commands from C#, i.e. send a list (or array) of values to a command (in this case the "new-mailcontact" command). But, I have not found any good example of how to send more than one value to a command and I need to supply many values (for example: -name $.emailAddress -displayname $.FullName, etc.) to the "new-mailcontact" command.
Is it possible to send a list (or array) in a similar way as the "import-csv" command (when using regular powershell) and will this be faster, or is there an evan better way? Would I get better performance if I use Powershell 3 instead of 1 (as I am using now).
Please provide working sample code i C#!
Please note that I cannot save a CSV file to disk and the execute powershell from CMD since I do not have write access to disk and that I do not think that I can run an entire script remotely (since remote scripting probably is disabled on Exchange Online).
The biggest reason I would think is because for each address you are creating a new Powershell instance and you are not multithreaded.
You code looks something like this from above:
Foreach email address{
Declare a new Powershell process
Add attributes to call later
Start Powershell and pipe stuff in
Close Powershell instance
}
I think you would be better off creating the Powershell instance / pipe once and then sending each object into it. More along the lines of:
Create PS Pipe
Foreach email address{
PS.SendArguments(Email, Name, DN, etc.);
}
I am not in an environment to get something working or tested right now, so hopefully this gives you at least most of what you need...

Load ActiveDirectory PowerShell Module in Runspace without AD Drive Letter

I'm trying to load the ActiveDirectory module inside a custom SnapIn that I'm working on. However, when I do so I get the annoying error
"Error initializing default drive: 'Unable to find a default server
with Active Directory Web Services running.'"
which takes a good 15 seconds or so to timeout. From within a normal PowerShell console I realize you can set a variable to disable the AD: drive mapping but, I cannot seem to get that working from within C# code.
InitialSessionState initial = InitialSessionState.CreateDefault();
initial.Variables.Add(new SessionStateVariableEntry("ADPS_LoadDefaultDrive",
0,
string.Empty));
initial.ImportPSModule(new string[] { "ActiveDirectory" });
using (Runspace runspace = RunspaceFactory.CreateRunspace(initial))
{
runspace.Open();
using (Pipeline p = runspace.CreatePipeline())
{
Command getGroup = new Command("Get-ADGroup");
getGroup.Parameters.Add("Filter", this.Group);
p.Commands.Add(getGroup);
var results = p.Invoke();
this.WriteObject(results, true);
}
}
I've included what I think should work but, the ADPS_LoadDefaultDrive setting seems to be ignored as each time I try to make a call into the ActiveDirectory module I get the same web services error (along with a painful timeout)
Try to set ADPS_LoadDefaultDrive as an Environment variable, not a regular session variable.

What is the Fastest way to read event log on remote machine?

I am working on an application which reads eventlogs(Application) from remote machines. I am making use of EventLog class in .net and then iterating on the Log entries but this is very slow. In some cases, some machines have 40000+ log entries and it takes hours to iterate through the entries.
what is the best way to accomplish this task? Are there any other classes in .net which are faster or in any other technology?
Man, I feel your pain. We had the exact same issue in our app.
Your solution has a branch depending on what server version you're running on and what server version your "target" machine is running on.
If you're both on Vista or Windows Server 2008, you're in luck. You should look at System.Diagnostics.Eventing.Reader.EventLogQuery and System.Diagnostics.Eventing.Reader.EventLogReader. These are new in .net 3.5.
Basically, you can build a query in XML and ship it over to run on the remote computer. Maybe you're just searching for events of a specific type, or maybe just new events from a specific point in time. The search runs on the remote machine, and then you just get back the matching events. The new classes are much faster than the old .net 2.0 way, but again, they are only supported on Vista or Windows Server 2008.
For our app when the target is NOT on Vista/Win2008, we downloaded the raw .evt file from the remote system, and then parsed the file using its binary format. There are several sources of data about the event log format for .evt files (pre-Vista), including link text and an article I recall on codeproject.com that had some c# code.
Vista and Windows Server 2008 machines use a new .evtx format that is a new format, so you can't use the same binary parsing approach across all versions. But the new EventLogQuery and EventLogReader classes are so fast that you won't have to. It's now perfectly speedy to just use the built-in classes.
Event Log Reader is horribly slow... too slow. WTF Microsoft?
Use LogParser 2.2 - Search for C# and LogParser on the Internet (or you can use the log parser commands from the command line). I don't want to duplicate the work already contributed by others.
I pull the log from the remote system by having the log exported as an EVTX file. I then copy the file from the remote system. This process is really quick - even with a network that spans the planet (I had issues with having the log exported to a network resource). Once you have it local, you can do your searches and processing.
There are multiple reasons for having the EVTX - I won't get into the reasons why we do this.
The following is a working example of the code to save a copy of the log as an EVTX:
(Notes: "device" is the network host name or IP. "LogName" is the name of the log desired: "System", "Security", or "Application". outputPathOnRemoteSystem is the path on the remote computer, such as "c:\temp\%hostname%.%LogName%.%YYYYMMDD_HH.MM%.evtx".)
static public bool DumpLog(string device, string LogName, string outputPathOnRemoteSystem, out string errMessage)
{
bool wasExported = false;
string errorMessage = "";
try
{
System.Diagnostics.Eventing.Reader.EventLogSession els = new System.Diagnostics.Eventing.Reader.EventLogSession(device);
els.ExportLogAndMessages(LogName, PathType.LogName, "*", outputPathOnRemoteSystem);
wasExported = true;
}
catch (UnauthorizedAccessException e)
{
errorMessage = "Unauthorized - Access Denied: " + e.Message;
}
catch (EventLogNotFoundException e)
{
errorMessage = "Event Log Not Found: " + e.Message;
}
catch (EventLogException e)
{
errorMessage = "Export Failed: " + e.Message + ", Log: " + LogName + ", Device: " + device;
}
errMessage = errorMessage;
return wasExported;
}
A good Explanation/Example can be found on MSDN.
EventLogSession session = new EventLogSession(Environment.MachineName);
// [System/Level=2] filters out the errors
// Where "Log" is the log you want to get data from.
EventLogQuery query = new EventLogQuery("Log", PathType.LogName, "*[System/Level=2]");
EventLogReader reader = new EventLogReader(query);
for (EventRecord eventInstance = reader.ReadEvent();
null != eventInstance;
eventInstance = reader.ReadEvent())
{
// Output or save your event data here.
}
When waiting 5-20 minutes with the old code this one does it in less than 10 seconds.
Maybe WMI can help you:
WMI with C#
Have you tried using the remoting features in powershell 2.0? They allow you to execute cmdlets (like ones to read event logs) on remote machines and return the results (as objects, of course) to the calling session.
You could place a Program at those machines that save the log to file and sends it to your webapplication i think that would be alot faster as you can do the looping local but im not sure how to do it so i cant ive you any code :(
I recently did such thing via WCF callback interface however my clients interacted with the server through WCF and adding a WCF Callback was easy in my project, full code with examples is available here
Just had the same issue and want to share my solution. It makes a search through application, system and security eventlogs from 260 seconds (using EventLog) about a 100 times faster (using EventLogQuery).
And this in a way where it is possible to check if the event message contains a pattern or any other check without the requirement of FormatDescription().
My trick is to use the same mechanism as PowerShells Get-WinEvent does and then pass it through the result check.
Here is my code to find all events within last 4 days where the event message contains a filter pattern.
string[] eventLogSources = {"Application", "System", "Security"};
var messagePattern = "*Your Message Search Pattern*";
var timeStamp = DateTime.Now.AddDays(-4);
var matchingEvents = new List<EventRecord>();
foreach (var eventLogSource in eventLogSources)
{
var i = 0;
var query = string.Format("*[System[TimeCreated[#SystemTime >= '{0}']]]",
timeStamp.ToUniversalTime().ToString("o"));
var elq = new EventLogQuery(eventLogSource, PathType.LogName, query);
var elr = new EventLogReader(elq);
EventRecord entryEventRecord;
while ((entryEventRecord = elr.ReadEvent()) != null)
{
if ((entryEventRecord.Properties)
.FirstOrDefault(x => (x.Value.ToString()).Contains(messagePattern)) != null)
{
matchingEvents.Add(entryEventRecord);
i++;
}
}
}
Maybe that the remote computers could do a little bit of computing. So this way your server would only deal with relevant information. It would be a kind of cluster using the remote computer to do some light filtering and the server would the the analysis part.

Categories

Resources