Skip to content

Azure DevOps Integration Guide

Integrate CloudSlash scanning into Azure Pipelines with automated quality gates and tabular scan summaries.

Quick Start (5-Minute Setup)

Prerequisites

  • Azure DevOps Organization (formerly Visual Studio Team Services)
  • Project with Pipeline permissions
  • CloudSlash API key (from dashboard)
  • (Optional) Azure Service Connection

Step 1: Install CloudSlash Task

Add CloudStencil to your azure-pipelines.yml:

# azure-pipelines.yml
trigger:
- main

stages:
- stage: Infrastructure_Scan
  displayName: Infrastructure Security & Cost Scan
  jobs:
  - job: CloudSlash_Scan
    displayName: Run Security Scan
    pool:
      vmImage: 'ubuntu-latest'
    steps:
    - checkout: self

    - task: CloudSlash@1
      displayName: 'CloudSlash Infrastructure Scan'
      inputs:
        api-key: $(CLOUDSLASH_API_KEY)
        config: cloudslash.yml
        mode: pr

    - task: PublishBuildArtifacts@1
      displayName: 'Publish Scan Report'
      inputs:
        PathtoPublish: 'cloudslash-output'
        ArtifactName: 'cloudslash-report'
        publishLocation: 'Container'

    - task: PublishPipelineSummary@1
      displayName: 'Add Pipeline Summary'
      condition: always()
      inputs:
        summaryText: |
          ## Infrastructure Scan Results
          **Status**: ${{ steps.CloudSlash_Scan.outputs['scan-passed'] }}
          **Findings**: ${{ steps.CloudSlash_Scan.outputs['findings-count'] }}
        selector: tail

Step 2: Add Variables

Go to your pipeline → Edit → Variables:

  • Add CLOUDSLASH_API_KEY with your CloudSlash API key

Note: Variables are per-project only; they aren't available at the org level.

Step 3: Configure CloudSlash

Create cloudslash.yml in your repository root:

version: 1.0
accounts:
  - provider: aws
    name: production
    accounts:
      - id: "123456789012"
        role_arn: "arn:aws:iam::123456789012:role/CloudSlashAccess"

security:
  compliance:
    enabled: true
    benchmark: cis

notifications:
  slack:
    webhook_url: "$(SLACK_WEBHOOK)"

Done! Your first scan will run on the next pipeline run.

Full Configuration Reference

Common Workflow Patterns

Pattern 1: PR/MR Cost Scanning

Prevent costly changes before merging:

# azure-pipelines.yml
trigger:
- main

variables:
  agentImage: ubuntu-latest

stages:
- stage: Cost_Analysis
  displayName: Cost Analysis for PR
  jobs:
  - deployment:
      environment: Staging
      name: Cost_Analysis_Job
    pool:
      vmImage: $(agentImage)
    strategy:
      runOnce:
        deploy:
          steps:
          - checkout: self

          - task: CloudSlash@1
            displayName: 'Cost Analysis Scan'
            inputs:
              api-key: $(CLOUDSLASH_API_KEY)
              config: cloudslash.yml
              mode: pr

          - task: PublishPipelineSummary@1
            displayName: 'Add Cost Analysis Summary'
            condition: always()
            inputs:
              summaryText: |
                ## Cost Analysis Report
                **Status**: ✅ PASSED
                **Repository**: $(Build.Repository.Name)
                **Branch**: $(Build.SourceBranchName)
                **Changed Files**: $(ChangedFiles)
              selector: tail

Pattern 2: Scheduled Drift Detection

Monitor production infrastructure changes continuously:

# azure-pipelines.yml
trigger:
- none  # Don't trigger on PR/commit

schedules:
  - cron: '0 2 * * *'  # Daily at 2 AM
    displayName: 'Daily Drift Detection'
    branches:
      include:
      - main
      - staging
    always: true

