# Azure DevOps Integration This guide covers all aspects of integrating Purplemet security analyses into Azure DevOps pipelines. ## Table of Contents - [Prerequisites](#prerequisites) - [Quick Start](#quick-start) - [Installation Methods](#installation-methods) - [Parameters](#parameters) - [Security Gates](#security-gates) - [Output Variables](#output-variables) - [Complete Pipeline Examples](#complete-pipeline-examples) - [Results and Exit Codes](#results-and-exit-codes) - [Advanced Usage](#advanced-usage) - [FAQ / Common Errors](#faq--common-errors) ## Prerequisites ### 1. Purplemet API Token Create a token at [cloud.purplemet.com](https://cloud.purplemet.com/#/tokens/create). The token must have the **Operator** role. The Administrator role is discouraged for CI/CD usage — the CLI will display a warning if an Administrator token is detected. ### 2. Install the Extension Install the Purplemet extension from the [Azure DevOps Marketplace](https://marketplace.visualstudio.com/items?itemName=purplemet.purplemet-asm-web). For Azure DevOps Server (on-premises), the extension must be uploaded manually by an administrator. ### 3. Secret Pipeline Variable Add `PURPLEMET_API_TOKEN` as a secret pipeline variable: **Option A — Pipeline variable:** 1. Edit your pipeline 2. Click **Variables** 3. Click **New variable** 4. Name: `PURPLEMET_API_TOKEN` 5. Value: your API token 6. Check **Keep this value secret** **Option B — Variable group (recommended for multi-pipeline use):** 1. Go to **Pipelines** → **Library** 2. Create a new **Variable group** 3. Add `PURPLEMET_API_TOKEN` with the lock icon (secret) 4. Link the variable group to your pipelines ### 4. Network Access Azure-hosted agents can reach the Purplemet API by default. For self-hosted agents, ensure outbound HTTPS access to `api.purplemet.com`. ## Quick Start Add to your `azure-pipelines.yml`: ```yaml trigger: - main pool: vmImage: 'ubuntu-latest' steps: - task: PurplemetAnalyze@1 inputs: apiToken: $(PURPLEMET_API_TOKEN) targetUrl: 'https://your-app.example.com' failSeverity: 'high' ``` ## Installation Methods ### Method 1: Marketplace Extension (recommended) Uses the official Purplemet task from the Azure DevOps Marketplace. ```yaml steps: - task: PurplemetAnalyze@1 inputs: apiToken: $(PURPLEMET_API_TOKEN) targetUrl: 'https://your-app.example.com' failSeverity: 'high' ``` ### Method 2: Docker Image Uses the official Docker image directly. No extension needed. ```yaml steps: - script: | docker run --rm \ -e PURPLEMET_API_TOKEN=$(PURPLEMET_API_TOKEN) \ ppmsupport/purplemet-cli analyze https://your-app.com \ --json --fail-on-severity high \ | tee $(Build.ArtifactStagingDirectory)/purplemet-report.json displayName: 'Purplemet Security Analysis' - publish: $(Build.ArtifactStagingDirectory)/purplemet-report.json artifact: purplemet-report condition: always() ``` ### Method 3: Binary Installation Downloads and installs the CLI binary. ```yaml steps: - script: | curl -sSL https://raw.githubusercontent.com/purplemet/cli/main/scripts/install.sh | sh purplemet-cli analyze https://your-app.com \ --json --fail-on-severity high \ | tee $(Build.ArtifactStagingDirectory)/purplemet-report.json displayName: 'Purplemet Security Analysis' env: PURPLEMET_API_TOKEN: $(PURPLEMET_API_TOKEN) - publish: $(Build.ArtifactStagingDirectory)/purplemet-report.json artifact: purplemet-report condition: always() ``` ## Parameters ### Task Inputs (`PurplemetAnalyze@1`) When using the Marketplace extension (Method 1), all gates and options are exposed as task `inputs:`. #### Required | Input | Default | Description | |-------|---------|-------------| | `apiToken` | — | API token (use secret variable `$(PURPLEMET_API_TOKEN)`) | | `targetUrl` | — | URL of the web application to analyze | #### General | Input | Default | Description | |-------|---------|-------------| | `failSeverity` | `high` | Severity threshold: `critical`, `high`, `medium`, `low`, `info` | | `timeout` | `1800000` | Polling timeout in milliseconds (30 min, `0` = unlimited) | | `version` | `latest` | CLI version to use (e.g. `v1.2.0`) | | `format` | `json` | Output format: `json`, `human`, `sarif`, `html` | | `baseUrl` | — | API base URL override | | `basicUser` | — | HTTP Basic Auth user (dev API only) | | `basicPass` | — | HTTP Basic Auth password (dev API only, use secret variable) | | `noCreate` | `false` | Do not auto-create site if URL not found | #### Security Gates | Input | Default | Description | |-------|---------|-------------| | `failRating` | — | Fail if rating is at or below this grade (`A`–`F`) | | `failCvss` | `0` | Fail if any CVE has CVSS score ≥ this value (e.g. `9.0`) | | `failOnEol` | `false` | Fail on end-of-life components | | `failOnSsl` | `false` | Fail on SSL/TLS protocol issues | | `failOnCert` | `false` | Fail on certificate issues | | `failOnHeaders` | `false` | Fail on HTTP security header issues (CSP, HSTS, X-Frame-Options) | | `failOnCookies` | `false` | Fail on insecure cookies (HttpOnly, Secure, SameSite) | | `failOnUnsafe` | `false` | Fail on unsafe component issues | | `failOnKev` | `false` | Fail on CISA Known Exploited Vulnerabilities | | `failOnEpss` | `0` | Fail if any issue has EPSS score ≥ this value (0.0–1.0) | | `failOnActiveExploits` | `false` | Fail on actively exploited vulnerabilities | | `failOnOssfScore` | `0` | Fail if any technology has OpenSSF Scorecard score below this value (0–10) | | `failOnCertExpiry` | `0` | Fail if certificate expires within N days | | `failOnIssueCount` | `0` | Fail if total issue count ≥ this value | | `requireWaf` | `false` | Fail if no WAF is detected | | `failOnSensitiveServices` | `false` | Fail if sensitive services are exposed on the site IP | | `excludeTech` | — | Fail if specified technologies are detected (comma-separated) | YAML types: booleans are unquoted (`failOnKev: true`), numeric thresholds are quoted strings (`failCvss: '9.0'`). ### Environment Variables (Docker & Binary methods only) Methods 2 and 3 don't use task inputs — they run `analyze.sh` which reads `PURPLEMET_*` environment variables. **These are not needed when using the extension.** | Variable | Default | Description | |----------|---------|-------------| | `PURPLEMET_API_TOKEN` | — | API authentication token (**required**) | | `PURPLEMET_TARGET_URL` | — | URL to analyze (**required**) | | `PURPLEMET_FAIL_SEVERITY` | — | Severity threshold | | `PURPLEMET_FAIL_RATING` | — | Rating threshold: `A`–`F` | | `PURPLEMET_FAIL_CVSS` | `0` | CVSS score threshold | | `PURPLEMET_FAIL_ON_EOL` | `false` | Block on end-of-life components | | `PURPLEMET_FAIL_ON_SSL` | `false` | Block on SSL/TLS issues | | `PURPLEMET_FAIL_ON_CERT` | `false` | Block on certificate issues | | `PURPLEMET_FAIL_ON_HEADERS` | `false` | Block on HTTP security header issues | | `PURPLEMET_FAIL_ON_COOKIES` | `false` | Block on insecure cookies | | `PURPLEMET_FAIL_ON_UNSAFE` | `false` | Block on unsafe components | | `PURPLEMET_FAIL_ON_KEV` | `false` | Block on CISA Known Exploited Vulnerabilities | | `PURPLEMET_FAIL_ON_EPSS` | `0` | EPSS score threshold | | `PURPLEMET_FAIL_ON_ACTIVE_EXPLOITS` | `false` | Block on actively exploited vulnerabilities | | `PURPLEMET_FAIL_ON_OSSF_SCORE` | `0` | Min OpenSSF Scorecard score | | `PURPLEMET_FAIL_ON_CERT_EXPIRY` | `0` | Block if certificate expires within N days | | `PURPLEMET_FAIL_ON_ISSUE_COUNT` | `0` | Block if total issues ≥ threshold | | `PURPLEMET_REQUIRE_WAF` | `false` | Block if no WAF detected | | `PURPLEMET_FAIL_ON_SENSITIVE_SERVICES` | `false` | Block if sensitive services exposed | | `PURPLEMET_EXCLUDE_TECH` | — | Block if specified technologies detected | | `PURPLEMET_WAIT_TIMEOUT` | `1800000` | Polling timeout (ms) | | `PURPLEMET_FORMAT` | `json` | Output format: `json`, `human`, `sarif`, `html` | | `PURPLEMET_BASE_URL` | — | API base URL override | ## Security Gates Multiple gates can be combined — the analysis fails (exit code 1) if **any** gate triggers. ### Example: Strict Policy ```yaml steps: - script: | curl -sSL https://raw.githubusercontent.com/purplemet/cli/main/scripts/install.sh | sh purplemet-cli analyze https://your-app.com \ --json \ --fail-on-severity high \ --fail-on-eol \ --fail-on-kev \ --fail-on-ssl \ --require-waf \ --fail-on-cert-expiry 30 displayName: 'Security Analysis (Strict)' env: PURPLEMET_API_TOKEN: $(PURPLEMET_API_TOKEN) ``` ## Output Variables The `PurplemetAnalyze@1` task exposes output variables for use in subsequent steps: | Variable | Description | Example | |----------|-------------|---------| | `PurplemetExitCode` | Exit code of the analysis | `0` | | `PurplemetRating` | Security rating | `B` | | `PurplemetIssues` | Total number of issues | `12` | ### Using Output Variables ```yaml steps: - task: PurplemetAnalyze@1 name: analysis inputs: apiToken: $(PURPLEMET_API_TOKEN) targetUrl: 'https://your-app.example.com' continueOnError: true - script: | echo "Rating: $(analysis.PurplemetRating)" echo "Issues: $(analysis.PurplemetIssues)" echo "Exit code: $(analysis.PurplemetExitCode)" displayName: 'Check Results' ``` ## Complete Pipeline Examples ### Basic: Analysis on Push to Main ```yaml trigger: - main pool: vmImage: 'ubuntu-latest' steps: - task: PurplemetAnalyze@1 inputs: apiToken: $(PURPLEMET_API_TOKEN) targetUrl: 'https://your-app.example.com' failSeverity: 'high' ``` ### Build → Deploy → Analysis Pipeline ```yaml trigger: - main pool: vmImage: 'ubuntu-latest' stages: - stage: Build jobs: - job: Build steps: - script: make build - script: make test - stage: Deploy dependsOn: Build jobs: - deployment: DeployStaging environment: staging strategy: runOnce: deploy: steps: - script: ./deploy.sh staging - stage: Security dependsOn: Deploy jobs: - job: Analysis steps: - task: PurplemetAnalyze@1 inputs: apiToken: $(PURPLEMET_API_TOKEN) targetUrl: 'https://staging.example.com' failSeverity: 'high' timeout: '600000' - stage: Production dependsOn: Security condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) jobs: - deployment: DeployProd environment: production strategy: runOnce: deploy: steps: - script: ./deploy.sh production ``` ### Scheduled Weekly Analysis ```yaml trigger: none schedules: - cron: '0 6 * * 1' # Every Monday at 6:00 UTC displayName: 'Weekly Security Analysis' branches: include: - main pool: vmImage: 'ubuntu-latest' steps: - task: PurplemetAnalyze@1 inputs: apiToken: $(PURPLEMET_API_TOKEN) targetUrl: 'https://production.example.com' failSeverity: 'medium' timeout: '600000' ``` ### Multi-Site Analysis (Matrix) ```yaml trigger: - main pool: vmImage: 'ubuntu-latest' strategy: matrix: app1: targetUrl: 'https://app1.example.com' app2: targetUrl: 'https://app2.example.com' api: targetUrl: 'https://api.example.com' steps: - task: PurplemetAnalyze@1 inputs: apiToken: $(PURPLEMET_API_TOKEN) targetUrl: $(targetUrl) failSeverity: 'high' ``` ### Warning Mode (Non-blocking) ```yaml steps: - task: PurplemetAnalyze@1 name: analysis inputs: apiToken: $(PURPLEMET_API_TOKEN) targetUrl: 'https://your-app.example.com' failSeverity: 'high' continueOnError: true - script: | if [ "$(analysis.PurplemetExitCode)" = "1" ]; then echo "##vso[task.logissue type=warning]Security analysis found $(analysis.PurplemetIssues) issue(s) — rating: $(analysis.PurplemetRating)" fi displayName: 'Report Results' condition: always() ``` ### Classic Pipeline 1. Add the **Purplemet Security Analysis** task from the task catalog 2. Configure: - **API Token**: `$(PURPLEMET_API_TOKEN)` (link a secret variable) - **Target URL**: your application URL - **Fail Severity**: `high` 3. Optionally check **Continue on error** for non-blocking mode ### Full Pipeline with Strict Gates ```yaml trigger: - main pool: vmImage: 'ubuntu-latest' stages: - stage: Build jobs: - job: BuildAndTest steps: - script: make build && make test - stage: Deploy dependsOn: Build jobs: - deployment: Staging environment: staging strategy: runOnce: deploy: steps: - script: ./deploy.sh - stage: Security dependsOn: Deploy jobs: - job: StrictAnalysis steps: - script: | curl -sSL https://raw.githubusercontent.com/purplemet/cli/main/scripts/install.sh | sh purplemet-cli analyze https://staging.example.com \ --json \ --fail-on-severity high \ --fail-on-eol \ --fail-on-kev \ --fail-on-ssl \ --fail-on-cert-expiry 30 \ --require-waf displayName: 'Security Analysis (Strict)' env: PURPLEMET_API_TOKEN: $(PURPLEMET_API_TOKEN) - stage: Production dependsOn: Security jobs: - deployment: Prod environment: production strategy: runOnce: deploy: steps: - script: ./deploy.sh production ``` ## Results and Exit Codes ### Exit Codes | Code | Meaning | Task Result | |------|---------|-------------| | **0** | No issues above threshold | **Succeeded** | | **1** | Issues found above threshold | **Succeeded with issues** | | **2** | Analysis error on Purplemet | **Failed** | | **3** | Timeout exceeded | **Failed** | | **4** | Network or API error | **Failed** | | **5** | Usage error (bad arguments) | **Failed** | | **6** | API contract error | **Failed** | ### Security Rating and Severity Levels Ratings (`A`–`F`) and severity levels (`CRITICAL`/`HIGH`/`MEDIUM`/`LOW`/`INFO`) are computed and defined by the Purplemet platform. See the [official Purplemet documentation](https://cloud.purplemet.com/docs/#/web%20applications/security-rating) for authoritative definitions. ## Advanced Usage ### Pin a Specific CLI Version ```yaml - task: PurplemetAnalyze@1 inputs: apiToken: $(PURPLEMET_API_TOKEN) targetUrl: 'https://your-app.example.com' version: 'v1.2.0' ``` ### Viewing the Report **For `json`, `sarif`, and `human` formats**, the `PurplemetAnalyze@1` task automatically uploads the analysis output as a build artifact named `purplemet-report`. Open the pipeline run → **Summary** tab → **Published** section to download it. No extra step required. **For `html` format**, the CLI writes `purplemet-report.html` directly to the pipeline workspace (not to stdout), so you must publish it explicitly: ```yaml steps: - task: PurplemetAnalyze@1 inputs: apiToken: $(PURPLEMET_API_TOKEN) targetUrl: 'https://your-app.example.com' format: 'html' failSeverity: 'high' - task: PublishPipelineArtifact@1 condition: always() # publish even if the gate fails inputs: targetPath: '$(System.DefaultWorkingDirectory)/purplemet-report.html' artifact: 'purplemet-report-html' publishLocation: 'pipeline' ``` Download the artifact from **Summary** → **Published** and open it locally. Azure DevOps does not render HTML inline — install the [HTML Report Publisher](https://marketplace.visualstudio.com/items?itemName=AllanTargino.html-report-publisher) extension if you want a dedicated tab inside the run. ### Generate HTML Report (Without the Extension) If you're using the binary directly (no extension): ```yaml steps: - script: | curl -sSL https://raw.githubusercontent.com/purplemet/cli/main/scripts/install.sh | sh purplemet-cli analyze https://your-app.com \ --format html --output-file $(Build.ArtifactStagingDirectory)/report.html \ --fail-on-severity high || true displayName: 'Generate HTML Analysis Report' env: PURPLEMET_API_TOKEN: $(PURPLEMET_API_TOKEN) - publish: $(Build.ArtifactStagingDirectory)/report.html artifact: security-report condition: always() ``` ### Publishing the Extension (Maintainers) ```bash cd integrations/azure-devops npm install -g tfx-cli tfx extension create --manifest-globs vss-extension.json tfx extension publish --token ``` ## FAQ / Common Errors ### `PURPLEMET_API_TOKEN` variable not found The pipeline variable is missing. **Fix:** Add `PURPLEMET_API_TOKEN` as a secret variable: Edit pipeline → Variables → New variable → check **Keep this value secret**. Or use a Variable group: Pipelines → Library → New variable group. --- ### "Access is not authorized without a valid session" The API token is invalid or expired. **Fix:** 1. Verify: `purplemet-cli auth check` 2. Create a new token at [cloud.purplemet.com](https://cloud.purplemet.com/#/tokens/create) 3. Update the pipeline variable --- ### Analysis times out (exit code 3) **Fix:** Increase the timeout: ```yaml - task: PurplemetAnalyze@1 inputs: apiToken: $(PURPLEMET_API_TOKEN) targetUrl: 'https://your-app.example.com' timeout: '600000' # 10 minutes ``` --- ### Task fails with exit code 1 but I want it to continue Exit code 1 means vulnerabilities were found. The task reports **Succeeded with issues** by default. To fully suppress the failure: ```yaml - task: PurplemetAnalyze@1 inputs: apiToken: $(PURPLEMET_API_TOKEN) targetUrl: 'https://your-app.example.com' continueOnError: true ``` --- ### Extension not available in Azure DevOps Server For on-premises installations, upload the extension manually: 1. Download the `.vsix` from the Marketplace 2. Go to **Organization Settings** → **Extensions** → **Browse local extensions** → Upload --- ### Network error (exit code 4) Self-hosted agents may not have access to the Purplemet API. **Fix:** Ensure outbound HTTPS access to `api.purplemet.com`. If behind a proxy, configure `HTTP_PROXY` / `HTTPS_PROXY`. --- ### How do I analyze multiple sites? Use a matrix strategy (see [Multi-Site Analysis example](#multi-site-analysis-matrix)). --- ### Where can I see the results? 1. **Pipeline log**: Summary in the task output 2. **Pipeline artifacts**: Download `purplemet-report.json` 3. **Output variables**: Access `PurplemetRating`, `PurplemetIssues` in subsequent steps 4. **Purplemet dashboard**: [cloud.purplemet.com](https://cloud.purplemet.com) for detailed results