Skip to content

CI/CD Integration

Integrate cc-scan into your pipeline to gate deployments on Azure Key Vault security findings.


How It Works

The scanner exits with a meaningful code your pipeline can act on. When several apply, the first one in this table wins:

Exit Code Meaning
2 Scan failed or read nothing (sign-in error, no subscriptions, every Key Vault list failed, timeout)
3 No API key and no terminal to register in: set CC_SCAN_KEY
1 A finding at or above --fail-on (default high)
4 Scan finished with gaps: some subscriptions, vaults or item types could not be read, or a --subscription or --vault was not found
0 None of the above

Any non-zero exit fails a pipeline step, which is what a gate should do. Treat 4 as a failure too: a scan that could not read a vault cannot tell you that vault is clean. Use --fail-on critical to gate on critical findings only, or --fail-on never to gate on scan health alone. See the Scanner reference for the full definitions and the list of security checks.


Authentication in Pipelines

The examples below sign in with workload identity federation: the pipeline proves who it is with a short-lived token, so no Azure secret is stored in it. The scanner then signs in one of two ways:

  • --auth cli (the default) uses the Azure CLI session. GitHub Actions and Azure DevOps use this: sign in with azure/login or inside an AzureCLI@2 task, then run cc-scan. azure/login signs in the Azure CLI only and does not set AZURE_FEDERATED_TOKEN_FILE, so keep the default --auth cli there.
  • --auth default uses the Azure SDK's DefaultAzureCredential, which reads AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_FEDERATED_TOKEN_FILE (WorkloadIdentityCredential). GitLab uses this, because its image has no Azure CLI.

A client secret still works: set AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_CLIENT_SECRET and run cc-scan --auth default.

Create the Pipeline Identity

Create a Microsoft Entra app registration (or a user-assigned managed identity) and add a federated credential that trusts your pipeline:

Then give the identity the scanner's read-only roles: Reader on each subscription, and Key Vault Reader on vaults that use Azure RBAC (List on secrets, certificates and keys plus Get on certificates for vaults that use access policies). The commands are in the Scanner reference. Do not use Key Vault Secrets User: it can read secret values and cannot list certificates or keys.

Get Your Scanner API Key

Register at certifyclouds.com or run cc-scan --register locally. The key looks like cc-scan-abc1234567. Store it as a pipeline secret named CC_SCAN_KEY. In CI the scanner never asks you to register: without a key it exits 3.


GitHub Actions

Secrets to Configure

In your repository, go to Settings > Secrets and variables > Actions and add:

Secret Value
AZURE_CLIENT_ID Client ID of the app registration or managed identity
AZURE_TENANT_ID Microsoft Entra tenant ID
AZURE_SUBSCRIPTION_ID Any subscription the identity can read (azure/login needs one; the scanner still scans every subscription the identity can see)
CC_SCAN_KEY CertifyClouds scanner API key

Workflow: Fail on Critical/High Findings

name: Key Vault Security Audit

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 6 * * 1'  # Every Monday at 06:00 UTC

permissions:
  id-token: write  # lets azure/login request the OIDC token
  contents: read

jobs:
  vault-audit:
    name: Audit Key Vaults
    runs-on: ubuntu-latest

    steps:
      - name: Sign in to Azure
        uses: azure/login@v3
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Install cc-scan
        run: pip install certifyclouds

      - name: Run Key Vault audit
        env:
          CC_SCAN_KEY: ${{ secrets.CC_SCAN_KEY }}
        run: |
          cc-scan \
            --format json \
            --output results.json

      - name: Upload scan results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: vault-audit-results
          path: results.json
          retention-days: 30

The workflow fails when cc-scan exits non-zero: 1 for a critical or high finding, 2 for a failed scan, 3 for a missing key and 4 for a scan with gaps.

Other Options

Add these to the cc-scan command in the workflow above:

  • --security checks for misconfigurations only, without a full inventory.
  • --subscription <id> scans only that subscription (repeatable).
  • --fail-on critical fails only on critical findings.

Soft failure mode

To audit without failing the build, capture the exit code manually:

      run: |
        cc-scan --format json --output results.json || EXIT=$?
        echo "Scanner exit code: ${EXIT:-0}"
        exit 0


Azure DevOps Pipelines

Service Connection and Variables

Create an Azure Resource Manager service connection that uses workload identity federation (see Create the Pipeline Identity). The pipeline refers to it by name.

In Pipelines > Library > Variable Groups, add a variable group (e.g., certifyclouds-scanner) with:

Variable Value Secret
CC_SCAN_KEY CertifyClouds scanner API key Yes

Pipeline: Fail on Critical/High Findings

The Azure CLI sign-in exists only inside the AzureCLI@2 task, so cc-scan runs in that task's script.

trigger:
  branches:
    include:
      - main

schedules:
  - cron: '0 6 * * 1'
    displayName: Weekly Monday audit
    branches:
      include:
        - main
    always: true

pool:
  vmImage: ubuntu-latest

variables:
  - group: certifyclouds-scanner