stages:
- stage: Drift_Detection
  displayName: Infrastructure Drift Detection
  jobs:
  - deployment:
      environment: Production
      name: Drift_Detection_Job
    pool:
      vmImage: ubuntu-latest
    steps:
    - checkout: self

    - task: CloudSlash@1
      displayName: 'Drift Detection Scan'
      inputs:
        api-key: $(CLOUDSLASH_API_KEY)
        config: cloudslash.yml
        mode: scheduled
        scan-id: $(Build.BuildId)

    - task: PublishPipelineSummary@1
      displayName: 'Add Drift Detection Summary'
      condition: always()
      inputs:
        summaryText: |
          ## Infrastructure Drift Detection
          **Schedule**: Daily at 2 AM UTC
          **Status**: ${{ steps.Clamp.outputs['nodrift'] }}
          **Repository**: $(Build.Repository.Name)
        selector: tail

Pattern 3: Deployment Gates

Block deployments that don't meet security requirements:

# azure-pipelines.yml
trigger:
- main

stages:
- stage: Security_Check
  displayName: Security Gate Check
  jobs:
  - deployment:
      environment: Production
      name: Security_Gate_Job
    pool:
      vmImage: ubuntu-latest
    strategy:
      runOnce:
        deploy:
          steps:
          - checkout: self

          - task: CloudSlash@1
            displayName: 'Security Gate Scan'
            inputs:
              api-key: $(CLOUDSLASH_API_KEY)
              config: cloudslash.yml
              mode: deployment-gate
              fail-on-warnings: true
              fail-on-errors: true

          - task: Bash@3
            displayName: 'Verify Security Check'
            inputs:
              targetType: inline
              script: |
                if [ "${{ steps.CloudSlash_Scan.outputs['scan-passed'] }}" != "true" ]; then
                  echo "❌ Security Gate Failed"
                  exit 1
                fi
                echo "✅ Security Gate Passed"

          - task: PublishPipelineSummary@1
            displayName: 'Add Security Gate Summary'
            condition: always()
            inputs:
              summaryText: |
                ## Deployment Security Gate
                **Environment**: Production
                **Status**: ${{ steps.CloudSlash_Scan.outputs['scan-status'] }}
              selector: tail

Pattern 4: Monorepo Setup

Centralized scanning for multiple repositories:

# azure-pipelines.yml (root monorepo)
trigger:
- main

stages:
- stage: Infrastructure
  displayName: Infrastructure Scan for All Subrepos
  jobs:
  - job: Monorepo_Scan
    displayName: Scan All Subrepos
    pool:
      vmImage: 'windows-latest'  # Works cross-platform often, but safer on windows for multi-repo
    strategy:
      matrix:
        frontend:
          subrepo: 'frontend'
        backend:
          subrepo: 'backend'
        infra:
          subrepo: 'infrastructure'
        data:
          subrepo: 'data-pipeline'
    steps:
    - checkout: self

    - task: CloudSlash@1
      displayName: 'CloudSlash Scan - $(subrepo)'
      inputs:
        api-key: $(CLOUDSLASH_API_KEY)
        config: cloudslash.yml

    - task: PublishBuildArtifacts@1
      displayName: 'Publish $(subrepo) Results'
      inputs:
        PathtoPublish: 'cloudslash-output'
        ArtifactName: 'cloudslash-$(subrepo)-report'
        publishLocation: 'Container'

- stage: Aggregate_Results
  displayName: Aggregate All Subrepo Results
  dependsOn: Infrastructure
  jobs:
  - job: Aggregate
    displayName: Aggregate Reports
    pool:
      vmImage: 'ubuntu-latest'
    steps:
    - download: current
      artifact: cloudslash-frontend-report
    - download: current
      artifact: cloudslash-backend-report
    - download: current
      artifact: cloudslash-infra-report
    - download: current
      artifact: cloudslash-data-report

    - task: PowerShell@2
      displayName: 'Aggregate Reports'
      inputs:
        targetType: inline
        script: |
          $frontend = Get-Content "$(Pipeline.Workspace)/cloudslash-frontend-report/*.json" | Out-String
          $backend = Get-Content "$(Pipeline.Workspace)/cloudslash-backend-report/*.json" | Out-String
          Write-Host "Frontend Findings: $frontend"
          Write-Host "Backend Findings: $backend"

