How do I pass common powershell command line parameters between cmdlets on the pipeline? - powershell

Say I've got two cmdlets, 'new-foo' and 'do-bar'. Both cmdlets need to authenticate to a service in order to perform their action, and 'do-bar' takes a foo. Today, I can do:
new-foo -host localhost -username user -password password -whateverOtherArgs
And I can do:
do-bar -host localhost -username user -password password -foo myFoo
And I can even chain them passing foo on the pipeline, e.g.:
new-foo <blah blah> | do-bar -host localhost -username user -password password
But I can't figure out how to pass the common parameters, such as the service location and the credentials between elements of the pipeline. If I've got a bunch of my cmdlets chained together I'd like to only pass the credentials the first time, and then re-use those for the rest of the pipeline.
What am I missing, seems like this should be obvious ...

You could have New-Foo spit out an object that contains both the original object that do-bar is interested in as well as the service location and the credentials as properties. Accept this object as a parameter and then pluck out the data you need if the user doesn't supply the ServiceLocation or Credential parameters.

does Get-Credential cover you need.

Are you developing this cmdlets, or are you just using them in your scripts?
If you are writing these cmdlets in any OOP language, I think a way to do it is that you have a base cmdlet class, and every other cmdlets should extend that base cmdlet class. In this case, the base cmdlet class should initialize an object that stores your credential, and have a cmdlet to ask for credentials, which initialize the object. Every other cmdlets that extends the base class can therefore look for credential in this object.

Related

Set default server in powershell for AD related commands

Recently my powershell scripts require to explicitly say which domain I want to connect to. Is it necessary to write this for each command? Or can I set it somehow once in the beginning of the script.
Instead of
Get-ADUser -Server server otherparameters
could I write in the beginning something like
Set-default server to connect to
?
Is it necessary to write this for each command?
No!
You can specify a default parameter value for a parameter belonging to one or more cmdlets by assigning it to the $PSDefaultParameterValues automatic variable:
$PSDefaultParameterValues['*-AD*:Server'] = 'mydc.mydomain.tld'
Any cmdlet you subsequently invoke that matches the *-AD* pattern by name and has a -Server parameter will now implicitly have 'mydc.mydomain.tld' bound to the -Server parameter unless an argument is explicitly passed.
In other words: next time you invoke Get-ADUser rsterba, PowerShell now calls Get-ADUser rsterba -Server 'mydc.mydomain.tld' instead.
For more information about $PSDefaultParameterValues and how it works, see the about_Parameters_Default_Values help topic

Powershell task: Hide not the output but the actual command containing sensitive info in devops logs

