Private/Coverage.ps1
|
function New-ICStructuredIssue { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidatePattern('^[A-Z][A-Z0-9_]+$')] [string]$Code, [Parameter(Mandatory)] [ValidateSet('Info', 'Warning', 'Error')] [string]$Severity, [Parameter(Mandatory)] [string]$Component, [Parameter(Mandatory)] [string]$Message, [AllowNull()] [string]$Source, [AllowNull()] [System.Collections.IDictionary]$Details ) return [pscustomobject][ordered]@{ code = $Code severity = $Severity component = $Component message = $Message source = $Source details = if ($null -ne $Details) { $Details } else { [ordered]@{} } } } function ConvertTo-ICStructuredIssue { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Component, [Parameter(Mandatory)] [string]$Message, [ValidateSet('Warning', 'Error')] [string]$Kind = 'Warning', [AllowNull()] [string]$Source ) $code = 'SOURCE_WARNING' $severity = if ($Kind -eq 'Error') { 'Error' } else { 'Warning' } switch -Regex ($Message) { '(?i)access.+denied|denied.+access|privilege|requires? elevation|not elevated' { $code = 'ACCESS_DENIED' break } '(?i)event channel.+unavailable|event log.+unavailable|channel.+not found' { $code = 'CHANNEL_UNAVAILABLE' break } '(?i)cmdlets? (are|is) unavailable|command.+not found|executable.+not found' { $code = 'COMMAND_UNAVAILABLE' break } '(?i)timed? out|timeout' { $code = 'TIMEOUT' break } '(?i)bounded|truncat|limit.+reached|maximum.+reached' { $code = 'LIMIT_REACHED' break } '(?i)insufficient.+space|not enough.+space|disk.+full' { $code = 'INSUFFICIENT_SPACE' break } '(?i)unavailable|does not exist|not available|missing' { $code = 'SOURCE_UNAVAILABLE' break } '(?i)failed|failure|fatal|error' { $code = 'COLLECTION_ERROR' break } } return New-ICStructuredIssue ` -Code $code ` -Severity $severity ` -Component $Component ` -Message $Message ` -Source $Source } function Get-ICCollectorIssue { [CmdletBinding()] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$CollectorResults ) $issues = New-Object System.Collections.ArrayList $seen = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::Ordinal) foreach ($result in @($CollectorResults)) { $component = [string](Get-ICPropertyValue -InputObject $result -Name 'name' -Default 'Unknown') if ([string]::IsNullOrWhiteSpace($component)) { $component = 'Unknown' } foreach ($existingIssue in @(Get-ICPropertyValue -InputObject $result -Name 'issues' -Default @())) { if ($null -eq $existingIssue) { continue } $message = if ($existingIssue -is [string]) { [string]$existingIssue } else { [string](Get-ICPropertyValue -InputObject $existingIssue -Name 'message') } if ([string]::IsNullOrWhiteSpace($message)) { continue } $issueComponent = [string](Get-ICPropertyValue -InputObject $existingIssue -Name 'component' -Default $component) if ([string]::IsNullOrWhiteSpace($issueComponent)) { $issueComponent = $component } $severity = [string](Get-ICPropertyValue -InputObject $existingIssue -Name 'severity' -Default 'Warning') if ($severity -notin @('Info', 'Warning', 'Error')) { $severity = 'Warning' } $code = [string](Get-ICPropertyValue -InputObject $existingIssue -Name 'code') if ($code -notmatch '^[A-Z][A-Z0-9_]+$') { $code = if ($severity -eq 'Error') { 'COLLECTION_ERROR' } else { 'SOURCE_WARNING' } } $sourceValue = Get-ICPropertyValue -InputObject $existingIssue -Name 'source' $source = if ($null -ne $sourceValue) { [string]$sourceValue } else { $null } $detailsValue = Get-ICPropertyValue -InputObject $existingIssue -Name 'details' $details = if ($detailsValue -is [System.Collections.IDictionary]) { $detailsValue } else { [ordered]@{} } $normalized = New-ICStructuredIssue ` -Code $code ` -Severity $severity ` -Component $issueComponent ` -Message $message ` -Source $source ` -Details $details $identity = '{0}|{1}|{2}|{3}' -f $normalized.code, $normalized.severity, $normalized.component, $normalized.message if ($seen.Add($identity)) { [void]$issues.Add($normalized) } } foreach ($warning in @(Get-ICPropertyValue -InputObject $result -Name 'warnings' -Default @())) { if (-not [string]::IsNullOrWhiteSpace([string]$warning)) { $normalized = ConvertTo-ICStructuredIssue -Component $component -Message ([string]$warning) $identity = '{0}|{1}|{2}|{3}' -f $normalized.code, $normalized.severity, $normalized.component, $normalized.message if ($seen.Add($identity)) { [void]$issues.Add($normalized) } } } $errorMessage = [string](Get-ICPropertyValue -InputObject $result -Name 'error') if (-not [string]::IsNullOrWhiteSpace($errorMessage)) { $normalized = ConvertTo-ICStructuredIssue -Component $component -Message $errorMessage -Kind Error $identity = '{0}|{1}|{2}|{3}' -f $normalized.code, $normalized.severity, $normalized.component, $normalized.message if ($seen.Add($identity)) { [void]$issues.Add($normalized) } } } return @($issues) } function Get-ICPrivacyScope { [CmdletBinding()] param( [Parameter(Mandatory)] [System.Collections.IDictionary]$Configuration ) $dataHandlingProfile = Get-ICPropertyValue -InputObject $Configuration -Name 'DataHandlingProfile' -Default 'Full' return [ordered]@{ dataHandlingProfile = [string]$dataHandlingProfile processCommandLines = [bool](Get-ICPropertyValue -InputObject $Configuration -Name 'IncludeProcessCommandLines' -Default $false) nativeEventLogs = [bool](Get-ICPropertyValue -InputObject $Configuration -Name 'ExportEvtx' -Default $false) scheduledTaskXml = [bool](Get-ICPropertyValue -InputObject $Configuration -Name 'ExportScheduledTaskXml' -Default $false) defenderPreferences = [bool](Get-ICPropertyValue -InputObject $Configuration -Name 'CollectDefenderPreferences' -Default $false) windowsUpdateHistory = [bool](Get-ICPropertyValue -InputObject $Configuration -Name 'CollectWindowsUpdateHistory' -Default $false) signedDriverInventory = [bool](Get-ICPropertyValue -InputObject $Configuration -Name 'CollectSignedDrivers' -Default $false) runningExecutableHashes = [bool](Get-ICPropertyValue -InputObject $Configuration -Name 'HashProcessExecutables' -Default $false) persistenceFileHashes = [bool](Get-ICPropertyValue -InputObject $Configuration -Name 'HashPersistenceFiles' -Default $false) spreadsheetSafeCsv = [bool](Get-ICPropertyValue -InputObject $Configuration -Name 'SpreadsheetSafeCsv' -Default $true) containsPotentiallySensitiveData = $true } } function Get-ICResourceLimit { [CmdletBinding()] param( [Parameter(Mandatory)] [System.Collections.IDictionary]$Configuration ) $keys = @( 'EventLookbackHours', 'MaximumEventsPerLog', 'MaximumEvtxBytesPerLog', 'MaximumExecutableHashes', 'MaximumWindowsUpdateHistory', 'MaximumSignedDrivers', 'MaximumFirewallRules', 'MaximumPrefetchFiles', 'MaximumArtifactFileBytes', 'MaximumCapsuleBytes', 'NativeCommandTimeoutSeconds', 'MaximumNativeOutputBytes', 'MaximumTimelineEntries', 'MaximumArchiveEntries', 'MaximumArchiveEntryBytes', 'MaximumArchiveExpandedBytes', 'MaximumArchiveCompressionRatio' ) $limits = [ordered]@{} foreach ($key in $keys) { $value = Get-ICPropertyValue -InputObject $Configuration -Name $key if ($null -ne $value) { $limits[$key] = $value } } return $limits } function New-ICCoverageData { [CmdletBinding()] param( [Parameter(Mandatory)] [object]$Context ) $configuration = Get-ICPropertyValue -InputObject $Context -Name 'Configuration' if (-not ($configuration -is [System.Collections.IDictionary])) { throw 'Coverage generation requires a resolved configuration dictionary.' } $selectedCollectors = @(Get-ICPropertyValue -InputObject $configuration -Name 'Collectors' -Default @()) $collectorResults = @(Get-ICPropertyValue -InputObject $Context -Name 'CollectorResults' -Default @()) $entries = New-Object System.Collections.ArrayList foreach ($name in @($script:ICCollectorDefinitions.Keys)) { $selected = $name -in $selectedCollectors $result = @( $collectorResults | Where-Object { (Get-ICPropertyValue -InputObject $_ -Name 'name') -eq $name } | Select-Object -First 1 ) $resultObject = if ($result.Count -gt 0) { $result[0] } else { $null } $status = if (-not $selected) { 'NotSelected' } elseif ($null -eq $resultObject) { 'NotRun' } else { [string](Get-ICPropertyValue -InputObject $resultObject -Name 'status' -Default 'Failed') } if ($status -notin @('NotSelected', 'NotRun', 'Succeeded', 'Partial', 'Failed', 'Skipped')) { $status = 'Failed' } $entryIssues = @( if ($null -ne $resultObject) { Get-ICCollectorIssue -CollectorResults @($resultObject) } ) $outputFileCount = 0 if ($null -ne $resultObject) { $outputFileCount = @(Get-ICPropertyValue -InputObject $resultObject -Name 'outputFiles' -Default @()).Count } [void]$entries.Add([pscustomobject][ordered]@{ name = $name selected = $selected status = $status outputFiles = $outputFileCount issueCount = $entryIssues.Count issues = @($entryIssues) }) } $allIssues = @(Get-ICCollectorIssue -CollectorResults $collectorResults) $summary = [ordered]@{ totalCollectors = @($script:ICCollectorDefinitions.Keys).Count selectedCollectors = @($entries | Where-Object selected -eq $true).Count notSelected = @($entries | Where-Object status -eq 'NotSelected').Count notRun = @($entries | Where-Object status -eq 'NotRun').Count succeeded = @($entries | Where-Object status -eq 'Succeeded').Count partial = @($entries | Where-Object status -eq 'Partial').Count failed = @($entries | Where-Object status -eq 'Failed').Count skipped = @($entries | Where-Object status -eq 'Skipped').Count issueCount = $allIssues.Count errorCount = @($allIssues | Where-Object severity -eq 'Error').Count warningCount = @($allIssues | Where-Object severity -eq 'Warning').Count limitsReached = @($allIssues | Where-Object code -eq 'LIMIT_REACHED').Count } return [ordered]@{ '$schema' = $script:ICCoverageSchema schemaVersion = $script:ICSchemaVersion capsuleId = $Context.CapsuleId generatedAtUtc = [datetime]::UtcNow.ToString('o') summary = $summary privacyScope = Get-ICPrivacyScope -Configuration $configuration resourceLimits = Get-ICResourceLimit -Configuration $configuration collectors = @($entries) issues = @($allIssues) } } function Write-ICCoverageData { [CmdletBinding()] param( [Parameter(Mandatory)] [object]$Context ) $coverage = New-ICCoverageData -Context $Context $path = Join-Path $Context.MetadataPath 'coverage.json' [void](Write-ICJsonFile -Path $path -InputObject $coverage -Depth 30) $Context | Add-Member -NotePropertyName Coverage -NotePropertyValue $coverage -Force $Context | Add-Member -NotePropertyName CoveragePath -NotePropertyValue $path -Force return $path } |