Pattern 5: Multi-Cloud Setup

Unified scanning across AWS, Azure, and GCP:

# azure-pipelines.yml
trigger:
- main

stages:
- stage: AWS_Scan
  displayName: AWS Infrastructure Scan
  jobs:
  - deployment:
      environment: Staging
      name: AWS_Scan_Job
    pool:
      vmImage: 'ubuntu-latest'
    strategy:
      runOnce:
        deploy:
          steps:
          - checkout: self

          - task: CloudSlash@1
            displayName: 'AWS Infrastructure Scan'
            inputs:
              api-key: $(AWS_CLOUDSLASH_API_KEY)
              config: aws-cloudslash.yml

- stage: Azure_Scan
  displayName: Azure Infrastructure Scan
  dependsOn: AWS_Scan
  jobs:
  - deployment:
      environment: Staging
      name: Azure_Scan_Job
    pool:
      vmImage: 'ubuntu-latest'
    steps:
    - checkout: self

    - task: CloudSlash@1
      displayName: 'Azure Infrastructure Scan'
      inputs:
        api-key: $(AZURE_CLOUDSLASH_API_KEY)
        config: azure-cloudslash.yml

- stage: GCP_Scan
  displayName: GCP Infrastructure Scan
  dependsOn: Azure_Scan
  jobs:
  - deployment:
      environment: Staging
      name: GCP_Scan_Job
    pool:
      vmImage: 'ubuntu-latest'
    steps:
    - checkout: self

    - task: CloudSlash@1
      displayName: 'GCP Infrastructure Scan'
      inputs:
        api-key: $(GCP_CLOUDSLASH_API_KEY)
        config: gcp-cloudslash.yml

- stage: MultiCloud_Aggregation
  displayName: Aggregate Multi-Cloud Results
  dependsOn: [AWS_Scan, Azure_Scan, GCP_Scan]
  jobs:
  - deployment:
      environment: Production
      name: Aggregate_Job
    pool:
      vmImage: 'ubuntu-latest'
    steps:
    - download: current
      artifact: AWS_Artifact
    - download: current
      artifact: Azure_Artifact
    - download: current
      artifact: GCP_Artifact

    - task: PowerShell@2
      displayName: 'Aggregate Multi-Cloud Reports'
      inputs:
        targetType: inline
        script: |
          Write-Host "✅ Multi-cloud scan completed successfully for $(Build.Repository.Name)"

# aws-cloudslash.yml
version: 1.0
accounts:
  - provider: aws
    name: production
    accounts:
      - id: "123456789012"

# azure-cloudslash.yml
version: 1.0
accounts:
  - provider: azure
    name: production
    subscriptions:
      - id: "11111111-1111-1111-1111-111111111111"

# gcp-cloudslash.yml
version: 1.0
accounts:
  - provider: gcp
    name: production
    projects:
      - id: "my-project-123"

Security Best Practices

1. Use Service Connections

For service-to-service authentication:

- task: AzureCLI@2
  displayName: 'Connect to Azure'
  inputs:
    azureSubscription: 'My-Service-Connection'

2. Restrict Pipeline Permissions

Limit who can run pipelines:

# In Azure DevOps project settings
- Lock pipelines or set up restriction (available in resource lock or otherwise)
- Use approvers for certain environments

3. Rate Limit Protection

Prevent API rate limit issues:

- task: AzurePowerShell@5
  displayName: 'Check Rate Limits'
  inputs:
    azureSubscription: 'My-Service-Connection'
    ScriptType: InlineScript
    Inline: |
      # Check API rate limits
      $consumerName = 'cloudslash-scan'
      Get-AzRateLimitInformation -MaxCapacity -ConsumerName $consumerName | Out-String

4. Validate Inputs

Prevent injection attacks:

