modules/Invoke-Falco.ps1
|
#requires -Version 7.0 <# .SYNOPSIS Wrapper for Falco runtime anomaly detection on AKS. .DESCRIPTION Default mode is non-intrusive query mode: reads Falco-related alerts from Microsoft Security alerts already present in Azure (for example Defender alert pipeline). Optional install mode (-InstallFalco) is explicit opt-in: installs Falco via Helm into each AKS cluster in scope, waits briefly, collects daemonset logs, and optionally uninstalls. .PARAMETER SubscriptionId Azure subscription ID (GUID). Required. .PARAMETER ClusterArmIds Optional explicit AKS cluster ARM IDs to limit scope. .PARAMETER InstallFalco Opt-in install mode. When omitted, query mode is used. .PARAMETER UninstallFalco In install mode, uninstall the Falco Helm release after collection. .PARAMETER KubeconfigPath Optional path to an existing kubeconfig file. In install mode this skips Azure Resource Graph discovery and `az aks get-credentials`, and targets the cluster reachable via this kubeconfig. Ignored in query mode (query mode reads Azure-side alerts, not cluster state). The file MUST exist when set explicitly; URLs are rejected. .PARAMETER KubeContext Optional kubeconfig context name passed to `helm` and `kubectl` via `--kube-context` / `--context` in install mode. .PARAMETER Namespace Namespace used in install mode for the Falco Helm release and the `kubectl logs daemonset/falco` collection. Default 'falco'. .PARAMETER KubeAuthMode Auth mode applied to the kubeconfig before each install-mode invocation. One of Default | Kubelogin | WorkloadIdentity. See docs/consumer/k8s-auth.md. .PARAMETER KubeloginServerId / KubeloginClientId / KubeloginTenantId AAD args for kubelogin convert-kubeconfig. ClientId+TenantId enables spn flow; otherwise azurecli flow is used. .PARAMETER WorkloadIdentityClientId / WorkloadIdentityTenantId / WorkloadIdentityServiceAccountToken Federated identity args. Path-or-value token; sets AZURE_CLIENT_ID / AZURE_TENANT_ID / AZURE_FEDERATED_TOKEN_FILE. #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [Parameter(Mandatory)] [string] $SubscriptionId, [string[]] $ClusterArmIds, [switch] $InstallFalco, [switch] $UninstallFalco, [ValidateRange(1, 60)] [int] $CaptureMinutes = 5, [string] $KubeconfigPath, [string] $KubeContext, [string] $Namespace = 'falco', [ValidateSet('Default', 'Kubelogin', 'WorkloadIdentity')] [string] $KubeAuthMode = 'Default', [string] $KubeloginServerId, [string] $KubeloginClientId, [string] $KubeloginTenantId, [string] $WorkloadIdentityClientId, [string] $WorkloadIdentityTenantId, [string] $WorkloadIdentityServiceAccountToken ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $retryPath = Join-Path $PSScriptRoot 'shared' 'Retry.ps1' if (Test-Path $retryPath) { . $retryPath } if (-not (Get-Command Invoke-WithRetry -ErrorAction SilentlyContinue)) { function Invoke-WithRetry { param([scriptblock]$ScriptBlock, [int]$MaxAttempts = 3) & $ScriptBlock } } $sanitizePath = Join-Path $PSScriptRoot 'shared' 'Sanitize.ps1' if (Test-Path $sanitizePath) { . $sanitizePath } if (-not (Get-Command Remove-Credentials -ErrorAction SilentlyContinue)) { function Remove-Credentials { param([string]$Text) return $Text } } $errorsPath = Join-Path $PSScriptRoot 'shared' 'Errors.ps1' if (Test-Path $errorsPath) { . $errorsPath } if (-not (Get-Command New-FindingError -ErrorAction SilentlyContinue)) { function New-FindingError { param([string]$Source,[string]$Category,[string]$Reason,[string]$Remediation,[string]$Details) return [pscustomobject]@{ Source=$Source; Category=$Category; Reason=$Reason; Remediation=$Remediation; Details=$Details } } } if (-not (Get-Command Format-FindingErrorMessage -ErrorAction SilentlyContinue)) { function Format-FindingErrorMessage { param([Parameter(Mandatory)]$FindingError) $line = "[{0}] {1}: {2}" -f $FindingError.Source, $FindingError.Category, $FindingError.Reason if ($FindingError.Remediation) { $line += " Action: $($FindingError.Remediation)" } return $line } } $kubeAuthPath = Join-Path $PSScriptRoot 'shared' 'KubeAuth.ps1' if (Test-Path $kubeAuthPath) { . $kubeAuthPath } $envelopePath = Join-Path $PSScriptRoot 'shared' 'New-WrapperEnvelope.ps1' if (Test-Path $envelopePath) { . $envelopePath } if (-not (Get-Command New-WrapperEnvelope -ErrorAction SilentlyContinue)) { function New-WrapperEnvelope { param([string]$Source,[string]$Status='Failed',[string]$Message='',[object[]]$FindingErrors=@()) return [PSCustomObject]@{ Source=$Source; SchemaVersion='1.0'; Status=$Status; Message=$Message; Findings=@(); Errors=@($FindingErrors) } } } # Bootstrap Invoke-WithTimeout for CLI timeout protection $cliTimeoutPath = Join-Path $PSScriptRoot 'shared' 'CliTimeout.ps1' if (Test-Path $cliTimeoutPath) { . $cliTimeoutPath } $result = [ordered]@{ SchemaVersion = '1.0' Source = 'falco' Status = 'Success' Message = '' Findings = @() Errors = @() Subscription = $SubscriptionId Timestamp = (Get-Date).ToUniversalTime().ToString('o') } # Validate -KubeconfigPath up front (applies to install mode; in query mode the # file is not used but we still reject obviously broken values to keep the # param contract consistent across wrappers). $kubeconfigModeRequested = $PSBoundParameters.ContainsKey('KubeconfigPath') -or ` $PSBoundParameters.ContainsKey('KubeContext') # Validate KubeAuthMode prerequisites up front. Default mode is a no-op. # Falco install mode is the only path that touches the cluster, but we # validate regardless so misconfigured query-mode invocations also fail # fast (consistent contract across the three K8s wrappers). try { Assert-KubeAuthMode ` -Mode $KubeAuthMode ` -KubeloginServerId $KubeloginServerId ` -KubeloginClientId $KubeloginClientId ` -KubeloginTenantId $KubeloginTenantId ` -WorkloadIdentityClientId $WorkloadIdentityClientId ` -WorkloadIdentityTenantId $WorkloadIdentityTenantId ` -WorkloadIdentityServiceAccountToken $WorkloadIdentityServiceAccountToken } catch { $authErr = New-FindingError -Source 'wrapper:falco' -Category 'InvalidParameter' -Reason (Remove-Credentials -Text "$_") -Remediation 'Check KubeAuthMode parameters.' return (New-WrapperEnvelope -Source 'falco' -Status 'Failed' -Message (Format-FindingErrorMessage $authErr) -FindingErrors @($authErr)) } $resolvedKubeconfig = $null if ($PSBoundParameters.ContainsKey('KubeconfigPath')) { if ([string]::IsNullOrWhiteSpace($KubeconfigPath)) { $valErr = New-FindingError -Source 'wrapper:falco' -Category 'InvalidParameter' -Reason 'Invalid -KubeconfigPath: value is empty.' -Remediation 'Provide a non-empty local file path via -KubeconfigPath.' return (New-WrapperEnvelope -Source 'falco' -Status 'Failed' -Message (Format-FindingErrorMessage $valErr) -FindingErrors @($valErr)) } if ($KubeconfigPath -match '^[a-z][a-z0-9+.-]*://') { $valErr = New-FindingError -Source 'wrapper:falco' -Category 'InvalidParameter' -Reason "Invalid -KubeconfigPath '$(Remove-Credentials -Text $KubeconfigPath)': URLs are not accepted; provide a local file path." -Remediation 'Use a local kubeconfig file path, not a URL.' return (New-WrapperEnvelope -Source 'falco' -Status 'Failed' -Message (Format-FindingErrorMessage $valErr) -FindingErrors @($valErr)) } if (-not (Test-Path -LiteralPath $KubeconfigPath -PathType Leaf)) { $valErr = New-FindingError -Source 'wrapper:falco' -Category 'NotFound' -Reason "Invalid -KubeconfigPath '$(Remove-Credentials -Text $KubeconfigPath)': file does not exist." -Remediation 'Provide an existing kubeconfig file path via -KubeconfigPath.' return (New-WrapperEnvelope -Source 'falco' -Status 'Failed' -Message (Format-FindingErrorMessage $valErr) -FindingErrors @($valErr)) } $resolvedKubeconfig = (Resolve-Path -LiteralPath $KubeconfigPath).ProviderPath } elseif ($kubeconfigModeRequested) { $candidate = if ($env:KUBECONFIG) { $env:KUBECONFIG } else { Join-Path $HOME '.kube' 'config' } if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) { $valErr = New-FindingError -Source 'wrapper:falco' -Category 'NotFound' -Reason "Invalid kubeconfig: -KubeContext was supplied but no kubeconfig found at '$(Remove-Credentials -Text $candidate)'. Set -KubeconfigPath or `$env:KUBECONFIG." -Remediation 'Set -KubeconfigPath or ensure $env:KUBECONFIG points to an existing kubeconfig file.' return (New-WrapperEnvelope -Source 'falco' -Status 'Failed' -Message (Format-FindingErrorMessage $valErr) -FindingErrors @($valErr)) } $resolvedKubeconfig = (Resolve-Path -LiteralPath $candidate).ProviderPath } $installKubeconfigMode = $InstallFalco -and $kubeconfigModeRequested if (-not $installKubeconfigMode) { if (-not (Get-Module -ListAvailable -Name Az.ResourceGraph)) { $result.Status = 'Skipped' $result.Message = 'Az.ResourceGraph module not installed; cannot discover AKS clusters or query alerts.' return [pscustomobject]$result } Import-Module Az.ResourceGraph -ErrorAction SilentlyContinue try { $null = Get-AzContext -ErrorAction Stop } catch { $result.Status = 'Skipped' $result.Message = 'Not signed in. Run Connect-AzAccount first.' return [pscustomobject]$result } } $clusters = @() if ($installKubeconfigMode) { $synthName = if ($KubeContext) { $KubeContext } else { 'kubeconfig-default' } $clusters += [pscustomobject]@{ id = "kubeconfig:$synthName" resourceGroup = '' name = $synthName kubeconfigPath = $resolvedKubeconfig kubeContext = $KubeContext } } elseif ($ClusterArmIds -and $ClusterArmIds.Count -gt 0) { foreach ($id in $ClusterArmIds) { $rg = if ($id -match '/resourceGroups/([^/]+)') { $Matches[1] } else { '' } $name = Split-Path $id -Leaf $clusters += [pscustomobject]@{ id = $id; resourceGroup = $rg; name = $name } } } else { try { $query = "Resources | where type =~ 'Microsoft.ContainerService/managedClusters' | where subscriptionId == '$SubscriptionId' | project id, name, resourceGroup" $argResp = Invoke-WithRetry -MaxAttempts 3 -ScriptBlock { Search-AzGraph -Query $using:query -First 200 -ErrorAction Stop } $clusters = @($argResp) } catch { $result.Status = 'Failed' $result.Message = "AKS discovery failed: $(Remove-Credentials -Text ([string]$_.Exception.Message))" return [pscustomobject]$result } } if (-not $clusters -or @($clusters).Count -eq 0) { $result.Status = 'Skipped' $result.Message = 'No AKS managed clusters in scope.' return [pscustomobject]$result } $clusterById = @{} foreach ($c in $clusters) { if ($c.id) { $clusterById[[string]$c.id.ToLowerInvariant()] = $c } } function Get-MatchValue { param([string]$Text, [string]$Pattern) if (-not $Text) { return '' } if ($Text -match $Pattern) { return [string]$Matches[1] } return '' } function Resolve-AksResourceId { param([pscustomobject]$Alert, [hashtable]$ClusterMap) $candidates = [System.Collections.Generic.List[string]]::new() foreach ($p in @('resourceId', 'resourceID', 'ResourceId', 'CompromisedEntity')) { if ($Alert.PSObject.Properties[$p] -and $Alert.$p) { $candidates.Add([string]$Alert.$p) } } if ($Alert.PSObject.Properties['ExtendedProperties'] -and $Alert.ExtendedProperties) { $candidates.Add([string]$Alert.ExtendedProperties) } foreach ($raw in $candidates) { if (-not $raw) { continue } if ($raw -match '(?i)(/subscriptions/[^/\s]+/resourcegroups/[^/\s]+/providers/microsoft\.containerservice/managedclusters/[^/\s"''\\]+)') { $id = $Matches[1].ToLowerInvariant() if ($ClusterMap.ContainsKey($id)) { return $id } } } return '' } function Convert-PriorityToSeverity { param([string]$Priority) switch -Regex (($Priority ?? '').Trim()) { '^(?i)critical$' { return 'Critical' } '^(?i)error$' { return 'High' } '^(?i)warning$' { return 'Medium' } '^(?i)notice$' { return 'Low' } default { return 'Info' } } } function New-FalcoRuleId { param([string]$RuleName) if ([string]::IsNullOrWhiteSpace($RuleName)) { return 'falco:runtime-alert' } $slug = ($RuleName.ToLowerInvariant() -replace '[^a-z0-9]+', '-').Trim('-') if ([string]::IsNullOrWhiteSpace($slug)) { $slug = 'runtime-alert' } return "falco:$slug" } function Get-FalcoMitreMapping { param( [string]$RuleName, [string]$Priority, [string]$Detail ) $text = "$RuleName $Detail".ToLowerInvariant() if ($text -match 'shell|exec') { return @{ Tactics = @('Execution') Techniques = @('T1059') } } if ($text -match 'capabilit|privilege|root|escalat') { return @{ Tactics = @('PrivilegeEscalation') Techniques = @('T1068') } } if ($text -match 'write|modify|executable|binary|filesystem') { return @{ Tactics = @('DefenseEvasion') Techniques = @('T1070') } } $p = ($Priority ?? '').ToLowerInvariant() if ($p -eq 'critical' -or $p -eq 'error') { return @{ Tactics = @('Execution') Techniques = @('T1059') } } return @{ Tactics = @() Techniques = @() } } function Get-FalcoFrameworks { param( [string]$RuleId, [string]$RuleName ) if ([string]::IsNullOrWhiteSpace($RuleName)) { return @() } return @( @{ Name = 'CIS Kubernetes Benchmark' ControlId = $RuleId Controls = @($RuleId) } ) } function Get-FalcoImpact { param([string]$Severity) switch ($Severity) { 'Critical' { return 'High' } 'High' { return 'High' } 'Medium' { return 'Medium' } 'Low' { return 'Low' } default { return 'Low' } } } function Get-FalcoEffort { param([string]$Severity) switch ($Severity) { 'Critical' { return 'Medium' } 'High' { return 'Medium' } 'Medium' { return 'Low' } 'Low' { return 'Low' } default { return 'Low' } } } function Get-FalcoToolVersion { $fallback = 'falco-alert-pipeline' if (-not (Get-Command falco -ErrorAction SilentlyContinue)) { return $fallback } try { $raw = @(& falco --version 2>$null) -join ' ' if ([string]::IsNullOrWhiteSpace($raw)) { return $fallback } return (($raw -replace '\s+', ' ').Trim()) } catch { return $fallback } } $falcoToolVersion = Get-FalcoToolVersion $findings = [System.Collections.Generic.List[object]]::new() if (-not $InstallFalco) { try { $alertsQuery = @" Resources | where type =~ 'Microsoft.Security/locations/alerts' | where subscriptionId == '$SubscriptionId' | extend alertName = tostring(properties.alertDisplayName), description = tostring(properties.description), severity = tostring(properties.severity), compromisedEntity = tostring(properties.compromisedEntity), resourceId = tostring(properties.resourceIdentifiers[0].azureResourceId), extendedProperties = tostring(properties.extendedProperties) | where tolower(alertName) has 'falco' or tolower(description) has 'falco' or tolower(extendedProperties) has 'falco' | project id, alertName, description, severity, compromisedEntity, resourceId, extendedProperties "@ $alerts = @(Invoke-WithRetry -MaxAttempts 3 -ScriptBlock { Search-AzGraph -Query $using:alertsQuery -First 1000 -ErrorAction Stop }) } catch { $result.Status = 'Failed' $result.Message = "Falco query mode failed: $(Remove-Credentials -Text ([string]$_.Exception.Message))" return [pscustomobject]$result } foreach ($a in $alerts) { $rid = Resolve-AksResourceId -Alert $a -ClusterMap $clusterById if (-not $rid) { continue } $priority = Get-MatchValue -Text ([string]$a.extendedProperties) -Pattern '(?i)"(?:priority|Priority)"\s*:\s*"([^"]+)"' if (-not $priority) { $priority = [string]$a.severity } if (-not $priority) { $priority = 'Notice' } $rule = Get-MatchValue -Text ([string]$a.extendedProperties) -Pattern '(?i)"(?:rule|Rule)"\s*:\s*"([^"]+)"' $pod = Get-MatchValue -Text ([string]$a.extendedProperties) -Pattern '(?i)"(?:pod|Pod|k8s\.pod\.name)"\s*:\s*"([^"]+)"' $proc = Get-MatchValue -Text ([string]$a.extendedProperties) -Pattern '(?i)"(?:proc|process|Process|proc\.name)"\s*:\s*"([^"]+)"' $sev = Convert-PriorityToSeverity -Priority $priority $ruleId = New-FalcoRuleId -RuleName $rule $impact = Get-FalcoImpact -Severity $sev $effort = Get-FalcoEffort -Severity $sev $mitre = Get-FalcoMitreMapping -RuleName $rule -Priority $priority -Detail ([string]$a.description) $frameworks = Get-FalcoFrameworks -RuleId $ruleId -RuleName $rule $ruleDisplay = if ($rule) { $rule } else { [string]$a.alertName } if (-not $ruleDisplay) { $ruleDisplay = 'Falco runtime alert' } $deepLinkUrl = if ($a.id) { "https://portal.azure.com/#view/Microsoft_Azure_Security/SecurityMenuBlade/~/6/id/$([uri]::EscapeDataString([string]$a.id))" } else { '' } $evidenceUris = @() if ($a.id) { $evidenceUris += [string]$a.id } if ($deepLinkUrl) { $evidenceUris += $deepLinkUrl } if (-not [string]::IsNullOrWhiteSpace([string]$rid)) { $evidenceUris += [string]$rid } $findings.Add([pscustomobject]@{ Id = if ($a.id) { "falco/$($a.id)" } else { "falco/$([guid]::NewGuid())" } Source = 'falco' Category = 'KubernetesRuntimeThreatDetection' RuleId = $ruleId Severity = $sev Priority = $priority Compliant = $false Title = "Falco: $ruleDisplay" Detail = Remove-Credentials ([string]$a.description) Remediation = 'Investigate Falco runtime behavior and validate if process/pod activity is expected.' ResourceId = $rid RuleName = $rule Pod = $pod Process = $proc LearnMoreUrl = 'https://falco.org/docs/' Frameworks = @($frameworks) Pillar = 'Security' Impact = $impact Effort = $effort DeepLinkUrl = $deepLinkUrl RemediationSnippets = @(@{ language = 'text' code = 'Investigate container activity, validate expected process behavior, and tighten pod security controls.' }) EvidenceUris = @($evidenceUris) BaselineTags = @('falco', 'aks-runtime-threat', $ruleId) MitreTactics = @($mitre.Tactics) MitreTechniques = @($mitre.Techniques) EntityRefs = @([string]$rid) ToolVersion = $falcoToolVersion }) | Out-Null } $result.Findings = @($findings) $result.Message = "Query mode: processed $($alerts.Count) Falco-related alert(s); emitted $($findings.Count) AKS finding(s)." return [pscustomobject]$result } if (-not (Get-Command helm -ErrorAction SilentlyContinue)) { $result.Status = 'Skipped' $result.Message = 'Install mode requested but helm is not installed.' return [pscustomobject]$result } if (-not (Get-Command kubectl -ErrorAction SilentlyContinue)) { $result.Status = 'Skipped' $result.Message = 'Install mode requested but kubectl is not installed.' return [pscustomobject]$result } if (-not $installKubeconfigMode -and -not (Get-Command az -ErrorAction SilentlyContinue)) { $result.Status = 'Skipped' $result.Message = 'Install mode requested but az CLI is not installed (skip by passing -KubeconfigPath).' return [pscustomobject]$result } $captureMinutes = $CaptureMinutes $scanned = 0 $failed = 0 foreach ($cluster in $clusters) { $isKubeconfigMode = $false if ($cluster.PSObject.Properties['kubeconfigPath'] -and $cluster.kubeconfigPath) { $isKubeconfigMode = $true } if (-not $isKubeconfigMode) { # Defense-in-depth: reject resources whose names contain shell metacharacters. if ($cluster.resourceGroup -notmatch '^[A-Za-z0-9._()-]{1,90}$' -or $cluster.name -notmatch '^[A-Za-z0-9-]{1,63}$') { Write-Warning "Skipping cluster with unsafe name/resourceGroup: $($cluster.name) in $($cluster.resourceGroup)" $failed++ continue } } if ($isKubeconfigMode) { $tmpKubeconfig = $cluster.kubeconfigPath $ctx = $cluster.kubeContext } else { $ctx = "falco-$($cluster.name)-$([guid]::NewGuid().ToString('N').Substring(0,8))" $tmpKubeconfig = Join-Path ([System.IO.Path]::GetTempPath()) "kubeconfig-$ctx.yaml" } $authPrep = $null try { if (-not $PSCmdlet.ShouldProcess([string]$cluster.name, 'Install Falco via Helm and collect daemonset logs')) { continue } if (-not $isKubeconfigMode) { & az aks get-credentials --subscription $SubscriptionId --resource-group $cluster.resourceGroup --name $cluster.name --file $tmpKubeconfig --context $ctx --overwrite-existing --only-show-errors 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { $failed++; continue } } $env:KUBECONFIG = $tmpKubeconfig $authPrep = $null if ($KubeAuthMode -ne 'Default') { $kubeconfigOwned = -not $isKubeconfigMode $authPrep = Initialize-KubeAuth ` -Mode $KubeAuthMode ` -KubeconfigPath $tmpKubeconfig ` -KubeconfigOwned:$kubeconfigOwned ` -KubeContext $ctx ` -KubeloginServerId $KubeloginServerId ` -KubeloginClientId $KubeloginClientId ` -KubeloginTenantId $KubeloginTenantId ` -WorkloadIdentityClientId $WorkloadIdentityClientId ` -WorkloadIdentityTenantId $WorkloadIdentityTenantId ` -WorkloadIdentityServiceAccountToken $WorkloadIdentityServiceAccountToken $env:KUBECONFIG = $authPrep.KubeconfigPath } & helm repo add falcosecurity https://falcosecurity.github.io/charts 2>&1 | Out-Null & helm repo update 2>&1 | Out-Null $helmArgs = @('upgrade', '--install', 'falco', 'falcosecurity/falco', '--namespace', $Namespace, '--create-namespace', '--wait', '--timeout', '5m') if ($ctx) { $helmArgs += @('--kube-context', $ctx) } $helmExec = Invoke-WithTimeout -Command 'helm' -Arguments $helmArgs -TimeoutSec 360 if ($helmExec.Output) { Write-Verbose "helm output: $($helmExec.Output)" } if ([int]$helmExec.ExitCode -eq -1) { $failed++; continue } if ([int]$helmExec.ExitCode -ne 0) { $failed++; continue } Start-Sleep -Seconds ($captureMinutes * 60) $logArgs = @() if ($ctx) { $logArgs += @('--context', $ctx) } $logArgs += @('-n', $Namespace, 'logs', 'daemonset/falco', '--since', "$($captureMinutes)m", '--tail', '5000') $logExec = Invoke-WithTimeout -Command 'kubectl' -Arguments $logArgs -TimeoutSec 120 $rawLogs = @(($logExec.Output -split "`n")) if ([int]$logExec.ExitCode -ne 0) { $failed++ Write-Warning "Falco log collection failed for cluster $($cluster.name): $(Remove-Credentials -Text ([string]($rawLogs -join ' ')))" continue } foreach ($line in @($rawLogs)) { if (-not $line) { continue } $priority = Get-MatchValue -Text $line -Pattern '(?i)"priority"\s*:\s*"([^"]+)"' if (-not $priority) { continue } $rule = Get-MatchValue -Text $line -Pattern '(?i)"rule"\s*:\s*"([^"]+)"' $pod = Get-MatchValue -Text $line -Pattern '(?i)"k8s\.pod\.name"\s*:\s*"([^"]+)"' $proc = Get-MatchValue -Text $line -Pattern '(?i)"proc\.name"\s*:\s*"([^"]+)"' $sev = Convert-PriorityToSeverity -Priority $priority $ruleId = New-FalcoRuleId -RuleName $rule $impact = Get-FalcoImpact -Severity $sev $effort = Get-FalcoEffort -Severity $sev $mitre = Get-FalcoMitreMapping -RuleName $rule -Priority $priority -Detail ([string]$line) $frameworks = Get-FalcoFrameworks -RuleId $ruleId -RuleName $rule $deepLinkUrl = if ($cluster.id) { "https://portal.azure.com/#@/resource$([string]$cluster.id)" } else { '' } $evidenceUris = @() if ($deepLinkUrl) { $evidenceUris += $deepLinkUrl } if (-not [string]::IsNullOrWhiteSpace([string]$cluster.id)) { $evidenceUris += [string]$cluster.id } $findings.Add([pscustomobject]@{ Id = "falco/$($cluster.id)/$([guid]::NewGuid())" Source = 'falco' Category = 'KubernetesRuntimeThreatDetection' RuleId = $ruleId Severity = $sev Priority = $priority Compliant = $false Title = if ($rule) { "Falco: $rule" } else { 'Falco runtime alert' } Detail = Remove-Credentials ([string]$line) Remediation = 'Investigate Falco runtime behavior and validate if process/pod activity is expected.' ResourceId = [string]$cluster.id RuleName = $rule Pod = $pod Process = $proc LearnMoreUrl = 'https://falco.org/docs/' Frameworks = @($frameworks) Pillar = 'Security' Impact = $impact Effort = $effort DeepLinkUrl = $deepLinkUrl RemediationSnippets = @(@{ language = 'text' code = 'Investigate container activity, validate expected process behavior, and tighten pod security controls.' }) EvidenceUris = @($evidenceUris) BaselineTags = @('falco', 'aks-runtime-threat', $ruleId) MitreTactics = @($mitre.Tactics) MitreTechniques = @($mitre.Techniques) EntityRefs = @([string]$cluster.id) ToolVersion = $falcoToolVersion }) | Out-Null } if ($UninstallFalco) { if ($PSCmdlet.ShouldProcess([string]$cluster.name, 'Uninstall Falco Helm release')) { $uninstallArgs = @('uninstall', 'falco', '-n', $Namespace) if ($ctx) { $uninstallArgs += @('--kube-context', $ctx) } & helm @uninstallArgs 2>&1 | Out-Null } } $scanned++ } catch { $failed++ Write-Warning "Falco install mode failed for cluster $($cluster.name): $(Remove-Credentials -Text ([string]$_.Exception.Message))" } finally { if ($authPrep -and $authPrep.Cleanup) { try { & $authPrep.Cleanup } catch {} } if ($env:KUBECONFIG) { Remove-Item Env:\KUBECONFIG -ErrorAction SilentlyContinue } if (-not $isKubeconfigMode -and $tmpKubeconfig -and (Test-Path $tmpKubeconfig)) { try { Remove-Item $tmpKubeconfig -Force -ErrorAction SilentlyContinue } catch {} } } } $result.Findings = @($findings) $result.Message = "Install mode: scanned $scanned AKS cluster(s); $failed failed; emitted $($findings.Count) Falco alert finding(s)." if ($scanned -eq 0 -and $failed -gt 0) { $result.Status = 'Failed' } elseif ($scanned -gt 0 -and $failed -gt 0) { $result.Status = 'PartialSuccess' } return [pscustomobject]$result |