public/maester/aiagent/Test-MtAIAgentRiskyHttpConfig.ps1
|
<# .SYNOPSIS Tests if AI agents have risky HTTP configurations. .DESCRIPTION Checks all Copilot Studio agents for HTTP actions that connect to non-standard ports or non-connector endpoints. HTTP actions to unexpected destinations may indicate data exfiltration, command-and-control communication, or misconfigured integrations. .OUTPUTS [bool] - Returns $true if no risky HTTP configurations are found, $false if any agent has suspicious HTTP actions, $null if data is unavailable. .EXAMPLE Test-MtAIAgentRiskyHttpConfig .LINK https://maester.dev/docs/commands/Test-MtAIAgentRiskyHttpConfig #> function Test-MtAIAgentRiskyHttpConfig { [CmdletBinding()] [OutputType([bool])] param() $agents = Get-MtAIAgentInfo if ($null -eq $agents) { Add-MtTestResultDetail -SkippedBecause 'Custom' -SkippedCustomReason 'No Copilot Studio agent data available. Ensure DataverseEnvironmentUrl is configured in maester-config.json and Connect-Maester -Service Dataverse has been run. See https://maester.dev/docs/tests/MT.1115 for prerequisites.' return $null } $failedAgents = @() foreach ($agent in $agents) { if ([string]::IsNullOrEmpty($agent.AgentTopicsDetails)) { continue } $topicsDetails = $null try { if ($agent.AgentTopicsDetails -is [string]) { $topicsDetails = $agent.AgentTopicsDetails | ConvertFrom-Json -ErrorAction Stop } else { $topicsDetails = $agent.AgentTopicsDetails } } catch { Write-Verbose "Could not parse AgentTopicsDetails for agent $($agent.AIAgentName): $_" continue } # Look for HTTP actions with non-standard ports or non-HTTPS URLs # Search the raw Data field (YAML) of each topic directly, not JSON-serialized $riskyActions = @() $items = if ($topicsDetails -is [array]) { $topicsDetails } else { @($topicsDetails) } foreach ($topic in $items) { $data = $topic.Data if ([string]::IsNullOrEmpty($data)) { continue } # Match HTTP URLs with explicit non-443 ports $portMatches = [regex]::Matches($data, 'https?://[^\s]+:(\d+)') foreach ($match in $portMatches) { $port = $match.Groups[1].Value if ($port -ne '443') { $riskyActions += "Non-standard port :$port in topic '$($topic.Name)'" } } # Match plain HTTP (non-HTTPS) URLs $httpMatches = [regex]::Matches($data, '(http://[^\s]+)') foreach ($match in $httpMatches) { $riskyActions += "Plain HTTP: $($match.Groups[1].Value) in topic '$($topic.Name)'" } # Match HttpRequestAction kind (direct HTTP calls from topics) if ($data -match 'kind:\s*HttpRequestAction') { $riskyActions += "HttpRequestAction in topic '$($topic.Name)'" } } if ($riskyActions.Count -gt 0) { $failedAgents += [PSCustomObject]@{ AIAgentName = $agent.AIAgentName EnvironmentId = $agent.EnvironmentId RiskyActions = ($riskyActions | Select-Object -Unique) -join '; ' } } } if ($failedAgents.Count -eq 0) { $testResultMarkdown = "Well done. No AI agents have risky HTTP configurations." } else { $testResultMarkdown = "Found $($failedAgents.Count) AI agent(s) with risky HTTP configurations.`n`n%TestResult%" $result = "| Agent Name | Environment | Risky Actions |`n" $result += "| --- | --- | --- |`n" foreach ($agent in $failedAgents) { $result += "| $($agent.AIAgentName) | $($agent.EnvironmentId) | $($agent.RiskyActions) |`n" } $testResultMarkdown = $testResultMarkdown -replace "%TestResult%", $result } Add-MtTestResultDetail -Result $testResultMarkdown -Severity "Medium" return ($failedAgents.Count -eq 0) } |