- task: PowerShell@2
  displayName: 'Validate Environment'
  inputs:
    targetType: inline
    script: |
      $allowedEnvironments = @('dev', 'staging', 'prod')
      if ($env:ENVIRONMENT -notin $allowedEnvironments) {
        Write-Error "Invalid environment: $env:ENVIRONMENT"
        exit 1
      }

5. Use Conditional Steps

Only run scans on specific triggers:

- task: CloudSlash@1
  displayName: 'Run Scan'
  condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/heads/main'))
  inputs:
    api-key: $(CLOUDSLASH_API_KEY)
    config: cloudslash.yml

Multi-Account Setup

Using Assumed Roles for AWS

- task: CloudSlash@1
  displayName: 'AWS Cross-Account Scan'
  inputs:
    api-key: $(CLOUDSLASH_API_KEY)
    config: cloudslash.yml
    assume-role-arn: "arn:aws:sts::123456789012:assumed-role/CloudSlashRole/scan-session"

Using RBAC for Azure

- task: AzurePowerShell@5
  displayName: 'Azure RBAC Scan'
  inputs:
    azureSubscription: 'My-Service-Connection'
    ScriptType: InlineScript
    Inline: |
      # Configure RBAC permissions
      Set-AzRoleAssignment -ObjectId $(CloudSlashPrincipalObjectID) -RoleDefinitionName 'Contributor' -Scope /subscriptions/$(SubscriptionID)

Using Organization-wide Scanning

# Offload or sync scanning jobs to a scan runner (optional)
trigger:
- main

stages:
- stage: SSA_SYNC
  displayName: Sync Scan to Scan Runner
  jobs:
  - deployment:
      environment: ScanRunner
      name: Deploy_Scan_Job
    pool:
      vmImage: windows-latest
    steps:
    - checkout: self

    - task: AzurePowerShell@5
      displayName: 'Deploy to Scan Runner'
      inputs:
        azureSubscription: 'My-Service-Connection'
        ScriptType: InlineScript
        Inline: |
          ... # Deploy scanning job to runner

- stage: SSA_OFFLOAD
  displayName: Offload Scanning to Runner
  dependsOn: SSA_SYNC
  jobs:
  - deployment:
      environment: ScanRunner
      name: Send_Scan_Request
    pool:
      vmImage: windows-latest
    steps:
    - task: AzurePowerShell@5
      displayName: 'Queue Scan Job'
      inputs:
        azureSubscription: 'My-Service-Connection'
        ScriptType: InlineScript
        Inline: |
          ... # Send scan request to runner

Notification Integration

Slack Integration

- task: PowerShell@2
  displayName: 'Send Slack Notification'
  condition: always()
  inputs:
    targetType: inline
    script: |
      $webhook = "$(SLACK_WEBHOOK)"
      $status = "${{ steps.CloudSlash_Scan.outputs['scan-passed'] }}"
      $body = @{
        blocks = @(
            @{
                type = "section"
                text = @{
                    type = "mrkdwn"
                    text = "### Infrastructure Scan"
                }
            },
            @{
                type = "section"
                fields = @(
                    @{
                        type = "mrkdwn"
                        text = "**Status**:`$status`"
                    }
                )
            },
            @{
                type = "section"
                fields = @(
                    @{
                        type = "mrkdwn"
                        text = "**Status**:`$status`"
                    }
                )
            }
        )
      }

      Invoke-RestMethod -Uri $webhook -Method Post -Body ($body | ConvertTo-Json) -ContentType "application/json"

Email Notifications

- task: PowerShell@2
  displayName: 'Send Email Report'
  condition: always()
  inputs:
    azureSubscription: 'My-Service-Connection'
    ScriptType: InlineScript
    Inline: |
      $reportPath = "$(Pipeline.Workspace)/cloudslash-report/cost-analysis.html"
      $subject = "Infrastructure Scan Report - $(Build.BuildId)"
      $body = Get-Content $reportPath | Out-String

      Send-AzNotification -ServicePrincipalId "$(EmailServicePrincipalId)" -CertificateThumbprint "$(CertificateThumbprint)" -TenantId "$(TenantId)" -Subject $subject -Body $body

