rules/AzureDevOps.Pipelines.Releases.Rule.ps1
|
# Synopsis: Release pipeline production environments should be protected by approval. Rule 'Azure.DevOps.Pipelines.Releases.Definition.ProductionApproval' ` -Ref 'ADO-RD-001' ` -Type 'Azure.DevOps.Pipelines.Releases.Definition' ` -If { $null -ne ($TargetObject.environments | Where-Object { $_.name -imatch "prd|prod|live|master|main"}) } ` -Tag @{ release = 'GA'} ` -Level Error { # Description 'Release pipeline production environments should be protected by approval.' Reason 'The release pipeline contains a production environment that is not protected by approval.' Recommend 'Consider adding approval to the production environment.' # Links 'https://docs.microsoft.com/en-us/azure/devops/pipelines/release/approvals/?view=azure-devops' $prodEnvironments = $TargetObject.environments | Where-Object { $_.name -imatch "prd|prod|live|master|main"} $prodEnvironments | ForEach-Object { $Assert.HasField($_, "preDeployApprovals", $true) $Assert.HasField($_.preDeployApprovals, "approvals[0].approver", $true) $Assert.HasFieldValue($_.preDeployApprovals, "approvals[0].approver.displayName") $Assert.GreaterOrEqual($_.preDeployApprovals.approvals, "count", $Configuration.GetValueOrDefault('releaseMinimumProductionApproverCount', 1)) } } # Synopsis: Users should not be able to approve their own release. Rule 'Azure.DevOps.Pipelines.Releases.Definition.SelfApproval' ` -Ref 'ADO-RD-002' ` -Type 'Azure.DevOps.Pipelines.Releases.Definition' ` -If { $null -ne ($TargetObject.environments | Where-Object { $_.name -imatch "prd|prod|live|master|main"}) } ` -Tag @{ release = 'GA'} ` -Level Error { # Description 'Users should not be able to approve their own release.' Reason 'The release pipeline contains a production where users can approve their own releases.' Recommend 'Consider not allowing user to approve their own work to the production environment.' # Links 'https://docs.microsoft.com/en-us/azure/devops/pipelines/release/approvals/?view=azure-devops' $prodEnvironments = $TargetObject.environments | Where-Object { $_.name -imatch "prd|prod|live|master|main"} $prodEnvironments | ForEach-Object { $Assert.HasField($_, "preDeployApprovals", $true) $Assert.HasField($_.preDeployApprovals, "approvalOptions.releaseCreatorCanBeApprover", $true) $Assert.HasFieldValue($_.preDeployApprovals, "approvalOptions.releaseCreatorCanBeApprover", $false) } } # Synopsis: Release pipeline should not inherit permissions from the project. Rule 'Azure.DevOps.Pipelines.Releases.Definition.InheritedPermissions' ` -Ref 'ADO-RD-003' ` -Type 'Azure.DevOps.Pipelines.Releases.Definition' ` -If { (Get-AzDevOpsEffectivePermissionAssessment -Evidence $TargetObject.PermissionEvidence -Policy $Configuration.effectivePermissionPolicies.'Azure.DevOps.Pipelines.Releases.Definition').Outcome -ne 'Unknown' } ` -Tag @{ release = 'GA'; assessment = 'EffectivePermissions'; legacyName = 'InheritedPermissions' } ` -Level Warning { $assessment = Get-AzDevOpsEffectivePermissionAssessment -Evidence $TargetObject.PermissionEvidence -Policy $Configuration.effectivePermissionPolicies.'Azure.DevOps.Pipelines.Releases.Definition' Reason $assessment.ReasonCode Recommend 'Restrict unauthorized effective privileges or update the approved permission policy.' $Assert.HasFieldValue($assessment, 'Outcome', 'Pass') } # Synopsis: Release pipeline should not have plain text secrets in variables Rule 'Azure.DevOps.Pipelines.Releases.Definition.NoPlainTextSecrets' ` -Ref 'ADO-RD-004' ` -Type 'Azure.DevOps.Pipelines.Releases.Definition' ` -Tag @{ release = 'GA'} ` -Level Warning { # Description 'Release pipeline should not have plain text secrets in variables' Reason 'The release pipeline contains a variable with a secret as plain text.' Recommend 'Consider using a secret variable for your pipeline' # Links 'https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#secret-variables' AllOf { $TargetObject.variables.psobject.Properties | ForEach-Object { # Check if the variable matches the regex patterns 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+;?").Reason('A configuration value matches a plaintext credential pattern.') # SQL conn strings $Assert.NotMatch($_, "Value.value", "(?# To match SQL/MySQL conn strings.)^Data Source=[^;]+;Initial Catalog=[^;]+;User ID=[^;]+;Password=[^;]+;$").Reason('A configuration value matches a plaintext credential pattern.') $Assert.NotMatch($_, "Value.value", "(?# To match SQL/MySQL conn strings.)((U|u)ser(Id)?|uid)\s*=\s*\w+;?").Reason('A configuration value matches a plaintext credential pattern.') # Azure storage keys, connection strings, and SAS $Assert.NotMatch($_, "Value.value", "(?# To match Azure storage keys.)^[A-Za-z0-9/+]{86}==$").Reason('A configuration value matches a plaintext credential pattern.') $Assert.NotMatch($_, "Value.value", "(?# To match Azure storage connection strings.)DefaultEndpointsProtocol=https;AccountName=\w+;AccountKey=[A-Za-z0-9/+]{86}==$").Reason('A configuration value matches a plaintext credential pattern.') $Assert.NotMatch($_, "Value.value", "(?# To match storage SAS.)([^?]*\?sv=)[^&]+(&s[a-z]=[^&]+){4}").Reason('A configuration value matches a plaintext credential pattern.') # Azure DevOps PATs $Assert.NotMatch($_, "Value.value", "(?# To match ADO PATs.)^[A-Za-z0-9]{75}AZDO[A-Za-z0-9]{5}$").Reason('A configuration value matches a plaintext credential pattern.') # 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}==$").Reason('A configuration value matches a plaintext credential pattern.') # 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}==$").Reason('A configuration value matches a plaintext credential pattern.') # 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+;?").Reason('A configuration value matches a plaintext credential pattern.') # Azure OpenAI API keys $Assert.NotMatch($_, "Value.value", "(?# To match Azure OpenAI API keys.)^sk-[a-zA-Z0-9]{32}$").Reason('A configuration value matches a plaintext credential pattern.') # AWS access keys $Assert.NotMatch($_, "Value.value", "(?# To match AWS access keys.)^AKIA[0-9A-Z]{16}$").Reason('A configuration value matches a plaintext credential pattern.') # MongoDB connection strings $Assert.NotMatch($_, "Value.value", "(?# To match MongoDB connection strings.)mongodb://[^:]+:[^@]+@[^/]+/[^?]+(\?[^&]+(&[^&]+)*)?$").Reason('A configuration value matches a plaintext credential pattern.') # MongoDB SRV connection strings $Assert.NotMatch($_, "Value.value", "(?# To match MongoDB SRV connection strings.)mongodb\+srv://[^:]+:[^@]+@[^/]+/[^?]+(\?[^&]+(&[^&]+)*)?$").Reason('A configuration value matches a plaintext credential pattern.') # Redis connection strings $Assert.NotMatch($_, "Value.value", "(?# To match Redis connection strings with password.)^redis://:[^@]+@[^:]+:\d+/?$").Reason('A configuration value matches a plaintext credential pattern.') # Redis SSL connection strings $Assert.NotMatch($_, "Value.value", "(?# To match Redis SSL connection strings.)^rediss://[^:]+:[^@]+@[^:]+:\d+/?$").Reason('A configuration value matches a plaintext credential pattern.') # MySQL connection strings $Assert.NotMatch($_, "Value.value", "(?# To match MySQL connection strings.)^Server=[^;]+;Port=\d+;Database=[^;]+;Uid=[^;]+;Pwd=[^;]+;$").Reason('A configuration value matches a plaintext credential pattern.') # PostgreSQL connection strings $Assert.NotMatch($_, "Value.value", "(?# To match PostgreSQL connection strings.)^Server=[^;]+;Port=\d+;Database=[^;]+;User Id=[^;]+;Password=[^;]+;$").Reason('A configuration value matches a plaintext credential pattern.') # 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?$").Reason('A configuration value matches a plaintext credential pattern.') } } else { $Assert.Null($_, "Value.value") } } # Loop through all the environments $TargetObject.environments | ForEach-Object { # Loop through all the environment variables $_.variables.psobject.Properties | ForEach-Object { # Check if the variable matches the regex patterns 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+;?").Reason('A configuration value matches a plaintext credential pattern.') # SQL conn strings $Assert.NotMatch($_, "Value.value", "(?# To match SQL/MySQL conn strings.)^Data Source=[^;]+;Initial Catalog=[^;]+;User ID=[^;]+;Password=[^;]+;$").Reason('A configuration value matches a plaintext credential pattern.') $Assert.NotMatch($_, "Value.value", "(?# To match SQL/MySQL conn strings.)((U|u)ser(Id)?|uid)\s*=\s*\w+;?").Reason('A configuration value matches a plaintext credential pattern.') # Azure storage keys, connection strings, and SAS $Assert.NotMatch($_, "Value.value", "(?# To match Azure storage keys.)^[A-Za-z0-9/+]{86}==$").Reason('A configuration value matches a plaintext credential pattern.') $Assert.NotMatch($_, "Value.value", "(?# To match Azure storage connection strings.)DefaultEndpointsProtocol=https;AccountName=\w+;AccountKey=[A-Za-z0-9/+]{86}==$").Reason('A configuration value matches a plaintext credential pattern.') $Assert.NotMatch($_, "Value.value", "(?# To match storage SAS.)([^?]*\?sv=)[^&]+(&s[a-z]=[^&]+){4}").Reason('A configuration value matches a plaintext credential pattern.') # Azure DevOps PATs $Assert.NotMatch($_, "Value.value", "(?# To match ADO PATs.)^[A-Za-z0-9]{75}AZDO[A-Za-z0-9]{5}$").Reason('A configuration value matches a plaintext credential pattern.') # 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}==$").Reason('A configuration value matches a plaintext credential pattern.') # 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}==$").Reason('A configuration value matches a plaintext credential pattern.') # 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+;?").Reason('A configuration value matches a plaintext credential pattern.') # Azure OpenAI API keys $Assert.NotMatch($_, "Value.value", "(?# To match Azure OpenAI API keys.)^sk-[a-zA-Z0-9]{32}$").Reason('A configuration value matches a plaintext credential pattern.') # AWS access keys $Assert.NotMatch($_, "Value.value", "(?# To match AWS access keys.)^AKIA[0-9A-Z]{16}$").Reason('A configuration value matches a plaintext credential pattern.') # MongoDB connection strings $Assert.NotMatch($_, "Value.value", "(?# To match MongoDB connection strings.)mongodb://[^:]+:[^@]+@[^/]+/[^?]+(\?[^&]+(&[^&]+)*)?$").Reason('A configuration value matches a plaintext credential pattern.') # MongoDB SRV connection strings $Assert.NotMatch($_, "Value.value", "(?# To match MongoDB SRV connection strings.)mongodb\+srv://[^:]+:[^@]+@[^/]+/[^?]+(\?[^&]+(&[^&]+)*)?$").Reason('A configuration value matches a plaintext credential pattern.') # Redis connection strings $Assert.NotMatch($_, "Value.value", "(?# To match Redis connection strings with password.)^redis://:[^@]+@[^:]+:\d+/?$").Reason('A configuration value matches a plaintext credential pattern.') # Redis SSL connection strings $Assert.NotMatch($_, "Value.value", "(?# To match Redis SSL connection strings.)^rediss://[^:]+:[^@]+@[^:]+:\d+/?$").Reason('A configuration value matches a plaintext credential pattern.') # MySQL connection strings $Assert.NotMatch($_, "Value.value", "(?# To match MySQL connection strings.)^Server=[^;]+;Port=\d+;Database=[^;]+;Uid=[^;]+;Pwd=[^;]+;$").Reason('A configuration value matches a plaintext credential pattern.') # PostgreSQL connection strings $Assert.NotMatch($_, "Value.value", "(?# To match PostgreSQL connection strings.)^Server=[^;]+;Port=\d+;Database=[^;]+;User Id=[^;]+;Password=[^;]+;$").Reason('A configuration value matches a plaintext credential pattern.') # 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?$").Reason('A configuration value matches a plaintext credential pattern.') } } else { $Assert.Null($_, "Value.value") } } } # If there were no variables, then the test passes $Assert.HasField($TargetObject, "variables", $true) } } # Synposis: Release definitions should not have direct permissions for Project Valid Users Rule 'Azure.DevOps.Pipelines.Releases.Definition.ProjectValidUsers' ` -Ref 'ADO-RD-005' ` -Type 'Azure.DevOps.Pipelines.Releases.Definition' ` -If { $null -ne $TargetObject.Acls } ` -Tag @{ release = 'GA' } ` -Level Warning { # Description 'Release definitions should not have direct permissions for Project Valid Users.' Reason 'The release definition has direct permissions for Project Valid Users.' Recommend 'Do not grant direct permissions for Project Valid Users for the release definition.' AllOf { # Loop through all the propeties of the first ACL $TargetObject.Acls.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) } } } } |