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

Element properties:

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

Source Code:

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

File Content: M365Library.ps1

#Requires -Version 5

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

Version History:
2021.12.02.1735 - Improved Output function to accept pipeline input
2021.10.20.1641 - Added text to $Ignore_RegEx.
2021.09.15.1700 - Decode-UserData: added handling for Test mode.
2021.09.08.1404 - Improved Decode function error handling. Improved LogIt.
2021.08.31.1441 - Improved whitespace handling in Create-RegistryEntries.
2021.07.30.1438 - Added Write-OnDemandDiscoveryEvent, Import-SCOMPowerShellModule
2021.05.20.1523 - Improved Create-RegistryEntries. Modified Verify-TLSVersion to respect 'ignore' text.
2021.05.12.1600 - Added Verify-TLSVersion. Added error handling to Verify-ShouldDeleteConfiguration.
2021.05.11.1701 - Modified Create-RegistryEntries. Cleaned up Encrypt-PlainAccountPassword.
2021.05.09.1427 - Added Assign-DiscoveryDefaultVariables
2021.05.04.1614 - Added $AssumeInherited_RegEx, Verify-DefaultVariables, Improved [Decode-UserData, Format-AccountPassword, Format-ClientSecret] logic and logging.
2021.03.03.1230 - fixed Catch output in BasicBag definition
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()"
Requires PowerShell 5+
#>
#-----------------------------------------------------------------------------
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]) "
}
}
}
#-----------------------------------------------------------------------------

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

# Create standard Script variables. This is typically used for discoveries.
# Expected to be dot-sourced
Function Assign-DiscoveryDefaultVariables {

$M365_AccountName = $RegProperties.M365_AccountName
$M365_AccountPassword = $RegProperties.M365_AccountPassword
$M365_ClientID = $RegProperties.M365_ClientID
$M365_ClientSecret = $RegProperties.M365_ClientSecret
$IntervalSeconds = $RegProperties.IntervalSeconds

}

###################################################################
<#
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
}

###################################################################
Function Import-SCOMPowerShellModule {

Import-Module OperationsManager

# Try to locate OperationsManager PowerShell module location
If (-NOT ((Get-Module OperationsManager).Count) ) {
Try {
$InstallDir = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\System Center Operations Manager\12\Setup\Powershell\V2").InstallDirectory
$SCOMPosh = (Join-Path $InstallDir "OperationsManager")
Import-module $SCOMPosh -ErrorAction Stop
If (-NOT ((Get-Module OperationsManager).Count) ) {
Throw
}
} Catch {
LogIt -EventID 9996 -Type $Critical -Message "Failed to import OperationsManager module. Exiting" -Proceed $WriteToEventLog -Line $(_LINE_); $Error.Clear()
Exit
}
}
}
########################################################################################################

<#
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 ;

'@
LogIt -EventID 9995 -Type $warn -Msg $ErrorMsg -Proceed $true -LINE $(_LINE_); $Error.Clear()
}

Return $enc
}
###################################################################
Function Decode-UserData {
Param (
[string]$Data
)

# Check if Data string includes a colon
If (($Data -match '^.*:') -OR ($Testing)) {

If ($Testing) {
# Will not require WHOAMI context in string
}

# Will require WHOAMI context in string to be valid and match current RunAs context
Else {
# Data string includes a colon which indicates original encryption user context is preceding the encrypted string. This is expected.
# Verify is current account context matches original encryption account context. (they should be identical)
If ( -NOT ($Data -match "$($whoami.Replace('\','\\'))" ) ){
$originalContext = ($Data -split ":" | Select-Object -First 1 )
$msg = @"
Function [Decode-UserData] ...
WARNING: Current account context of this workflow [$($whoami)] does not match original encryption account context of encrypted string [$($originalContext)].
Original context: [$originalContext]
Current context: [$whoami]
Data string to be decoded:
$($Data)
Aborting Decryption process.
"@
LogIt -EventID 9995 -Type $info -Msg $msg -Proceed $true -LINE $(_LINE_); $Error.Clear()
Throw $msg
Return
}
}



# Will remove 'domain/account:' if present.
$tmpCleanData = $Data -replace '^.*\\.*:', ''

$secureString = ($tmpCleanData | ConvertTo-SecureString)
$msg = @"
Function [Decode-UserData] ...

Original String:`t`t`t`t[$($Data)]

String (cleaned):`t[$($tmpCleanData)]

Converted to Secure String:
`$secureString: [$($secureString)]

Proceed to attempt decode of Secure String data...
"@

LogIt -EventID 9994 -Type $info -Msg $msg -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()

# Submit only a pure encrypted string for decode.
Return ([Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR(($secureString))) )
}

# Encrypted Data variable is not in correct format.
Else {
$msg = @"
Function [Decode-UserData] ...
WARNING: Data variable is not in the expected format or length.
Expected format: domain\username:<324 digit hex>
Example: contoso\svcomaa:01000000d08c9ddf0115d1118c7a00c04.....
Current context (whoami): [$whoami]

Data string to be decoded:
*********** ENCRYPTED STRING BELOW THIS LINE ***********
$($Data)
*********** ENCRYPTED STRING ABOVE THIS LINE ***********

Aborting Decryption process.
"@
LogIt -EventID 9995 -Type $info -Msg $msg -Proceed $true -LINE $(_LINE_); $Error.Clear()
Throw $msg
Return
}

}