Webhook Integration

- task: PowerShell@2
  displayName: 'Send Webhook Notification'
  condition: always()
  inputs:
    targetType: inline
    script: |
      $webhookUrl = "$(CUSTOM_WEBHOOK_URL)"
      $status = "${{ steps.CloudSlash_Scan.outputs['scan-passed'] }}"

      $body = @{
        pipelineId = "$(Build.BuildId)"
        status = $status
        timestamp = (Get-Date).ToUniversalTime()
        repository = "$(Build.Repository.Name)"
      } | ConvertTo-Json

      Invoke-RestMethod -Uri $webhookUrl -Method Post -Body $body -ContentType "application/json"

Troubleshooting

Common Issues

Issue 1: "api-key is missing"

Symptom: CloudSlash task fails with authentication error

Solution:

- task: CloudSlash@1
  displayName: 'CloudSlash Scan'
  inputs:
    api-key: $(CLOUDSLASH_API_KEY)  # Variable must exist in project

Check: Verify variable exists in Project → Process → Variables Check: Variable was saved Check: Variable is not empty Check: Variable has unclosed double quotes causing yaml to fail

Issue 2: Scan tasks fail with "not found"

Symptom: "The task 'CloudSlash' was not found"

Solution:

- task: CloudSlash@1  # Ensure correct version format and presence
  displayName: 'CloudSlash Scan'
  inputs:
    api-key: $(CLOUDSLASH_API_KEY)

# If missing, add library search path
# Deploy cloudslash-task to group/library via nuget or deploy azure devops tasks library

Issue 3: Pipeline permissions

Symptom: "Unauthorized - Access denied"

Solution:

# Verify user has access in azure devops project
# Grant Contributor role for pipeline operation
# Verify VSTS_ACCESS_TOKEN exists or use personal access token or service connection

Issue 4: Auth token issues

Symptom: "Invalid credentials" when using assumed-role-arn

Solution:

- task: AzurePowerShell@5
  displayName: 'Configure Assumed Role'
  inputs:
    azureSubscription: 'My-Service-Connection'
    ScriptType: InlineScript
    Inline: |
      $roleArn = "arn:aws:sts::123456789012:assumed-role/CloudSlashRole/$(New-GUID)"
      $stsResult = Invoke-AzRoleAssignment -RoleArn $roleArn -Duration Hours 24
      $assumedRoleArn = $stsResult.RoleArn
      Write-Host "##vso[task.setvariable variable=ASSUMED_ROLE_ARN]$assumedRoleArn"

Issue 5: Repository references

Symptom: Error referencing repository or branch

Solution:

# use $(Build.Repository.Name), $(Build.SourceBranchName), $(Build.BuildId)
# Use default trigger variables instead of local attempts

Feature Comparison Matrix

Azure DevOps vs Other Platforms

Feature GitHub Actions GitLab CI Azure DevOps CircleCI Jenkins
Setup Time (min) ⚡ 2-3 ⚡ 2-3 ⭐ 3-5 ⚡ 2-4 ⭐ 5-10
PR/MR Comments ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐
Free Plan ⭐⭐⭐⭐⭐ (unlimited) ⭐⭐⭐⭐⭐ (unlimited) ⭐⭐⭐⭐ (limited) ⭐⭐⭐ (limited) ⭐⭐ (limited)
Scheduled Jobs ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
Monorepo Support ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
Multi-Cloud Support ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
Docker Execution ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
Custom Actions ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
Cost Build Minutes ⚡ Free (public) ⚡ Free (public) ⭐ Free (small) ⭐ Limited ⚡ Free (self-hosted)
Integrations ⭐⭐⭐⭐⭐ (AWS, Azure, GCP) ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐

Best Overall: N/A (Tool-dependent; use based on existing Azure presence; for additional cross-platform options see GitHub Actions, GitLab CI, CircleCI, Jenkins)