I have a powershell script in my release pipeline stage that runs a command and passes values of a secret variable to it. The issue is that the Logs show each and every command as they are run including each arguments passed, one of which is from a secret variable.
How do I make the powershell output not show the command it is running? output of the command is okay to show if it can't be hidden.
Secrets shouldn't be converted to plain text but kept as such and passed as a SecureString to your application. In other words, the solution lays in making sure that your concerned application accepts a hashed password, a SecureString or a PSCredential object also knowing that sending a plain text password to an application isn't secure by itself.
#iRon Say this to Microsoft. I am trying to call their schtasks
I just did: #16502: Set-ScheduledTask shouldn't accept a plain text Password
As a workaround, you might keep your password covered in a SecureString as long as possible:
$Credentials = Get-Credential
Set-ScheduledTask -User $Credential.UserName -Password $Credential.GetNetworkCredential().Password
This will prevent that the passwords are revealed by logging but as there is still a potentially security risk that the password could be read from memory, I recommend to do a garbage collection ([system.gc]::Collect()) right after this command.
⚠️ Important
A SecureString object should never be constructed from a String, because the sensitive data is already subject to the memory persistence consequences of the immutable String class. The best way to construct a SecureString object is from a character-at-a-time unmanaged source, such as the Console.ReadKey method.
To be completely safe, you might also consider to run Set-ScheduledTask (without -User and -Password) under the credentials of the targeted user Start-Process -Credential $Credential ...
Update 2022-02-24:
Sadly😭, I got zero response on my feedback hub "Set-ScheduledTask shouldn't accept a plain text Password" (security) issue. Therefore, I have also just created a new Microsoft Feedback Portal issue for this: Windows-PowerShell: Set-ScheduledTask shouldn't accept a plain text Password
Anyhow, the organization I work for, deals with the same general issue where the use-case is defined as: "how can we hide sensitive information as passwords used by invoked 3rd party applications in PowerShell scripts"
As suggested before: the problem in not due to any (PowerShell) scripting limitations but how the information (as plain text) is provided (input) to the script and how it is expected to be passed (output) to any other application.
To make this clear and to supply at least some (easy) solution, I have created an [HiddenString] class that might be used in a script to hide information as much as possible end-to-end inside the script itself.
class HiddenString {
hidden [SecureString]$SecureString = [SecureString]::new()
HiddenString([Object]$String) {
if ($String -is [SecureString]) { $This.SecureString = $String }
else {
foreach ($Character in [Char[]]$String) { $This.SecureString.AppendChar($Character) }
}
}
[String]Reveal(){
$Ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUnicode($This.SecureString)
$String = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($Ptr)
[System.Runtime.InteropServices.Marshal]::ZeroFreeCoTaskMemUnicode($Ptr)
Return $String
}
}
Note that I am using the SecureString type in the class not for better security but just for better hiding the concerned string.
Usage example:
function MyScript([String]$TaskName, [String]$UserName, [HiddenString]$Password) {
Start-Transcript -Path .\Transcript.txt
Write-Host "Scheduling $TaskName for $UserName/$Password" # Write-Log ...
Set-ScheduledTask -TaskName $TaskName -User $UserName -Password $Password.Reveal()
Stop-Transcript
}
Recommended invocation of MyScript:
$SecuredString = Read-Host 'Enter Password' -AsSecuredString
MyScript NotePad.Exe JohnDoe $SecuredString
Just hiding the sensitive information inside the MyScript:
$String = 'Sensitive Information'
MyScript NotePad.Exe JohnDoe $String
Transcript started, output file is .\Transcript.txt
Scheduling NotePad.Exe for JohnDoe/HiddenString
Transcript stopped, output file is .\Transcript.txt
Again, (I can't stress this enough):
warning: as a whole, this workaround is nothing more than security through obscurity
As Microsoft states themselves at SecureString shouldn't be used:
The general approach of dealing with credentials is to avoid them and instead rely on other means to authenticate, such as certificates or Windows authentication.
(which they should also do in their own cmdlets along with Set-ScheduledTask)
I have created an enhancement request for this idea:
#16921 Add [HiddenString] Class
You can register secrets with the agent to ensure they're scrubbed from the logs.
Write this to the output:
write-output "##vso[task.setsecret]THEVALUEYOUWANTHIDDEN"
This should register the secret with the agent. If you know your script will also popentially log the base64 value or another representation of the secret, make sure you register all permutations.

Using System.Management.Automation and an exported session, how do I specify credentials for an O365 call?

I'm building a small console app that uses constructs in the System.Management.Automation namespace to connect to ExchangeOnline and perform various tasks. The overhead time of creating and importing a new session with each run during my dev & test is prohibitive.
Thus, I've elected to save the session to disk using Export-PSSession. This all works fine from a PowerShell prompt, like so:
Import-Module ExchangeOnline
Get-Mailbox
I'm prompted for my credentials, and off we go.
Unfortunately, the same can't be said for running the same sequence under Automation:
System.Management.Automation.MethodInvocationException: Exception calling "GetSteppablePipeline" with "1" argument(s): "Exception calling "PromptForCredential" with "4" argument(s): "A command that prompts the user failed because the host program or the command type does not support user interaction. The host was attempting to request confirmation with the following message: Enter your credentials for https://outlook.office365.com/powershell-liveid/.""
How do I send my credentials to O365 when using System.Management.Automation?
This Q&A almost answers it, but not quite.
Here's my code.
Implementation
Friend Class Monad
Implements IDisposable
Public Sub New()
Me.SessionState = InitialSessionState.CreateDefault
Me.Monad = PowerShell.Create
End Sub
Public Sub ImportModule(Modules As String())
If Me.RunSpace.IsNotNothing Then
Me.RunSpace.Dispose()
Me.RunSpace = Nothing
End If
Me.SessionState.ImportPSModule(Modules)
Me.RunSpace = RunspaceFactory.CreateRunspace(Me.SessionState)
Me.RunSpace.Open()
Me.Invoker = New RunspaceInvoke(Me.RunSpace)
End Sub
Public Function ExecuteScript(Script As String) As Collection(Of PSObject)
Dim oErrors As Collection(Of ErrorRecord)
ExecuteScript = Me.Invoker.Invoke(Script)
oErrors = Me.Monad.Streams.Error.ReadAll
If oErrors.Count > 0 Then
Throw New PowerShellException(oErrors)
End If
End Function
Protected Overridable Sub Dispose(IsDisposing As Boolean)
If Not Me.IsDisposed Then
If IsDisposing Then
If Me.RunSpace.IsNotNothing Then Me.RunSpace.Dispose()
If Me.Invoker.IsNotNothing Then Me.Invoker.Dispose()
If Me.Monad.IsNotNothing Then Me.Monad.Dispose()
Me.RunSpace = Nothing
Me.Invoker = Nothing
Me.Monad = Nothing
End If
End If
Me.IsDisposed = True
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Me.Dispose(True)
End Sub
Private ReadOnly SessionState As InitialSessionState
Private IsDisposed As Boolean
Private RunSpace As Runspace
Private Invoker As RunspaceInvoke
Private Monad As PowerShell
End Class
Call
Friend Function GetMailbox() As IEnumerable(Of PSObject)
Using oMonad As New Monad
oMonad.ImportModule({"ExchangeOnline"})
Return oMonad.ExecuteScript("Get-Mailbox")
End Using
End Function
The ExchangeOnline module has some issues with that. It wants to be able to display an interactive modern authentication dialog, and there's no reliable way to stop it. You can feed it credentials, but it will barf if it needs to display an interactive dialog (as it does for MFA).
For storing and retrieving the credentials, you can use ConvertFrom-SecureString and Export-Csv or Export-CliXml as in the answer from InteXX, but that will stop working if basic authentication is disabled for the account you're using, or after 13 October 2020 when basic auth is disabled in exchange online (see KB4521831 ref https://support.microsoft.com/en-us/help/4521831/exchange-online-deprecating-basic-auth). Until then, you can also use a module like VaultCredential to manage credentials (note that it won't work across accounts).
So the next question is probably how you get a token for modern auth and how you present it to Exchange Online to authenticate with powershell. That's not hard at all.
You can bang against the endpoints with .NET web calls without too much effort, but it's easier to use one of the officially sanctioned libraries like ADAL.PS (deprecated) and MSAL.PS. Go ahead and Install-Module -Name MSAL.PS from the powershell gallery. Once it's there (and run Get-Module -Refresh -ListAvailable to make sure it can autoload) you can run:
$Token = Get-MsalToken -ClientId "a0c73c16-a7e3-4564-9a95-2bdf47383716" -RedirectUri "urn:ietf:wg:oauth:2.0:oob" -Scopes "https://outlook.office365.com/AdminApi.AccessAsUser.All","https://outlook.office365.com/FfoPowerShell.AccessAsUser.All","https://outlook.office365.com/RemotePowerShell.AccessAsUser.All"
You can also add "-LoginHint " (i.e. probably your email address) to skip the initial locator prompt, and it might help to add "-TenantId '00000000-0000-0000-0000-000000000000'" (except use the actual GUID for your Azure AD tenant).
Once a token is acquired, MSAL will keep it in its cache and automatically refresh it for you. This might make non-interactive use a little more difficult. You can review preferred ways to deal with it (https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-acquire-cache-tokens), or work with the libraries or interfaces at a lower level (such as using AcquireTokenSilentAsync()).
To actually use the token against Exchange Online, you need to use basic auth. It will still work even after they deprecate it, but it won't validate your account's password, it will only do other stuff like accept encoded tokens. Basically you need to Get-PsSession -Credential $EncodedBasicCredential where $EncodedBasicCredential is constructed with your UPN as the username and the Base64-encoded authorization header value as the password. For example:
$EncodedBasicCredential = [System.Management.Automation.PSCredential]::new($Token.account.username,(ConvertTo-SecureString -AsPlainText -Force -String ($Token.CreateAuthorizationHeader())))
Note that $Token.CreateAuthorizationHeader() just takes the value of $Token.AccessToken
and prepends it with "Bearer ". Now all you need to do is create a New-PsSession with the appropriate ConnectionUri, ConfigurationName, and your credential object:
$ExchangeOnlineSession = New-PSSession -Name ExchangeOnline -ConnectionUri "https://outlook.office365.com/PowerShell-LiveId?BasicAuthToOAuthConversion=true" -ConfigurationName Microsoft.Exchange -Credential $EncodedBasicCredential -Authentication Basic
And you can import that to the parent session as you like:
$ExchangeOnlineModule = Import-PSSession -Session ($ExchangeOnlineSession) -WarningAction Ignore
To answer your specific question (how you specify credentials), it all depends on how you access your identity authority. If you are using ADFS on premises and your script runs on premises, then you should be able to run the process as the desired identity and Get-MsalToken will automatically use integrated windows authentication against ADFS without prompting you. If you are using PTA or native auth directly against Azure AD, then you'll need to look at creating a client application and using a secret or a certificate to authenticate against Azure AD to get your token.
I think this will be reasonably easy to translate from powershell to C#, but I'm a scripting sys admin, not a coder.
I was able to accomplish this by editing the exported module.
First, export your O365 credentials:
Get-Credential | Export-Clixml -Path D:\Modules\O365Credential.xml
With that XML file now in hand, open the module for editing in your preferred IDE
Search for the function call PromptForCredential
Note that the call is a part of a larger call to the Set-PSImplicitRemotingSession cmdlet
Comment out that entire outer cmdlet call, e.g.:
Insert an uncommented copy of the call (optional: fix the broken indentation)
For the -Credential parameter, replace the value with $Credential
Above the new Set-PSImplicitRemotingSession call, add this line:
$Credential = Import-Clixml -Path D:\Modules\O365Credential.xml
Your finished code should look something like this:
Save the module
To test, open a new PowerShell prompt and import the updated module. Run Get-Mailbox. You should receive a list of the mailboxes hosted by your O365 tenant, without being prompted for credentials.
Note that the module uses and decrements a RunSpace connection against your quota. You'll want to close that connection gracefully when you're done with your session:
Get-PSSession | Where-Object { $_.ConfigurationName -eq 'Microsoft.Exchange' } | Remove-PSSession -ErrorAction SilentlyContinue
Now you can import and use the module under the System.Management.Automation namespace, as shown in the VB.NET code above.