###################################################################
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 {
[CmdletBinding(DefaultParameterSetName='Parameter Set 1',
SupportsShouldProcess=$false,
HelpUri = 'http://www.microsoft.com/')]
Param (
[Parameter(Mandatory=$true,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false,
Position=0,
ParameterSetName='Parameter Set 1')]
[Alias("PropertyBag")]
[system.object[]]$bag,
[string]$Tag='<no tag provided>'
)
Begin {}

Process {
ForEach ($item in $bag) {
Switch ($ScriptOutputType){

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

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

End {}
}

#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; $Error.Clear()
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; $Error.Clear()
} #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; $Error.Clear()
}
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; $Error.Clear()
$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; $Error.Clear()

$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; $Error.Clear()
$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; $Error.Clear()
}
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; $Error.Clear()
}
}
} #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 = ''
$MgmtApiURLInfo = ''
$ApiURLInfo = ''
If (-NOT $M365_AccountName.Length) {
$M365_AccountName = '<none>'
}

If ($SenderEmailAddress) {
$authInfo += @"
ReceiverEmailAddress: $ReceiverEmailAddress
ReceiverPassword: $ReceiverPassword
SenderEmailAddress: $SenderEmailAddress
SenderPassword: $SenderPassword
M365_AccountName (if relevant): $M365_AccountName
MailflowDirection: $MailflowDirection
ReceiveTimeoutSeconds: $ReceiveTimeoutSeconds
ExchangeURL: $ExchangeURL
ExchangeWebServicesPath: $ExchangeWebServicesPath
"@
}

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

If ($ApiURL) {
$ApiURLInfo = @"
ApiTokenScopeURL: $ApiTokenScopeURL
ApiTokenURL: $ApiTokenURL
ApiURL: $ApiURL
"@
}

If (-NOT ($OnDemandDiscovery.Length)) {
$tmpOnDemandDiscovery = "N/A"
}
Else {
$tmpOnDemandDiscovery = $OnDemandDiscovery
}
$output = @"
Message: $msg

WorkflowName: $WorkflowName
ScriptName: $ScriptName
Invocation/Function: $($MyInvocation.InvocationName)

ThisScriptInstanceGUID: $ThisScriptInstanceGUID
ScriptLine: $Line
Running As: $whoami
$authInfo
ClientID: $M365_ClientID
M365_AccountName: $M365_AccountName
MgmtGroupRegKey: $MgmtGroupRegKey
TenantName: $TenantName
$($ApiURLInfo)$($MgmtApiURLInfo)
OnDemandDiscovery: $tmpOnDemandDiscovery
TLSVersion: $TLSVersion
PoshLibraryPath: $($PoshLibraryPath.Trim(','))
ScriptOutputType: $ScriptOutputType
EventIDFilter: $EventIDFilter
WriteToEventLog: $WriteToEventLog
Any Errors: $($Error[0] | Out-String -Stream)

"@

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

# Create array of items to dump to event log Data fields
[System.Collections.ArrayList]$arrMessage = @()
$NULL = $arrMessage.Add($output)
$NULL = $arrMessage.Add($Msg)
$NULL = $arrMessage.Add($ScriptName)
$NULL = $arrMessage.Add($TenantName)
$NULL = $arrMessage.Add($NameSpace)

