rules/AzureDevOps.Tasks.VariableGroups.Rule.ps1

# PSRule rule definitions for Azure DevOps Variable Groups

# Synopsis: A Variable Group should not contain secrets when not linked to a Key Vault
Rule 'Azure.DevOps.Tasks.VariableGroup.NoKeyVaultNoSecrets' `
    -Ref 'ADO-VG-001' `
    -Type 'Azure.DevOps.Tasks.VariableGroup' `
    -If { $TargetObject.type -eq 'Vsts' } `
    -Tag @{ release = 'GA'} `
    -Level Error {
        # Description 'Variable Groups should not contain secrets when not linked to a Key Vault.'
        Reason 'The Variable Group is not linked to a Key Vault and it contains secrets.'
        Recommend 'Consider backing the Variable Group with a Key Vault.'
        # Links 'https://learn.microsoft.com/en-us/azure/devops/organizations/security/security-best-practices?view=azure-devops#tasks'
        $Assert.NotHasField($TargetObject, "variables[*].isSecret", $true)
}

# Synopsis: Variable groups should have a description
Rule 'Azure.DevOps.Tasks.VariableGroup.Description' `
    -Ref 'ADO-VG-002' `
    -Type 'Azure.DevOps.Tasks.VariableGroup' `
    -Tag @{ release = 'GA'} `
    -Level Information {
        # Description 'Variable groups should have a description.'
        Reason 'No description is configured for the variable group.'
        Recommend 'Add a description to the variable group to make it easier to understand its purpose.'
        $Assert.HasField($TargetObject, "description", $true)
        $Assert.HasFieldValue($TargetObject, "description")
}

# Synopsis: Variable groups should not contain plain text secrets
Rule 'Azure.DevOps.Tasks.VariableGroup.NoPlainTextSecrets' `
    -Ref 'ADO-VG-003' `
    -Type 'Azure.DevOps.Tasks.VariableGroup' `
    -Tag @{ release = 'GA'} `
    -Level Error {
        # Description 'Variable groups should not contain plain text secrets.'
        Reason 'The variable group contains a plain text secret.'
        Recommend 'Consider using a secret variable in a keyvault instead.'

        # None of the variable should match the following regex patterns or the value should be null
        $TargetObject.variables.psobject.Properties.GetEnumerator() | ForEach-Object {
            If($null -ne $_.Value.value) {
                AllOf {
                    $Assert.HasFieldValue($_, "Value.value")
                    # $Assert.NotMatch($_, "value", "^(?=\D*\d)(?=[^a-z]*[a-z])(?=[^A-Z]*[A-Z])(?=(\w*\W|\w*))[0-9\Wa-zA-Z]{7,20}$")
                    $Assert.NotMatch($_, "Value.value", "((P|p)assword|pwd)\s*=\s*\w+;?")
                    # SQL conn strings
                    $Assert.NotMatch($_, "Value.value", "(?# To match SQL/MySQL conn strings.)^Data Source=[^;]+;Initial Catalog=[^;]+;User ID=[^;]+;Password=[^;]+;$")
                    $Assert.NotMatch($_, "Value.value", "(?# To match SQL/MySQL conn strings.)((U|u)ser(Id)?|uid)\s*=\s*\w+;?")
                    # Azure storage keys, connection strings, and SAS
                    $Assert.NotMatch($_, "Value.value", "(?# To match Azure storage keys.)^[A-Za-z0-9/+]{86}==$")
                    $Assert.NotMatch($_, "Value.value", "(?# To match Azure storage connection strings.)DefaultEndpointsProtocol=https;AccountName=\w+;AccountKey=[A-Za-z0-9/+]{86}==$")
                    $Assert.NotMatch($_, "Value.value", "(?# To match storage SAS.)([^?]*\?sv=)[^&]+(&s[a-z]=[^&]+){4}")
                    # Azure AD client secrets
                    $Assert.NotMatch($_, "Value.value", "(?# To match Azure AD client secrets.)^[A-Za-z0-9-._~]{32}$")
                    # Azure DevOps PATs
                    $Assert.NotMatch($_, "Value.value", "(?# To match ADO PATs.)^[a-z2-7]{52}$")
                    # Azure Event Hub connection strings
                    $Assert.NotMatch($_, "Value.value", "(?# To match Azure Event Hub connection strings.)Endpoint=sb://[^/]+\.servicebus\.windows\.net/;SharedAccessKeyName=[^;]+;SharedAccessKey=[A-Za-z0-9+/=]{44}==$")
                    # Azure Service Bus connection strings
                    $Assert.NotMatch($_, "Value.value", "(?# To match Azure Service Bus connection strings.)Endpoint=sb://[^/]+\.servicebus\.windows\.net/;SharedAccessKeyName=[^;]+;SharedAccessKey=[A-Za-z0-9+/=]{44}==$")
                    # Azure Service Bus SAS
                    $Assert.NotMatch($_, "Value.value", "(?# To match Azure Service Bus SAS.)((S|s)hared(A|a)ccess(S|s)ignature|sas)\s*=\s*\w+;?")
                    # Azure OpenAI API keys
                    $Assert.NotMatch($_, "Value.value", "(?# To match Azure OpenAI API keys.)^sk-[a-zA-Z0-9]{32}$")
                    # Azure Cognitive Services API keys
                    $Assert.NotMatch($_, "Value.value", "(?# To match Azure Cognitive Services API keys.)^[a-zA-Z0-9]{32}$")

                    # AWS access keys
                    $Assert.NotMatch($_, "Value.value", "(?# To match AWS access keys.)^AKIA[0-9A-Z]{16}$")
                    # AWS secret keys
                    $Assert.NotMatch($_, "Value.value", "(?# To match AWS secret keys.)^[A-Za-z0-9/+=]{40}$")

                    # MongoDB connection strings
                    $Assert.NotMatch($_, "Value.value", "(?# To match MongoDB connection strings.)mongodb://[^:]+:[^@]+@[^/]+/[^?]+(\?[^&]+(&[^&]+)*)?$")
                    # MongoDB SRV connection strings
                    $Assert.NotMatch($_, "Value.value", "(?# To match MongoDB SRV connection strings.)mongodb\+srv://[^:]+:[^@]+@[^/]+/[^?]+(\?[^&]+(&[^&]+)*)?$")

                    # Redis connection strings
                    $Assert.NotMatch($_, "Value.value", "(?# To match Redis connection strings with password.)^redis://:[^@]+@[^:]+:\d+/?$")
                    # Redis SSL connection strings
                    $Assert.NotMatch($_, "Value.value", "(?# To match Redis SSL connection strings.)^rediss://[^:]+:[^@]+@[^:]+:\d+/?$")

                    # MySQL connection strings
                    $Assert.NotMatch($_, "Value.value", "(?# To match MySQL connection strings.)^Server=[^;]+;Port=\d+;Database=[^;]+;Uid=[^;]+;Pwd=[^;]+;$")

                    # PostgreSQL connection strings
                    $Assert.NotMatch($_, "Value.value", "(?# To match PostgreSQL connection strings.)^Server=[^;]+;Port=\d+;Database=[^;]+;User Id=[^;]+;Password=[^;]+;$")
                    
                    # PEM files
                    $Assert.NotMatch($_, "Value.value", "(?# To match PEM files.)^-----BEGIN [A-Z ]+-----\r?\n([A-Za-z0-9+/=]{64}\r?\n)*-----END [A-Z ]+-----\r?\n?$")
                    
                }
            } else {
                $Assert.Null($_, "Value.value")
            }

        }
}

# Synposis: Variable groups should not inherit permissions
Rule 'Azure.DevOps.Tasks.VariableGroup.InheritedPermissions' `
    -Ref 'ADO-VG-004' `
    -Type 'Azure.DevOps.Tasks.VariableGroup' `
    -Tag @{ release = 'GA'} `
    -Level Warning {
        # Description 'Variable groups should not inherit permissions.'
        Reason 'The variable group inherits permissions.'
        Recommend 'Do not inherit permissions for the variable group.'
        AllOf {
            $Assert.HasField($TargetObject, "Acls", $true)
            $Assert.HasField($TargetObject.Acls[0], "inheritPermissions", $true)
            $Assert.HasFieldValue($TargetObject.Acls[0], "inheritPermissions", $false)
        }
}

# Synposis: Variable groups should not have direct permissions for Project Valid Users
Rule 'Azure.DevOps.Tasks.VariableGroup.ProjectValidUsers' `
    -Ref 'ADO-VG-005' `
    -Type 'Azure.DevOps.Tasks.VariableGroup' `
    -Tag @{ release = 'GA' } `
    -Level Warning {
        # Description 'Variable groups should not have direct permissions for Project Valid Users.'
        Reason 'The variable group has direct permissions for Project Valid Users.'
        Recommend 'Do not grant direct permissions for Project Valid Users for the variable group.'
        AllOf {
            # Loop through all the propeties of the first ACL
            $TargetObject.Acls[0].acesDictionary.psobject.Properties.GetEnumerator() | ForEach-Object {
                # Assert the property name does not end with -0-0-0-0-3 wich is the Project Valid Users group SID
                AnyOf {
                    $Assert.NotMatch($_.Value, "descriptor", "-0-0-0-0-3")
                    $Assert.HasFieldValue($_.Value, "allow", 1)
                }
            }
        }
}