public/maester/aiagent/Test-MtAIAgentHardCodedCredentials.ps1

<#
.SYNOPSIS
    Tests if AI agents have hard-coded credentials in topic definitions.

.DESCRIPTION
    Scans all Copilot Studio agent topics for patterns that suggest hard-coded
    credentials, API keys, connection strings, or secrets. Hard-coded credentials
    in agent topics can be extracted by prompt injection attacks and often persist
    after key rotation is performed elsewhere.

.OUTPUTS
    [bool] - Returns $true if no hard-coded credentials are found, $false if any
    agent topics contain credential patterns, $null if data is unavailable.

.EXAMPLE
    Test-MtAIAgentHardCodedCredentials

.LINK
    https://maester.dev/docs/commands/Test-MtAIAgentHardCodedCredentials
#>


function Test-MtAIAgentHardCodedCredentials {
    [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.1119 for prerequisites.'
        return $null
    }

    # Patterns that commonly indicate hard-coded credentials
    # Note: Patterns with key-value separators use "? to handle JSON-encoded data
    # where keys are quoted (e.g., "x-api-key":"value" instead of x-api-key: value)
    $credentialPatterns = @(
        @{ Name = 'API Key header';          Pattern = '(?i)(x-api-key|api[_-]?key|apikey)"?\s*[:=]\s*["\x27]?[A-Za-z0-9\-_\.]{16,}' }
        @{ Name = 'Bearer token';            Pattern = '(?i)bearer\s+[A-Za-z0-9\-_\.]{20,}' }
        @{ Name = 'Authorization header';    Pattern = '(?i)authorization"?\s*[:=]\s*["\x27]?(Basic|Bearer)\s+[A-Za-z0-9\+/=\-_\.]{16,}' }
        @{ Name = 'Connection string';       Pattern = '(?i)(Server|Data Source|AccountKey|SharedAccessKey)\s*=' }
        @{ Name = 'Secret/Password literal'; Pattern = '(?i)(password|secret|client_secret|clientsecret)"?\s*[:=]\s*["\x27]?[^\s"]{8,}' }
        @{ Name = 'AWS-style key';           Pattern = '(?i)(AKIA|ASIA)[A-Z0-9]{16}' }
        @{ Name = 'Private key block';       Pattern = '-----BEGIN (RSA |EC )?PRIVATE KEY-----' }
    )

    $failedAgents = @()

    foreach ($agent in $agents) {
        if ([string]::IsNullOrEmpty($agent.AgentTopicsDetails)) {
            continue
        }

        $topicsJson = $null
        try {
            if ($agent.AgentTopicsDetails -is [string]) {
                $topicsJson = $agent.AgentTopicsDetails
            } else {
                $topicsJson = $agent.AgentTopicsDetails | ConvertTo-Json -Depth 10 -ErrorAction SilentlyContinue
            }
        } catch {
            Write-Verbose "Could not process AgentTopicsDetails for agent $($agent.AIAgentName): $_"
            continue
        }

        if ($null -eq $topicsJson) { continue }

        $matchedPatterns = @()
        foreach ($cred in $credentialPatterns) {
            if ($topicsJson -match $cred.Pattern) {
                $matchedPatterns += $cred.Name
            }
        }

        if ($matchedPatterns.Count -gt 0) {
            $failedAgents += [PSCustomObject]@{
                AIAgentName    = $agent.AIAgentName
                EnvironmentId  = $agent.EnvironmentId
                CredentialType = ($matchedPatterns | Select-Object -Unique) -join ', '
            }
        }
    }

    if ($failedAgents.Count -eq 0) {
        $testResultMarkdown = "Well done. No AI agents have hard-coded credentials in topic definitions."
    } else {
        $testResultMarkdown = "Found $($failedAgents.Count) AI agent(s) with potential hard-coded credentials in topics.`n`n%TestResult%"
        $result = "| Agent Name | Environment | Credential Types Found |`n"
        $result += "| --- | --- | --- |`n"
        foreach ($agent in $failedAgents) {
            $result += "| $($agent.AIAgentName) | $($agent.EnvironmentId) | $($agent.CredentialType) |`n"
        }
        $testResultMarkdown = $testResultMarkdown -replace "%TestResult%", $result
    }

    Add-MtTestResultDetail -Result $testResultMarkdown -Severity "High"
    return ($failedAgents.Count -eq 0)
}