$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: `t$RegValueType
RegValueName: `t$RegValueName
NewData (attempted to write):`t$RegValueData
Verified Registry Data:`t`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 Connect-SCOMMgmtGroup {
# Connect to SCOM mgmt group
Try{
$MG = Get-SCOMManagementGroup -ErrorAction Stop
If (-NOT $MG) {
Throw "No SCOMManagementGroupConnection"
}
} Catch {
Try {
LogIt -EventID 9992 -Type $info -Message "No mgmt group connection. Establish new connection..." -Proceed $true -Line $(_LINE_); $Error.Clear()
$MGConnSetting = New-Object Microsoft.EnterpriseManagement.ManagementGroupConnectionSettings('localhost')
$MG = New-Object Microsoft.EnterpriseManagement.ManagementGroup($MGConnSetting)
If (-NOT $MG) {
Throw "No SCOMManagementGroupConnection"
}
}Catch {
LogIt -EventID 9996 -Type $critical -Message "Unable to establish connect to mgmt group on localhost. Exiting." -Proceed $true -Line $(_LINE_); $Error.Clear()
Exit
}
}
}

########################################################################################################
Function Create-RegistryEntries {
Param (
$NewSettings
)
# Add Settings
ForEach ($RegEntry in @($NewSettings.Keys) ) {

# Adding some extra variables to provide extra details in the logging. These conditions must be the same as the IF decision below.
#Rejection Condition #1
$RegEx1_bool = [bool]($NewSettings.$RegEntry.ValueData -notmatch "^\s$|^ $|^-1$|$AssumeInherited_RegEx|$Ignore_RegEx")
$RegEx1 = @"
($($NewSettings.$RegEntry.ValueData) -NOTMATCH "^\s$|^ $|^-1$|$($AssumeInherited_RegEx)|$($Ignore_RegEx)")'
"@


#Rejection Condition #2
$RegEx2_bool = [bool](CanBeNumber $NewSettings.$RegEntry.ValueData)
$RegEx2 = @"
(CanBeNumber $($NewSettings.$RegEntry.ValueData))'
"@


#Rejection Condition #3
$RegEx3_bool = [bool]($NewSettings.$RegEntry.ValueData -le 0)
$RegEx3 = @"
($($NewSettings.$RegEntry.ValueData) -LE 0)
"@


#Rejection Condition #4
$RegEx4_bool = [bool](($RegEntry -MATCH 'location') -AND ($NewSettings.$RegEntry.ValueData -NOTMATCH "$AssumeInherited_RegEx|$Ignore_RegEx"))
$RegEx4 = @"
($($RegEntry) -match "location" ) -AND ($($NewSettings.$RegEntry.ValueData)) -NOTMATCH "$($AssumeInherited_RegEx)|$($Ignore_RegEx)")
"@

$RegExResults = @"
RegEx Rejection Filter Details:
RegEx1_bool:
$($RegEx1_bool.ToString().ToUpper()) : $("$RegEx1")

RegEx2_bool:
$($RegEx2_bool.ToString().ToUpper()) : $("$RegEx2")

RegEx3_bool:
$($RegEx3_bool.ToString().ToUpper()) : $("$RegEx3")

RegEx4_bool:
$($RegEx4_bool.ToString().ToUpper()) : $("$RegEx4")
"@

$RegValueData = "RegValueData: [$($NewSettings.$RegEntry.ValueData)]"

# This allows the user to enter -1 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 forbidden char AND not a negative number. Except 'location'. Location can have spaces or -1.
If (`
(`
($NewSettings.$RegEntry.ValueData -NOTMATCH "^\s$|^ $|^-1$|$AssumeInherited_RegEx|$Ignore_RegEx") `
-AND`
(`
-NOT`
(`
(CanBeNumber $NewSettings.$RegEntry.ValueData) `
-AND ($NewSettings.$RegEntry.ValueData -LE 0) `
)`
)`
)`
`
-OR
(` # restrictions on 'Location' are much more relaxed.
($RegEntry -MATCH "location" )`
-AND ($NewSettings.$RegEntry.ValueData -NOTMATCH "$AssumeInherited_RegEx|$Ignore_RegEx")`
)`
)`
{



$activity = "Adding..."
$pref = $ErrorActionPreference
Try {
$ErrorActionPreference = 'Stop'
# Trim any preceding/trailing whitespaces as they are never appropriate for any of the reg data fields.
$TrimmedData = [string]($NewSettings.$RegEntry.ValueData).Trim()
If ([string]($NewSettings.$RegEntry.ValueData).Length -gt $TrimmedData.Length) {
$RegValueData = "Data (original): [$($NewSettings.$RegEntry.ValueData)]`nRegValueData Actual(Trimmed): [$($TrimmedData)]"
}
} Catch {
#Empty Catch is fine here.
}
$ErrorActionPreference = $pref

