Res.M365SONE.M365Library.ps1.Resource (DeployableResource)

Element properties:

TypeDeployableResource
File NameM365Library.ps1
AccessibilityPublic
CommentPowerShell library with some common functions

Source Code:

<DeployableResource ID="Res.M365SONE.M365Library.ps1.Resource" Accessibility="Public" FileName="M365Library.ps1" HasNullStream="false" Comment="PowerShell library with some common functions"/>

File Content: M365Library.ps1

<#

M365Library.ps1
Description: Some common functions are defined here.
Author: Tyson Paul
Blog: MonitoringGuys.com

Version History:
2021.02.23.1536 - $NameSpace default is now $ScriptName
2020.12.08.2141 - Updated LogIt to support multiple data fields
2020.11.20.1153 - Modified LogIt, added mgmt API info
2002.11.17.2130 - Updated Get-AccessToken, Get-MgmtAccessToken, Add-RegData
2020.11.06.1660 - Added Verify-ShouldDeleteConfiguration
2020.11.05.1909 - Added Remove-RegKey
2020.10.27.1122 - Updated $ThisScriptInstanceGUID.
2020.10.20.1854 - Added function Get-MgmtAccessToken
2020.10.02.1143 - Modified LogIt
2020.09.28.1901 - Added Format-AccountPassword function
2020.09.25.0939 - Added account context to prefix of encrypted strings.
Added context removal on Decode function.
Improved Create-RegistryEntries ignore matching
2020.09.25.0810 - Changed alias '__LINE__' to function name.
2020.09.24.1216 - Improved encryption functions.
Changed alias '_LINE_' to function name.
2020.09.18.1555 - Added Create-RegistryEntries, Encrypt-PlainAccountPassword, Format-ClientSecret, Verify-TenantName
2020.09.17.1721 - Added TlS
2020.09.16.1552 - Updated Logging functions (EventIDFilter)
2020.08.20.1605 - Updated Alias creation; enclosed in Try/Catch.
2020.08.12.1319 - Updated LogIt function
2020.05.18 - v1.0

#>
########################################################################################################

<#
Notes: Define simple class with one method: "AddValue()"
#>
#-----------------------------------------------------------------------------
Class BasicBag {

# Add name/value pairs to bag
AddValue($varName,$varValue) {
Try{
$this | Add-Member -MemberType NoteProperty -Name $varName -Value $varValue
}Catch{
Throw "Failure adding to bag in AddValue method. Data: $varName, $varValue . $error[0] "
}
}
}
#-----------------------------------------------------------------------------

###################################################################

<#
Will return True if value can be converted to an integer.
This is useful to identify if an integer might be cast as a string.
#>

Function CanBeNumber($Value) {
$rtn = ""

# If an integer inside a string...
If ([Double]::TryParse($Value,[ref]$rtn)) {
Return $true
}
# Otherwise return false
Return $false
}

###################################################################
<#
Will return True if the value is any type of number
#>
Function isNumeric($Value) {
return $Value -is [byte] -or $Value -is [int16] -or $Value -is [int32] -or $Value -is [int64] `
-or $Value -is [sbyte] -or $Value -is [uint16] -or $Value -is [uint32] -or $Value -is [uint64] `
-or $Value -is [float] -or $Value -is [double] -or $Value -is [decimal]
}

###################################################################
Function Remove-RegKey {
Param (
[string]$RegKeyPath
)
Try {
REG DELETE $RegKey /f
Return $true
} Catch {
Return $false
}
}
###################################################################
Function Encode-UserData {
Param (
[string]$Data
)
$enc = $NULL
Try {
$enc = (ConvertTo-SecureString -AsPlainText $Data -Force -ErrorAction Stop | ConvertFROM-SecureString -ErrorAction Stop)
} Catch {
$ErrorMsg = @"
Error encountered during encoding of string. One reason this can happen during component configuration is if no local user profile exists for the account used to perform the configuration task.
This is somewhat common on SCOM management servers when the default action account is configured to use a domain service account
(also know as "Management Server Action Account" and often times named something like "domain\svcSCOMAction")
and this account has never been used to log on locally to the server. Therefore no user profile context exists in which to encrypt the string.
Strings encrypted with PowerShell in this management pack may only be derypted by the same account that performed the original encryption and only on the same server.
If this is the case you will see an error similar to this:
"ConvertFROM-SecureString : The data protection operation was unsuccessful.
This may have been caused by not having the user profile loaded for the
current thread's user context, which may be the case when the thread is
impersonating."

Solution:
One solution is to simply log into the target server with whichever account is running the workflow by default. Another option is to use PowerShell remotely.
PowerShell Example:
$Cred = Get-Credential -UserName 'Domain\YourRunAsAccount' -Message "." ;
Enter-PSSession -ComputerName 'YourWatcherNodeFQDN' -Credential $Cred ;

"@

}

Return $enc
}
###################################################################
Function Decode-UserData {
Param (
[string]$Data
)
# Will remove 'domain/account:' if present.
$tmpCleanData = $Data -replace '^.*:', ''

# Submit only a pure encrypted string for decode.
Return ([Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR(($tmpCleanData | ConvertTo-SecureString))) )
}

