Conversation
…aester into feature/azdo
| 3. Select Policies, locate the Request Access policy and toggle it to off. | ||
| 4. Provide the URL to your internal process for gaining access. Users see this URL in the error report when they try to access the organization or a project within the organization that they don't have permission to access. | ||
|
|
||
| **Results:** |
There was a problem hiding this comment.
Shouldn't the detailed 401 go to users in the organization and the 404 go to users not in the organization?
There was a problem hiding this comment.
I do agree!
Seems the experience has changed since;
I'll update with the new information the article.
powershell/public/maester/azdo/Test-AzdoAllowRequestAccessToken.md
Outdated
Show resolved
Hide resolved
powershell/public/maester/azdo/Test-AzdoAllowTeamAdminsInvitationsAccessToken.md
Outdated
Show resolved
Hide resolved
powershell/public/maester/azdo/Test-AzdoAllowTeamAdminsInvitationsAccessToken.md
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
This pull request adds 31 Azure DevOps security tests to the Maester project, providing comprehensive security assessments for Azure DevOps organizations. Each test includes both PowerShell implementation and markdown documentation describing the security rationale and remediation steps.
Changes:
- Added 31 new Azure DevOps security test functions covering authentication, access control, pipeline security, and resource management
- Created comprehensive markdown documentation for each test with remediation guidance
- Updated the module manifest to export all new test functions
- Added a test runner file that orchestrates all Azure DevOps security tests
Reviewed changes
Copilot reviewed 65 out of 65 changed files in this pull request and generated 28 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Maester/Azdo/Test-Azdo.Tests.ps1 | Test runner that executes all 31 Azure DevOps security tests |
| tests/Maester/Azdo/README.md | Overview documentation for Azure DevOps tests |
| powershell/public/maester/azdo/*.ps1 | 31 PowerShell functions implementing security checks |
| powershell/public/maester/azdo/*.md | 31 markdown documentation files with rationale and remediation steps |
| powershell/Maester.psd1 | Module manifest updated to export new functions |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
...ll/public/maester/azdo/Test-AzdoOrganizationLimitJobAuthorizationScopeNonReleasePipeline.ps1
Outdated
Show resolved
Hide resolved
powershell/public/maester/azdo/Test-AzdoOrganizationStageChooser.md
Outdated
Show resolved
Hide resolved
Corrected typos and formatting in the documentation.
Removed unnecessary commas and spaces. Added line breaks for MD linting.
Corrected grammatical errors and added punctuation for clarity.
Updated description to clarify the function's purpose and improve readability.
Corrected grammatical errors and improved clarity in remediation instructions.
Clarified the purpose of the Azure DevOps tests and updated the reference link.
…thorizationScopeNonReleasePipeline.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 65 out of 65 changed files in this pull request and generated 11 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| $result = (Get-ADOPSOrganizationPipelineSettings).disableStageChooser | ||
|
|
||
| if (-not $result) { | ||
| $resultMarkdown = "Users are able to select stages to skip from the Queue Pipeline panel." | ||
| } | ||
| else { | ||
| $resultMarkdown = "Well done. Users will not be able to select stages to skip from the Queue Pipeline panel." | ||
| } | ||
|
|
||
| Add-MtTestResultDetail -Result $resultMarkdown -Severity 'High' | ||
|
|
||
| return $result |
There was a problem hiding this comment.
Return value logic conflict. When disableStageChooser is $true (secure state), the function returns $true, but the test expects $false (line 100 in Test-Azdo.Tests.ps1: "Should -Be $false"). Either the test expectation is wrong, or the function should return the inverse. Based on the test comment "Users should not be able to select stages," it seems the test expects $false to mean "stage chooser is disabled" (secure state), but the function returns $true when disabled. Consider inverting the return value: return -not $result to align with test expectations.
powershell/public/maester/azuredevops/Test-AzdoOrganizationTaskRestrictionsDisableNode6Task.ps1
Outdated
Show resolved
Hide resolved
| $result = $false | ||
| } | ||
| else { | ||
| $resultMarkdown = "Classic build pipelines can be created / imported." | ||
| $result = $true |
There was a problem hiding this comment.
The same return value logic issue exists here as in Test-AzdoOrganizationCreationClassicReleasePipeline. When disableClassicBuildPipelineCreation is $true (secure state), the function returns $false, which conflicts with the test expectation that secure state should return $false to indicate "creation should not be allowed". Review the intended semantics and align the return value with the test expectations.
| $result = $false | |
| } | |
| else { | |
| $resultMarkdown = "Classic build pipelines can be created / imported." | |
| $result = $true | |
| $result = $true | |
| } | |
| else { | |
| $resultMarkdown = "Classic build pipelines can be created / imported." | |
| $result = $false |
|
|
||
| It "AZDO.1017: Limit job authorization scope to current project for classic release pipelines. See https://learn.microsoft.com/en-us/azure/devops/pipelines/process/access-tokens?view=azure-devops&tabs=yaml#job-authorization-scope" -Tag "AZDO.1017" { | ||
|
|
||
| Test-AzdoOrganizationLimitJobAuthorizationScopeReleasePipeline | Should -Be $true -Because "With this option enabled, you can reduce the scope of access for all non-release pipelines to the current project." |
There was a problem hiding this comment.
The "Because" message describes the behavior for "non-release pipelines" but the test is for "classic release pipelines". The message should state: "With this option enabled, you can reduce the scope of access for all classic release pipelines to the current project."
powershell/public/maester/azuredevops/Test-AzdoSSHAuthentication.md
Outdated
Show resolved
Hide resolved
| [OutputType([bool])] | ||
| param() | ||
|
|
||
| Write-verbose 'Not connected to Azure DevOps' |
There was a problem hiding this comment.
Inconsistent connection checking across Azure DevOps tests. Some tests check for Azure DevOps connection using Get-ADOPSConnection (like Test-AzdoExternalGuestAccess, Test-AzdoEnforceAADConditionalAccess, etc.) while most tests only output a verbose message without actually checking the connection. This test should either check the connection status and skip with a proper message, or the Write-Verbose statement on line 27 is misleading. Consider implementing consistent connection checking across all AzDO tests, following the pattern in Test-AzdoExternalGuestAccess.ps1.
| $result = $false | ||
| } | ||
| else { | ||
| $resultMarkdown = "Classic release pipelines, task groups, and deployment groups can be created / imported." | ||
| $result = $true |
There was a problem hiding this comment.
The return value logic appears inverted. When disableClassicReleasePipelineCreation is $true (meaning creation is disabled, which is the desired secure state), the function returns $false, but the test expects $false when creation should NOT be allowed. This creates a logic conflict. If the setting name means "disable creation" and it's true, the function should return $true (secure state achieved), not $false. The same issue exists in Test-AzdoOrganizationCreationClassicBuildPipeline.ps1.
| $result = $false | |
| } | |
| else { | |
| $resultMarkdown = "Classic release pipelines, task groups, and deployment groups can be created / imported." | |
| $result = $true | |
| $result = $true | |
| } | |
| else { | |
| $resultMarkdown = "Classic release pipelines, task groups, and deployment groups can be created / imported." | |
| $result = $false |
powershell/public/maester/azuredevops/Test-AzdoOrganizationOwner.ps1
Outdated
Show resolved
Hide resolved
| Rationale: Third-party application access should not be used for Azure DevOps. | ||
|
|
||
| #### Remediation action: | ||
| Disable the policy to stops these requests and notifications. |
There was a problem hiding this comment.
Grammar error appears consistently across multiple markdown files: "to stops these" should be "to stop these". This phrase doesn't make sense in most contexts anyway - the remediation actions are about enabling/disabling policies, not stopping requests and notifications. Consider revising to match the actual action being taken or removing this phrase entirely. Found in: Test-AzdoThirdPartyAccessViaOauth.md, Test-AzdoSSHAuthentication.md, Test-AzdoPublicProject.md, Test-AzdoOrganizationTaskRestrictionsShellTaskArgumentValidation.md, Test-AzdoOrganizationTaskRestrictionsDisableNode6Task.md, Test-AzdoOrganizationTaskRestrictionsDisableMarketplaceTask.md, Test-AzdoOrganizationStageChooser.md, Test-AzdoOrganizationRepositorySettingsGravatarImage.md, Test-AzdoOrganizationBadgesArePrivate.md, Test-AzdoLogAuditEvent.md.
Corrected spelling of 'indiviual' to 'individual' in description.
Removed an extra comment-based help header section.
| [OutputType([bool])] | ||
| param() | ||
|
|
||
| Write-verbose 'Not connected to Azure DevOps' |
There was a problem hiding this comment.
Missing the actual check for an ADO connection here. 🙂
| [OutputType([bool])] | ||
| param() | ||
|
|
||
| Write-verbose 'Not connected to Azure DevOps' |
There was a problem hiding this comment.
Missing the actual check for an ADO connection here. 🙂
| [OutputType([bool])] | ||
| param() | ||
|
|
||
| Write-verbose 'Not connected to Azure DevOps' |
There was a problem hiding this comment.
Missing the actual check for an ADO connection here. 🙂
| [OutputType([bool])] | ||
| param() | ||
|
|
||
| Write-verbose 'Not connected to Azure DevOps' |
There was a problem hiding this comment.
Missing the actual check for an ADO connection here. 🙂
| [OutputType([bool])] | ||
| param() | ||
|
|
||
| Write-verbose 'Not connected to Azure DevOps' |
There was a problem hiding this comment.
Missing the actual check for an ADO connection here. 🙂
| [OutputType([bool])] | ||
| param() | ||
|
|
||
| Write-verbose 'Not connected to Azure DevOps' |
There was a problem hiding this comment.
Missing the actual check for an ADO connection here. 🙂
| [OutputType([bool])] | ||
| param() | ||
|
|
||
| Write-verbose 'Not connected to Azure DevOps' |
There was a problem hiding this comment.
Missing the actual check for an ADO connection here. 🙂
| [OutputType([bool])] | ||
| param() | ||
|
|
||
| Write-verbose 'Not connected to Azure DevOps' |
There was a problem hiding this comment.
Missing the actual check for an ADO connection here. 🙂
| [OutputType([bool])] | ||
| param() | ||
|
|
||
| Write-verbose 'Not connected to Azure DevOps' |
There was a problem hiding this comment.
Missing the actual check for an ADO connection here. 🙂
| [OutputType([bool])] | ||
| param() | ||
|
|
||
| Write-verbose 'Not connected to Azure DevOps' |
There was a problem hiding this comment.
Missing the actual check for an ADO connection here. 🙂
|
Sorry for all of the comments! This is awesome work! |
Thank you @merill , I'll add the docs and a blog post to showcase / outline the new tests! |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 68 out of 68 changed files in this pull request and generated 12 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if ($Member.subjectKind -eq 'group') { | ||
| Write-Verbose "Finding members in group '$($Member.DisplayName)' - Descriptor '$($_.Descriptor)'" | ||
| Get-ADOPSMembership -Descriptor $Member.descriptor -Direction 'down' | Foreach-object { | ||
| Write-Verbose "Processing member '$($_.DisplayName)' - Descriptor '$($_.Descriptor)'" |
There was a problem hiding this comment.
Inside Get-NestedAdoMembership, the first Write-Verbose message references $($_.Descriptor) even though $_ isn’t set in that scope; this will log an empty/incorrect descriptor. Use $Member.Descriptor there to match the rest of the function.
| $resultMarkdown = "Pipeliens may utilize a task with Node 6 execution handler." | ||
| } |
There was a problem hiding this comment.
Typo in result text: "Pipeliens" should be "Pipelines".
| "Severity": "High", | ||
| "Title": "Additional protections whne using public registries" | ||
| }, |
There was a problem hiding this comment.
Typo in title: "whne" should be "when".
| Checks the status of when you sign in to the web portal of a Microsoft Entra ID-backed organization, | ||
| Microsoft Entra ID always performs validation for any Conditional Access Policies (CAPs) set by tenant administrators. | ||
|
|
||
| https://learn.microsoft.com/en-us/azure/devops/organizations/audit/auditing-streaming?view=azure-devops |
There was a problem hiding this comment.
The comment header link in the description points to audit streaming (.../audit/auditing-streaming...) which doesn’t match this test’s purpose (Conditional Access enforcement). Updating it to the Conditional Access policy documentation would avoid confusing users reading the help.
| https://learn.microsoft.com/en-us/azure/devops/organizations/audit/auditing-streaming?view=azure-devops | |
| https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/manage-conditional-access?view=azure-devops&tabs=preview-page |
| It "AZDO.1000: Azure DevOps OAuth apps can access resources in your organization through OAuth. See https://aka.ms/vstspolicyoauth" -Tag "AZDO.1000" { | ||
|
|
||
| Test-AzdoThirdPartyAccessViaOauth | Should -Be $false -Because "Your tenant should restrict Azure DevOps OAuth apps to access resources in your organization through OAuth." | ||
|
|
There was a problem hiding this comment.
These Pester tests call the Test-Azdo* functions directly in the assertion. Many of those functions return $null when not connected to Azure DevOps, which will cause these assertions to fail (null is neither $true nor $false). Capture the function output into a variable and only assert when it’s non-null (same pattern used in other Maester test files).
| $data = @' | ||
| Prevent pipelines from making secrets available to fork builds is set to '{0}'\ | ||
| Prevent pipelines from making fork builds have the same permissions as regular builds is set to '{1}'\ | ||
| Require a team member's comment before building a pull request is set to '{2}' ({3}) | ||
| '@ -f $settings.enforceNoAccessToSecretsFromForks, $settings.enforceJobAuthScopeForForks, $settings.isCommentRequiredForPullRequest, $AdditionalInfo |
There was a problem hiding this comment.
The here-string includes trailing backslashes at the end of lines, which will be emitted literally in the result output (they don’t act as line continuations inside a here-string). If the intent is multiline formatting, remove the trailing \ characters and rely on the here-string newlines (or build the string with explicit newlines).
| Write-verbose 'Not connected to Azure DevOps' | ||
| Add-MtTestResultDetail -SkippedBecause Custom -SkippedCustomReason 'Not connected to Azure DevOps' | ||
| return $null | ||
| break |
There was a problem hiding this comment.
break after return $null is unreachable and can be removed. This pattern appears across the Azure DevOps test functions, so it would be good to clean it up throughout to avoid dead code.
| break |
| $resultMarkdown = "Your tenant have not restricted Azure DevOps OAuth apps to access resources in your organization through OAuth." | ||
| } else { |
There was a problem hiding this comment.
Minor grammatical issue in the result text: "Your tenant have not restricted ..." should be "Your tenant has not restricted ...".
| $ApplicationPolicies = Get-ADOPSOrganizationPolicy -PolicyCategory 'ApplicationConnection' | ||
| $Policy = $ApplicationPolicies.policy | where-object -property name -eq 'Policy.DisallowSecureShell' | ||
| $result = $Policy.effectiveValue | ||
| if ($result) { | ||
| $resultMarkdown = "Your tenant allows developers to connect to your Git repos through SSH on macOS, Linux, or Windows to connect with Azure DevOps" | ||
| } else { | ||
| $resultMarkdown = "Well done. Your tenant does not allow developers to connect to your Git repos through SSH on macOS, Linux, or Windows to connect with Azure DevOps" | ||
| } |
There was a problem hiding this comment.
The policy queried is 'Policy.DisallowSecureShell', but the current logic treats $Policy.effectiveValue -eq $true as “SSH allowed” and returns $true. This appears inverted relative to the policy name and will likely report the opposite of the intended security posture. Consider inverting the boolean / message so that enabling the disallow policy yields the secure outcome.
| $result = $Policy.effectiveValue | ||
| if ($result) { | ||
| $resultMarkdown = "External user(s) can be added to the organization to which they were invited and has immediate access. A guest user can add other guest users to the organization after being granted the Guest Inviter role in Microsoft Entra ID." | ||
| } else { | ||
| $resultMarkdown = "Well done. External users should not be allowed access to your Azure DevOps organization" |
There was a problem hiding this comment.
The policy queried is 'Policy.DisallowAadGuestUserAccess', but the current logic treats $Policy.effectiveValue -eq $true as “external guests have access” and returns $true. This looks inverted relative to the policy name and will likely mark a secure configuration as failing. Consider inverting the returned boolean and aligning the result text so that disallowing guest access corresponds to the secure outcome.
| $result = $Policy.effectiveValue | |
| if ($result) { | |
| $resultMarkdown = "External user(s) can be added to the organization to which they were invited and has immediate access. A guest user can add other guest users to the organization after being granted the Guest Inviter role in Microsoft Entra ID." | |
| } else { | |
| $resultMarkdown = "Well done. External users should not be allowed access to your Azure DevOps organization" | |
| # $Policy.effectiveValue is $true when guest access is disallowed. | |
| # $result represents the secure configuration: $true when external guest access is not allowed. | |
| $result = -not $Policy.effectiveValue | |
| if ($result) { | |
| $resultMarkdown = "Well done. External users should not be allowed access to your Azure DevOps organization" | |
| } else { | |
| $resultMarkdown = "External user(s) can be added to the organization to which they were invited and has immediate access. A guest user can add other guest users to the organization after being granted the Guest Inviter role in Microsoft Entra ID." |
Description
Fixes #1368
Adding 31 Azure DevOps security tests.
Each of the tests is added to the new folder azdo in maester\public\maester
All of the tests have a markdown file, with :
Contribution Checklist
Before submitting this PR, please confirm you have completed the following:
/powershell/tests/pester.ps1on your local system.