Add-RegData -RegKey $NewSettings.$RegEntry.Key -RegValueName $RegEntry -RegValueData $NewSettings.$RegEntry.ValueData -RegValueType $NewSettings.$RegEntry.ValueType
}
Else {
$activity = "Skipping this data..."
}

$msg = @"
$($activity)
RegValueName: [$($RegEntry)]
$($RegValueData)
RegValueType: [$($NewSettings.$RegEntry.ValueType)]

$RegExResults
"@
LogIt -EventID 9992 -Type $info -Proceed $WriteToEventLog -msg $msg -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$|$AssumeInherited_RegEx|$Ignore_RegEx") -OR ($M365_AccountPassword.Length -gt $MaxPwdSize) ){
$Msg = "Either M365_AccountPassword matches '-1' or it matches 'placeholder/ignore' RegEx: '$($AssumeInherited_RegEx)' or '$($Ignore_RegEx)' or length -gt 'MaxPwdSize': [$($MaxPwdSize)]. 'M365_AccountPassword' value will not be encrypted."
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.
Will add account context [$($whoami)] to front of encrypted string value. This will indicate who performed the encryption of the 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'
}
}
ElseIf (-NOT $M365_AccountPassword.Length){
$Msg = "M365_AccountPassword length: [$($M365_AccountPassword.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_AccountPassword_ENCRYPTED = $null
}
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$|$AssumeInherited_RegEx|$Ignore_RegEx") -OR ($M365_ClientSecret.Length -gt $ClientSecretLength) ){
$Msg = "Either M365_ClientSecret matches '-1' or it matches 'placeholder/ignore' RegEx: '$($AssumeInherited_RegEx)' or '$($Ignore_RegEx)' or length -gt the allowed length of [$($ClientSecretLength)]. 'M365_ClientSecret' value will not be encrypted."
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.
Will add account context [$($whoami)] to front of encrypted string value. This will indicate who performed the encryption of the string.
Proceed to encrypt...
"@
LogIt -EventID 9992 -Type $info -Msg $Msg -Proceed $true -LINE $(_LINE_); $Error.Clear()
# Will add account context [$($whoami)] to front of encrypted string value. This will indicate who performed the encryption of 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'
}
}
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 = $null
}

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

Function Encrypt-PlainAccountPassword {
Param (
[string]$passwdString
)
$passwdString = $passwdString.Trim()
# We need to determine if a legitimate password value is provided.
# '-1' indicates intentional skip. Only a legitimate password value will get encrypted.
If ($passwdString -match '^-1$') {
$Msg = "Password of length: [$($passwdString.Length)] matches '-1' and therefore will be ignored/skipped."
#Value will not be encrypted
$passwdString_ENCRYPTED = $passwdString
}
# Matching these RegEx indicates intentional skip. Only a legitimate password value will get encrypted.
ElseIf ($passwdString -match "$AssumeInherited_RegEx|$Ignore_RegEx") {
$Msg = "Password matches 'placeholder/ignore' RegEx: '$($AssumeInherited_RegEx)' or '$($Ignore_RegEx)' "
#Value will not be encrypted
$passwdString_ENCRYPTED = $passwdString
}
ElseIf ($passwdString.Length -gt 0) {
$Msg = "Password of length: [$($passwdString.Length)] exists and therefore will be encrypted."
# 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)."
#Set to empty [string]
$passwdString_ENCRYPTED = ''
}

LogIt -EventID 9992 -Type $info -Msg $Msg -Proceed $true -LINE $(_LINE_); $Error.Clear()
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()

Try {
$result = Remove-RegKey -RegKeyPath $RegKey
If ($result) {
$Message += "`n$NameSpace configuration removal success. Exiting."
LogIt -EventID 9992 -Type $info -Msg $Message -Proceed $true -LINE $(_LINE_); $Error.Clear()
}
Else {
Throw
}
} Catch {
$Message += "`n$NameSpace configuration removal failed, key: [$($RegKey)]. See error data."
LogIt -EventID 9996 -Type $warn -Msg $Message -Proceed $true -LINE $(_LINE_); $Error.Clear()

}
LogIt -EventID 9991 -Type $info -Proceed $WriteToEventLog -msg "Script End. Finished in [$($ScriptTimer.Elapsed.TotalSeconds)] seconds. `n" -LINE $(_LINE_); $Error.Clear()
Write-OnDemandDiscoveryEvent
Write-Output $Message
Exit
}
#endregion DeleteConfiguration
}

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