Get current user's credentials object in Powershell without prompting

I have a Powershell script that is going to be run through an automation tool against multiple servers.
It works fine on Windows machines, as the remote calls use the tool's service account without any need for prompting or exposing any credentials in code.
This script also runs against Linux machines via SSH using the SharpSSH package. SharpSSH does not automatically use the Powershell user's credentials but requires either a username and password, an RSA key file, or a PSCredential object.
I can't prompt for credentials using Get-Credential, because it's being run through the automation tool. I don't want to expose the username and password in code or have an RSA key sitting out there. I would like to construct a PSCredential object from the current Powershell user (the service account).
Trying [System.Net.CredentialCache]::DefaultNetworkCredentials shows a blank, and [System.Security.Principal.WindowsIdentity]::GetCurrent() doesn't provide the object or information I need.
Does anyone have a method for creating a PSCredential object from the current user? Or maybe a completely different alternative for this problem?
Many thanks!
The Windows API will not expose the information you need, which is why Powershell can't get to them. Its an intentional feature of the security subsystem. The only way for this to work is for the Linux machines to trust the calling machine, such as joining them to an Active Directory (or any kerberos setup really).
Aside from that, you'd need to store and pass this information somehow.
You could store the RSA key in the user's keystore and extract it at runtime (using the .NET Crypto/Keystore libs), so you aren't storing the key around with the code. That way the key itself would be protected by the OS and available only when the calling user was authenticated. You'd have one more thing to install, but may be the only way to achieve what you are aiming for.
"Trying [System.Net.CredentialCache]::DefaultNetworkCredentials shows a blank, and [System.Security.Principal.WindowsIdentity]::GetCurrent() doesn't provide the object or information I need."
You already have your answer. I use this to pass the currently logged in user's credentials along in several scripts:
$Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials
$Username = $Credentials.UserName
$Password = $Credentials.Password
If you try to dump them to any kind of readable output, those values are empty when you dump them (for obvious security reasons), however they do work where you need a PSCredential object.
How about encrypting the password using the service account's encryption key?
A quick example:
Run PowerShell as the service account, run the following and save the output to a text file (or embed it in the scheduled task call):
$String = '<PASSWORD>'
ConvertFrom-SecureString -SecureString (ConvertTo-SecureString -String $String -AsPlainText -Force)
Use the following in your scheduled task in order to decrypt and utilize the password:
$EncryptedString = '<ENCRYPTED PASSWORD FROM ABOVE>'
[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR((ConvertTo-SecureString -String $EncryptedString)))
That should do the trick. You cannot reuse the encrypted password on a different computer, though, or if you for whatever reason destroy you local key store :)
Since you can get the password in plaintext from a credential object, I doubt you can get this without prompting.