steps:
  - task: UsePythonVersion@0
    inputs:
      versionSpec: '3.13'
    displayName: Set up Python

  - script: pip install certifyclouds
    displayName: Install cc-scan

  - task: AzureCLI@2
    displayName: Run Key Vault audit
    inputs:
      azureSubscription: certifyclouds-scanner-connection  # your service connection name
      scriptType: bash
      scriptLocation: inlineScript
      inlineScript: |
        cc-scan \
          --format json \
          --output $(Build.ArtifactStagingDirectory)/results.json
    env:
      CC_SCAN_KEY: $(CC_SCAN_KEY)

  - task: PublishBuildArtifacts@1
    condition: always()
    inputs:
      pathToPublish: $(Build.ArtifactStagingDirectory)
      artifactName: vault-audit-results
    displayName: Publish scan results

For a security check only, add --security to the cc-scan command.

Pipeline: Soft Failure (Warn, Don't Block)

  - task: AzureCLI@2
    displayName: Run Key Vault audit (warn only)
    continueOnError: true
    inputs:
      azureSubscription: certifyclouds-scanner-connection
      scriptType: bash
      scriptLocation: inlineScript
      inlineScript: |
        cc-scan --format json --output results.json || SCAN_EXIT=$?
        echo "##vso[task.setvariable variable=SCAN_EXIT]${SCAN_EXIT:-0}"
    env:
      CC_SCAN_KEY: $(CC_SCAN_KEY)

  - script: |
      echo "Scanner exit code: $(SCAN_EXIT)"
      if [ "$(SCAN_EXIT)" != "0" ]; then
        echo "##vso[task.logissue type=warning]cc-scan exited $(SCAN_EXIT), review results.json"
      fi
    displayName: Evaluate scan results
    condition: always()

GitLab CI

CI/CD Variables to Configure

Add a federated credential to the app registration for your GitLab project (see Create the Pipeline Identity). Its audience must match the aud of the job's ID token; the examples below use https://gitlab.com.

In your project, go to Settings > CI/CD > Variables and add:

Variable Value Masked
AZURE_CLIENT_ID Client ID of the app registration No
AZURE_TENANT_ID Microsoft Entra tenant ID No
CC_SCAN_KEY CertifyClouds scanner API key Yes

Pipeline: Fail on Critical/High Findings

The hidden .cc-scan job writes the job's ID token to a file and points AZURE_FEDERATED_TOKEN_FILE at it. The jobs below extend it and run cc-scan --auth default.

stages:
  - audit

.cc-scan:
  image: python:3.11-slim
  id_tokens:
    AZURE_OIDC_TOKEN:
      aud: https://gitlab.com
  before_script:
    - printf '%s' "$AZURE_OIDC_TOKEN" > /tmp/azure-oidc-token
    - export AZURE_FEDERATED_TOKEN_FILE=/tmp/azure-oidc-token
    - pip install certifyclouds

vault-audit:
  extends: .cc-scan
  stage: audit
  rules:
    - if: $CI_PIPELINE_SOURCE == "push"
    - if: $CI_PIPELINE_SOURCE == "schedule"
  script:
    - cc-scan
        --auth default
        --format json
        --output results.json
  artifacts:
    when: always
    paths:
      - results.json
    expire_in: 30 days

The job fails when cc-scan exits non-zero: 1 for a critical or high finding, 2 for a failed scan, 3 for a missing key and 4 for a scan with gaps.

Pipeline: Security Findings Only

vault-security-check:
  extends: .cc-scan
  stage: audit
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  script:
    - cc-scan --auth default --security

Pipeline: Scheduled Weekly Audit

Add a CI/CD schedule in CI/CD > Schedules (every Monday at 06:00 UTC: 0 6 * * 1), then configure a job that only runs on schedules:

weekly-vault-audit:
  extends: .cc-scan
  stage: audit
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
  script:
    - cc-scan
        --auth default
        --format html
        --output vault-audit.html
  artifacts:
    when: always
    paths:
      - vault-audit.html
    expire_in: 90 days

Pipeline: Soft Failure (Allow Failure)

vault-audit-warn:
  extends: .cc-scan
  stage: audit
  allow_failure:
    exit_codes: [1, 4]
  script:
    - cc-scan --auth default --format json --output results.json
  artifacts:
    when: always
    paths:
      - results.json
    expire_in: 30 days

allow_failure: exit_codes: [1, 4] lets the pipeline continue on findings (1) or a scan with gaps (4), and still fails on a failed scan (2) or a missing key (3).


Tips

Pin the scanner version to avoid unexpected behaviour on upgrades:

pip install certifyclouds==1.4.0

Cache the install to speed up pipelines. For GitHub Actions:

      - uses: actions/cache@v4
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-certifyclouds
      - run: pip install certifyclouds

Scan only expiring items if you want a lighter check:

cc-scan --expiring 30

This lists only secrets, certificates and keys that expire within 30 days or have already expired. The vault checks still run, so a vault finding at or above --fail-on still exits 1. The score always covers the whole scan.