Function Verify-TLSVersion {
Param (
[string]$Version
)

Switch ($Version)
{
{$_ -match '1\.[1-3|0]' } { Return $Version }
{$_ -match "$AssumeInherited_RegEx|$Ignore_RegEx" } {Return $Version}
Default { Return $TLSDefaultVersion}
}
}
########################################################################################################

Function Verify-URLFormat {
Param (
[string]$URL
)
<#
I've identified 2 circumstances where the URL likely doesn't need to be modified (prefixed with 'https://')
1) URL is already prefixed properly.
2) URL variable contains control text indicating it should be ignored.
#>
If (($URL -match '^https:\/\/') -OR ($URL -match "\s|^ $|^-1$|$AssumeInherited_RegEx|$Ignore_RegEx")) {
#URL is probably fine
Return $URL
}
Else {
# Add http protocol prefix to string
Return ("https://$($URL)")
}

}


########################################################################################################
# For Discoveries. Verify that the required auth variables have values. If they are placeholders (or blank), use the default values from the WatcherNode Tenant key
# This is expected to be dot-sourced
Function Verify-DefaultVariables {

$TenantKeyPath = $TenantKey.Replace("HKLM\",'HKLM:\')

Try {
$null = Test-Path $TenantKeyPath -ErrorAction Stop
} Catch {
LogIt -EventID 9991 -Type $info -Msg "'Verify-DefaultVariables': RegKey does not exist:[$($TenantKeyPath)] . Exiting workflow: [$WorkflowName]. " -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
Exit
}

$TenantProperties = Get-ItemProperty -Path $TenantKeyPath

#Ensure these required variables have values. If blank, use default value from tenant
"M365_AccountName","M365_AccountPassword","M365_ClientID","M365_ClientSecret","IntervalSeconds" | ForEach-Object {
$val = (Get-Variable -Name $_).Value

If ((-NOT ($val.Length)) -OR ($val -match $AssumeInherited_RegEx) ){
LogIt -EventID 9992 -Type $info -Msg "No value provided for parameter:[$($_)]. Will use Tenant default value. " -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
Set-Variable -Name $_ -Value ($TenantProperties.$_)
}
}
}


########################################################################################################
# Will write event log entry to trigger WA on mgmt server to run designated discoveries on demand.
Function Write-OnDemandDiscoveryEvent {

<# Examples:
InvokeOnDemandDiscovery;<DiscoveryIdGUID>,<InstanceIdGUID>
InvokeOnDemandDiscovery;<DiscoveryIdGUID>,<InstanceIdGUID>;<DiscoveryIdGUID>,<InstanceIdGUID>
#>

If ($OnDemandDiscovery.Length) {
$WODDActivity = "attempt to replace braces in `$OnDemandDiscovery variable."
Try {
$OnDemandDiscoveryString = ($OnDemandDiscovery -replace '{|}','')
# This specific msg text will trigger the event rule WA to launch OnDemand task on mgmt server
$Msg = "InvokeOnDemandDiscovery;$($OnDemandDiscoveryString)"
LogIt -EventID 9992 -Type $info -Msg "Successful $WODDActivity" -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
LogIt -EventID 9994 -Type $info -Msg $Msg -Proceed $True -LINE $(_LINE_); $Error.Clear()
} Catch {
LogIt -EventID 9996 -Type $warn -Msg "Failed $WODDActivity" -Proceed $True -LINE $(_LINE_); $Error.Clear()
}

}
Else {
$Msg = "'Write-OnDemandDiscoveryEvent' has been called but no value for `$OnDemandDiscovery exists. No discovery will be triggered."
LogIt -EventID 9996 -Type $warn -Msg $Msg -Proceed $True -LINE $(_LINE_); $Error.Clear()
}
}

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

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

#[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
[string]$TLSDefaultVersion = '1.2'
$AssumeInherited_RegEx = '.*INHERIT.*DEFAULT.*VALUE.*'
$Ignore_RegEx = 'leave.*blank|.*to.*ignore|YOURDOMAIN\.COM|YOUR_'
$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)
}