modules/AzStack.Insights/rules/AzStackHci.DiagnosticSettings.OsConfig.HealthCheck.ps1
|
<#
.SYNOPSIS Maps a single OS configuration "health section" (as produced by Get-AzStackHciOsConfigSettings) onto an Insights rule result. .DESCRIPTION Data-driven rule invoked once per top-level health section returned by the AzStackHci.DiagnosticSettings module (e.g. ArcAgentHealth, NonMicrosoftServices, StorageHealth, KeyServicesHealth, StabilityEvents, GPOHealthCheck, NetworkATCHealth). Unlike the cross-node ConsistencyChecks collection, each of these is a single named object on the health summary carrying its own Result / Severity / Message. The stable SECTION NAME (the summary property name) selects a fixed identity (GUID + friendly name + remediation) from the locale table, and the upstream Result string is mapped onto an InsightStatus. No data is collected or compared here; the module is the source of truth. Result -> InsightStatus (case-insensitive, tolerant of health-status vocabulary): PASS / HEALTHY / OK / SUCCESS -> SUCCESS WARNING / WARN / DEGRADED -> WARNING FAILURE / FAIL / UNHEALTHY / CRITICAL / ERROR -> FAILURE INFO / INFORMATIONAL -> INFO SKIPPED / NOTAPPLICABLE / NOT_APPLICABLE -> NOT_APPLICABLE GPOHealthCheck + SKIPPED -> UNKNOWN (collection was attempted but evidence is unavailable) anything else / null -> UNKNOWN SecurityBaselineHealth and DefenderHealth use the strict OSConfig 1.4 contract: PASS, WARNING and SKIPPED, plus baseline aggregate INFO for partial evaluation. Counts, source outcomes and coverage are validated without re-evaluating catalog predicates. Baseline PASS requires complete applicable coverage; Defender PASS can retain incomplete source coverage. Schema 1.3 reports capability unavailable. Invalid security contracts report UNKNOWN independently. Status, counts and coverage are projected into the returned Insights object. Selected differences and related assignments can be supplied separately to the HTML renderer. Raw exports, provenance and error text remain in the linked producer report. .PARAMETER SchemaVersion The OSConfig summary schema, not the producer module or per-node artifact version. .PARAMETER ExpectedNodes Requested nodes from the summary, used to detect omitted security node assessments. .PARAMETER HtmlEvidence Optional report-only destination for confirmed differences and related policy assignments. Never attached to the returned rule or its event payload. #> [CmdletBinding()] param( # A single health-section object from the Get-AzStackHciOsConfigSettings health summary. # Expected members: Result, Severity, Message; optional CheckId, Details, and evidence metadata. [Parameter(Mandatory = $true)] [psobject]$Section, # The stable top-level property name of the section on the summary (e.g. 'ArcAgentHealth'). # Used to resolve the section's fixed identity/remediation from the locale table. [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$SectionName, [version]$SchemaVersion = '1.4', [string[]]$ExpectedNodes = @(), [System.Collections.IDictionary]$HtmlEvidence ) # Import localization strings (SectionName -> identity/remediation table) from the locale directory. Import-LocalizedData -BindingVariable 'localizedData' -BaseDirectory "$PSScriptRoot\locale" -UICulture $PSUICulture # The live module returns each health section as an [ordered] dictionary (OrderedDictionary), not a # PSCustomObject. PSObject.Properties does NOT expose dictionary ENTRIES (only the dictionary's own .NET # members - Count / Keys / Values), so the member-existence guards below would never see Result / Message / # Details on the raw dictionary and every section would collapse to UNKNOWN. Normalise to a PSCustomObject # up front so every property read works regardless of the upstream shape. (Round-tripping through # ConvertTo/From-Json - as the unit tests do - already yields a PSCustomObject, which is why this only # reproduces against the live module output.) if ($Section -is [System.Collections.IDictionary]) { $Section = [pscustomobject]$Section } # Resolve the section's stable identity from its SectionName. if ($localizedData.Sections.ContainsKey($SectionName)) { $definition = $localizedData.Sections[$SectionName] } else { # Unknown / future section - surface it under a stable generic identity so it is never silently dropped. $definition = $localizedData.Fallback } $properties = @{ Name = $definition.Name Description = $definition.Description PublicDocs = if ($definition.ContainsKey('PublicDocs') -and $definition.PublicDocs) { @($definition.PublicDocs) } else { @() } SupportabilityID = 0 } $insightRule = Initialize-InsightRule -Id $definition.Id -Name $definition.Name -Properties $properties if ($null -ne $HtmlEvidence) { $HtmlEvidence.Remove($definition.Id.ToString()) $HtmlEvidence.Remove($definition.Id.ToString() + '.SourceCoverage') } <# START INSIGHT RULE LOGIC #> try { if ($SectionName -in @('SecurityBaselineHealth', 'DefenderHealth')) { if ($null -eq $SchemaVersion -or $SchemaVersion.Major -ne 1) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } if ($SchemaVersion -lt [version]'1.4') { $insightRule.Status = [InsightStatus]::NOT_APPLICABLE $insightRule.Properties += [pscustomobject]@{ Message = $localizedData.SecurityCapabilityUnavailable } Write-InsightEvent -Insight $insightRule return $insightRule } $isBaseline = $SectionName -eq 'SecurityBaselineHealth' $aggregateResults = if ($isBaseline) { @('PASS', 'WARNING', 'INFO', 'SKIPPED') } else { @('PASS', 'WARNING', 'SKIPPED') } $requiredFields = @('Severity', 'Result', 'Message', 'Counts', 'EvidenceComplete', 'EvaluatedNodes', 'MissingNodes', 'Details') if ($isBaseline) { $requiredFields += 'NotApplicableNodes' } else { $requiredFields += 'CheckId' } foreach ($fieldName in $requiredFields) { if (-not $Section.PSObject.Properties[$fieldName]) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } } $expectedCheckId = if ($isBaseline) { 'SecurityBaseline' } else { 'DefenderAsr' } if (($Section.PSObject.Properties['CheckId'] -and $Section.CheckId -cne $expectedCheckId) -or ($isBaseline -and $Section.PSObject.Properties['SchemaVersion'] -and ($Section.SchemaVersion -isnot [string] -or $Section.SchemaVersion -cne '1.0')) -or $Section.Severity -cne 'High' -or $Section.Result -isnot [string] -or $Section.Result -cnotin $aggregateResults -or $Section.Message -isnot [string] -or $Section.EvidenceComplete -isnot [bool] -or $Section.Details -isnot [System.Collections.IList]) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $coverageFields = @('EvaluatedNodes', 'MissingNodes') if ($isBaseline) { $coverageFields += 'NotApplicableNodes' } foreach ($fieldName in $coverageFields) { if ($Section.$fieldName -isnot [System.Collections.IList]) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } foreach ($nodeName in $Section.$fieldName) { if ($nodeName -isnot [string] -or [string]::IsNullOrWhiteSpace($nodeName)) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } } } $countNames = if ($isBaseline) { @('Match', 'Delta', 'NotEvaluated', 'NotApplicable') } else { @('Pass', 'Warning', 'Skipped') } $requiredCountNames = @('Total') + $countNames if ($isBaseline) { $requiredCountNames += @('Evaluated', 'Matched') } $counts = $Section.Counts if ($counts -is [System.Collections.IDictionary]) { $counts = [pscustomobject]$counts } $safeCounts = [ordered]@{} foreach ($countName in $requiredCountNames) { $hasCount = $null -ne $counts -and $counts.PSObject.Properties[$countName] if ((-not $hasCount -and (-not $isBaseline -or $countName -notin @('Matched', 'Evaluated'))) -or ($hasCount -and $counts.PSObject.Properties[$countName].Name -cne $countName)) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $countValue = if ($hasCount) { $counts.$countName } elseif ($countName -eq 'Matched') { $safeCounts.Match } else { $safeCounts.Match + $safeCounts.Delta } if ($null -eq $countValue -or [System.Type]::GetTypeCode($countValue.GetType()).ToString() -notin @('Byte', 'SByte', 'Int16', 'UInt16', 'Int32', 'UInt32', 'Int64', 'UInt64', 'Single', 'Double', 'Decimal')) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $numericCount = [double]$countValue if ([double]::IsNaN($numericCount) -or [double]::IsInfinity($numericCount) -or $numericCount -lt 0 -or $numericCount -ne [math]::Truncate($numericCount)) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $safeCounts[$countName] = $countValue } $sum = 0 foreach ($countName in $countNames) { $sum += $safeCounts[$countName] } $deltaCount = if ($isBaseline) { $safeCounts.Delta } else { $safeCounts.Warning } if ($safeCounts.Total -ne $sum -or ($isBaseline -and ($safeCounts.Match -ne $safeCounts.Matched -or $safeCounts.Evaluated -ne ($safeCounts.Matched + $safeCounts.Delta))) -or ($Section.Result -eq 'WARNING' -and $deltaCount -eq 0) -or ($Section.Result -ne 'WARNING' -and $deltaCount -gt 0) -or ($Section.Result -eq 'PASS' -and ($safeCounts.Total -eq 0 -or ($isBaseline -and -not $Section.EvidenceComplete)))) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $readSecurityRecord = { param([object]$Value, [string[]]$Fields = @(), [hashtable]$Types = @{}) if ($Value -is [System.Collections.IDictionary]) { $Value = [pscustomobject]$Value } foreach ($field in @($Fields) + @($Types.Keys)) { if ($null -eq $Value -or -not $Value.PSObject.Properties[$field]) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $fieldValue = $Value.$field $validType = switch ($Types[$field]) { 'String' { $fieldValue -is [string] } 'NullableString' { $null -eq $fieldValue -or $fieldValue -is [string] } 'NullableBoolean' { $null -eq $fieldValue -or $fieldValue -is [bool] } 'Timestamp' { $timestamp = [datetimeoffset]::MinValue $null -eq $fieldValue -or $fieldValue -is [datetime] -or $fieldValue -is [datetimeoffset] -or ($fieldValue -is [string] -and [datetimeoffset]::TryParse($fieldValue, [ref]$timestamp)) } default { $true } } if (-not $validType) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } } return $Value } $nodeTypes = @{ Node = 'String'; Result = 'String'; Message = 'String'; Source = 'NullableString'; Snapshot = 'NullableString'; Sha256 = 'NullableString'; CapturedAtUtc = 'Timestamp' } $settingTypes = @{ CheckId = 'String'; Severity = 'String'; Result = 'String'; Expected = 'NullableString'; BaselineSeverity = 'NullableString' } $nodeFields = @('Node', 'Result', 'EvidenceComplete', 'Message', 'Counts', 'Profile', 'Source', 'Snapshot', 'Sha256', 'CapturedAtUtc', 'Settings', 'CollectionErrors') $settingFields = @('CheckId', 'Severity', 'BaselineSeverity', 'Expected', 'Result', 'GpoEvidence') if ($isBaseline) { $nodeTypes.DomainJoined = 'NullableBoolean'; $nodeTypes.OSBuild = 'NullableString'; $nodeTypes.AssessmentStatus = 'String' foreach ($fieldName in @('DisplayName', 'Category', 'Expected', 'PublishedExpected', 'RuleSource', 'Validation', 'ExpectedType', 'EvidenceKind', 'Reason', 'Message', 'AssessmentResult')) { $settingTypes[$fieldName] = 'String' } foreach ($fieldName in @('ObservedType', 'RegistryPath', 'RegistryValue', 'RelatedCheckId')) { $settingTypes[$fieldName] = 'NullableString' } $nodeFields += @('AssessmentStatus', 'OSBuild', 'DomainJoined') $settingFields += @('DisplayName', 'AssessmentResult', 'Category', 'PublishedExpected', 'RuleSource', 'Validation', 'ExpectedType', 'Observed', 'ObservedType', 'Present', 'RegistryPath', 'RegistryValue', 'EvidenceKind', 'SourceGpo', 'StoredConfigurationEvidence', 'PolicyObservation', 'Reason', 'Message', 'RelatedCheckId') } else { $nodeFields += @('CheckId', 'Severity', 'RuntimeStatus') $settingFields += @('RuleId', 'OSConfigDefault', 'EvidenceComplete', 'SourceDisagreement', 'Sources') } $nodeTotals = @{} foreach ($countName in $requiredCountNames) { $nodeTotals[$countName] = 0 } $seenNodes = @() $safeNodeAssessments = @() $htmlSourceCoverage = @() $storedAssessments = @() $relatedAssessments = @() $htmlDifferences = @() $collectionErrorCount = 0 $allNodesComplete = $true foreach ($detail in $Section.Details) { $node = & $readSecurityRecord -Value $detail -Fields $nodeFields -Types $nodeTypes if ($node.Node -isnot [string] -or [string]::IsNullOrWhiteSpace($node.Node) -or $node.Node -in $seenNodes -or $node.Result -cnotin $aggregateResults -or $node.EvidenceComplete -isnot [bool] -or $node.Settings -isnot [System.Collections.IList] -or $node.CollectionErrors -isnot [System.Collections.IList] -or ($null -ne $node.Profile -and $node.Profile -cnotin @('DomainJoined', 'Adless'))) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } if (-not $isBaseline -and ($node.CheckId -cne 'DefenderAsr' -or $node.Severity -cne 'High')) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } if ($isBaseline -and (($node.PSObject.Properties['CheckId'] -and $node.CheckId -cne $expectedCheckId) -or ($node.PSObject.Properties['SchemaVersion'] -and ($node.SchemaVersion -isnot [string] -or $node.SchemaVersion -cne '1.0')))) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $seenNodes += $node.Node if (-not $node.EvidenceComplete -and (-not $isBaseline -or $node.Node -notin $Section.NotApplicableNodes)) { $allNodesComplete = $false } foreach ($collectionError in $node.CollectionErrors) { $null = & $readSecurityRecord -Value $collectionError -Types @{ Source = 'String'; Message = 'String' } } $rowCounts = @{} foreach ($countName in $requiredCountNames) { $rowCounts[$countName] = 0 } $sourceCoverage = [ordered]@{ NotConfigured = 0; Unavailable = 0; OtherSkipped = 0; NodeUnavailable = ($node.Settings.Count -eq 0) } $allSettingsComplete = $true $seenSettings = @() foreach ($row in $node.Settings) { $policyObservation = $null if ($isBaseline) { $originalSetting = if ($row -is [System.Collections.IDictionary]) { [pscustomobject]$row } else { $row } $compatibleSetting = [ordered]@{} foreach ($property in $originalSetting.PSObject.Properties) { $compatibleSetting[$property.Name] = $property.Value } $defaults = @{ Reason = $originalSetting.Message; RelatedCheckId = $null; RuleSource = 'PublishedBaseline'; PublishedExpected = $originalSetting.Expected } foreach ($fieldName in $defaults.Keys) { if (-not $compatibleSetting.Contains($fieldName)) { $compatibleSetting[$fieldName] = $defaults[$fieldName] } } $row = $compatibleSetting } $setting = & $readSecurityRecord -Value $row -Fields $settingFields -Types $settingTypes if ($setting.CheckId -isnot [string] -or [string]::IsNullOrWhiteSpace($setting.CheckId) -or $setting.Result -cnotin @('PASS', 'WARNING', 'SKIPPED')) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $rowCounts.Total++ if ($isBaseline) { if ($setting.CheckId -in $seenSettings) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $seenSettings += $setting.CheckId $assessmentResults = @{ Match = 'PASS'; Delta = 'WARNING'; NotEvaluated = 'SKIPPED'; NotApplicable = 'SKIPPED' } if ($setting.AssessmentResult -cnotin @('Match', 'Delta', 'NotEvaluated', 'NotApplicable') -or $setting.Result -cne $assessmentResults[$setting.AssessmentResult] -or [string]::IsNullOrWhiteSpace($setting.DisplayName) -or $setting.RuleSource -cnotin @('PublishedBaseline', 'NoExpectation') -or ($null -ne $setting.Present -and $setting.Present -isnot [bool]) -or $null -ne $setting.SourceGpo) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } if ($setting.Reason -cne $setting.Message -or ($setting.AssessmentResult -in @('NotApplicable', 'NotEvaluated') -and [string]::IsNullOrWhiteSpace($setting.Reason))) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $rowCounts[$setting.AssessmentResult]++ $rowCounts.Matched = $rowCounts.Match $rowCounts.Evaluated = $rowCounts.Match + $rowCounts.Delta if ($setting.AssessmentResult -eq 'NotEvaluated') { $allSettingsComplete = $false } if ($setting.RuleSource -ceq 'NoExpectation') { if ($setting.CheckId -cne 'EnabledNTPClient' -or $setting.AssessmentResult -cne 'NotApplicable' -or -not [string]::IsNullOrEmpty($setting.Expected) -or -not [string]::IsNullOrEmpty($setting.PublishedExpected) -or [string]::IsNullOrWhiteSpace($setting.Reason) -or $setting.Message -cne $setting.Reason -or $setting.RelatedCheckId -cne 'TimeSyncHealth') { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $relatedAssessments += [pscustomobject]@{ Node = $node.Node CheckId = $setting.CheckId DisplayName = $setting.DisplayName AssessmentResult = $setting.AssessmentResult Reason = $setting.Reason RelatedCheckId = $setting.RelatedCheckId } } elseif ($null -ne $setting.RelatedCheckId) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } if ($null -ne $setting.PolicyObservation) { $policyObservation = & $readSecurityRecord -Value $setting.PolicyObservation -Fields @('Collected', 'NativeStatus', 'Sids', 'Value') -Types @{ Source = 'String'; PolicyName = 'String'; Type = 'String' Present = 'NullableBoolean'; Error = 'NullableString' } $observationTypes = @{ LsaEnumerateAccountsWithUserRight = 'LsaUserRight'; Win32_UserAccount = 'LocalAccount'; NetUserModalsGet = 'LocalAccountPolicy'; SchannelProtocolRegistry = 'ProtocolMinimum'; SamQueryInformationDomain = 'LocalSecurityPolicy'; LsaQuerySecurityObject = 'LocalSecurityPolicy' } if ($policyObservation.Source -cnotin @($observationTypes.Keys) -or $policyObservation.Type -cne $observationTypes[$policyObservation.Source] -or [string]::IsNullOrWhiteSpace($policyObservation.PolicyName) -or $policyObservation.Collected -isnot [bool] -or $policyObservation.Sids -isnot [System.Collections.IList]) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $isLsaObservation = $policyObservation.Source -ceq 'LsaEnumerateAccountsWithUserRight' $isProtocolObservation = $policyObservation.Source -ceq 'SchannelProtocolRegistry' $isLocalSecurityObservation = $policyObservation.Type -ceq 'LocalSecurityPolicy' $numericTypes = @('Byte', 'SByte', 'Int16', 'UInt16', 'Int32', 'UInt32', 'Int64', 'UInt64', 'Single', 'Double', 'Decimal') if ($null -ne $policyObservation.Value -and $policyObservation.Value -isnot [string]) { if ($isLsaObservation -or $isProtocolObservation -or [System.Type]::GetTypeCode($policyObservation.Value.GetType()).ToString() -notin $numericTypes -or [double]::IsNaN([double]$policyObservation.Value) -or [double]$policyObservation.Value -lt 0 -or [double]$policyObservation.Value -gt [uint32]::MaxValue -or [double]$policyObservation.Value -ne [math]::Truncate([double]$policyObservation.Value)) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } } $nativeStatus = $policyObservation.NativeStatus if ($null -ne $nativeStatus) { if ([System.Type]::GetTypeCode($nativeStatus.GetType()).ToString() -notin @('Byte', 'SByte', 'Int16', 'UInt16', 'Int32', 'UInt32', 'Int64', 'UInt64', 'Single', 'Double', 'Decimal') -or [double]::IsNaN([double]$nativeStatus) -or [double]$nativeStatus -lt 0 -or [double]$nativeStatus -gt [uint32]::MaxValue -or [double]$nativeStatus -ne [math]::Truncate([double]$nativeStatus)) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } } foreach ($sid in $policyObservation.Sids) { if ($sid -isnot [string] -or [string]::IsNullOrWhiteSpace($sid) -or [System.Security.Principal.SecurityIdentifier]::new($sid).Value -cne $sid) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } } $samRawValue = $null if ($policyObservation.Source -ceq 'Win32_UserAccount') { if ($null -ne $nativeStatus -or ($policyObservation.Collected -and ($policyObservation.Sids.Count -ne 1 -or $policyObservation.Sids[0] -cnotmatch '^S-1-5-21-\d+-\d+-\d+-(500|501)$'))) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } } elseif ($policyObservation.Source -ceq 'NetUserModalsGet') { $null = & $readSecurityRecord -Value $policyObservation -Fields @('RawValue') if ($policyObservation.Sids.Count -ne 0 -or ($policyObservation.Collected -and $null -eq $policyObservation.RawValue)) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } if ($null -ne $policyObservation.RawValue) { $samRawValue = & $readSecurityRecord -Value $policyObservation.RawValue -Fields @('Level', 'Values') if ($null -eq $samRawValue.Level -or [System.Type]::GetTypeCode($samRawValue.Level.GetType()).ToString() -notin $numericTypes -or $samRawValue.Level -notin @(0, 3) -or $samRawValue.Values -isnot [System.Collections.IList] -or $samRawValue.Values.Count -ne (@{ 0 = 5; 3 = 3 }[[int]$samRawValue.Level]) -or $null -eq $nativeStatus -or $nativeStatus -ne 0) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } foreach ($rawNumber in $samRawValue.Values) { if ($null -eq $rawNumber -or [System.Type]::GetTypeCode($rawNumber.GetType()).ToString() -notin $numericTypes -or [double]::IsNaN([double]$rawNumber) -or [double]$rawNumber -lt 0 -or [double]$rawNumber -gt [uint32]::MaxValue -or [double]$rawNumber -ne [math]::Truncate([double]$rawNumber)) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } } } } $protocolRows = @() if ($isProtocolObservation) { $null = & $readSecurityRecord -Value $policyObservation -Fields @('Evaluable', 'RawValue') -Types @{ Reason = 'String' } if ($null -ne $nativeStatus -or $policyObservation.Sids.Count -ne 0 -or $policyObservation.Evaluable -isnot [bool] -or $policyObservation.RawValue -isnot [System.Collections.IList] -or ($policyObservation.Collected -and -not [string]::IsNullOrEmpty($policyObservation.Error))) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } if ($policyObservation.Evaluable) { if (-not $policyObservation.Collected -or $policyObservation.Present -ne $true -or $null -eq $policyObservation.Value -or $policyObservation.RawValue.Count -eq 0) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } } elseif ($null -ne $policyObservation.Present -or $null -ne $policyObservation.Value) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } foreach ($protocolValue in $policyObservation.RawValue) { $protocolRecord = & $readSecurityRecord -Value $protocolValue -Fields @('Enabled', 'DisabledByDefault') -Types @{ Protocol = 'String'; Version = 'String'; Role = 'String'; RegistryPath = 'String'; State = 'String' } if ($protocolRecord.Role -cnotin @('Client', 'Server') -or $protocolRecord.State -cnotin @('Enabled', 'Disabled', 'WindowsDefault', 'Invalid', 'Unavailable')) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $privateProtocol = [ordered]@{ Protocol = $protocolRecord.Protocol; Version = $protocolRecord.Version; Role = $protocolRecord.Role RegistryPath = $protocolRecord.RegistryPath; State = $protocolRecord.State } foreach ($registryName in @('Enabled', 'DisabledByDefault')) { $registryObservation = & $readSecurityRecord -Value $protocolRecord.$registryName -Fields @('Collected', 'Present', 'Value') -Types @{ Type = 'NullableString'; Error = 'NullableString' } if ($registryObservation.Collected -isnot [bool] -or $registryObservation.Present -isnot [bool]) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $privateProtocol[$registryName] = [pscustomobject]@{ Collected = $registryObservation.Collected; Present = $registryObservation.Present Value = $registryObservation.Value; Type = $registryObservation.Type ErrorPresent = -not [string]::IsNullOrEmpty($registryObservation.Error) } } $protocolRows += [pscustomobject]$privateProtocol } } elseif ($isLocalSecurityObservation) { $null = & $readSecurityRecord -Value $policyObservation -Fields @('Evaluable', 'RawValue') -Types @{ Reason = 'String'; StatusKind = 'NullableString' } $policyNames = if ($policyObservation.Source -ceq 'SamQueryInformationDomain') { @('PasswordComplexity', 'ClearTextPassword') } else { @('LSAAnonymousNameLookup') } if ($policyObservation.PolicyName -cnotin $policyNames -or $policyObservation.Evaluable -isnot [bool] -or $policyObservation.Sids.Count -ne 0 -or ($null -ne $policyObservation.StatusKind -and $policyObservation.StatusKind -cnotin @('NTSTATUS', 'NET_API_STATUS')) -or ($null -ne $nativeStatus -and $null -eq $policyObservation.StatusKind) -or ($policyObservation.Collected -and ($null -eq $nativeStatus -or $nativeStatus -ne 0 -or $policyObservation.StatusKind -cne 'NTSTATUS' -or -not [string]::IsNullOrEmpty($policyObservation.Error)))) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } if ($policyObservation.Evaluable) { if (-not $policyObservation.Collected -or $policyObservation.Present -ne $true -or $null -eq $policyObservation.Value -or $policyObservation.Value -is [string] -or $policyObservation.Value -notin @(0, 1)) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } } elseif ($null -ne $policyObservation.Present -or $null -ne $policyObservation.Value -or [string]::IsNullOrWhiteSpace($policyObservation.Reason) -or $setting.AssessmentResult -cne 'NotEvaluated') { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $localSecurityRawValue = $null if ($null -ne $policyObservation.RawValue) { $localSecurityRawValue = & $readSecurityRecord -Value $policyObservation.RawValue -Fields @('Collected', 'NativeStatus', 'PasswordProperties') -Types @{ Stage = 'NullableString'; StatusKind = 'NullableString'; Error = 'NullableString' LocalDomainName = 'NullableString'; LocalDomainSid = 'NullableString'; DaclSddl = 'NullableString' } if ($localSecurityRawValue.Collected -isnot [bool] -or $localSecurityRawValue.NativeStatus -cne $nativeStatus -or $localSecurityRawValue.StatusKind -cne $policyObservation.StatusKind -or ($policyObservation.Collected -and -not $localSecurityRawValue.Collected)) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } foreach ($rawNumber in @($localSecurityRawValue.NativeStatus, $localSecurityRawValue.PasswordProperties)) { if ($null -ne $rawNumber -and ([System.Type]::GetTypeCode($rawNumber.GetType()).ToString() -notin $numericTypes -or [double]::IsNaN([double]$rawNumber) -or [double]$rawNumber -lt 0 -or [double]$rawNumber -gt [uint32]::MaxValue -or [double]$rawNumber -ne [math]::Truncate([double]$rawNumber))) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } } } elseif ($policyObservation.Collected -or $null -ne $nativeStatus -or $null -ne $policyObservation.StatusKind) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } } elseif ($policyObservation.Collected) { if ($policyObservation.Present -ne $true -or $null -eq $policyObservation.Value -or -not [string]::IsNullOrEmpty($policyObservation.Error) -or ($isLsaObservation -and ($null -eq $nativeStatus -or $nativeStatus -notin @(0, 2147483674) -or (($policyObservation.Sids.Count -eq 0) -ne ($policyObservation.Value.Length -eq 0)) -or ($nativeStatus -eq 2147483674 -and $policyObservation.Sids.Count -ne 0))) -or ($policyObservation.Source -ceq 'NetUserModalsGet' -and ($null -eq $nativeStatus -or $nativeStatus -ne 0))) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } } elseif ($null -ne $policyObservation.Present -or $null -ne $policyObservation.Value -or $policyObservation.Sids.Count -ne 0) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } } if ($null -ne $setting.StoredConfigurationEvidence) { $storedAssessment = [ordered]@{ Node = $node.Node CheckId = $setting.CheckId BaselineResult = $setting.Result Scope = if ($setting.CheckId -ceq 'DeviceLockAccountLockoutPolicy') { $localizedData.StoredLocalAccountLockoutScope } else { 'Stored configuration only' } Source = $null Collected = $null Present = $null Result = 'UNKNOWN' ContractStatus = 'Invalid' } try { $stored = & $readSecurityRecord -Value $setting.StoredConfigurationEvidence -Fields @('Collected', 'Value') -Types @{ Source = 'String'; EvidenceKind = 'String'; PolicyName = 'String'; Artifact = 'NullableString' Present = 'NullableBoolean'; RawValue = 'NullableString'; Result = 'String'; Message = 'String' } if ($stored.Source -cne 'SeceditDefaultDatabaseExport' -or $stored.Collected -isnot [bool] -or $stored.Result -cnotin @('PASS', 'WARNING', 'SKIPPED') -or (-not $stored.Collected -and ($null -ne $stored.Present -or $stored.Result -ne 'SKIPPED')) -or ($stored.Result -ne 'SKIPPED' -and $null -eq $stored.Present)) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $storedAssessment.Source = $stored.Source $storedAssessment.Collected = $stored.Collected $storedAssessment.Present = $stored.Present $storedAssessment.Result = $stored.Result $storedAssessment.ContractStatus = 'Valid' } catch { $storedAssessment.ContractStatus = 'Invalid' } $storedAssessments += [pscustomobject]$storedAssessment } } else { $ruleGuid = [guid]::Empty if ($setting.RuleId -isnot [string] -or -not [guid]::TryParse($setting.RuleId, [ref]$ruleGuid) -or $setting.EvidenceComplete -isnot [bool] -or $setting.SourceDisagreement -isnot [bool] -or $ruleGuid -in $seenSettings -or ($setting.Result -ne 'SKIPPED' -and [string]::IsNullOrWhiteSpace($setting.Expected))) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $seenSettings += $ruleGuid $sources = & $readSecurityRecord -Value $setting.Sources -Fields @('OSConfig', 'DefenderReported', 'PolicyRegistry', 'LocalRegistry') $sourceResults = @() foreach ($sourceName in @('OSConfig', 'DefenderReported', 'PolicyRegistry', 'LocalRegistry')) { $sourceFields = @('Collected', 'Present', 'Value', 'Action', 'Result', 'Message') if ($sourceName -in @('PolicyRegistry', 'LocalRegistry')) { $sourceFields += 'ValueType' } $observation = & $readSecurityRecord -Value $sources.$sourceName -Fields $sourceFields -Types @{ Result = 'String'; Message = 'String'; Action = 'NullableString' } if ($observation.Collected -isnot [bool] -or ($null -ne $observation.Present -and $observation.Present -isnot [bool]) -or $observation.Result -cnotin @('PASS', 'WARNING', 'SKIPPED') -or ($observation.Result -ne 'SKIPPED' -and (-not $observation.Collected -or $observation.Present -ne $true -or $null -eq $observation.Value -or $observation.Value -eq 5))) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $sourceResults += $observation.Result if ($observation.Result -ceq 'SKIPPED') { $explicitNotConfigured = ($observation.Value -is [string] -and $observation.Value -ceq '5') -or ($null -ne $observation.Value -and [System.Type]::GetTypeCode($observation.Value.GetType()).ToString() -in @('Byte', 'SByte', 'Int16', 'UInt16', 'Int32', 'UInt32', 'Int64', 'UInt64', 'Single', 'Double', 'Decimal') -and $observation.Value -eq 5) if (-not $observation.Collected) { $sourceCoverage.Unavailable++ } elseif ($observation.Present -eq $false -or $explicitNotConfigured) { $sourceCoverage.NotConfigured++ } else { $sourceCoverage.OtherSkipped++ } } } $sourceAggregate = if ('WARNING' -in $sourceResults) { 'WARNING' } elseif ('PASS' -in $sourceResults) { 'PASS' } else { 'SKIPPED' } if ($setting.Result -cne $sourceAggregate -or ($setting.EvidenceComplete -and 'SKIPPED' -in $sourceResults)) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $countKey = switch ($setting.Result) { 'PASS' { 'Pass' }; 'WARNING' { 'Warning' }; 'SKIPPED' { 'Skipped' } } $rowCounts[$countKey]++ if (-not $setting.EvidenceComplete) { $allSettingsComplete = $false } } $gpoEvidence = $null if (-not $isBaseline -or $null -ne $setting.GpoEvidence) { $gpoEvidence = & $readSecurityRecord -Value $setting.GpoEvidence -Fields @('Collected', 'Message', 'Assignments') if ($gpoEvidence.Collected -isnot [bool] -or $gpoEvidence.Message -isnot [string] -or $gpoEvidence.Assignments -isnot [System.Collections.IList]) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } } if ($null -ne $HtmlEvidence) { $assignments = @() $invalidAssignments = 0 if ($null -ne $gpoEvidence -and $gpoEvidence.Collected) { foreach ($assignmentValue in $gpoEvidence.Assignments) { try { $assignment = & $readSecurityRecord -Value $assignmentValue -Fields @('Precedence', 'Value', 'ValueType', 'Deleted') -Types @{ GPOName = 'String'; GPOId = 'String'; GPOGuid = 'NullableString'; Result = 'String' } $precedence = 0 if ([string]::IsNullOrWhiteSpace($assignment.GPOId) -or -not [int]::TryParse([string]$assignment.Precedence, [ref]$precedence) -or $precedence -le 0 -or $assignment.Deleted -isnot [bool] -or $assignment.Result -cnotin @('PASS', 'WARNING', 'SKIPPED') -or ($assignment.Result -ne 'SKIPPED' -and ($assignment.Deleted -or $null -eq $assignment.Value)) -or (-not $isBaseline -and $assignment.Result -ne 'SKIPPED')) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $assignments += [pscustomobject]@{ GPOName = $assignment.GPOName GPOId = $assignment.GPOId GPOGuid = $assignment.GPOGuid Precedence = $precedence AssignedValue = $assignment.Value ValueType = $assignment.ValueType Deleted = $assignment.Deleted ComparisonResult = $assignment.Result } } catch { $invalidAssignments++ } } } $assignmentConflict = $isBaseline -and @($assignments | Where-Object { $_.ComparisonResult -eq 'WARNING' }).Count -gt 0 if ($setting.Result -eq 'WARNING' -or $assignmentConflict) { $difference = [ordered]@{ Node = $node.Node CheckId = $setting.CheckId DisplayName = if ($isBaseline) { $setting.DisplayName } else { $setting.CheckId } Severity = if ($isBaseline) { $setting.BaselineSeverity } else { $setting.Severity } Expected = $setting.Expected ConfiguredResult = $setting.Result GpoEvidenceCollected = if ($null -ne $gpoEvidence) { $gpoEvidence.Collected } else { $null } InvalidAssignmentCount = $invalidAssignments GpoAssignments = $assignments } if ($isBaseline) { $difference.RegistryPath = $setting.RegistryPath $difference.RegistryValue = $setting.RegistryValue $difference.Validation = $setting.Validation $difference.Observed = $setting.Observed $difference.ObservedType = $setting.ObservedType $difference.Present = $setting.Present $difference.GpoAssignmentConflict = $assignmentConflict $difference.EvidenceKind = $setting.EvidenceKind $difference.PolicyObservation = if ($null -ne $policyObservation) { [pscustomobject]@{ Source = $policyObservation.Source PolicyName = $policyObservation.PolicyName Collected = $policyObservation.Collected Present = $policyObservation.Present Value = $policyObservation.Value Type = $policyObservation.Type NativeStatus = $policyObservation.NativeStatus Sids = @($policyObservation.Sids) ErrorPresent = -not [string]::IsNullOrEmpty($policyObservation.Error) } } else { $null } if ($null -ne $policyObservation -and $policyObservation.Source -ceq 'NetUserModalsGet') { $privateRawValue = if ($null -ne $samRawValue) { [pscustomobject]@{ Level = $samRawValue.Level; Values = @($samRawValue.Values) } } else { $null } Add-Member -InputObject $difference.PolicyObservation -NotePropertyName RawValue -NotePropertyValue $privateRawValue } if ($null -ne $policyObservation -and $policyObservation.Source -ceq 'SchannelProtocolRegistry') { Add-Member -InputObject $difference.PolicyObservation -NotePropertyName Evaluable -NotePropertyValue $policyObservation.Evaluable Add-Member -InputObject $difference.PolicyObservation -NotePropertyName Reason -NotePropertyValue $policyObservation.Reason Add-Member -InputObject $difference.PolicyObservation -NotePropertyName RawValue -NotePropertyValue $protocolRows } if ($null -ne $policyObservation -and $policyObservation.Type -ceq 'LocalSecurityPolicy') { Add-Member -InputObject $difference.PolicyObservation -NotePropertyName Evaluable -NotePropertyValue $policyObservation.Evaluable Add-Member -InputObject $difference.PolicyObservation -NotePropertyName Reason -NotePropertyValue $policyObservation.Reason Add-Member -InputObject $difference.PolicyObservation -NotePropertyName StatusKind -NotePropertyValue $policyObservation.StatusKind $privateRawValue = if ($null -ne $localSecurityRawValue) { $nativeFields = [ordered]@{} foreach ($fieldName in @('Collected', 'Stage', 'NativeStatus', 'StatusKind', 'Error', 'LocalDomainName', 'LocalDomainSid', 'PasswordProperties', 'DaclSddl')) { $nativeFields[$fieldName] = $localSecurityRawValue.$fieldName } [pscustomobject]$nativeFields } else { $null } Add-Member -InputObject $difference.PolicyObservation -NotePropertyName RawValue -NotePropertyValue $privateRawValue } } else { $difference.RuleId = $setting.RuleId $difference.Sources = @(foreach ($sourceName in @('OSConfig', 'DefenderReported', 'PolicyRegistry', 'LocalRegistry')) { $observation = $sources.$sourceName if ($observation.Result -eq 'WARNING') { [pscustomobject]@{ Source = $sourceName Value = $observation.Value Action = $observation.Action Result = $observation.Result } } }) } $htmlDifferences += [pscustomobject]$difference } } } $nodeCounts = & $readSecurityRecord -Value $node.Counts -Fields (@('Total') + $countNames) foreach ($countName in $requiredCountNames) { $nodeCountValue = if ($nodeCounts.PSObject.Properties[$countName]) { $nodeCounts.$countName } elseif ($countName -eq 'Matched') { $nodeCounts.Match } else { $nodeCounts.Match + $nodeCounts.Delta } if ($null -eq $nodeCountValue -or [System.Type]::GetTypeCode($nodeCountValue.GetType()).ToString() -notin @('Byte', 'SByte', 'Int16', 'UInt16', 'Int32', 'UInt32', 'Int64', 'UInt64', 'Single', 'Double', 'Decimal') -or $nodeCountValue -ne $rowCounts[$countName]) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $nodeTotals[$countName] += $rowCounts[$countName] } $nodeDelta = if ($isBaseline) { $rowCounts.Delta } else { $rowCounts.Warning } $nodeSkipped = if ($isBaseline) { $rowCounts.NotEvaluated } else { $rowCounts.Skipped } $nodeEvaluated = if ($isBaseline) { $rowCounts.Match + $rowCounts.Delta -gt 0 } else { $rowCounts.Pass + $rowCounts.Warning -gt 0 } $nodeNotApplicable = $isBaseline -and $node.Node -in $Section.NotApplicableNodes if ($isBaseline) { $nodeAggregate = if ($nodeDelta -gt 0) { 'WARNING' } elseif ($nodeEvaluated -and $node.EvidenceComplete) { 'PASS' } elseif ($nodeEvaluated) { 'INFO' } else { 'SKIPPED' } $assessmentStatus = if ($nodeNotApplicable) { 'NotApplicable' } elseif ($nodeDelta -gt 0) { 'Delta' } elseif ($nodeEvaluated -and $node.EvidenceComplete) { 'Match' } elseif ($nodeEvaluated) { 'Partial' } else { 'NotEvaluated' } if ($node.Result -cne $nodeAggregate -or $node.AssessmentStatus -cne $assessmentStatus) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } } if (($nodeEvaluated -and ($null -eq $node.Profile -or ($isBaseline -and $null -eq $node.DomainJoined))) -or ($nodeEvaluated -ne ($node.Node -in $Section.EvaluatedNodes)) -or ((-not $node.EvidenceComplete -and -not $nodeNotApplicable) -ne ($node.Node -in $Section.MissingNodes)) -or ($nodeNotApplicable -and ($node.Result -ne 'SKIPPED' -or $node.AssessmentStatus -ne 'NotApplicable' -or ($rowCounts.Total -gt 0 -and ($rowCounts.NotApplicable -ne $rowCounts.Total -or $rowCounts.Evaluated -ne 0 -or $rowCounts.NotEvaluated -ne 0))))) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } if (($node.EvidenceComplete -and (-not $allSettingsComplete -or $rowCounts.Total -eq 0)) -or ($node.Result -eq 'WARNING' -and $nodeDelta -eq 0) -or ($node.Result -ne 'WARNING' -and $nodeDelta -gt 0) -or ($node.Result -eq 'PASS' -and ($rowCounts.Total -eq 0 -or $nodeSkipped -gt 0 -or ($isBaseline -and -not $node.EvidenceComplete)))) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } $collectionErrorCount += $node.CollectionErrors.Count $safeNodeAssessment = [ordered]@{ Node = $node.Node Result = $node.Result EvidenceComplete = $node.EvidenceComplete Counts = [pscustomobject]$rowCounts CollectionErrorCount = $node.CollectionErrors.Count } if ($isBaseline) { $safeNodeAssessment.CheckId = $expectedCheckId if ($node.PSObject.Properties['SchemaVersion']) { $safeNodeAssessment.SchemaVersion = $node.SchemaVersion } } elseif ($null -ne $HtmlEvidence) { $htmlSourceCoverage += [pscustomobject]@{ Node = $node.Node; SourceCoverage = [pscustomobject]$sourceCoverage } } $safeNodeAssessments += [pscustomobject]$safeNodeAssessment } foreach ($countName in $requiredCountNames) { if ($nodeTotals[$countName] -ne $safeCounts[$countName]) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } } if ($ExpectedNodes.Count -gt 0 -and (@($ExpectedNodes | Where-Object { $_ -notin $seenNodes }).Count -gt 0 -or @($seenNodes | Where-Object { $_ -notin $ExpectedNodes }).Count -gt 0)) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } foreach ($fieldName in $coverageFields) { foreach ($nodeName in $Section.$fieldName) { if ($nodeName -notin $seenNodes) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } } } $applicableSkippedNodes = @($Section.Details | Where-Object { $_.Result -eq 'SKIPPED' -and (-not $isBaseline -or $_.Node -notin $Section.NotApplicableNodes) }) $aggregateResult = if ($deltaCount -gt 0) { 'WARNING' } elseif ($safeCounts.Total -gt 0 -and (($isBaseline -and $safeCounts.Evaluated -gt 0 -and $Section.EvidenceComplete) -or (-not $isBaseline -and $applicableSkippedNodes.Count -eq 0 -and $safeCounts.Skipped -eq 0))) { 'PASS' } elseif ($isBaseline -and $safeCounts.Evaluated -gt 0) { 'INFO' } else { 'SKIPPED' } if (($Section.EvidenceComplete -and (-not $allNodesComplete -or $Section.EvaluatedNodes.Count -eq 0 -or $Section.MissingNodes.Count -gt 0)) -or ($Section.Result -eq 'PASS' -and $applicableSkippedNodes.Count -gt 0) -or $Section.Result -cne $aggregateResult) { throw [System.IO.InvalidDataException]::new($localizedData.SecurityContractInvalid) } if ($null -ne $HtmlEvidence -and $htmlDifferences.Count -gt 0) { $HtmlEvidence[$definition.Id.ToString()] = $htmlDifferences } if ($null -ne $HtmlEvidence -and -not $isBaseline) { $HtmlEvidence[$definition.Id.ToString() + '.SourceCoverage'] = $htmlSourceCoverage } $safeSection = [ordered]@{ Result = $Section.Result Message = $localizedData.SecurityResultMessage -f $definition.Name, $Section.Result EvidenceComplete = $Section.EvidenceComplete Counts = [pscustomobject]$safeCounts CollectionErrorCount = $collectionErrorCount NodeAssessments = $safeNodeAssessments } if ($isBaseline) { $safeSection.CheckId = $expectedCheckId if ($Section.PSObject.Properties['SchemaVersion']) { $safeSection.SchemaVersion = $Section.SchemaVersion } $safeSection.StoredConfigurationAssessments = $storedAssessments $safeSection.RelatedAssessments = $relatedAssessments } foreach ($fieldName in $coverageFields) { $safeSection[$fieldName] = $Section.$fieldName } $Section = [pscustomobject]$safeSection } $result = if ($Section.PSObject.Properties['Result'] -and $Section.Result) { [string]$Section.Result } else { '' } switch -Regex ($result) { '^(PASS|HEALTHY|OK|SUCCESS)$' { $insightRule.Status = [InsightStatus]::SUCCESS } '^(WARNING|WARN|DEGRADED)$' { $insightRule.Status = [InsightStatus]::WARNING } '^(FAILURE|FAIL|UNHEALTHY|CRITICAL|ERROR)$' { $insightRule.Status = [InsightStatus]::FAILURE } '^(INFO|INFORMATIONAL)$' { $insightRule.Status = [InsightStatus]::INFO } '^(SKIPPED|NOTAPPLICABLE|NOT_APPLICABLE)$' { $insightRule.Status = [InsightStatus]::NOT_APPLICABLE } default { $insightRule.Status = [InsightStatus]::UNKNOWN } } if ($SectionName -eq 'GPOHealthCheck' -and $result -eq 'SKIPPED') { $insightRule.Status = [InsightStatus]::UNKNOWN } # Capture the upstream section message (a short human-readable summary of the section outcome, e.g. # "No domain GPOs applied - only Local Group Policy detected"). $sectionMessage = if ($Section.PSObject.Properties['Message'] -and $Section.Message) { [string]$Section.Message } else { '' } # Schema 1.3 identifies which nodes contributed evidence. Preserve that contract in the rule output so a # retained FAILURE/WARNING cannot be mistaken for a complete-cluster observation, and a SKIPPED result # shows exactly where evidence was unavailable. $evidence = [ordered]@{} foreach ($fieldName in @('EvidenceComplete', 'EvaluatedNodes', 'MissingNodes', 'NotApplicableNodes', 'Counts', 'CollectionErrorCount', 'NodeAssessments', 'StoredConfigurationAssessments', 'RelatedAssessments')) { if ($Section.PSObject.Properties[$fieldName]) { $evidence[$fieldName] = $Section.$fieldName } } if ($evidence.Count -gt 0) { if ($SectionName -eq 'SecurityBaselineHealth') { foreach ($fieldName in @('CheckId', 'SchemaVersion')) { if ($Section.PSObject.Properties[$fieldName]) { $evidence[$fieldName] = $Section.$fieldName } } } $insightRule.Properties += [pscustomobject]$evidence } # On a healthy / informational outcome, surface the confirming message as a property row so the section # reads meaningfully in the report instead of blank (e.g. a clean GPO check shows "No domain GPOs applied - # only Local Group Policy detected"). On FAILURE/WARNING the message is carried as ErrorMessage (below), so # it is not duplicated here. if ($sectionMessage -and $insightRule.Status -ne [InsightStatus]::FAILURE -and $insightRule.Status -ne [InsightStatus]::WARNING) { $insightRule.Properties += [pscustomobject]@{ Message = $sectionMessage } } # Surface the upstream per-item detail (if any) so the HTML report shows the contributing items # per node on every status (e.g. the non-Microsoft services present on each node) - this per-node # inventory is useful information even when the section result is healthy. if ($Section.PSObject.Properties['Details'] -and $Section.Details) { foreach ($detail in @($Section.Details)) { $insightRule.Properties += $detail } } # Mixed GPO evidence reports nodes with applied domain GPOs in Details and nodes whose evidence is missing # in a separate UncollectedNodes string array. Project those missing nodes as report rows as well. if ($SectionName -eq 'GPOHealthCheck' -and $Section.PSObject.Properties['UncollectedNodes'] -and $Section.UncollectedNodes) { $existingNodes = @($insightRule.Properties | ForEach-Object { $propertyRow = $_ if ($propertyRow -is [System.Collections.IDictionary]) { $propertyRow = [pscustomobject]$propertyRow } if ($propertyRow.PSObject.Properties['Node']) { [string]$propertyRow.Node } }) foreach ($uncollectedNode in @($Section.UncollectedNodes)) { if ([string]$uncollectedNode -notin $existingNodes) { $insightRule.Properties += [pscustomobject]@{ Node = [string]$uncollectedNode Status = 'Not collected' } } } } # On a non-healthy outcome, carry the upstream message + the section-specific remediation. if ($insightRule.Status -eq [InsightStatus]::FAILURE -or $insightRule.Status -eq [InsightStatus]::WARNING) { $insightRule.ErrorMessage = if ($sectionMessage) { $sectionMessage } else { ($localizedData.DefaultErrorMessage -f $definition.Name) } $insightRule.ErrorCode = $definition.ErrorCode $insightRule.Remediation = $definition.Remediation } Write-AzsSupportLog -Message ($localizedData.Evaluated -f $SectionName, $result, $insightRule.Status) } catch { $_ | Write-AzsSupportLog -Level 'Exception' $insightRule.ScriptStackTrace = Get-FormattedException -Exception $_ $insightRule.Status = [InsightStatus]::UNKNOWN } <# END INSIGHT RULE LOGIC #> $insightRule.Duration = New-TimeSpan -Start $insightRule.OccurrenceTimeUTC -End $([System.DateTime]::UtcNow) Write-InsightEvent -Insight $insightRule return $insightRule # SIG # Begin signature block # MIInKAYJKoZIhvcNAQcCoIInGTCCJxUCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAejYLU0bv8VxKu # Cp493ui2uNBhpKKoEeOZBPVnX+y/KqCCDLowggX1MIID3aADAgECAhMzAAACHU0Z # yE7XD1dIAAAAAAIdMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD # VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD # b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQzWhcNMjcwNDE1MTg1 # OTQzWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD # VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB # DwAwggEKAoIBAQDQvewXxx9gZZFC6Ys1WBay8BJ8kGA4JQnH5CMafqOASlTpK9H8 # o5ZXTXt0caVQTNMUPt445wXYD+dFtaKWTwDn1I52oUSrC9vJin1Gsqt+zyKJL5Dg # 3eQXbQNR61DmMy20GLTIO3SFed9Rfi/ophgCLGFLDR3r0KvHjwMb/jYWS0celV/4 # Lz27LfAekm8v9E5IXaeiXbAUYZKK090n4CVl3JBtbN+9DtI9SNu/yjvozW52/u7R # X/Ttpa/KDlpuokZ+Zcbvmtd9ur9gFLvZzh41o9MsE/clQtdaFWGvuo6Jua/ntpgk # ey3E5/vBFe+MJPG6phdnuo6r57ZudCudiI1bAgMBAAGjggGbMIIBlzAOBgNVHQ8B # Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O # BBYEFH6QuMwqcPG0hQlQ6c5jCtTTLrVeMEUGA1UdEQQ+MDykOjA4MR4wHAYDVQQL # ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xFjAUBgNVBAUTDTIzMDAxMis1MDc1NTkw # HwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEwYAYDVR0fBFkwVzBVoFOg # UYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0 # JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNybDBtBggrBgEFBQcBAQRh # MF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMv # Y2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy # dDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQBKTbYOjzwTG/DXGaz9 # s6+fQeaTtDcFmMY+5UyVFCyj7Pv+5i37qfX8lSL/tBIfYQfWsMuBQlfZurJD6r4H # VJ2CeH+1fgiq8dcHdVKoZ3Sa2qXoX3cq9iS8cVb06B7+5/XJ7I0OxHH9fDsvJ3T3 # w5V/ZtAIFmLrl+P0CtG+92uzRsn0nTbdFjOkLMLWPLAU3THohKRlSEMgFJpPkm5n # 5UAZ35xX6FWCrDLsSKb555bTifwa8mJBwdlof0bmfYidH+dxZ1FdDxvLnNl9zeKs # A4kejaaIqqIPguhwAti5Ql7BlTNoJNwxCvBmqW2MQLnCkYN/VVUsR3V2x/rcTNzo # Bf/Z/SpROvdaA2ZOOd1uioXJt3tdLQ7vHpqpib0KfWr/FWXW10q38VxfCnRQBqzb # SuztR7nEMuzX7Ck+B/XaPDXd1qh72+QYyB0Z2VzWmO9zsnb9Uq/dwu8LGeQqnyu6 # 7SDGACvnXii2fb9+US492VTnXSnFKyqwgzUyFMtZK1/sHYTv6bG4TtQUygQxTN+Z # V+aJIlKO2MqZ7bKrAnOzS9m6NgoTdWOq11bTOZwKlIEV/EhV9SWkDmdpR/hPPT2v # 6TEj4F8PT/zHjRezIU5c/DGlt/VhY/pK0XkJtEyMmmS1BMtjU/rqBZVMIm3dnxQs # /TBByr+Cf8Z1r7aifQVQ+WSqzjCCBr0wggSloAMCAQICEzMAAAA5O7Y3Gb8GHWcA # AAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX # YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg # Q29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRl # IEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoXDTM2MDMyMjIyMTMwNFow # VzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEo # MCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAyNDCCAiIwDQYJ # KoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeqlRYHNa265v4IY9fH8TKh # emHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo0dtS/EW6I/yEL/bLSY8h # KpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATvQVL4tcf03aTycsz8QeCd # M0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a1uv1zerOYMnsneRRwCbp # yW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1FyQfK0fVkaya8SmVHQ/t # Of23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfOGSWHIIV4YrTJTT6PNty5 # REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7ttOu1bVnXfHaqPYl2rPs # 20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJuz2MXMCt7iw7lFPG9LXK # Gjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxSCwyoGIq0PhaA7Y+VPct5 # pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOmVQop36wUVUYklUy++vDW # eEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3SkE/xIkgpfl22MM1itkZ # 35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8EBAMCAYYwEAYJKwYBBAGC # NxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPXLQaUEggxMBkGCSsGAQQB # gjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU # ci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDovL2Ny # bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0MjAx # MV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAwTgYIKwYBBQUHMAKGQmh0 # dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0MjAx # MV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOCAgEAFJQfOChP7onn6fLI # MKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D5W4wMwYeLystcEqfkjz4 # NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBYnbu0+THSuVHTe0VTTPVh # ily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSIvgn0JksVBVMYVI5QFu/q # hnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6aR9y34aiM1qmxaxBi6OU # nyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4wPKC5OmHm1DQIt/MNokbb # H3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7RTX8AdBPo0I6OEojf39z # uFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK/fg8B2qjW88MT/WF5V5u # vZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSKYBv0VisCzfxgeU+dquXW # 9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkwYTu/9dLeH2pDqeJZAABV # DWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVTQl0v4q8J/AUmQN5W4n10 # 1cY2L4A7GTQG1h32HHAvfQESWP0xghnEMIIZwAIBATBuMFcxCzAJBgNVBAYTAlVT # MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jv # c29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIdTRnITtcPV0gAAAAAAh0w # DQYJYIZIAWUDBAIBBQCggZAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwLwYJ # KoZIhvcNAQkEMSIEIKL3jznZcvDeQBlT4FnmMM/fK4eEbQWTAZVUNqOkUMCvMEIG # CisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8v # d3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAEggEAGeXh4U0g8GAFBCrX # IDhiovMhX0teGEfYJwbbcAvuAydzJM+6+h4rxuACgq0ZKxgalu9i4rsFgOBaTWKJ # OtRcOs20U0qItP4+OzRgbSAP2EfxgB0D8btmEZC7Eti6WhifxpXUNAgA19RohfF3 # D4o9njqmnNljMgTys1M7NNb/3n1owEmMpVLzJ6eZnFQA8YpQOpsERj4BEyx+7NrH # aQ+aboODhyL8E38XD5FkdxYFHr9giGbNVxZ2/omVCBLHWA6HXE5/kvSnXNL0CbX8 # F7nL+4/UdMRFfIXeVq9xVx8HtJmJaBVadGa3I7oilVyJ2fDvBEFufhLlNqHqXACv # pAtPP6GCF5QwgheQBgorBgEEAYI3AwMBMYIXgDCCF3wGCSqGSIb3DQEHAqCCF20w # ghdpAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsqhkiG9w0BCRABBKCCAUEEggE9 # MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQCAQUABCBkK8epeOcxB7F+ # Cxt3fHEpWoeHzh7wTyiKM0ew8jYffQIGaqqQ5MoJGBMyMDI2MTEwMjE0NDczNy4y # MDlaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu # Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv # cmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScw # JQYDVQQLEx5uU2hpZWxkIFRTUyBFU046RjAwMi0wNUUwLUQ5NDcxJTAjBgNVBAMT # HE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2WgghHqMIIHIDCCBQigAwIBAgIT # MwAAAiAk4ebgF7m0jgABAAACIDANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGlt # ZS1TdGFtcCBQQ0EgMjAxMDAeFw0yNjAyMTkxOTM5NTJaFw0yNzA1MTcxOTM5NTJa # MIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH # UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQL # ExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxk # IFRTUyBFU046RjAwMi0wNUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1l # LVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDR # YY7yr7ijW6CR178uKveIMufutWOicxgJwKOce/2GOQceus6ZWfX14i3jNg3JOP7M # GJMkOAucwWBwiA8URp+ZYkGjpVoVkGZsV27WjqLwpf2AwqBsJ/TzqwE7JFFaxup3 # Ldxj8GjdJymDFRrdVN/pYHoBFrjD1IkIDu8b1CWn8tgomiKRSY+STvJq99mVkdph # MBIUGOegQny8qRd24VME0xi8Oomks9Zq9EjDeKHGpvAbXUEQ6m3cROoEPhTE/miw # eQH9TqJt3IOsqPv3L8urojB747XBC2y0CDIHlKLcLl3ZG8D7JXKnWTFen3msMPJp # cvrQ3zUBVJrH/mI3RxHmCh9ppDP0uG1+PJwk6H/x+sfoG9hW64xoXkpx6DEfNZNf # cXdKbXF28XEXdLNnzo3SLNVymeQJhNqOSKhnU84QnKmrjEk541JiurlDCkCWO9lU # BUMb9x0nyfXUbNRPVLgP+PTMRdXOowJdYCzCQfN2ZqL0s4YI28F1Dbn7Bgw2E4P1 # E9unsvMzJHtzhS2Th3TpCfBbOGalIlF9x/DJZ/ssm/yyzT9YtIFeqmfNxBPTE3aO # uh6HxmTICzfYAATvWNhBbo19QwsjPeA9JvhqTLC2KUNgrXroGy4eDZo0n7jFYjZk # Uih1Ty+8E6qEvV2Na6Z5gUyD5a+tHGDmq69CmUiHfwIDAQABo4IBSTCCAUUwHQYD # VR0OBBYEFNvInOCIhxGA8mY7l1g07UHvyNgzMB8GA1UdIwQYMBaAFJ+nFV0AXmJd # g/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCGTmh0dHA6Ly93d3cubWljcm9z # b2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0El # MjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4wXAYIKwYBBQUHMAKGUGh0dHA6 # Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVGlt # ZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwGA1UdEwEB/wQCMAAwFgYDVR0l # AQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUA # A4ICAQCtKGBto1BSvm4WFI+J0NSyVhU1LHL7F3fbjZ2d7F5Kn/FCTBZXpzrDVl63 # FLRNcIFpnJy4/nlg43r7T5sJPdo4Ms8ADSHQEJnHSu3x9UpjCzREBPi9+nHhvDgR # x/1WmBD6gQUZJLOhcN2TxW4KJyhinMtiBFtkNRZ2vmZ1MAdNXTm5d0Lwk3wzj+/f # 7VCCTWCXJSoqNa3VU/6sACHI97Evbnzg8bd3hxrfz6CcCVuf77egvRHinthJuwSR # ePP7aVmcevb1nWUIAICdBebHQOrzNIeWBIQwvcFaS3SFc+49rqrwQOMFDR4FYBzS # 7b0QeBVxFuLL2iVu4KAHMNUhLLSD4iKLDFBNTOtTzTlhGvMgG77A1cjeQrDMHa6o # ReMDeUDqHUrxv8g7IRdIh+h0gDLkzN0xIuzli0Bv7JtybGJbV6JxaDF4CzSCIMRp # K59nI6iKo4LgnbQBZJW7+6akYsKG/pXPlfxNv2InpD10tSCkCvw9kr6W1+NRN+Eu # ZczRgAwWlcK9XJZ3uu/v/oxHtO7/kmVIs51F9qV6Y2QNXd6tU46YPrK98m2QDys+ # lvLNimK0e1xZ7Z1GawKohKGvlLALWDlZQqgHfJ31CB0LlIDI7iLyYTpd2iyKjqsk # bQiyMtICH+RmH/oCg7JOK0ZA3XIMba9aSWgBF3QZ6pG3EGeQqjCCB3EwggVZoAMC # AQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNV # BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w # HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29m # dCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDEwMB4XDTIxMDkzMDE4MjIy # NVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp # bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw # b3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAw # ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDk4aZM57RyIQt5osvXJHm9 # DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25PhdgM/9cT8dm95VTcVrifkpa/rg2 # Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPFdvWGUNzBRMhxXFExN6AKOG6N # 7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6GnszrYBbfowQHJ1S/rboYiXc # ag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBpDco2LXCOMcg1KL3jtIckw+DJ # j361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50ZuyjLVwIYwXE8s4mKyzbnijYjk # lqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3EXzTdEonW/aUgfX782Z5F37Zy # L9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0lBw0gg/wEPK3Rxjtp+iZfD9M # 269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1qGFphAXPKZ6Je1yh2AuIzGHLX # pyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ+QuJYfM2BjUYhEfb3BvR/bLU # HMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PAPBXbGjfHCBUYP3irRbb1Hode # 2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkwEgYJKwYBBAGCNxUBBAUCAwEA # ATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxGNSnPEP8vBO4wHQYDVR0OBBYE # FJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARVMFMwUQYMKwYBBAGCN0yDfQEB # MEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMv # RG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAKBggrBgEFBQcDCDAZBgkrBgEE # AYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB # /zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEug # SaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9N # aWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsG # AQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jv # b0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAnVV9/Cqt # 4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0xM7U518JxNj/aZGx80HU5bbsP # MeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmCVgADsAW+iehp4LoJ7nvfam++ # Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449xvNo32X2pFaq95W2KFUn0CS9 # QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wMnosZiefwC2qBwoEZQhlSdYo2 # wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDSPeZKPmY7T7uG+jIa2Zb0j/aR # AfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2dY3RILLFORy3BFARxv2T5JL5z # bcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxnGSgkujhLmm77IVRrakURR6nx # t67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+CrvsQWY9af3LwUFJfn6Tvsv4O+S3 # Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokLjzbaukz5m/8K6TT4JDVnK+AN # uOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL6Xu/OHBE0ZDxyKs6ijoIYn/Z # cGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNNMIICNQIBATCB+aGB0aSBzjCB # yzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl # ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMc # TWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEnMCUGA1UECxMeblNoaWVsZCBU # U1MgRVNOOkYwMDItMDVFMC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1T # dGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQCTGA9vpsJ6glqCLmI0rggGx4YE # EqCBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw # DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x # JjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMA0GCSqGSIb3 # DQEBCwUAAgUA7pL9HDAiGA8yMDI2MTEwMjEyMTQyMFoYDzIwMjYxMTAzMTIxNDIw # WjB0MDoGCisGAQQBhFkKBAExLDAqMAoCBQDukv0cAgEAMAcCAQACAgSrMAcCAQAC # AhSRMAoCBQDulE6cAgEAMDYGCisGAQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwKg # CjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZIhvcNAQELBQADggEBADj9E34d # MSCOfTcHBfUs+2tv/3gGySEGKyZNjlxFHTkw4LuDq+WzcE0AFo2XE5mNUaD+oY/g # ++eKIIoEd6d7kNJKWGeydEnQQHX1t08/1p8wMnFmOJfiJ5IVoiAASH3sNWP5Z+SG # GUvGWdXrfFDySG1aLmotfH5gY+hfwTrK43+2ZVgd+VB1172mR5BTyVe5LjKDLyQf # Z8rXl5QLpwDx61fLTn3A/ev6H7fLNiNqnISJwpa7FPDMaXBJj1krj76t6KfCmwad # UsVGlWdUJhQv61RXVRYUC/31A2emR482lnyR/fx8u3RY2e9Il6rx/mkILfzDueeq # HKBX25vDjFPDQvYxggQNMIIECQIBATCBkzB8MQswCQYDVQQGEwJVUzETMBEGA1UE # CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z # b2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQ # Q0EgMjAxMAITMwAAAiAk4ebgF7m0jgABAAACIDANBglghkgBZQMEAgEFAKCCAUow # GgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCDdIWRE # KKlu9FNVDFoNvPIaOEKtYeS550sJYYezcK7KBjCB+gYLKoZIhvcNAQkQAi8xgeow # gecwgeQwgb0EION7vyOlPA1VqlEp0QIVGlNd8S5YWBnKj97LuTWHSO2vMIGYMIGA # pH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT # B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE # AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAIgJOHm4Be5tI4A # AQAAAiAwIgQgZCelp6Ygv320LHujoCeFRX8oZzewVwqSLYwrizmV+fswDQYJKoZI # hvcNAQELBQAEggIAHO0lMngZqTxzGycJ+md4ejRtMdnrM6FVJYrX6KADgXRMuu8l # 5QDheehGpQDMnwMreum9muqS0c+J46VxRlV91Dsgr1J9LIrC3nTXEYaJzDNBsMq3 # i0Q3wyGRKSW0V6KZDobKki+LDn+bJXnqHmQxh4Eq6E3dd8cf27sAqR6VGuu2XTTv # 538S3s5L1e/2Rvv1+nRAqAIhFFMSeGQ7pG1OqVFPU+JObt727jFLTOnroqUWPdtu # +eeTJjRoKfK5Go72SfYMjveTTHl4SbAgE1GDGY3nw72F5u2STRQBZDxus9pL6b2o # P3hBjvmtYTio6MTEukNbgKfM3aC4p2EEIE+0r4FKoLR93ijxzHgWiqOz53pkejE7 # lGuFtTl3PCihp/N1TFkuW2uK3VeMD9DxOgP65T404QrxrmhISABNrCQeiywmYpvh # NJIvTo5IyipBGfJQP3fOMYStr/6CWnGI6rPhFlBOxcDWPbjDX3ZhwWy09+P4sfes # f4qZivsU1Rhd4EhQhu8vyfmZWLLS2xsBwCcnnx2Ge+2o/lC3kNYLULW800tx66ia # Oaew8yX9tIw+s9OZnEEMO88AnLOQk1rhHSkYsSaHBf77D6fXkcmv0B3IhJcNNPHP # bWSQVCZ1XHuxBkITWqUh14lw4MbO5zrwa4phQkO7RwAG9udSA694AIalyTo= # SIG # End signature block |