SaaS

Secure Azure DevOps with cnspec

Scan Azure DevOps organizations, projects, and Git repositories against security and compliance best practices with cnspec.

Azure DevOps holds the source code your teams ship and the pipelines that deploy it, so a gap in how branches are protected or who can push to them reaches production. cnspec evaluates the projects in an Azure DevOps organization, the branch policies and repository permissions that protect each Git repository, the checks that guard pipeline environments, the service hooks that send repository events outside Azure DevOps, and the state and alerts of GitHub Advanced Security for Azure DevOps.

If you're new to cnspec, start with the Quickstart. For an overview of every SaaS service cnspec can scan, see the SaaS scanning overview.

Prerequisites

To scan Azure DevOps with cnspec, you must have:

  • cnspec installed on your workstation
  • An Azure DevOps Services organization (dev.azure.com). Azure DevOps Server, the on-premises product, isn't supported.
  • A personal access token, or a Microsoft Entra service principal that's a user of the organization

Authenticate

cnspec reads Azure DevOps through read-only REST queries. It accepts a personal access token or a Microsoft Entra service principal.

Personal access token

In the Azure DevOps portal, go to User settings > Personal access tokens and create a token for the organization you want to scan. Give it these scopes:

  • Code (Read)
  • Project and Team (Read)

Pass the token with --token, or set it in the AZURE_DEVOPS_TOKEN environment variable.

Microsoft Entra service principal

To scan with a service principal, first add it to the organization in Organization settings > Users. Azure DevOps rejects every request from a service principal that isn't a user of the organization. Then add it to each project you want to scan. Azure DevOps hides the projects a credential has no access to, so a service principal that belongs to the organization but to none of its projects sees an empty organization.

Pass the service principal with --tenant-id, --client-id, and --client-secret, or set them in the AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET environment variables.

When you pass --tenant-id or --client-id, cnspec uses the service principal even if a token is also set. Otherwise a token takes priority over service principal environment variables, so a tenant ID you exported for another tool doesn't override your token.

Permissions for every check

A credential that reads code and projects is enough to discover and scan repositories. Some fields need more. For example, webhooks needs the View subscriptions permission on the project, which the Readers group lacks by default.

A credential that can't read a field reports it as a forbidden error rather than an empty result, so a check never passes on data cnspec wasn't allowed to read. If a check reports a forbidden error, grant the credential read access to that part of the project or repository.

Environment variables

You can export the connection details once and reuse them across commands:

export AZURE_DEVOPS_TOKEN=YOUR_TOKEN

When these are set, you can omit the matching flags from the commands below.

Connection options

OptionDescription
--tokenAzure DevOps personal access token (also AZURE_DEVOPS_TOKEN)
--tenant-idMicrosoft Entra tenant ID of the service principal (also AZURE_TENANT_ID)
--client-idMicrosoft Entra client ID of the service principal (also AZURE_CLIENT_ID)
--client-secretClient secret of the service principal (also AZURE_CLIENT_SECRET)
--reposOnly include repositories that match these comma-separated <project>/<repo> patterns
--repos-excludeLeave out repositories that match these comma-separated <project>/<repo> patterns
--discoverWhich assets to discover: auto, organization, repos, terraform, k8s-manifests, or all

The organization argument accepts the organization name, its dev.azure.com/<name> address, or its <name>.visualstudio.com address, with or without https://.

Verify with a quick Azure DevOps check

Confirm that cnspec can reach your organization by opening a cnspec shell on the organization alone:

cnspec shell azuredevops org lunalectric --token YOUR_TOKEN --discover organization
cnspec> azuredevops.organization { name id deploymentType }
azuredevops.organization: {
  name: "lunalectric"
  id: "6f1c2b9e-4d7a-4b1e-9a3c-2e8f5d0c7a14"
  deploymentType: "hosted"
}

If cnspec connects and reports hosted as the deployment type, you're ready to scan.

Scan Azure DevOps

Scan an organization and every repository in it:

cnspec scan azuredevops org lunalectric --token YOUR_TOKEN

Scan a single repository:

cnspec scan azuredevops repo lunalectric/platform/payments-api --token YOUR_TOKEN

When a scan completes, cnspec prints a summary of all the checks it ran, grouped by policy, along with a risk score from 0 (no risk) to 100 (highest risk). Failed checks include remediation guidance to help you fix issues. To learn more about reading scan results, read Understand cnspec Results.

Mondoo doesn't yet ship an out-of-the-box Azure DevOps policy, so use the checks below as a starting point and create your own policies to meet your specific requirements. A policy targets repository assets with asset.platform == "azuredevops-repo" and the organization asset with asset.platform == "azuredevops-org".

Choose what to discover

An organization scan discovers the organization and each of its repositories by default. Use --discover to change that:

TargetWhat cnspec discovers
autoThe organization and its repositories (the default)
organizationThe organization asset alone
reposOne asset for each repository
terraformA Terraform asset for each repository that holds a .tf file
k8s-manifestsA Kubernetes manifest asset for each repository that holds YAML files
allEvery target above

For example, to scan the Terraform code in your repositories along with the repositories themselves:

cnspec scan azuredevops org lunalectric --token YOUR_TOKEN --discover repos,terraform

cnspec skips files under a hidden path, such as .github or .azure, when it looks for Terraform and Kubernetes files.

Only repositories that have commits and aren't disabled become assets in an organization scan. cnspec reports the empty and disabled repositories it skips, and it reports any project whose repositories the credential can't list. A repo scan still scans the repository you name when it's empty or disabled, but doesn't discover Terraform or Kubernetes assets in it.

Filter repositories

Narrow a scan with --repos and --repos-exclude. Each takes a comma-separated list of <project>/<repo> patterns, matched without regard to letter case. cnspec keeps the repositories that match the include list, then removes the ones that match the exclude list:

cnspec scan azuredevops org lunalectric --token YOUR_TOKEN --repos "platform/*" --repos-exclude "platform/archive-*"

A * doesn't cross the slash, so platform/* is every repository in the platform project and */infra-* is every repository whose name starts with infra- in any project. Use ** to match across the slash. A pattern without a slash, such as infra-*, matches nothing.

Setting either filter leaves the organization asset out of the scan, since a filter asks for specific repositories.

Explore and test checks interactively

Open a cnspec shell to discover resources and try out checks. A shell connects to one asset, so ask for the organization alone:

cnspec shell azuredevops org lunalectric --token YOUR_TOKEN --discover organization

List the projects in the organization

cnspec> azuredevops.organization.projects { name visibility state lastUpdateTime }

List the repositories cnspec can scan

cnspec> azuredevops.organization.repositories.where(status == "ready") { fullName defaultBranch }

Find the repositories a scan skips, and why

status is ready, empty (no commits yet), or disabled:

cnspec> azuredevops.organization.repositories.where(status != "ready") { fullName status }

Find projects the credential can't fully read

unreadableProjects names the projects the credential can see but whose repositories it can't list. Azure DevOps leaves a project the credential has no access to out of projects entirely, so compare projects with the projects you expect to find:

cnspec> azuredevops.organization.unreadableProjects

Review the protection of each default branch

protectionRules summarizes the enabled, blocking branch policies that apply to a branch, together with the repository permissions that decide who can force push or bypass those policies:

cnspec> azuredevops.organization.repositories.where(status == "ready") {
    fullName
    branches.where(isDefault == true) {
      name
      isProtected
      protectionRules {
        requiredApprovingReviewCount
        requireCodeOwnerReviews
        requiredConversationResolutionEnabled
        requiredStatusChecksEnabled
        allowForcePushesEnabled
        enforceAdminsEnabled
      }
    }
  }

List the branch policies of a repository

Open a shell on one repository to query it directly:

cnspec shell azuredevops repo lunalectric/platform/payments-api --token YOUR_TOKEN
cnspec> azuredevops.repository.policies { typeName enabled blocking scope settings }

Review the checks that guard pipeline environments

cnspec> azuredevops.organization.projects {
    name
    environments {
      name
      protectionRules { type preventSelfReview minRequiredApprovers }
    }
  }

List service hooks that send repository events outside Azure DevOps

cnspec reports the scheme and host of each target address but leaves out the full address, since its path or query can carry a secret:

cnspec> azuredevops.repository.webhooks { eventType consumerId host isHttps active }

Review Advanced Security alerts

cnspec> azuredevops.repository.advancedSecurity.alerts.where(state == "active") { alertType severity title gitRef }

Find pipeline definitions

pipelineFiles finds every azure-pipelines.yml or azure-pipelines.yaml at any depth, and every YAML file under a .azure-pipelines or .azuredevops folder at the root:

cnspec> azuredevops.repository.pipelineFiles { path }

Example security checks

Run these checks in a shell on a repository, or use them in a policy that targets repository assets.

Ensure the default branch is protected

A branch is protected when an enabled, blocking branch policy applies to it:

cnspec> azuredevops.repository.branches.where(isDefault == true).all(isProtected == true)
[ok] value: true

Ensure pull requests into the default branch need at least two approvals

cnspec> azuredevops.repository.branches.where(isDefault == true).all(protectionRules.requiredApprovingReviewCount >= 2)
[ok] value: true

Ensure a required reviewer policy applies to the default branch

A required reviewers policy is the Azure DevOps counterpart of code owner review:

cnspec> azuredevops.repository.branches.where(isDefault == true).all(protectionRules.requireCodeOwnerReviews == true)
[ok] value: true

