powershell-command-executor
Version:
Provides a registry and gateway for execution powershell commands through long-lived established remote PSSessions via a stateful-process-command-proxy pool of powershell processes
631 lines (546 loc) • 19.4 kB
JavaScript
/**
* getO365PSInitCommands()
*
* Returns an array of Powershell initialization commands suitable
* for setting up shells spawned with StatefulProcessCommandProxy
* to be able to establish a remote PSSession with o365
*
* @see https://github.com/bitsofinfo/powershell-credential-encryption-tools
*
* This function takes the full path to:
* - decryptUtil.ps1 from the project above
* - path the encrypted credentials file generated with decryptUtil.ps1
* - path to the secret key needed to decrypt the credentials
*
* In addition there are parameter to define the PSSessionOption timeouts
*
* Note this is just an example (which works) however you may want to
* replace this with your own set of init command tailored to your specific
* use-case
*
* @see the getO365PSDestroyCommands() below for the corresponding cleanup
* commands for these init commands
*/
module.exports.getO365PSInitCommands = function (pathToDecryptUtilScript,
pathToCredsFile,
pathToKeyFile,
openTimeout,
operationTimeout,
idleTimeout) {
return [
// #0 Encoding UTF8
'chcp 65001',
'$OutputEncoding = [System.Text.Encoding]::GetEncoding(65001)',
// #1 import some basics
'Import-Module MSOnline',
// #2 source the decrypt utils script
// https://github.com/bitsofinfo/powershell-credential-encryption-tools/blob/master/decryptUtil.ps1
('. ' + pathToDecryptUtilScript),
// #3 invoke decrypt2PSCredential to get the PSCredential object
// this function is provided by the sourced file above
('$PSCredential = decrypt2PSCredential ' + pathToCredsFile + ' ' + pathToKeyFile),
// #4+ establish the session to o365
('$sessionOpt = New-PSSessionOption -OpenTimeout ' + openTimeout + ' -OperationTimeout ' + operationTimeout + ' -IdleTimeout ' + idleTimeout),
'$session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $PSCredential -Authentication Basic -AllowRedirection -SessionOption $sessionOpt',
// #5 import the relevant cmdlets (TODO: make this configurable)
'Import-PSSession $session -CommandName *DistributionGroup* -AllowClobber',
'Import-PSSession $session -CommandName *Contact* -AllowClobber',
// #6 connect to azure as well
'Connect-MsolService -Credential $PSCredential',
// #7 cleanup
'Remove-Variable -Force -ErrorAction SilentlyContinue $PSCredential'
];
};
/**
* Destroy commands that correspond to the session
* established w/ the initCommands above
*/
module.exports.getO365PSDestroyCommands = function () {
return [
'Get-PSSession | Remove-PSSession -ErrorAction SilentlyContinue',
'Remove-PSSession -Session $session',
'Remove-Module MsOnline'
];
};
/**
* getO365PSKeyInitCommands()
*
* Returns an array of Powershell initialization commands suitable
* for setting up shells spawned with StatefulProcessCommandProxy
* to be able to establish a remote PSSession with o365
*
* @see https://github.com/bitsofinfo/powershell-credential-encryption-tools
* @see https://docs.microsoft.com/en-us/powershell/module/exchange/connect-exchangeonline?view=exchange-ps
* @see https://adamtheautomator.com/exchange-online-powershell-mfa/#Authenticating_Using_Local_PFX_Certificate
*
* This function takes the full path to:
* - decryptUtil.ps1 from the project above
* - path the encrypted credentials file generated with decryptUtil.ps1
* - path to the secret key needed to decrypt the credentials
* - path to the certificate configured for Exchange app authentication
* - certificate password
* - application id created for the Exchange app integration
* - tenant/organizationId for the Exchange
* - comma separatged list of commands to import, widcard supported.
* Everything is imported if empty
*
* In addition there are parameter to define the PSSessionOption timeouts
*
* Note this is just an example (which works) however you may want to
* replace this with your own set of init command tailored to your specific
* use-case
*
* @see the getO365PSKeyDestroyCommandsgetO365PSKeyDestroyCommands() below
for the corresponding cleanup
* commands for these init commands
*/
module.exports.getO365PSKeyInitCommands = function (pathToDecryptUtilScript,
pathToCredsFile,
pathToKeyFile,
pathToAuthCertificate,
authCertificatePassword,
applicationId,
organizationId,
openTimeout,
operationTimeout,
idleTimeout,
commandsToImport = '') {
let psCommandsToImport = '';
if (commandsToImport != '') {
psCommandsToImport = '-CommandName ' + commandsToImport;
}
return [
// #0 Encoding UTF8
'chcp 65001',
'$OutputEncoding = [System.Text.Encoding]::GetEncoding(65001)',
// #1 import some basics
'Import-Module MSOnline',
'Import-Module ExchangeOnlineManagement',
// #2 source the decrypt utils script
// https://github.com/bitsofinfo/powershell-credential-encryption-tools/blob/master/decryptUtil.ps1
('. ' + pathToDecryptUtilScript),
// #3 invoke decrypt2PSCredential to get the PSCredential object
// this function is provided by the sourced file above
('$PSCredential = decrypt2PSCredential ' + pathToCredsFile + ' ' + pathToKeyFile),
// #4 connect to azure as well
'Connect-MsolService -Credential $PSCredential',
// #5 get session options, certificate file and secure password
('$sessionOpt = New-PSSessionOption -OpenTimeout ' + openTimeout + ' -OperationTimeout ' + operationTimeout + ' -IdleTimeout ' + idleTimeout),
'$CertificateFilePath = (Resolve-Path "' + pathToAuthCertificate + '").Path',
'$CertificatePassword = (ConvertTo-SecureString -String "' + authCertificatePassword + '" -AsPlainText -Force)',
// #6 connect to exchange
'Connect-ExchangeOnline -CertificateFilePath $CertificateFilePath -CertificatePassword $CertificatePassword -AppID ' + applicationId + ' -Organization ' + organizationId + ' ' + psCommandsToImport,
// #7 cleanup
'Remove-Variable -Force -ErrorAction SilentlyContinue $PSCredential'
];
};
/**
* Destroy commands that correspond to the session
* established w/ the KeyinitCommands above
*/
module.exports.getO365PSKeyDestroyCommands = function () {
return [
'Get-PSSession | Remove-PSSession -ErrorAction SilentlyContinue',
'Remove-Module MsOnline',
'Remove-Module ExchangeOnlineManagement',
];
};
/**
* getO365PSKeyInitCommands()
*
* Returns an array of Powershell initialization commands suitable
* for setting up shells spawned with StatefulProcessCommandProxy
* to be able to establish a remote PSSession with o365
*
* @see https://github.com/bitsofinfo/powershell-credential-encryption-tools
* @see https://docs.microsoft.com/en-us/powershell/module/exchange/connect-exchangeonline?view=exchange-ps
* @see https://adamtheautomator.com/exchange-online-powershell-mfa/#Authenticating_Using_Certificate_Thumbprint
*
* This function takes the full path to:
* - decryptUtil.ps1 from the project above
* - path the encrypted credentials file generated with decryptUtil.ps1
* - path to the secret key needed to decrypt the credentials
* - certificate thumbprint configured for Exchange app authentication
* - application id created for the Exchange app integration
* - tenant/organizationId for the Exchange
* - comma separatged list of commands to import, widcard supported.
* Everything is imported if empty
*
* In addition there are parameter to define the PSSessionOption timeouts
*
* Note this is just an example (which works) however you may want to
* replace this with your own set of init command tailored to your specific
* use-case
*
* @see the getO365PSKeyDestroyCommandsgetO365PSKeyDestroyCommands() below
for the corresponding cleanup
* commands for these init commands
*/
module.exports.getO365PSThumbprintInitCommands = function (pathToDecryptUtilScript,
pathToCredsFile,
pathToKeyFile,
certificateThumbPrint,
applicationId,
organizationId,
openTimeout,
operationTimeout,
idleTimeout,
commandsToImport = '') {
let psCommandsToImport = '';
if (commandsToImport != '') {
psCommandsToImport = '-CommandName ' + commandsToImport;
}
return [
// #0 Encoding UTF8
'chcp 65001',
'$OutputEncoding = [System.Text.Encoding]::GetEncoding(65001)',
// #1 import some basics
'Import-Module MSOnline',
'Import-Module ExchangeOnlineManagement',
// #2 source the decrypt utils script
// https://github.com/bitsofinfo/powershell-credential-encryption-tools/blob/master/decryptUtil.ps1
('. ' + pathToDecryptUtilScript),
// #3 invoke decrypt2PSCredential to get the PSCredential object
// this function is provided by the sourced file above
('$PSCredential = decrypt2PSCredential ' + pathToCredsFile + ' ' + pathToKeyFile),
// #4 connect to azure as well
'Connect-MsolService -Credential $PSCredential',
// #5 get session options
('$sessionOpt = New-PSSessionOption -OpenTimeout ' + openTimeout + ' -OperationTimeout ' + operationTimeout + ' -IdleTimeout ' + idleTimeout),
// #6 connect to exchange
'Connect-ExchangeOnline -CertificateThumbPrint ' + certificateThumbPrint + ' -AppID ' + applicationId + ' -Organization ' + organizationId + ' ' + psCommandsToImport,
// #7 cleanup
'Remove-Variable -Force -ErrorAction SilentlyContinue $PSCredential'
];
};
/**
* Destroy commands that correspond to the session
* established w/ the KeyinitCommands above
*/
module.exports.getO365PSThumbprintDestroyCommands = function () {
return [
'Get-PSSession | Remove-PSSession -ErrorAction SilentlyContinue',
'Remove-Module MsOnline',
'Remove-Module ExchangeOnlineManagement',
];
};
/**
* Some example blacklisted commands
*/
module.exports.getO365BlacklistedCommands = function () {
return [{
'regex': '.*Invoke-Expression.*',
'flags': 'i'
},
{
'regex': '.*ScriptBlock.*',
'flags': 'i'
},
{
'regex': '.*Get-Acl.*',
'flags': 'i'
},
{
'regex': '.*Set-Acl.*',
'flags': 'i'
},
{
'regex': '.*Get-Content.*',
'flags': 'i'
},
{
'regex': '.*-History.*',
'flags': 'i'
},
{
'regex': '.*Out-File.*',
'flags': 'i'
}
];
};
/**
* Configuration auto invalidation, checking PSSession availability
* @param checkIntervalMS
*/
module.exports.getO365AutoInvalidationConfig = function (checkIntervalMS) {
return {
'checkIntervalMS': checkIntervalMS,
'commands': [
// no remote pssession established? invalid!
{
'command': 'Get-PSSession',
'regexes': {
'stdout': [{
'regex': '.*Opened.*',
'flags': 'i',
'invalidOn': 'noMatch'
}]
}
}
]
};
};
/**
* Defines a registry of Powershell commands
* that can be injected into the PSCommandService
* instance.
*
* Note these are just some example configurations specifically for a few
* o365 functions and limited arguments for each, (they work) however you may want to
* replace this with your own set of init command tailored to your specific
* use-case
*/
var o365CommandRegistry = {
/*******************************
*
* o365 Powershell Command registry
*
* argument properties (optional):
* - quoted: true|false, default true
* - valued: true|false, default true
* - default: optional default value (only if valued..)
*
* return properties:
* type: none, text or json are valid values
*
********************************/
/*******************************
* MsolUser
********************************/
'getMsolUser': {
'command': 'Get-MsolUser {{{arguments}}} | ConvertTo-Json',
'arguments': {
'UserPrincipalName': {}
},
'return': {
type: 'json'
}
},
'newMsolUser': {
'command': 'New-MsolUser {{{arguments}}} | ConvertTo-Json',
'arguments': {
'DisplayName': {},
'UserPrincipalName': {}
},
'return': {
type: 'json'
}
},
'removeMsolUser': {
'command': 'Remove-MsolUser -Force {{{arguments}}} ',
'arguments': {
'UserPrincipalName': {}
},
'return': {
type: 'none'
}
},
/*******************************
* DistributionGroups
********************************/
'getDistributionGroup': {
'command': 'Get-DistributionGroup {{{arguments}}} | ConvertTo-Json',
'arguments': {
'Identity': {}
},
'return': {
type: 'json'
}
},
'newDistributionGroup': {
'command': 'New-DistributionGroup -Confirm:$False {{{arguments}}} | ConvertTo-Json',
'arguments': {
'Name': {},
'DisplayName': {},
'Alias': {},
'PrimarySmtpAddress': {},
'Type': {
'quoted': false,
'default': 'Security'
},
'ManagedBy': {},
'Members': {}, // specifying members on create does not seem to work
'ModerationEnabled': {
'default': '0',
'quoted': false
},
'MemberDepartRestriction': {
'default': 'Closed'
},
'MemberJoinRestriction': {
'default': 'Closed'
},
'SendModerationNotifications': {
'default': 'Never',
'quoted': false
},
},
'return': {
type: 'json'
}
},
'setDistributionGroup': {
'command': 'Set-DistributionGroup -Confirm:$False {{{arguments}}}',
'arguments': {
'Identity': {},
'Name': {},
'DisplayName': {},
'Alias': {},
'PrimarySmtpAddress': {},
'ManagedBy': {},
'Members': {},
'MailTip': {},
'ModerationEnabled': {
'default': '0',
'quoted': false
},
'MemberDepartRestriction': {
'default': 'Closed'
},
'MemberJoinRestriction': {
'default': 'Closed'
},
'SendModerationNotifications': {
'default': 'Never',
'quoted': false
},
'BypassSecurityGroupManagerCheck': {
'valued': false
}
},
'return': {
type: 'none'
}
},
'removeDistributionGroup': {
'command': 'Remove-DistributionGroup {{{arguments}}} -Confirm:$false',
'arguments': {
'Identity': {},
// needed if invoking as global admin who is not explicitly a group admin.. stupid... yes.
'BypassSecurityGroupManagerCheck': {
'valued': false
}
},
'return': {
type: 'none'
}
},
'getDistributionGroupMember': {
'command': 'Get-DistributionGroupMember {{{arguments}}} -ResultSize Unlimited | ConvertTo-Json',
'arguments': {
'Identity': {}
},
'return': {
type: 'json'
}
},
'addDistributionGroupMember': {
'command': 'Add-DistributionGroupMember {{{arguments}}}',
'arguments': {
'Identity': {},
'Member': {},
// needed if invoking as global admin who is not explicitly a group admin.. stupid... yes.
'BypassSecurityGroupManagerCheck': {
'valued': false
}
},
'return': {
type: 'none'
}
},
// members specified w/ this are a full overwrite..
'updateDistributionGroupMembers': {
'command': 'Update-DistributionGroupMember -Confirm:$false {{{arguments}}}',
'arguments': {
'Identity': {},
'Members': {},
// needed if invoking as global admin who is not explicitly a group admin.. stupid... yes.
'BypassSecurityGroupManagerCheck': {
'valued': false
}
},
'return': {
type: 'none'
}
},
'removeDistributionGroupMember': {
'command': 'Remove-DistributionGroupMember {{{arguments}}} -Confirm:$false',
'arguments': {
'Identity': {},
'Member': {},
// needed if invoking as global admin who is not explicitly a group admin.. stupid... yes.
'BypassSecurityGroupManagerCheck': {
'valued': false
}
},
'return': {
type: 'none'
}
},
/*******************************
* MailContacts
********************************/
'getMailContact': {
'command': 'Get-MailContact {{{arguments}}} | ConvertTo-Json',
'arguments': {
'Identity': {}
},
'return': {
type: 'json'
}
},
'newMailContact': {
'command': 'New-MailContact -Confirm:$False {{{arguments}}} | ConvertTo-Json',
'arguments': {
'Name': {},
'ExternalEmailAddress': {}
},
'return': {
type: 'json'
}
},
'setMailContact': {
'command': 'Set-MailContact -Confirm:$False {{{arguments}}}',
'arguments': {
'Identity': {},
'Name': {},
'DisplayName': {},
'ExternalEmailAddress': {}
},
'return': {
type: 'none'
}
},
'removeMailContact': {
'command': 'Remove-MailContact {{{arguments}}} -Confirm:$false',
'arguments': {
'Identity': {}
},
'return': {
type: 'none'
}
},
getStatus: {
command: 'Get-ConnectionInformation | ConvertTo-Json',
return: {
type: 'json'
}
},
};
module.exports.o365CommandRegistry = o365CommandRegistry;
/**
* Some example whitelisted commands
* (only permit) what is in the registry
*/
module.exports.getO365WhitelistedCommands = function () {
var whitelist = [];
for (var cmdName in o365CommandRegistry) {
var config = o365CommandRegistry[cmdName];
var commandStart = config.command.substring(0, config.command.indexOf(' ')).trim();
whitelist.push({
'regex': '^' + commandStart + '\\s+.*',
'flags': 'i'
});
}
return whitelist;
};