Creating file in a user context in powershell

I am trying to create a file using powershell in a specific user context. E.g I have a user user01 on my local machine and I want to create a file in its context.
I am doing something like
New-Item c:\file.txt -Credential User01
It works but prompts me for password which I dont want it to. Is there any way I can accomplish this without having it prompt for password ?
The credential parameter on new-item is not actually supported for filesystems, so I'm not sure what you mean by "it works." It does NOT create the file as the passed user. In fact, the filesystem provider will say:
"The provider does not support the use of credentials. Perform the operation again without specifying credentials."
Taking an educated guess, I'd say you're trying to create a file with a different owner. PowerShell cannot do this on its own, so you'll need the following non-trivial script:
http://cosmoskey.blogspot.com/2010/07/setting-owner-on-acl-in-powershell.html
It works by enabling the SeBackup privilege for your security token (but you must already be an administrator.) This allows you to set any arbitrary owner on a file. Normally you can only change owner to administrators or your own account.
Oh, and this script is for powershell 2.0 only.
Rather than use a PowerShell cmdlet or .NET scripting on this one, you might take a look at the Windows utility takeown.exe. However, even it requires you supply the user's password that you're assigning ownership to.
Ok, I do start process in the user context and then create a file. Works like a charm.
Password, FilePath and UserName are passed in as arguments from command line.
$pw = convertto-securestring "$Password" -asplaintext –force
$credential = new-object -typename system.management.automation.pscredential -argumentlist "-default-",$pw
$localArgs = "/c echo>$FilePath"
[System.Diagnostics.Process]::Start("cmd", $localArgs, "$UserName", $credential.Password, "$Computer")
Or just make a call to SUBINACL.EXE? No need for password then.