Ensure every pull request comment is resolved before completion

cnspec> azuredevops.repository.branches.where(isDefault == true).all(protectionRules.requiredConversationResolutionEnabled == true)
[ok] value: true

Ensure a build must pass before a pull request completes

cnspec> azuredevops.repository.branches.where(isDefault == true).all(protectionRules.requiredStatusChecksEnabled == true)
[ok] value: true

Ensure only service identities can force push

A force push rewrites history that reviewers already approved. allowForcePushesEnabled is true when a user or group other than a build or release service identity holds the Force push permission on the repository:

cnspec> azuredevops.repository.branches.where(isDefault == true).all(protectionRules.allowForcePushesEnabled == false)
[ok] value: true

Ensure no one can bypass branch policies

Azure DevOps has no setting that applies branch policies to administrators too. enforceAdminsEnabled is true when no user or group other than a build or release service identity can bypass the policies when pushing or completing a pull request:

cnspec> azuredevops.repository.branches.where(isDefault == true).all(protectionRules.enforceAdminsEnabled == true)
[ok] value: true

Ensure Advanced Security is turned on

cnspec> azuredevops.repository.advancedSecurity.enabled == true
[ok] value: true

Ensure no critical or high severity alert is open

When Advanced Security is off, alerts is null, so this check doesn't apply rather than passing on an empty list:

cnspec> azuredevops.repository.advancedSecurity.alerts.none(state == "active" && (severity == "critical" || severity == "high"))
[ok] value: true

Ensure no leaked secret is open

cnspec> azuredevops.repository.advancedSecurity.alerts.none(state == "active" && alertType == "secret")
[ok] value: true

Ensure every service hook sends events over HTTPS

cnspec> azuredevops.repository.webhooks.all(isHttps == true)
[ok] value: true

Ensure the repository has a security policy

securityFile looks for SECURITY.md at the root, in docs, or in .github:

cnspec> azuredevops.repository.securityFile.exists == true
[ok] value: true

Ensure the repository holds no binaries

cnspec> azuredevops.repository.allFiles.none(isBinary == true)
[ok] value: true

Ensure deployments to production need an approval

cnspec> azuredevops.repository.project.environments.where(name == "production").all(protectionRules.any(type == "Approval"))
[ok] value: true

Ensure the person who ran a pipeline can't approve its deployment

cnspec> azuredevops.repository.project.environments.all(protectionRules.where(type == "Approval").all(preventSelfReview == true))
[ok] value: true

Ensure projects are private

Run this in a shell on the organization:

cnspec> azuredevops.organization.projects.all(visibility == "private")
[ok] value: true

Troubleshooting

  • A 401 error or a message about the personal access token: The token expired, was revoked, or belongs to another organization. Create a new one.
  • Every request fails for a service principal: The service principal isn't a user of the organization yet. Add it in Organization settings > Users.
  • An error that the credential can't read the repositories of any project: The token lacks the Code (Read) scope, or the service principal isn't a member of the projects. Fix the scope or the project membership.
  • A repository is missing from a scan: A --repos or --repos-exclude filter might leave it out, its project might be in azuredevops.organization.unreadableProjects, or it might be empty or disabled. Query azuredevops.organization.repositories { fullName status } to find out.
  • A repository has no Terraform or Kubernetes asset: The default auto target doesn't discover them. Pass --discover terraform, --discover k8s-manifests, or --discover all.
  • A 429 error or slow scans: Azure DevOps throttles requests by cost for each user. cnspec backs off on its own. If it keeps happening, narrow the scan with --repos.

Learn more

On this page

PrerequisitesAuthenticatePersonal access tokenMicrosoft Entra service principalPermissions for every checkEnvironment variablesConnection optionsVerify with a quick Azure DevOps checkScan Azure DevOpsChoose what to discoverFilter repositoriesExplore and test checks interactivelyList the projects in the organizationList the repositories cnspec can scanFind the repositories a scan skips, and whyFind projects the credential can't fully readReview the protection of each default branchList the branch policies of a repositoryReview the checks that guard pipeline environmentsList service hooks that send repository events outside Azure DevOpsReview Advanced Security alertsFind pipeline definitionsExample security checksEnsure the default branch is protectedEnsure pull requests into the default branch need at least two approvalsEnsure a required reviewer policy applies to the default branchEnsure every pull request comment is resolved before completionEnsure a build must pass before a pull request completesEnsure only service identities can force pushEnsure no one can bypass branch policiesEnsure Advanced Security is turned onEnsure no critical or high severity alert is openEnsure no leaked secret is openEnsure every service hook sends events over HTTPSEnsure the repository has a security policyEnsure the repository holds no binariesEnsure deployments to production need an approvalEnsure the person who ran a pipeline can't approve its deploymentEnsure projects are privateTroubleshootingLearn more