Framework/Core/SVT/Services/LogicApps.ps1
Set-StrictMode -Version Latest class LogicAppControl { [string] $Name = "" [string] $Automated = "" [string] $MethodName = "" [string] $Remarks = "" } class LogicAppApprovedConnector { [string] $connectorName = "" [LogicAppControl[]] $ApplicableControls = @() [LogicAppControl[]] $NotApplicableControls = @() } class LogicAppNotApprovedConnector { [string] $connectorName = "" [string] $Remarks = "" } class LogicAppConnectorsMetadata { [LogicAppApprovedConnector[]] $ApprovedConnectors = @() [LogicAppNotApprovedConnector[]] $notApprovedConnectors = @() } class LogicApps: SVTBase { hidden [PSObject] $ResourceObject; hidden [LogicAppConnectorsMetadata] $LogicAppConnectorsMetadata LogicApps([string] $subscriptionId, [string] $resourceGroupName, [string] $resourceName): Base($subscriptionId, $resourceGroupName, $resourceName) { $this.GetResourceObject(); $this.LogicAppConnectorsMetadata = [LogicAppConnectorsMetadata] ($this.LoadServerConfigFile("LogicApps.Connectors.json")); } LogicApps([string] $subscriptionId, [SVTResource] $svtResource): Base($subscriptionId, $svtResource) { $this.GetResourceObject(); $this.LogicAppConnectorsMetadata = [LogicAppConnectorsMetadata] ($this.LoadServerConfigFile("LogicApps.Connectors.json")); if(Get-Member -InputObject $this.ResourceObject.Properties.parameters -Name '$connections' -MemberType Properties) { $apiConnections = $this.ResourceObject.Properties.parameters.'$connections'.value if($null -ne $apiConnections) { $apiConnections | Get-Member -MemberType *Property | ForEach-Object{ try { $apiConId = ($apiConnections.($_.name) | Select-Object connectionId).connectionId $childSvtObject = $this.CreateSVTResource($apiConId, $svtResource.ResourceGroupName, $svtResource.ResourceName + "/" + $_.name, "Microsoft.Web/connections", $svtResource.Location, "APIConnection") $this.ChildSvtObjects += New-Object -TypeName $($childSvtObject.ResourceTypeMapping.ClassName) -ArgumentList $this.SubscriptionContext.SubscriptionId, $childSvtObject } catch { #Consuming the exception intentionally to prevent adding deleted connections } } } } $Definition=$this.ResourceObject.Properties.definition if($null -ne $Definition.Actions -and -not[string]::IsNullOrEmpty($this.ResourceObject.Properties.definition.actions)) { $this.ChildResourceNames = @(); $Definition.Actions | Get-Member -MemberType *Property | ForEach-Object{ $Name=$_.name if($Definition.Actions.$Name.type -ne 'ApiConnection') { $newResourceId = $svtResource.ResourceId.replace($svtResource.ResourceName,$_.name).replace("Microsoft.Logic/workflows/","Microsoft.Web/connections/custom/") $childSvtObject = $this.CreateSVTResource($newResourceId, $svtResource.ResourceGroupName, $svtResource.ResourceName + "/" + $_.name, "Microsoft.Web/connections", $svtResource.Location, "APIConnection") $this.ChildResourceNames += $svtResource.ResourceName + "/" + $_.name; $this.ChildSvtObjects += New-Object -TypeName $($childSvtObject.ResourceTypeMapping.ClassName) -ArgumentList $this.SubscriptionContext.SubscriptionId, $childSvtObject } } } #if($null -ne $Definition.triggers -and -not[string]::IsNullOrEmpty($this.ResourceObject.Properties.definition.triggers)) #{ # $Definition.Triggers | Get-Member -MemberType *Property | ForEach-Object{ # $Name=$_.name # if($Definition.Triggers.$Name.type -ne 'ApiConnection') # { # $newResourceId = $svtResource.ResourceId.replace($svtResource.ResourceName,$_.name).replace("Microsoft.Logic/workflows/","Microsoft.Web/connections/custom/") # $childSvtObject = $this.CreateSVTResource($newResourceId, $svtResource.ResourceGroupName, $svtResource.ResourceName + "/" + $_.name, "Microsoft.Web/connections", $svtResource.Location, "APIConnection") # $this.ChildSvtObjects += New-Object -TypeName $($childSvtObject.ResourceTypeMapping.ClassName) -ArgumentList $this.SubscriptionContext.SubscriptionId, $childSvtObject # } # } #} } hidden [PSObject] GetResourceObject() { if (-not $this.ResourceObject) { $this.ResourceObject = Get-AzResource -Name $this.ResourceContext.ResourceName ` -ResourceGroupName $this.ResourceContext.ResourceGroupName -ResourceType $this.ResourceContext.ResourceType if(-not $this.ResourceObject) { throw ([SuppressedException]::new(("Resource '{0}' not found under Resource Group '{1}'" -f ($this.ResourceContext.ResourceName), ($this.ResourceContext.ResourceGroupName)), [SuppressedExceptionType]::InvalidOperation)) } } return $this.ResourceObject; } hidden [ControlResult[]] CheckConnectorsAADAuth([ControlResult] $controlResult) { [ControlResult[]] $controlResultList = @() [PSObject[]] $Connectors = @() if(Get-Member -InputObject $this.ResourceObject.Properties.parameters -Name '$connections' -MemberType Properties) { $apiConnections = $this.ResourceObject.Properties.parameters.'$connections'.value if($null -ne $apiConnections) { $apiConnections | Get-Member -MemberType *Property | ForEach-Object{ try { $apiConId = ($apiConnections.($_.name) | Select-Object connectionId).connectionId $apiConObj = Get-AzResource -ResourceId $apiConId $apiName=$apiConObj.Properties.Api.Name $Connector = New-Object PSObject Add-Member -InputObject $Connector -MemberType NoteProperty -Name ConnectorName -Value $apiName Add-Member -InputObject $Connector -MemberType NoteProperty -Name ConnectorType -Value $apiName Add-Member -InputObject $Connector -MemberType NoteProperty -Name ConnectorObj -Value $null $Connectors+=$Connector } catch { #Consuming the exception intentionally to prevent adding deleted connections } } } } $Definition=$this.ResourceObject.Properties.definition if($null -ne $Definition.Actions -and -not[string]::IsNullOrEmpty($this.ResourceObject.Properties.definition.actions)) { $Definition.Actions | Get-Member -MemberType *Property | ForEach-Object{ $Name=$_.name if($Definition.Actions.$Name.type -ne 'ApiConnection') { $Connector = New-Object PSObject Add-Member -InputObject $Connector -MemberType NoteProperty -Name ConnectorName -Value $Name Add-Member -InputObject $Connector -MemberType NoteProperty -Name ConnectorType -Value $Definition.Actions.$Name.type Add-Member -InputObject $Connector -MemberType NoteProperty -Name ConnectorObj -Value $Definition.Actions.$Name $Connectors+=$Connector } } } if($null -ne $Definition.triggers -and -not[string]::IsNullOrEmpty($this.ResourceObject.Properties.definition.triggers)) { $Definition.Triggers | Get-Member -MemberType *Property | ForEach-Object{ $Name=$_.name if($Definition.Triggers.$Name.type -ne 'ApiConnection') { $Connector = New-Object PSObject Add-Member -InputObject $Connector -MemberType NoteProperty -Name ConnectorName -Value $Name Add-Member -InputObject $Connector -MemberType NoteProperty -Name ConnectorType -Value $Definition.Triggers.$Name.type Add-Member -InputObject $Connector -MemberType NoteProperty -Name ConnectorObj -Value $Definition.Triggers.$Name $Connectors+=$Connector } } } if($Connectors.Count -gt 0) { $Result = $this.GetConnectorsStatus($Connectors , "AAD", $controlResult) if($null -ne $Result) { $controlResultList += $Result } } else { $controlResult.AddMessage([VerificationResult]::Passed,"Logic app workflow is empty. No connectors found.") $controlResultList += $controlResult } return $controlResultList } hidden [ControlResult[]] CheckConnectorsEncryptionInTransit([ControlResult] $controlResult) { [ControlResult[]] $controlResultList = @() [PSObject[]] $Connectors = @() if(Get-Member -InputObject $this.ResourceObject.Properties.parameters -Name '$connections' -MemberType Properties) { $apiConnections = $this.ResourceObject.Properties.parameters.'$connections'.value if($null -ne $apiConnections) { $apiConnections | Get-Member -MemberType *Property | ForEach-Object{ try { $apiConId = ($apiConnections.($_.name) | Select-Object connectionId).connectionId $apiConObj = Get-AzResource -ResourceId $apiConId $apiName=$apiConObj.Properties.Api.Name $Connector = New-Object PSObject Add-Member -InputObject $Connector -MemberType NoteProperty -Name ConnectorName -Value $apiName Add-Member -InputObject $Connector -MemberType NoteProperty -Name ConnectorType -Value $apiName Add-Member -InputObject $Connector -MemberType NoteProperty -Name ConnectorObj -Value $null $Connectors+=$Connector } catch { #Consuming the exception intentionally to prevent adding deleted connections } } } } $Definition=$this.ResourceObject.Properties.definition if($null -ne $Definition.Actions -and -not[string]::IsNullOrEmpty($this.ResourceObject.Properties.definition.actions)) { $Definition.Actions | Get-Member -MemberType *Property | ForEach-Object{ $Name=$_.name if($Definition.Actions.$Name.type -ne 'ApiConnection') { $Connector = New-Object PSObject Add-Member -InputObject $Connector -MemberType NoteProperty -Name ConnectorName -Value $Name Add-Member -InputObject $Connector -MemberType NoteProperty -Name ConnectorType -Value $Definition.Actions.$Name.type Add-Member -InputObject $Connector -MemberType NoteProperty -Name ConnectorObj -Value $Definition.Actions.$Name $Connectors+=$Connector } } } if($null -ne $Definition.triggers -and -not[string]::IsNullOrEmpty($this.ResourceObject.Properties.definition.triggers)) { $Definition.Triggers | Get-Member -MemberType *Property | ForEach-Object{ $Name=$_.name if($Definition.Triggers.$Name.type -ne 'ApiConnection') { $Connector = New-Object PSObject Add-Member -InputObject $Connector -MemberType NoteProperty -Name ConnectorName -Value $Name Add-Member -InputObject $Connector -MemberType NoteProperty -Name ConnectorType -Value $Definition.Triggers.$Name.type Add-Member -InputObject $Connector -MemberType NoteProperty -Name ConnectorObj -Value $Definition.Triggers.$Name $Connectors+=$Connector } } } if($Connectors.Count -gt 0) { $Result = $this.GetConnectorsStatus($Connectors , "EncryptionTransit", $controlResult) if($null -ne $Result) { $controlResultList += $Result } } else { $controlResult.AddMessage([VerificationResult]::Passed,"Logic app workflow is empty. No connectors found.") $controlResultList += $controlResult } return $controlResultList } hidden [ControlResult] CheckConnectorsSecretsHandling([ControlResult] $controlResult) { $complianceStatus = [VerificationResult]::Manual $userMsg = [string]::Empty $IsFailed = $false $Definition=$this.ResourceObject.Properties.definition $NonCompliantConnectors = @() if($null -ne $Definition.Actions -and -not[string]::IsNullOrEmpty($this.ResourceObject.Properties.definition.actions)) { $Definition.Actions | Get-Member -MemberType *Property | ForEach-Object{ $connectorName=$_.name $Connector = $Definition.Actions.$connectorName if($Connector.type -eq 'http') { $ConnectorResultObject = $this.CheckSecretsHandlingForHttp($Connector) if($ConnectorResultObject.ComplianceStatus -eq [VerificationResult]::Failed) { $IsFailed = $true $currentStateObject = "" | Select-Object "ConnectorName","ConnectorType","AuthenticationType" $currentStateObject.ConnectorName = $connectorName $currentStateObject.ConnectorType = $Connector.type $currentStateObject.AuthenticationType = $ConnectorResultObject.AuthenticationType $NonCompliantConnectors += $currentStateObject $userMsg += "Connector - " + $connectorName + "`r`nType - " + $Connector.type ` +"`r`nSecret(s) are given as plain text in Code View, must use 'SecureString' parameter" } } } } if($null -ne $Definition.triggers -and -not[string]::IsNullOrEmpty($this.ResourceObject.Properties.definition.triggers)) { $Definition.Triggers | Get-Member -MemberType *Property | ForEach-Object{ $connectorName=$_.name $Connector = $Definition.Triggers.$connectorName if($Connector.type -eq 'http') { $ConnectorResultObject = $this.CheckSecretsHandlingForHttp($Connector) if($ConnectorResultObject.ComplianceStatus -eq [VerificationResult]::Failed) { $IsFailed = $true $currentStateObject = "" | Select-Object "ConnectorName","ConnectorType","AuthenticationType" $currentStateObject.ConnectorName = $connectorName $currentStateObject.ConnectorType = $Connector.type $currentStateObject.AuthenticationType = $ConnectorResultObject.AuthenticationType $NonCompliantConnectors += $currentStateObject $userMsg += "Connector - " + $connectorName + "`r`nType - " + $Connector.type ` +"`r`nSecret(s) are given as plain text in Code View, must use 'SecureString' parameter" } } } } #No HTTP connector is present. Display generic message for users. if($userMsg -eq [string]::Empty) { $userMsg = "Please verify manually that Logic App code view doesn't contain any secrets/credentials in plain text" } if($IsFailed) { $controlResult.SetStateData("Connectors which contains secret(s) as plain text in code view:", $NonCompliantConnectors); $complianceStatus = [VerificationResult]::Failed } $controlResult.AddMessage($complianceStatus , $userMsg) return $controlResult } hidden [ControlResult] CheckLogicAppsInSameRG([ControlResult] $controlResult) { $OtherAppsinSameRG= Get-AzResource -ResourceGroupName $this.ResourceContext.ResourceGroupName -ResourceType $this.ResourceContext.ResourceType | Where-Object{$_.ResourceId -ne $this.ResourceObject.ResourceId } if($null -ne $OtherAppsinSameRG) { $controlResult.AddMessage("Below are the Logic Apps present in the same resource group as " + $this.ResourceContext.ResourceName + " - Logic App") $controlResult.AddMessage([VerificationResult]::Verify, "Validate that these Logic Apps trust each other",$OtherAppsinSameRG) $controlResult.SetStateData("Logic Apps present in same resource group", $OtherAppsinSameRG); } else { $controlResult.AddMessage([VerificationResult]::Passed, "No other logic apps found in resource group ["+ $this.ResourceContext.ResourceGroupName +"]") } return $controlResult } hidden [ControlResult] CheckTriggersAccessControl([ControlResult] $controlResult) { $IsFailed = $False $IsAccessConfigSet = ($null -ne (Get-Member -InputObject $this.Resourceobject.Properties -Name accessControl -MemberType Properties)) if($IsAccessConfigSet) { $IsTriggerRestricted = ($null -ne (Get-Member -InputObject $this.Resourceobject.Properties.accessControl -Name triggers -MemberType Properties)) #Check trigger access control if($IsTriggerRestricted) { if($this.ResourceObject.Properties.accessControl.triggers.allowedCallerIpAddresses.Count -eq 0) { #verify scenario $controlResult.AddMessage("Access control for triggers is set to `"Only other Logic Apps`"") } else { $triggerIPRange = $this.ResourceObject.Properties.accessControl.triggers.allowedCallerIpAddresses.addressRange if($triggerIPRange -contains $this.ControlSettings.UniversalIPRange) { #fail if universal IP range found $IsFailed = $True $controlResult.AddMessage("IP range $($this.ControlSettings.UniversalIPRange) must be removed from triggers IP ranges") } else { $controlResult.AddMessage("Please verify below:") } $controlResult.AddMessage("IP ranges for triggers :",$triggerIPRange) $controlResult.SetStateData("IP ranges for triggers", $triggerIPRange); } } else { #fail if no trigger access control found $IsFailed = $True $controlResult.AddMessage("Access control for triggers is not found") } } else { $IsFailed = $True $controlResult.AddMessage("Access control for triggers is not found") } if($IsFailed -eq $True) { $controlResult.VerificationResult = [VerificationResult]::Failed } else { $controlResult.VerificationResult = [VerificationResult]::Verify } return $controlResult } hidden [ControlResult] CheckContentsAccessControl([ControlResult] $controlResult) { $IsFailed = $False $IsAccessConfigSet = ($null -ne (Get-Member -InputObject $this.Resourceobject.Properties -Name accessControl -MemberType Properties)) if($IsAccessConfigSet) { $IsContentRestricted = ($null -ne (Get-Member -InputObject $this.Resourceobject.Properties.accessControl -Name contents -MemberType Properties)) #check content access control if($IsContentRestricted) { $contentIPRange = $this.ResourceObject.Properties.accessControl.contents.allowedCallerIpAddresses.addressRange if($contentIPRange -contains $this.ControlSettings.UniversalIPRange) { #fail if universal IP range assigned $IsFailed = $True $controlResult.AddMessage("IP range $($this.ControlSettings.UniversalIPRange) must be removed from contents IP ranges") } else { $controlResult.AddMessage("Please verify below:") } $controlResult.AddMessage("IP ranges for contents :", $contentIPRange) $controlResult.SetStateData("IP ranges for contents", $contentIPRange); } else { #fail if content access control not found $IsFailed = $True $controlResult.AddMessage("Access control for contents is not found") } } else { $IsFailed = $True $controlResult.AddMessage("Access control for contents is not found") } if($IsFailed -eq $True) { $controlResult.VerificationResult = [VerificationResult]::Failed } else { $controlResult.VerificationResult = [VerificationResult]::Verify } return $controlResult } #internal functions hidden [boolean] CheckSecretParameter([string] $secretString,[PSObject[]] $parametersList) { if(!$secretString.Trim().StartsWith("@parameters(")) { return $false } else { $temp=($secretString.replace(' ','')).split('(')[1] $parametervalue=$temp.split(')')[0].Trim("'") $type=$parametersList.$parametervalue.type if($type -ne "securestring") {return $false} else {return $true} } } hidden [PSObject] CheckSecretsHandlingForHttp([PSObject] $Connector) { $ConnectorObject = [PSObject]::new(); Add-Member -InputObject $ConnectorObject -Name "ComplianceStatus" -MemberType NoteProperty -Value [VerificationResult]::Manual Add-Member -InputObject $ConnectorObject -Name "AuthenticationType" -MemberType NoteProperty -Value $null if(Get-Member -inputobject $Connector.inputs -name "authentication" -Membertype Properties) { $authentication = $Connector.inputs.authentication if([Helpers]::CheckMember($authentication, "Type")) { $ConnectorObject.AuthenticationType = $authentication.type switch($authentication.type) { "ActiveDirectoryOAuth" { $IsValidSecret=$this.CheckSecretParameter($authentication.secret,$this.ResourceObject.Properties.definition.parameters) if($IsValidSecret -ne $true) { $ConnectorObject.ComplianceStatus = [VerificationResult]::Failed } } "ClientCertificate" { $IsValidPw=$this.CheckSecretParameter($authentication.Password,$this.ResourceObject.Properties.definition.parameters) if($IsValidPw -ne $true) { $ConnectorObject.ComplianceStatus = [VerificationResult]::Failed } } "Basic" { $IsValidPw=$this.CheckSecretParameter($authentication.Password,$this.ResourceObject.Properties.definition.parameters) if($IsValidPw -ne $true) { $ConnectorObject.ComplianceStatus = [VerificationResult]::Failed } } "default" { $ConnectorObject.ComplianceStatus = [VerificationResult]::Manual } } } } return $ConnectorObject } hidden [ControlResult] CheckAadAuthForHttp([string] $remarks , [ControlResult] $childControlResult , [PSObject]$Connector) { $isPassed = $false if(Get-Member -inputobject $Connector.ConnectorObj.inputs -name "authentication" -Membertype Properties) { if([Helpers]::CheckMember($Connector.ConnectorObj.inputs.authentication,"type") -and $Connector.ConnectorObj.inputs.authentication.type -eq "ActiveDirectoryOAuth") { $isPassed = $true } } if($isPassed) { $childControlResult.AddMessage([VerificationResult]::Passed,"AAD Authentication is used in connector - "+ $Connector.ConnectorName) } else { $childControlResult.AddMessage([VerificationResult]::Failed, "AAD Authentication is not used in connector - "+ $Connector.ConnectorName) } return $childControlResult } hidden [ControlResult] CheckEncryptionTransitForHttp([string] $remarks , [ControlResult] $childControlResult , [PSObject]$Connector) { $isPassed = $true $uriString = $Connector.ConnectorObj.inputs.uri if(([system.Uri]$uriString).Scheme -ne 'https') { $isPassed = $false } if($isPassed) { $childControlResult.AddMessage([VerificationResult]::Passed,"Connector name : " + $Connector.ConnectorName + "`r`nConnector URI : "+ $uriString) } else { $childControlResult.AddMessage([VerificationResult]::Failed, ` "Must use HTTPS URI for below connector`r`n" ` + "Connector name : " + $Connector.ConnectorName + "`r`nConnector URI : "+$uriString) } return $childControlResult } hidden [ControlResult] CheckEncryptionTransitForWebhook([string] $remarks , [ControlResult] $childControlResult , [PSObject]$Connector) { $isPassed = $true $subURI = "" $unSubURI = "" if(([Helpers]::CheckMember($Connector.connectorObj.inputs,"subscribe")) -and ([Helpers]::CheckMember($Connector.connectorObj.inputs.subscribe,"uri"))) { $subURI = $Connector.connectorObj.inputs.subscribe.uri } if(([Helpers]::CheckMember($connector.connectorObj.inputs,"unsubscribe")) -and ([Helpers]::CheckMember($connector.connectorObj.inputs.unsubscribe,"uri"))) { $unSubURI = $connector.connectorObj.inputs.unsubscribe.uri } if(($subURI -ne "" -and ([system.Uri]$subURI).Scheme -ne 'https') -or ($unSubURI -ne "" -and ([system.Uri]$unSubURI).Scheme -ne 'https')) { $isPassed = $false } if($isPassed) { $childControlResult.AddMessage([VerificationResult]::Passed, "Connector name : " + $Connector.ConnectorName ` + "`r`nWebhook subscribe URI : "+ $subURI` + "`r`nWebhook unsubscribe URI : "+$unSubURI) } else { $childControlResult.AddMessage([VerificationResult]::Failed, "Must use HTTPS URI(s) for below connector`r`n" ` + "Connector name : " + $Connector.ConnectorName ` + "`r`nWebhook subscribe URI : "+ $subURI` + "`r`nWebhook unsubscribe URI : "+$unSubURI) } return $childControlResult } hidden [ControlResult[]] GetConnectorsStatus([PSObject] $Connectors,[string] $controlName, [ControlResult] $controlResult) { $controlResultList = @() $Connectors | ForEach-Object{ $Connector = $_ $connectorName=$Connector.ConnectorName $ConnectorObj = $Connector.ConnectorObj $connectorType=$Connector.ConnectorType [ControlResult] $childControlResult = $this.CreateChildControlResult($connectorName + " ("+$connectorType+" connector)", $controlResult); if($connectorName -eq "manual") { $connectorName = $connectorType } #check if this connector belongs to not approved list $notApprovedConnector = $this.LogicAppConnectorsMetadata.NotApprovedConnectors | Where-Object {$_.connectorName -eq $connectorType} if($notApprovedConnector) { $childControlResult.AddMessage([VerificationResult]::Failed, $notApprovedConnector.Remarks) } else { #Check if it belongs to approved connectors $approvedConnector = $this.LogicAppConnectorsMetadata.ApprovedConnectors | Where-Object {$_.connectorName -eq $connectorType} if(($approvedConnector|Measure-Object).Count -gt 0) { #check if control is applicable on this connector $applicableControl = $approvedConnector.ApplicableControls | Where-Object {$_.Name -eq $controlName} $notApplicableControl = $approvedConnector.NotApplicableControls | Where-Object {$_.Name -eq $controlName} if(($applicableControl|Measure-Object).Count -gt 0) { #Get method name $methodName = $applicableControl.MethodName $childControlResult = $this.$methodName($applicableControl.Remarks , $childControlResult, $Connector) } else { $methodName = $notApplicableControl.MethodName if($notApplicableControl.Remarks -eq [string]::Empty) { $notApplicableControl.Remarks = "This control is not applicable on connector type - " + $connectorType } $childControlResult = $this.$methodName($notApplicableControl.Remarks , $childControlResult, $Connector) } } else { $childControlResult.AddMessage([VerificationResult]::Manual, $connectorType+" connector is not evaluated yet") } } $controlResultList += $childControlResult } return $controlResultList } hidden [ControlResult] DefaultPassed([string] $remarks,[ControlResult] $childControlResult, [PSObject] $Connector) { $childControlResult.AddMessage([VerificationResult]::Passed, $remarks) return $childControlResult } hidden [ControlResult] DefaultManual([string] $remarks,[ControlResult] $childControlResult, [PSObject] $Connector) { $childControlResult.AddMessage([VerificationResult]::Manual, $remarks) return $childControlResult } } |