Skip to content

Feature/azdo#1371

Open
SebastianClaesson wants to merge 52 commits intomaester365:mainfrom
SebastianClaesson:feature/azdo
Open

Feature/azdo#1371
SebastianClaesson wants to merge 52 commits intomaester365:mainfrom
SebastianClaesson:feature/azdo

Conversation

@SebastianClaesson
Copy link
Contributor

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 :

  • Rationale
  • Remediation action
  • Related links (Official links / blog posts describing the security issue)

Contribution Checklist

Before submitting this PR, please confirm you have completed the following:

  • 📖 Read the guidelines for contributing to this repository.
  • 🧪 Ensure the build and unit tests pass by running /powershell/tests/pester.ps1 on your local system.

@SebastianClaesson SebastianClaesson requested review from a team as code owners December 11, 2025 08:06
@SamErde SamErde requested a review from Copilot January 21, 2026 11:19
@SamErde SamErde added enhancement New feature or request maester-test Related to a Maester test labels Jan 21, 2026
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:**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the detailed 401 go to users in the organization and the 404 go to users not in the organization?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do agree!
Seems the experience has changed since;

MicrosoftDocs/azure-devops-docs@91c4410#diff-a28e8dd823f1493651d0c6322e6b2dd976bccab79e279de5aea876ce20272729R44

I'll update with the new information the article.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 28 to 39
$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
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 32 to 36
$result = $false
}
else {
$resultMarkdown = "Classic build pipelines can be created / imported."
$result = $true
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
$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

Copilot uses AI. Check for mistakes.

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."
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."

Copilot uses AI. Check for mistakes.
[OutputType([bool])]
param()

Write-verbose 'Not connected to Azure DevOps'
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 31 to 35
$result = $false
}
else {
$resultMarkdown = "Classic release pipelines, task groups, and deployment groups can be created / imported."
$result = $true
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
$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

Copilot uses AI. Check for mistakes.
Rationale: Third-party application access should not be used for Azure DevOps.

#### Remediation action:
Disable the policy to stops these requests and notifications.
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing the actual check for an ADO connection here. 🙂

[OutputType([bool])]
param()

Write-verbose 'Not connected to Azure DevOps'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing the actual check for an ADO connection here. 🙂

[OutputType([bool])]
param()

Write-verbose 'Not connected to Azure DevOps'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing the actual check for an ADO connection here. 🙂

[OutputType([bool])]
param()

Write-verbose 'Not connected to Azure DevOps'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing the actual check for an ADO connection here. 🙂

[OutputType([bool])]
param()

Write-verbose 'Not connected to Azure DevOps'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing the actual check for an ADO connection here. 🙂

[OutputType([bool])]
param()

Write-verbose 'Not connected to Azure DevOps'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing the actual check for an ADO connection here. 🙂

[OutputType([bool])]
param()

Write-verbose 'Not connected to Azure DevOps'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing the actual check for an ADO connection here. 🙂

[OutputType([bool])]
param()

Write-verbose 'Not connected to Azure DevOps'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing the actual check for an ADO connection here. 🙂

[OutputType([bool])]
param()

Write-verbose 'Not connected to Azure DevOps'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing the actual check for an ADO connection here. 🙂

[OutputType([bool])]
param()

Write-verbose 'Not connected to Azure DevOps'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing the actual check for an ADO connection here. 🙂

@SamErde
Copy link
Contributor

SamErde commented Feb 18, 2026

Sorry for all of the comments! This is awesome work!

@SebastianClaesson
Copy link
Contributor Author

@SebastianClaesson this is top notch work! Love it.

I would suggest two additions.

  1. Add doc - Update the installation doc to include the optinal module to install and the command for connecting website/docs/installation.md . See https://maester.dev/docs/installation#optional-modules-and-permissions
  2. Add a blog post that outlines that calls out the new tests to website/blog See https://maester.dev/blog

Thank you @merill , I'll add the docs and a blog post to showcase / outline the new tests!

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +38 to +41
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)'"
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +39
$resultMarkdown = "Pipeliens may utilize a task with Node 6 execution handler."
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in result text: "Pipeliens" should be "Pipelines".

Copilot uses AI. Check for mistakes.
Comment on lines +1683 to +1685
"Severity": "High",
"Title": "Additional protections whne using public registries"
},
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in title: "whne" should be "when".

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +6
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."

Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +48
$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
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Write-verbose 'Not connected to Azure DevOps'
Add-MtTestResultDetail -SkippedBecause Custom -SkippedCustomReason 'Not connected to Azure DevOps'
return $null
break
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
break

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +39
$resultMarkdown = "Your tenant have not restricted Azure DevOps OAuth apps to access resources in your organization through OAuth."
} else {
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor grammatical issue in the result text: "Your tenant have not restricted ..." should be "Your tenant has not restricted ...".

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +40
$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"
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +38
$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"
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
$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."

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request maester-test Related to a Maester test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🙏 Azure DevOps tests

3 participants

Comments