Assessment/SecretAssessment.ps1
|
# Shared evidence evaluation for the current control and its retained legacy alias. function Get-AzDevOpsVariableGroupSecretAssessment { [CmdletBinding()] param([Parameter(Mandatory)][object]$TargetObject) if ($null -eq $TargetObject.variables -or ($TargetObject.variables -isnot [System.Collections.IDictionary] -and $TargetObject.variables -isnot [pscustomobject])) { return [pscustomobject]@{ Outcome = 'Unknown'; ReasonCode = 'VariablesUnavailable' } } $patterns = @( '((P|p)assword|pwd)\s*=\s*\w+;?' '(?# To match SQL/MySQL conn strings.)^Data Source=[^;]+;Initial Catalog=[^;]+;User ID=[^;]+;Password=[^;]+;$' '(?# To match SQL/MySQL conn strings.)((U|u)ser(Id)?|uid)\s*=\s*\w+;?' '(?# To match Azure storage keys.)^[A-Za-z0-9/+]{86}==$' '(?# To match Azure storage connection strings.)DefaultEndpointsProtocol=https;AccountName=\w+;AccountKey=[A-Za-z0-9/+]{86}==$' '(?# To match storage SAS.)([^?]*\?sv=)[^&]+(&s[a-z]=[^&]+){4}' '(?# To match ADO PATs.)^[A-Za-z0-9]{75}AZDO[A-Za-z0-9]{5}$' '(?# To match Azure Event Hub connection strings.)Endpoint=sb://[^/]+\.servicebus\.windows\.net/;SharedAccessKeyName=[^;]+;SharedAccessKey=[A-Za-z0-9+/=]{44}==$' '(?# To match Azure Service Bus connection strings.)Endpoint=sb://[^/]+\.servicebus\.windows\.net/;SharedAccessKeyName=[^;]+;SharedAccessKey=[A-Za-z0-9+/=]{44}==$' '(?# To match Azure Service Bus SAS.)((S|s)hared(A|a)ccess(S|s)ignature|sas)\s*=\s*\w+;?' '(?# To match Azure OpenAI API keys.)^sk-[a-zA-Z0-9]{32}$' '(?# To match AWS access keys.)^AKIA[0-9A-Z]{16}$' '(?# To match MongoDB connection strings.)mongodb://[^:]+:[^@]+@[^/]+/[^?]+(\?[^&]+(&[^&]+)*)?$' '(?# To match MongoDB SRV connection strings.)mongodb\+srv://[^:]+:[^@]+@[^/]+/[^?]+(\?[^&]+(&[^&]+)*)?$' '(?# To match Redis connection strings with password.)^redis://:[^@]+@[^:]+:\d+/?$' '(?# To match Redis SSL connection strings.)^rediss://[^:]+:[^@]+@[^:]+:\d+/?$' '(?# To match MySQL connection strings.)^Server=[^;]+;Port=\d+;Database=[^;]+;Uid=[^;]+;Pwd=[^;]+;$' '(?# To match PostgreSQL connection strings.)^Server=[^;]+;Port=\d+;Database=[^;]+;User Id=[^;]+;Password=[^;]+;$' '(?# To match PEM files.)^-----BEGIN [A-Z ]+-----\r?\n([A-Za-z0-9+/=]{64}\r?\n)*-----END [A-Z ]+-----\r?\n?$' ) $variables = if ($TargetObject.variables -is [System.Collections.IDictionary]) { @($TargetObject.variables.Values) } else { @($TargetObject.variables.psobject.Properties.Value) } $incomplete = $false foreach ($variable in $variables) { if ($null -eq $variable) { $incomplete = $true continue } $fields = if ($variable -is [System.Collections.IDictionary]) { @($variable.Keys) } else { @($variable.psobject.Properties.Name) } # The collected value must be present, including an explicit null for masked secrets. if ('value' -notin $fields) { $incomplete = $true continue } if ($null -ne $variable.value) { if ($variable.isSecret -eq $true) { return [pscustomobject]@{ Outcome = 'Fail'; ReasonCode = 'UnmaskedSecret' } } foreach ($pattern in $patterns) { if ([string]$variable.value -match $pattern) { return [pscustomobject]@{ Outcome = 'Fail'; ReasonCode = 'PlainTextCredential' } } } } } if ($incomplete) { return [pscustomobject]@{ Outcome = 'Unknown'; ReasonCode = 'VariableEvidenceIncomplete' } } return [pscustomobject]@{ Outcome = 'Pass'; ReasonCode = 'NoExposedCredentialDetected' } } |