Click for detailed feature descriptions ### Azure DevOps Strengths - ✅ **Enterprise-focused**: Rich enterprise features - ✅ **Azure-native**: Seamless integration with Azure services - ✅ **Service Connections**: Built-in authentication for Azure resources - ✅ **Approval Gates**: Built-in deployment approvals for production ### Azure DevOps Weaknesses - ❌ Markdown limitations: Markdown parsing is limited, formatting up to 2048 literals cannot be split - ❌ Matrix-type multiplexing not ideal: Matrix jobs can be verbose but complete (used for usage in monorepos) - ❌ Self-hosted runners: Use of self-hosted runners (e.g., Windows for Docker) not free - ❌ Privacy controls: Restrict pipeline loading in external storage not improved - ❌ Join-like Artifact download not available: Cross-jobs artifact download supported only within same stage (no cross-stage)

Common Patterns

Pattern 1: Conditional Scanning

Run scans based on changes:

- deployment:
      environment: Staging
      name: Conditional_Scan
    pool:
      vmImage: ubuntu-latest
    steps:
    - task: PowerShell@2
      displayName: 'Check for Changes'
      inputs:
        targetType: inline
        script: |
          $changedFiles = git diff --name-only main
          if ($changedFiles.Count -gt 0) {
            Write-Host "##vso[task.setvariable variable=SCAN_REQUIRED]true"
          }
    - task: CloudSlash@1
      displayName: 'CloudSlash Scan'
      condition: eq(variables.SCAN_REQUIRED, 'true')
      inputs:
        api-key: $(CLOUDSLASH_API_KEY)
        config: cloudslash.yml

Pattern 2: Tag-based Scanning

Run scans only on tags:

trigger:
- none  # Don't trigger on other branches

schedules:
  # None, use only tags for optional schedule
  # Tag-based triggers can integrate with git tags and Azure DevOps tag handlers/runners

stages:
- stage: Tag_Scan
  displayName: Tag-Based Scan
  condition: startsWith(dependencies.Build.resources.buildSourceVersion, 'refs/tags/v')
  jobs:
  - deployment:
      environment: Production
      name: Tag_Scan_Job
    pool:
      vmImage: ubuntu-latest
    steps:
    - task: CloudSlash@1
      displayName: 'Run Tag Scan'
      inputs:
        api-key: $(CLOUDSLASH_API_KEY)
        config: cloudslash.yml

Pattern 3: Environment-specific Rules

Different rules based on environment:

# Self-hosted runner for custom scan tool locations
stages:
- stage: Infrastructure
  displayName: Infrastructure
  jobs:
  - deployment:
      environment: Production
      name: Scan
    pool:
      vmImage: ubuntu-latest
    strategy:
      deploy:
        steps:
          # Proxy or offload scanning via self-hosted runner
          - task: AzurePowerShell@5
            displayName: 'Offload to Self-Hosted'
            inputs:
              azureSubscription: 'My-Service-Connection'
              ScriptType: InlineScript
              script: |
                ... # script sends scan request to self-hosted runner

FAQ

Are variables available in all stages? No: Variables are per-project only and not thread-safe; they are cleared between stages (job/trigger overrides can cause invalid derivations). Use pool.vmImage='windows-latest' and rebuild artifacts accordingly.
Can I use self-hosted runners? Yes, use $(agentpool): azure-self-hosted-pool.
How do I set up environment-specific variables? Use Variable Groups in Azure DevOps: Project → Pipelines → Library → Variable Groups: - Add `dev`, `staging`, `prod` to a group with prefixed variables (e.g., `dev.CLOUDSLASH_API_KEY`, `prod.CLOUDSLASH_API_KEY`) - Set with group: $(dev) or $(prod) in pipeline prior to deployment. This avoids the previous limitation (two deploys, ensures selectivity).
Can I deploy pipelines to other orgs? No: Pipelines are not deployable to other orgs or CSAs.

Documentation Version: 2.0.0 Last Updated: September 10, 2026 Related: CI/CD Integration Guide | Best Practices