modules/normalizers/Normalize-KubeBench.ps1
|
#Requires -Version 7.4 <# .SYNOPSIS Normalizer for kube-bench wrapper output. .DESCRIPTION Converts v1 kube-bench wrapper output to v2 FindingRows. Maps kube-bench FAIL/WARN checks onto the AKS cluster AzureResource entity. #> [CmdletBinding()] param () . "$PSScriptRoot\..\shared\Schema.ps1" . "$PSScriptRoot\..\shared\Canonicalize.ps1" function Normalize-KubeBench { [CmdletBinding()] param ( [Parameter(Mandatory)] [PSCustomObject] $ToolResult ) if ($ToolResult.Status -ne 'Success' -or -not $ToolResult.Findings) { return @() } $runId = [guid]::NewGuid().ToString() $normalized = [System.Collections.Generic.List[PSCustomObject]]::new() function ConvertTo-KubeBenchStringArray { param([object]$Value) $result = [System.Collections.Generic.List[string]]::new() $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($item in @($Value)) { if ($null -eq $item) { continue } $text = [string]$item if ([string]::IsNullOrWhiteSpace($text)) { continue } $trimmed = $text.Trim() if ($seen.Add($trimmed)) { $result.Add($trimmed) } } return $result.ToArray() } function Resolve-KubeBenchImpactFromSeverity { param([string]$Severity) switch -Regex ($Severity) { '^(?i)(critical|high)$' { 'High' } '^(?i)medium$' { 'Medium' } default { 'Low' } } } function Resolve-KubeBenchSnippetLanguage { param([string]$Text) if ([string]::IsNullOrWhiteSpace($Text)) { return '' } if ($Text -match '(?im)^\s*(apiVersion|kind|metadata|spec)\s*:') { return 'yaml' } return 'bash' } function ConvertTo-KubeBenchFrameworks { param( [object]$RawFrameworks, [string]$ControlId, [string]$ResourceId ) $frameworks = [System.Collections.Generic.List[hashtable]]::new() foreach ($fw in @($RawFrameworks)) { if ($null -eq $fw) { continue } $kind = '' $control = '' if ($fw -is [System.Collections.IDictionary]) { $kind = [string]($fw['kind'] ?? $fw['Kind'] ?? $fw['Name']) $control = [string]($fw['controlId'] ?? $fw['ControlId']) } else { $kind = [string]($fw.kind ?? $fw.Kind ?? $fw.Name) $control = [string]($fw.controlId ?? $fw.ControlId) } if ([string]::IsNullOrWhiteSpace($kind)) { continue } if ([string]::IsNullOrWhiteSpace($control)) { $control = $ControlId } if ([string]::IsNullOrWhiteSpace($control)) { continue } $frameworks.Add(@{ kind = $kind.Trim(); controlId = $control.Trim() }) | Out-Null } if ($frameworks.Count -eq 0 -and -not [string]::IsNullOrWhiteSpace($ControlId)) { $frameworks.Add(@{ kind = 'CIS Kubernetes Benchmark'; controlId = $ControlId }) | Out-Null if (-not [string]::IsNullOrWhiteSpace($ResourceId) -and $ResourceId -match '(?i)/providers/microsoft\.containerservice/managedclusters/') { $frameworks.Add(@{ kind = 'CIS-AKS'; controlId = $ControlId }) | Out-Null } } $deduped = [System.Collections.Generic.List[hashtable]]::new() $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) foreach ($fw in $frameworks) { $key = "$($fw.kind)|$($fw.controlId)" if ($seen.Add($key)) { $deduped.Add($fw) | Out-Null } } return $deduped.ToArray() } foreach ($f in $ToolResult.Findings) { $statusRaw = if ($f.PSObject.Properties['Status'] -and $f.Status) { [string]$f.Status } else { '' } if ($statusRaw -and $statusRaw -notmatch '^(?i)(FAIL|WARN)$') { continue } $rawId = if ($f.PSObject.Properties['ResourceId'] -and $f.ResourceId) { [string]$f.ResourceId } else { '' } if (-not $rawId) { continue } $subId = '' $rg = '' if ($rawId -match '/subscriptions/([^/]+)') { $subId = $Matches[1] } if ($rawId -match '/resourceGroups/([^/]+)') { $rg = $Matches[1] } try { $canonicalId = (ConvertTo-CanonicalEntityId -RawId $rawId -EntityType 'AzureResource').CanonicalId } catch { $canonicalId = $rawId.ToLowerInvariant() } $severity = if ($f.PSObject.Properties['Severity'] -and $f.Severity) { switch -Regex ([string]$f.Severity) { '^(?i)critical$' { 'Critical' } '^(?i)high$' { 'High' } '^(?i)medium$' { 'Medium' } '^(?i)low$' { 'Low' } '^(?i)info$' { 'Info' } default { 'Medium' } } } else { if ($statusRaw -match '^(?i)FAIL$') { 'High' } else { 'Medium' } } $findingId = if ($f.PSObject.Properties['Id'] -and $f.Id) { [string]$f.Id } else { [guid]::NewGuid().ToString() } $title = if ($f.PSObject.Properties['Title'] -and $f.Title) { [string]$f.Title } else { 'kube-bench finding' } $detail = if ($f.PSObject.Properties['Detail']) { [string]$f.Detail } else { '' } $remediation = if ($f.PSObject.Properties['Remediation']) { [string]$f.Remediation } else { '' } $learnMore = if ($f.PSObject.Properties['LearnMoreUrl']) { [string]$f.LearnMoreUrl } else { '' } $controlId = if ($f.PSObject.Properties['ControlId'] -and $f.ControlId) { [string]$f.ControlId } else { '' } $statusUpper = if ($statusRaw) { $statusRaw.ToUpperInvariant() } else { '' } $ruleId = if (-not [string]::IsNullOrWhiteSpace($controlId)) { "kube-bench:$controlId" } else { '' } $pillar = if ($f.PSObject.Properties['Pillar'] -and $f.Pillar) { [string]$f.Pillar } else { 'Security' } $impact = if ($f.PSObject.Properties['Impact'] -and $f.Impact) { [string]$f.Impact } else { Resolve-KubeBenchImpactFromSeverity -Severity $severity } $frameworks = ConvertTo-KubeBenchFrameworks -RawFrameworks $(if ($f.PSObject.Properties['Frameworks']) { $f.Frameworks } else { @() }) -ControlId $controlId -ResourceId $rawId $baselineTags = ConvertTo-KubeBenchStringArray -Value @( $(if ($f.PSObject.Properties['BaselineTags']) { @($f.BaselineTags) } else { @() }), $controlId, $statusUpper ) $deepLink = if ($f.PSObject.Properties['DeepLinkUrl'] -and $f.DeepLinkUrl) { [string]$f.DeepLinkUrl } else { $learnMore } $entityRefs = ConvertTo-KubeBenchStringArray -Value @( $(if ($f.PSObject.Properties['EntityRefs']) { @($f.EntityRefs) } else { @() }), $rawId, $(if ($f.PSObject.Properties['NodeRef']) { [string]$f.NodeRef } else { '' }) ) $toolVersion = if ($f.PSObject.Properties['ToolVersion'] -and $f.ToolVersion) { [string]$f.ToolVersion } else { '' } $remediationSnippets = @() if ($f.PSObject.Properties['RemediationSnippets'] -and @($f.RemediationSnippets).Count -gt 0) { $remediationSnippets = @($f.RemediationSnippets | ForEach-Object { if ($null -eq $_) { return } if ($_ -is [System.Collections.IDictionary]) { @{ language = [string]$_['language'] content = [string]$_['content'] } return } @{ language = [string]$_.language content = [string]$_.content } }) } elseif (-not [string]::IsNullOrWhiteSpace($remediation)) { $language = Resolve-KubeBenchSnippetLanguage -Text $remediation if (-not [string]::IsNullOrWhiteSpace($language)) { $remediationSnippets = @(@{ language = $language content = $remediation }) } } $row = New-FindingRow -Id $findingId ` -Source 'kube-bench' -EntityId $canonicalId -EntityType 'AzureResource' ` -Title $title -RuleId $ruleId -Compliant $false -ProvenanceRunId $runId ` -Platform 'Azure' -Category 'KubernetesNodeSecurity' -Severity $severity ` -Detail $detail -Remediation $remediation ` -LearnMoreUrl $learnMore -ResourceId $rawId ` -SubscriptionId $subId -ResourceGroup $rg ` -Pillar $pillar -Impact $impact ` -Frameworks @($frameworks) ` -Controls $(if ($controlId) { @($controlId) } else { @() }) ` -DeepLinkUrl $deepLink ` -RemediationSnippets @($remediationSnippets) ` -BaselineTags @($baselineTags) ` -EntityRefs @($entityRefs) ` -ToolVersion $toolVersion if ($null -ne $row) { $normalized.Add($row) } } return @($normalized) } |