###################################################################
Function Format-Number {
Param (
[Double]$Num,
[int]$DecimalPlaces = 2
)
Return ([Double]("{0:N$($DecimalPlaces)}" -F $Num))
}
###################################################################

# This allows the script to be used for both agent tasks and propertybags
Function Output {
Param (
$bag,
[string]$Tag='<no tag provided>'
)

Switch ($ScriptOutputType){
'Serialized' {
LogIt -EventID 9992 -Type $info -Msg "Serialized output..." -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
Write-Output $bag
Break
}

Default {
LogIt -EventID 9992 -Type $info -Msg "PropertyBag output..." -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
$bag | Build-PropertyBag -FunctionTag $Tag
}
}#end Switch
}

#region Functions
###################### FUNCTIONS ##############################

<#

Function: Build-PropertyBag
Author: Tyson Paul (https://monitoringguys.com)
Version History:
2020.06.12 - Improved variable sanitizing.
2019.late - v1

NOTE: These variables are typically defined in the scope above this function:
$Testing
$ScriptName
$WriteToEventLog

Below is a snippet that can be used for dev/testing of this function:
###################### TESTING ##############################
$Testing = $true
$ScriptName = "TEST_Build-PropertyBag.ps1"
$WriteToEventLog = $true
[System.Collections.ArrayList]$tstArray = @()
1..3 | % {
$randGuid = New-Guid
$MyBag = [PsCustomObject]@{
Tag = "This is my Test Tag: $($randGuid)"
MyRouteNumber = (Get-Random -Minimum 10000 -Maximum 100000)
MyString = [string]"This is my string: $($randGuid)"
}
$null = $tstArray.Add($MyBag)
}

$randGuid = New-Guid
$MyBag = [PsCustomObject]@{
Tag = ""
MyRouteNumber = (Get-Random -Minimum 10000 -Maximum 100000)
MyString = [string]"This is my string: $($randGuid)"
}
$null = $tstArray.Add($MyBag)

$tstArray | Build-PropertyBag -FunctionTag PUMPKINGFISHPIE
###################### TESTING ##############################


Also, this can be used to easily convert a traditional property bag script to use a generic object for storing name/value pairs, then utilize this PB function.
#--------------------------------------------------------
Class BasicBag {
# Add name/value pairs to bag
AddValue($varName,$varValue) {
$this | Add-Member -MemberType NoteProperty -Name $varName -Value $varValue
}
}
#--------------------------------------------------------
$bag = New-Object -TypeName BasicBag
$bag.AddValue('Metric',999)
$bag.AddValue('DatabaseName',$MyDBNameVar)
$bag | Build-PropertyBag -FunctionTag BagHasValues


#>
Function Build-PropertyBag
{
[CmdletBinding(DefaultParameterSetName = 'Parameter Set 1',
SupportsShouldProcess = $True,
PositionalBinding = $false,
HelpUri = 'http://www.microsoft.com/')]

Param(
# a tag useful for logging this function activity
[Parameter(Mandatory = $False,
ValueFromPipeline = $False,
Position = 1,
ParameterSetName = 'Parameter Set 1')]
$FunctionTag = '<Build-PropertyBag: no tag provided>',

[Parameter(Mandatory = $True,
ValueFromPipeline = $True,
Position = 0,
ParameterSetName = 'Parameter Set 1')]
$Var
)

Begin{
[int]$info = 0
[int]$Critical = 1
[int]$warning = 2
$api = New-Object -ComObject MOM.ScriptAPI
#######################################################################################################
Function __LINE__
{
$MyInvocation.ScriptLineNumber
}
#######################################################################################################
<#
Try{
New-Alias -Name __LINE__ -Value Get-CurrentLineNumber -Description 'Returns the current line number in a PowerShell script file.' -ErrorAction SilentlyContinue
} Catch {
#Alias might already exist which is not a big deal
}
#>
#######################################################################################################
Function Clean-BagData {
Param (
$var
)

$type = $var.Gettype().Name
switch ($type)
{

#common number types are fine
{$_ -match 'decimal|double|int|float|byte|long'} {
Return ($var)
}

#anything other than a common number should be converted to String
Default {
Try{
$tmpVar = $var.ToString()
Return $tmpVar
}Catch {
$msg = "Unable to convert $($var) of type:[$($type)] to type:[String] in function: [Clean-BagData]"
Write-Verbose $msg
LogIt -EventID 9997 -Type $critical -Message $msg -Proceed $true
Return "ERROR_CONVERTING_VAR_TOSTRING"
}
}
}#end Switch
}

########################################################################################################
Function LogIt ([int]$EventID, [int]$Type = 2, [string]$Message = 'No message specified.', [bool]$Proceed = $false) {
If ($Proceed -AND (($EventIDFilter -match $EventID) -OR ($EventIDFilter.Length -eq 0) ))
{
$TimeStamp = (Get-Date -Format 'yyyy-MM-dd-HHmmss')
$output = @"

WorkflowName: $WorkflowName
Message: $Message

Tag: $Tag
EventIDFilter: $EventIDFilter
ThisScriptInstanceGUID: $ThisScriptInstanceGUID
Whoami: $(whoami.exe)
TimeStamp: $TimeStamp
WriteToEventLog:$WriteToEventLog

Any errors will appear below:

$($Error | Select-Object -Property *)

"@

$oEvent = New-Object -ComObject 'MOM.ScriptAPI'
$oEvent.LogScriptEvent("$ScriptName",$EventID,$Type,$output)
}
}
########################################################################################################

LogIt -EventID 9993 -Type $info -Message "Line#: $(__LINE__):Processing bag for $FunctionTag now..." -Proceed $WriteToEventLog
} #end Begin

Process {
$Error.Clear()
# a Tag is not required but makes it easier to identify individual bag items during processing and troubleshooting.
If ($Var.Tag.Length -gt 0) {
$Tag = $Var.Tag
LogIt -EventID 9993 -Type $info -Message "Line#: $(__LINE__):Processing bag for:`n$Tag" -Proceed $WriteToEventLog
}
Else {
$Tag = "<no individual obj tag assigned>"
}

$Properties = (($Var | Get-Member) | Where-Object -FilterScript {
$_.MemberType -like '*property*'
} ).Name
$bag = $api.CreatePropertyBag()
ForEach ($Prop in $Properties)
{
#Build Property Bag
If ($Var.$Prop.Length -eq 0)
{
$msg = "Line#: $(__LINE__):$($Tag):`nNULL:$($Prop)"
If ($Testing){
Write-Verbose $msg
}
LogIt -EventID 9993 -Type $info -Message $msg -Proceed $WriteToEventLog
$bag.AddValue($Prop, $Null)
Continue
}
Else {
If ($Testing){
Write-Verbose "$($Prop):$($Var.$Prop)"
}
# If Var contains an array, add to single comma-separated list; a single string.
If ($Var.$Prop.GetType().Name -match 'Object\[\]') {
[string]$str=''
($Var.$Prop | Out-String | ForEach-Object {$str += "[$($_)],"} )
LogIt -EventID 9993 -Type $info -Message "Line#: $(__LINE__):$($Tag):`n$($Prop):$($str)" -Proceed $WriteToEventLog

$tmpVal = Clean-BagData -var $str.Trim(',')
$bag.AddValue($Prop, $tmpVal)
}
Else {
$tmpVal = Clean-BagData -var ($Var.$Prop)
LogIt -EventID 9993 -Type $info -Message "Line#: $(__LINE__):$($Tag):`n$($Prop):$($tmpVal), Type:$($tmpVal.GetType())" -Proceed $WriteToEventLog
$bag.AddValue($Prop, $tmpVal)
}
Remove-Variable -Name tmpval
}
} #end ForEach

$bag

If ($Error)
{
LogIt -EventID 9997 -Type $warning -Message "Line#: $(__LINE__):Processing bag for $Tag. $($Error.Count) Errors detected." -Proceed $WriteToEventLog
}
If ($Testing)
{
$api.Return($bag)
}
} #end Process

End {
# Supports UNdiscovery of objects.
If (-Not $bag) {
$api = New-Object -ComObject MOM.ScriptAPI
$bag = $api.CreatePropertyBag()
$bag.AddValue('Tag', "EMPTYBAG_$($Tag)_EMPTYBAG")
If ($Testing) {
Write-Verbose "Line#: $(__LINE__):Return empty bag..." -ForegroundColor Red -BackgroundColor Yellow
}
$bag
LogIt -EventID 9993 -Type $info -Message "Line#: $(__LINE__):Processing EMPTY bag for $Tag." -Proceed $WriteToEventLog
}
}
} #end Build-PropertyBag
########################################################################################################

Function LogIt {
[CmdletBinding(DefaultParameterSetName='Parameter Set 1',
SupportsShouldProcess=$true,
PositionalBinding=$false,
HelpUri = 'http://MonitoringGuys.com/',
ConfirmImpact='Medium')]
Param
(
[int]$EventID,

[Parameter(Mandatory=$true,
ParameterSetName='Parameter Set 1')]
[ValidateRange(0,4)]
[int]$Type,

[Alias("Message")]
[string]$msg = 'No message specified.',

[bool]$Proceed,

$Line
)


If ($Proceed -AND (($EventIDFilter -match $EventID) -OR ($EventIDFilter.Length -eq 0) )) {
$authInfo = ''

If ($SenderEmailAddress) {
$authInfo += @"
ReceiverEmailAddress: $ReceiverEmailAddress
ReceiverPassword: $ReceiverPassword
SenderEmailAddress: $SenderEmailAddress
SenderPassword: $SenderPassword
MailflowDirection: $MailflowDirection
"@
}
If ($M365_AccountName) {
$authInfo += @"
M365_AccountName: $M365_AccountName
"@
}

If ($MgmtApiURL) {
$MgmtApiURLInfo += @"
MgmtApiURL: $MgmtApiURL
MgmtApiTokenURL = $MgmtApiTokenURL
MgmtApiTokenScopeURL: $MgmtApiTokenScopeURL
"@
}

$output = @"
WorkflowName: $WorkflowName
Message: $msg

Invocation/Function: $($MyInvocation.InvocationName)
ThisScriptInstanceGUID: $ThisScriptInstanceGUID
ScriptLine: $Line
Running As: $whoami
$authInfo
ClientID: $M365_ClientID
MgmtGroupRegKey: $MgmtGroupRegKey
TenantName: $TenantName
ApiTokenScopeURL: $ApiTokenScopeURL
ApiTokenURL: $ApiTokenURL
ApiURL: $ApiURL
$MgmtApiURLInfo
TLSVersion: $TLSVersion
PoshLibraryPath: $($PoshLibraryPath.Trim(','))
ScriptOutputType: $ScriptOutputType
EventIDFilter: $EventIDFilter
WriteToEventLog: $WriteToEventLog
Any Errors: $($Error[0])

"@

#$oEvent = New-Object -ComObject 'MOM.ScriptAPI'
If ($output.Length -gt $maxLogLength){
$output = ($output.Substring(0,([math]::Min($output.Length,$maxLogLength) )) + '...TRUNCATED...')
}
# $oEvent.LogScriptEvent("$ScriptName",$EventID,$Type,$output )
$Error.Clear()
$objEvent = New-Object System.Diagnostics.EventLog
$objEvent.Source = $EventSource
$objEvent.Log = $EventLogName
$objEventID = New-Object System.Diagnostics.EventInstance($EventID,1,$Type)

#
$info = "$($output)^$($Msg)^$($ScriptName)^$($TenantName)^$($NameSpace)"
[array]$arrMessage = @($info.Split('^'))
$objEvent.WriteEvent($objEventID, @($arrMessage))
}
}
################################################################
Function _LINE_ {
$MyInvocation.ScriptLineNumber
}

################################################################
Function Add-RegData {
Param(
[string]$RegKey,
[string]$RegValueName,
[string]$RegValueType,
[string]$RegValueData
)

$eventid = 9991
$eventtype = $info
$RegKey = ($RegKey -replace '\\$','')

#empty or match any non-number
If (-NOT $RegValueData -OR ($RegValueData -match 'N/A') ){
$msg = @"
No data provided for RegValueName: [$($RegValueName)]. No changes made.
"@
Write-Output $msg
Return
}
Else {
reg add "$RegKey" /v "$RegValueName" /t "$RegValueType" /d "$RegValueData" /f
If ($Error) {
$eventid = 9997
$eventtype = $critical
$msg = @"
Error attempting to modify registry.
Error: $_
"@
}
Else {
$msg = @"
Successful registry modification with below data!
"@
}

$msg2 = $msg + @"

RegKey: $RegKey
RegValueType: $RegValueType
RegValueName: $RegValueName
NewData (attempted):`t$RegValueData
Actual Registry Value:`t$(Get-ItemProperty -Path $RegKey.Replace("HKLM\",'HKLM:\') -Name $RegValueName | Select-Object $RegValueName -ExpandProperty $RegValueName)

"@
Write-Output $msg2
}
LogIt -EventID $eventid -Type $eventtype -Message $msg2 -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear();
}
########################################################################################################


<#
Function:
Author: Tyson Paul (https://monitoringguys.com/)
Version History:
2020.09.28.1446 - removed param validation

Notes: This function should be dot-sourced
#>
Function Set-TLS {
Param (
[Parameter(Mandatory=$false,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false)]
#[ValidateSet('1.0','1.1','1.2','1.3','-1')] #'-1' indicates skip/ignore
[string]$TLSVersion = '1.2'
)

Switch ($TLSVersion)
{
'1.3' { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls13 }
'1.2' { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 }
'1.1' { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls11 }
'1.0' { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls }
Default { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12}
}
}
#endregion Functions


########################################################################################################
Function Create-RegistryEntries {
Param (
$NewSettings
)
# Add Settings
ForEach ($RegEntry in @($NewSettings.Keys) ) {
# This allows the user to enter ' ' (white space) to effectively skip the setting. This provides the user with a way to not overwrite existing values.
# This could prove useful if the user simply wants to update a single setting but not overwrite any existing settings. Also, none of these values should contain a single whitespace anyway.
# If not a single whitespace char AND not a negative number...
If (($NewSettings.$RegEntry.ValueData -notmatch '^ $|^-1$') -and (-NOT ( (CanBeNumber $NewSettings.$RegEntry.ValueData) -and ($NewSettings.$RegEntry.ValueData -le 0))) ) {
Add-RegData -RegKey $NewSettings.$RegEntry.Key -RegValueName $RegEntry -RegValueData $NewSettings.$RegEntry.ValueData -RegValueType $NewSettings.$RegEntry.ValueType
LogIt -EventID 9992 -Type $info -Proceed $WriteToEventLog -msg "Adding RegValueName: [$($RegEntry)], RegValueData: [$($NewSettings.$RegEntry.ValueData)], RegValueType: [$($NewSettings.$RegEntry.ValueType)]" -LINE $(_LINE_); $Error.Clear()
}
Else {
LogIt -EventID 9992 -Type $info -Proceed $WriteToEventLog -msg "Skipping RegValueName: [$($RegEntry)]." -LINE $(_LINE_); $Error.Clear()
}
}
}


########################################################################################################
Function Format-AccountPassword {
Param (
$M365_AccountPassword,

# It is assumed that the password will not be larger than 128. (closer to 34 chars, actually)
# Smallest encrypted string is found to be 292 on my test server, however this might differ depending on your environment.
# Therefore 128 seems like a fair compromise between largest password and smallest encyrpted string.
$MaxPwdSize = 128
)

<#
The Secret should normally be inherited from WatcherNode unless overridden and made unique for this entity.
If inherited, it should be already encrypted and 420 chars long, and this encrypted string will become stored in the registry.
If the variable contains a whitespace char, it will be skipped/ignored.
The standard unencrypted secret/string is usually 32 chars. If the variable is the standard secret length (or less) it will
be assumed to be a valid secret/string and therefore will get encrypted and stored in the registry.
#>
If ( ($M365_AccountPassword -match '^-1$') -OR ($M365_AccountPassword.Length -gt $MaxPwdSize) ){
$Msg = "Either M365_AccountPassword matches '-1' or length -gt [$($MaxPwdSize)]. M365_AccountPassword will be stored as is; no changes/encryption needed."
LogIt -EventID 9992 -Type $info -Msg $Msg -Proceed $true -LINE $(_LINE_); $Error.Clear()
$M365_AccountPassword_ENCRYPTED = $M365_AccountPassword
}
ElseIf (($M365_AccountPassword.Length -gt 0) -and ($M365_AccountPassword.Length -le $MaxPwdSize) ) {
If ($M365_AccountPassword -notmatch '^-1$') {
$Msg = "M365_AccountPassword length: [$($M365_AccountPassword.Length)] indicates that it is plain text and should be stored as encrypted string. Proceed to encrypt..."
LogIt -EventID 9992 -Type $info -Msg $Msg -Proceed $true -LINE $(_LINE_); $Error.Clear()
# Will add user context to front of encrypted string value. This will help to reveal who encrypted the string.
$M365_AccountPassword_ENCRYPTED = "$($whoami):$(Encode-UserData -Data $M365_AccountPassword)"
}
ElseIf ($M365_AccountPassword -match '\s') {
LogIt -EventID 9995 -Type $warn -Proceed $true -msg "Whitespace detected in 'M365_AccountPassword' : [$($M365_AccountPassword)]. This is not a valid value." -LINE $(_LINE_); $Error.Clear()
Return 'WHITESPACE_ERROR'
}
}
Return $M365_AccountPassword_ENCRYPTED
}
########################################################################################################

Function Format-ClientSecret {
Param (
$M365_ClientSecret,

# It is assumed that the secret will not be larger than 128. (closer to 34 chars, actually)
# Smallest encrypted string is found to be 292 on my test server, however this might differ depending on your environment.
# Therefore 128 seems like a fair compromise between largest secret and smallest encyrpted string.
$ClientSecretLength = 128
)
<#
The Secret should normally be inherited from WatcherNode unless overridden and made unique for this entity.
If inherited, it should be already encrypted and 420 chars long, and this encrypted string will become stored in the registry.
If the variable contains a whitespace char, it will be skipped/ignored.
The standard unencrypted secret/string is usually 32 chars. If the variable is the standard secret length (or less) it will
be assumed to be a valid secret/string and therefore will get encrypted and stored in the registry.
#>
If ( ($M365_ClientSecret -match '^-1$') -OR ($M365_ClientSecret.Length -gt $ClientSecretLength) ){
$Msg = "Either M365_ClientSecret matches '-1' or length -gt [$($ClientSecretLength)]. M365_ClientSecret will be stored as is; no changes/encryption needed."
LogIt -EventID 9992 -Type $info -Msg $Msg -Proceed $true -LINE $(_LINE_); $Error.Clear()
$M365_ClientSecret_ENCRYPTED = $M365_ClientSecret
}
ElseIf (($M365_ClientSecret.Length -gt 0) -and ($M365_ClientSecret.Length -le $ClientSecretLength) ) {
If ($M365_ClientSecret -notmatch '^-1$') {
$Msg = "M365_ClientSecret length: [$($M365_ClientSecret.Length)] indicates that it is plain text and should be stored as encrypted string. Proceed to encrypt..."
LogIt -EventID 9992 -Type $info -Msg $Msg -Proceed $true -LINE $(_LINE_); $Error.Clear()
# Will add user context to front of encrypted string value. This will help to reveal who encrypted the string.
$M365_ClientSecret_ENCRYPTED = "$($whoami):$(Encode-UserData -Data $M365_ClientSecret)"
}
ElseIf ($M365_ClientSecret -match '\s') {
LogIt -EventID 9995 -Type $warn -Proceed $true -msg "Whitespace detected in 'M365_ClientSecret' : [$($M365_ClientSecret)]. This is not a valid value." -LINE $(_LINE_); $Error.Clear()
Return 'WHITESPACE_ERROR'
}
}
# Technically this outcome should only be possible when configuring WatcherNode.
ElseIf (-NOT $M365_ClientSecret.Length){
$Msg = "M365_ClientSecret length: [$($M365_ClientSecret.Length)] does not exist therefore this reg value data will get cleared (become empty)."
LogIt -EventID 9992 -Type $info -Msg $Msg -Proceed $true -LINE $(_LINE_); $Error.Clear()
$M365_ClientSecret_ENCRYPTED = ''
}

Return $M365_ClientSecret_ENCRYPTED
}
########################################################################################################

Function Encrypt-PlainAccountPassword {
Param (
$passwdString
)
# We need to determine if a legitimate password value is provided.
# - Whitespace or '-1' indicates intentional skip. Legit value will get encrypted.
If ($passwdString -match '\s|^-1$') {
$Msg = "Password of length: [$($passwdString.Length)] matches '-1' or ' ' (whitespace) and therefore will be ignored/skipped."
LogIt -EventID 9992 -Type $info -Msg $Msg -Proceed $true -LINE $(_LINE_); $Error.Clear()
#if set to whitespace, it will be skipped/ignored
$passwdString_ENCRYPTED = $passwdString
}
ElseIf ($passwdString.Length -gt 0) {
$Msg = "Password of length: [$($passwdString.Length)] exists and therefore will be encrypted."
LogIt -EventID 9992 -Type $info -Msg $Msg -Proceed $true -LINE $(_LINE_); $Error.Clear()

#$passwdString_ENCRYPTED = Encode-UserData -Data $passwdString
# Will add user context to front of encrypted string value. This will help to reveal who encrypted the string
$passwdString_ENCRYPTED = "$($whoami):$(Encode-UserData -Data $passwdString)"
}
Else {
$Msg = "Password of length: [$($passwdString.Length)] does not exist therefore this reg value data will get cleared (become empty)."
LogIt -EventID 9992 -Type $info -Msg $Msg -Proceed $true -LINE $(_LINE_); $Error.Clear()
$passwdString_ENCRYPTED = ''
}
Return $passwdString_ENCRYPTED
}
########################################################################################################

Function Get-AccessToken {
Param (
# Indicates Delegated permissions
[Parameter(Mandatory=$true,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false,
ParameterSetName='Delegated')]
[switch]$Delegated,

[Parameter(Mandatory=$true,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false,
ParameterSetName='Delegated')]
[string]$User,

[Parameter(Mandatory=$true,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false,
ParameterSetName='Delegated')]
[string]$Pass,

# Indicates Application permissions
[Parameter(Mandatory=$true,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false,
ParameterSetName='Application')]
[switch]$Application,

[Parameter(Mandatory=$false,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false)]
[string]$ClientID,

[Parameter(Mandatory=$false,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false)]
[string]$ClientSecret,

[Parameter(Mandatory=$false,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false)]
[string]$ApiTokenScopeURL = 'https://graph.microsoft.com/.default',

[Parameter(Mandatory=$false,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false)]
[string]$ApiTokenUrl = 'https://login.microsoftonline.com',

[Parameter(Mandatory=$false,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false)]
[string]$TenantName
)

If ($Application) {
$ReqTokenBody = @{
grant_type = "client_credentials"
Scope = $ApiTokenScopeURL
client_Id = $ClientID
Client_Secret = $ClientSecret
}
}

If ($Delegated) {
$ReqTokenBody = @{
grant_type = "password"
Scope = $ApiTokenScopeURL
client_Id = $ClientID
Client_Secret = $ClientSecret
username = $User
password = $Pass
}
}

$TokenResponse = Invoke-RestMethod -Uri "$($ApiTokenUrl)/$($TenantName)/oauth2/v2.0/token" -Method POST -Body $ReqTokenBody
Return $TokenResponse
}
########################################################################################################

Function Get-MgmtAccessToken {
Param (
[Parameter(Mandatory=$false,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false)]
[string]$ClientID,

[Parameter(Mandatory=$false,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false)]
[string]$ClientSecret,

[Parameter(Mandatory=$false,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false)]
[string]$ApiTokenUrl = 'https://login.windows.net',

[Parameter(Mandatory=$false,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false)]
[string]$ApiTokenScopeURL = 'https://manage.office.com/',

[Parameter(Mandatory=$false,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false)]
[string]$TenantName
)

$ReqTokenBody = @{
grant_type = "client_credentials"
resource = $ApiTokenScopeURL
client_Id = $ClientID
Client_Secret = $ClientSecret
}

$Response = Invoke-RestMethod -Uri "$($ApiTokenUrl)/$($TenantName)/oauth2/token" -Body $ReqTokenBody -Method Post
Return $Response
}

########################################################################################################
# Generic property bag for service connectivity failure
Function Service-StatusBag {
Param (
[string]$Message='<no status data provided>',

[Parameter(Mandatory=$true,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false)]
[ValidateSet('FAILURE','SUCCESS')]
[string]$Status
)
# Set up BasicBag with defaults
$bag = New-Object -TypeName BasicBag
$bag.AddValue('Category',"ConnectionStatus")
$bag.AddValue('Connection',"$($Status.ToUpper())")
$bag.AddValue('Message',"$Message")
OutPut $bag -Tag "Service$($Status)"
}
########################################################################################################
Function Get-TempFolderPath {
# Return first valid path
$TempFolders = @('C:\Windows\Temp',"$($Env:Tmp)","$($Env:TEMP)")
ForEach ($Path in $TempFolders) {
$NewPath = (Join-Path $Path "M365SSM\$($NameSpace)")
$NULL = New-Item -Path $NewPath -ItemType Directory -Force -ErrorAction Ignore
If (Test-Path -Path $NewPath ) {
Return $NewPath
}
}
}
########################################################################################################
<#
This will verify that a TenantName has been provided by the user.
This is used for Configure/Delete watcher node activities.
#>
Function Verify-TenantName {
Param (
$TenantName,
$MgmtGroupRegKey
)
# If the user has not provided a valid Tenant Name
If (($TenantName -match '^-1$') -OR (-NOT ($TenantName.Length)) ) {
If ($MgmtGroupRegKey.Length ) {
$FoundTenantName = @((Get-ChildItem $MgmtGroupRegKey.Replace('HKLM','HKLM:') ) | Select-Object PSChildName -ExpandProperty PSChildName)
$message = "TenantName: [$($TenantName)] does not exist at the mgmt group regkey. This param cannot be skipped/ignored. Please specify valid TenantName. How am I supposed to know which Tenant to modify if you don't provide the name? `nAny existing values will appear below:`n$($FoundTenantName | Out-String)`nExiting."
}
Else {
#This should not be possible
$message = "MgmtGroupRegKey: [$($MgmtGroupRegKey)] does not exist. Invalid value. Exiting."
}
LogIt -EventID 9995 -Type $warn -msg $message -Proceed $WriteToEventLog -Line $(_LINE_); $Error.Clear();
$message
Exit
}
}
########################################################################################################

# Will proceed to delete config if appropriate. This function is designed to be run dot-sourced
Function Verify-ShouldDeleteConfiguration {

#region DeleteConfiguration
If ($DeleteConfiguration) {
$Message = "$NameSpace configuration removal initiated. Will attempt to remove registry key: [$($RegKey)]."
LogIt -EventID 9992 -Type $info -Msg $Message -Proceed $true -LINE $(_LINE_); $Error.Clear()
$Message
$result = Remove-RegKey -RegKeyPath $RegKey
If ($result) {
$Message = "$NameSpace configuration removal success. Exiting."
LogIt -EventID 9992 -Type $info -Msg $Message -Proceed $true -LINE $(_LINE_); $Error.Clear()
$Message
}
Else {
$Message = "$NameSpace configuration removal failed, key: [$($RegKey)]. $($Error[0].Exception | Out-String)"
LogIt -EventID 9992 -Type $info -Msg $Message -Proceed $true -LINE $(_LINE_);
$Message
}
LogIt -EventID 9991 -Type $info -Proceed $WriteToEventLog -msg "Script End. Finished in [$($ScriptTimer.Elapsed.TotalSeconds)] seconds. `n" -LINE $(_LINE_)
Exit
}
#endregion DeleteConfiguration
}

########################################################################################################

Function Verify-URLFormat {
Param (
[string]$URL
)

If ($URL -match '^https:\/\/') {
#URL is probably fine
Return $URL
}
Else {
Return ("https://$($URL)")
}

}
########################################################################################################
########################################################################################################
########################################################################################################
########################################################################################################

#[Int]$info=0 #MOM.ScriptAPI
[Int]$info=4 #System.Diagnostics.EventInstance
[Int]$critical=1
[Int]$warn=2

[string]$whoami = whoami.exe
[int]$maxLogLength = 31000 #max chars to allow for event log messages
[int]$MaxPwdSize = 128
$ThisScriptInstanceGUID = [System.GUID]::NewGuid().ToString().Substring((35 ) -5).ToUpper()
$ScriptTimer = [System.Diagnostics.Stopwatch]::StartNew()
If (-NOT $NameSpace.Length) { $NameSpace = $ScriptName}
$EventSource = "M365 Supplemental"
$EventLogName = "Application"
# Create event Source if it does not already exist.
If ($False -eq [System.Diagnostics.EventLog]::SourceExists($EventSource))
{
[System.Diagnostics.EventLog]::CreateEventSource($EventSource, $EventLogName)
}