# GitLab CI/CD Integration This guide covers all aspects of integrating Purplemet security analyses into GitLab CI/CD pipelines. ## Table of Contents - [Prerequisites](#prerequisites) - [Quick Start](#quick-start) - [Installation Methods](#installation-methods) - [Parameters](#parameters) - [Outputs & Artifacts](#outputs--artifacts) - [Security Gates](#security-gates) - [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. CI/CD Variable Add the token as a masked CI/CD variable: 1. Go to your project on GitLab 2. Navigate to **Settings** → **CI/CD** → **Variables** 3. Click **Add variable** 4. Key: `PURPLEMET_API_TOKEN` 5. Value: your API token 6. Check **Mask variable** — hides the value in job logs 7. Check **Protect variable** — only available on protected branches/tags (optional) 8. Click **Add variable** > **Group-level variable:** For multi-project usage, add the variable at the group level (Group → Settings → CI/CD → Variables). ### 3. Network Access The GitLab Runner must be able to reach: - `api.purplemet.com` (HTTPS, port 443) — Purplemet API - `registry.hub.docker.com` — Docker Hub (for the CLI image) If behind a proxy, set `HTTP_PROXY` / `HTTPS_PROXY` in your runner configuration. ## Quick Start Add to your `.gitlab-ci.yml`: ```yaml include: - project: 'purplemet/integrations/gitlab-ci-templates' ref: 'v1' file: '/purplemet-analyze.gitlab-ci.yml' purplemet: extends: .purplemet-analyze variables: PURPLEMET_TARGET_URL: "https://your-app.example.com" ``` This: - Includes the official Purplemet template - Runs a security analysis with JSON output - Fails on **high** or **critical** issues (default threshold) - Saves the report as an artifact ## Installation Methods ### Method 1: Include Template (recommended) Uses the official reusable template. Handles installation, execution, and artifact management. **From project (private GitLab instances):** ```yaml include: - project: 'purplemet/integrations/gitlab-ci-templates' ref: 'v1.0.0' file: '/purplemet-analyze.gitlab-ci.yml' purplemet: extends: .purplemet-analyze variables: PURPLEMET_TARGET_URL: "https://your-app.example.com" ``` **From remote URL:** ```yaml include: - remote: 'https://raw.githubusercontent.com/Purplemet/cli/v1.0.0/integrations/gitlab/purplemet-analyze.gitlab-ci.yml' purplemet: extends: .purplemet-analyze variables: PURPLEMET_TARGET_URL: "https://your-app.example.com" ``` ### Method 2: Docker Image (no template) Uses the official Docker image directly. Provides more control over execution. ```yaml purplemet-analysis: image: ppmsupport/purplemet-cli:latest stage: test script: - purplemet-cli analyze "$PURPLEMET_TARGET_URL" --json --fail-on-severity high | tee purplemet-report.json artifacts: paths: - purplemet-report.json when: always expire_in: 30 days ``` ### Method 3: Binary Installation Downloads the CLI binary. Useful with custom images or when Docker-in-Docker is not available. ```yaml purplemet-analysis: image: alpine:3.19 stage: test before_script: - apk add --no-cache curl - curl -sSL https://raw.githubusercontent.com/purplemet/cli/main/scripts/install.sh | sh script: - purplemet-cli analyze "$PURPLEMET_TARGET_URL" --json --fail-on-severity high | tee purplemet-report.json artifacts: paths: - purplemet-report.json when: always ``` ## Parameters ### CI/CD Variables | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `PURPLEMET_API_TOKEN` | **Yes** | — | API authentication token (masked CI/CD variable) | | `PURPLEMET_TARGET_URL` | **Yes** | — | URL of the web application to analyze | | `PURPLEMET_FAIL_SEVERITY` | No | `high` | Severity threshold: `critical`, `high`, `medium`, `low`, `info` | | `PURPLEMET_WAIT_TIMEOUT` | No | `1800000` | Polling timeout in milliseconds (30 min, 0 = unlimited) | | `PURPLEMET_CLI_VERSION` | No | `latest` | CLI Docker image tag (e.g. `v1.0.0`) | | `PURPLEMET_FORMAT` | No | `json` | Output format: `json`, `human`, `sarif`, `html` | | `PURPLEMET_NO_CREATE` | No | `false` | Do not auto-create the site if the URL is not found | | `PURPLEMET_BASE_URL` | No | — | API base URL override | ### Security Gate Variables All security gates are configured via environment variables: | Variable | Default | Description | |----------|---------|-------------| | `PURPLEMET_FAIL_SEVERITY` | `high` | Severity threshold | | `PURPLEMET_FAIL_RATING` | — | Rating threshold: `A`–`F` | | `PURPLEMET_FAIL_CVSS` | `0` | CVSS score threshold (e.g. `9.0`) | | `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 (e.g. `0.75`) | | `PURPLEMET_FAIL_ON_ACTIVE_EXPLOITS` | `false` | Block on actively exploited vulnerabilities | | `PURPLEMET_FAIL_ON_OSSF_SCORE` | `0` | Min OpenSSF Scorecard score (e.g. `5.0`) | | `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 are exposed | | `PURPLEMET_EXCLUDE_TECH` | — | Block if specified technologies detected (comma-separated) | ### Available Templates Two template files are shipped, one per runner executor type: - `purplemet-analyze.gitlab-ci.yml` — for **Docker-executor** runners (default in GitLab SaaS). Uses the `ppmsupport/purplemet-cli` image. - `purplemet-analyze-shell.gitlab-ci.yml` — for **shell / SSH / VM-executor** runners. Installs the binary from GitHub at job start. Requires `curl` + `bash` on the runner. | Template | File | Executor | Behavior on exit code 1 | |----------|------|----------|------------------------| | `.purplemet-analyze` | `purplemet-analyze.gitlab-ci.yml` | docker | Warning (pipeline continues) | | `.purplemet-analyze-blocking` | `purplemet-analyze.gitlab-ci.yml` | docker | Blocks pipeline | | `.purplemet-analyze-sarif` | `purplemet-analyze.gitlab-ci.yml` | docker | Warning — JSON + SARIF reports | | `.purplemet-analyze-shell` | `purplemet-analyze-shell.gitlab-ci.yml` | shell / ssh | Warning | | `.purplemet-analyze-shell-blocking` | `purplemet-analyze-shell.gitlab-ci.yml` | shell / ssh | Blocks pipeline | Example — shell runner: ```yaml include: - project: 'purplemet/integrations/gitlab-ci-templates' ref: 'v1.0.0' file: '/purplemet-analyze-shell.gitlab-ci.yml' security_scan: extends: .purplemet-analyze-shell variables: PURPLEMET_TARGET_URL: "https://your-app.example.com" PURPLEMET_FAIL_SEVERITY: "high" ``` ## Outputs & Artifacts ### Artifacts | File | Description | |------|-------------| | `purplemet-report.json` | Full analysis results in JSON | | `purplemet-report.sarif` | SARIF 2.1.0 report (`.purplemet-analyze-sarif` template only) | | `purplemet-report.env` | Dotenv artifact with key metrics | | `purplemet-stderr.log` | CLI stderr output (warnings/errors) | ### Dotenv Variables (available in downstream jobs) | Variable | Description | |----------|-------------| | `PURPLEMET_EXIT_CODE` | CLI exit code | | `PURPLEMET_RATING` | Security rating (A–F) | | `PURPLEMET_ISSUES` | Total number of issues | | `PURPLEMET_TARGET` | Analyzed URL | Use these in downstream jobs: ```yaml notify: stage: deploy needs: - purplemet script: - echo "Rating: $PURPLEMET_RATING" - echo "Issues: $PURPLEMET_ISSUES" ``` ## Security Gates Gates let you define precise pass/fail criteria for your pipeline. Multiple gates can be combined — the analysis fails (exit code 1) if **any** gate triggers. ### Example: Strict Security Policy ```yaml purplemet: extends: .purplemet-analyze-blocking variables: PURPLEMET_TARGET_URL: "https://your-app.example.com" PURPLEMET_FAIL_SEVERITY: "high" PURPLEMET_FAIL_ON_EOL: "true" PURPLEMET_FAIL_ON_KEV: "true" PURPLEMET_FAIL_ON_SSL: "true" PURPLEMET_FAIL_ON_CERT: "true" PURPLEMET_REQUIRE_WAF: "true" PURPLEMET_FAIL_ON_CERT_EXPIRY: "30" ``` ### Example: CVSS + EPSS Thresholds ```yaml purplemet: extends: .purplemet-analyze-blocking variables: PURPLEMET_TARGET_URL: "https://your-app.example.com" PURPLEMET_FAIL_CVSS: "9.0" PURPLEMET_FAIL_ON_EPSS: "0.75" PURPLEMET_FAIL_ON_ACTIVE_EXPLOITS: "true" ``` ### Example: Technology Policy ```yaml purplemet: extends: .purplemet-analyze-blocking variables: PURPLEMET_TARGET_URL: "https://your-app.example.com" PURPLEMET_FAIL_ON_EOL: "true" PURPLEMET_EXCLUDE_TECH: "php,java" PURPLEMET_FAIL_ON_OSSF_SCORE: "5.0" ``` ## Complete Pipeline Examples ### Basic: Analysis on Every Push ```yaml include: - project: 'purplemet/integrations/gitlab-ci-templates' ref: 'v1' file: '/purplemet-analyze.gitlab-ci.yml' purplemet: extends: .purplemet-analyze variables: PURPLEMET_TARGET_URL: "https://your-app.example.com" PURPLEMET_FAIL_SEVERITY: "high" ``` ### Build → Deploy → Analysis Pipeline ```yaml include: - project: 'purplemet/integrations/gitlab-ci-templates' ref: 'v1' file: '/purplemet-analyze.gitlab-ci.yml' stages: - build - deploy - test build: stage: build script: - docker build -t my-app . deploy: stage: deploy script: - ./deploy.sh staging environment: name: staging purplemet: extends: .purplemet-analyze-blocking stage: test variables: PURPLEMET_TARGET_URL: "https://staging.example.com" PURPLEMET_FAIL_SEVERITY: "high" needs: - deploy ``` ### Scheduled Nightly Analysis ```yaml include: - project: 'purplemet/integrations/gitlab-ci-templates' ref: 'v1' file: '/purplemet-analyze.gitlab-ci.yml' purplemet:nightly: extends: .purplemet-analyze variables: PURPLEMET_TARGET_URL: "https://production.example.com" PURPLEMET_FAIL_SEVERITY: "medium" rules: - if: $CI_PIPELINE_SOURCE == "schedule" ``` Configure the schedule in **CI/CD** → **Schedules** → **New schedule**. ### With GitLab Security Dashboard (SARIF) ```yaml include: - project: 'purplemet/integrations/gitlab-ci-templates' ref: 'v1' file: '/purplemet-analyze.gitlab-ci.yml' purplemet: extends: .purplemet-analyze-sarif variables: PURPLEMET_TARGET_URL: "https://your-app.example.com" ``` The SARIF report is registered as a DAST artifact and appears in the GitLab **Security Dashboard** (requires GitLab Ultimate). ### Multi-Environment Analysis ```yaml include: - project: 'purplemet/integrations/gitlab-ci-templates' ref: 'v1' file: '/purplemet-analyze.gitlab-ci.yml' .purplemet-base: extends: .purplemet-analyze-blocking variables: PURPLEMET_FAIL_SEVERITY: "high" PURPLEMET_FAIL_ON_KEV: "true" purplemet:staging: extends: .purplemet-base variables: PURPLEMET_TARGET_URL: "https://staging.example.com" rules: - if: $CI_COMMIT_BRANCH == "develop" purplemet:production: extends: .purplemet-base variables: PURPLEMET_TARGET_URL: "https://app.example.com" rules: - if: $CI_COMMIT_BRANCH == "main" ``` ### Use Analysis Results in Downstream Jobs ```yaml include: - project: 'purplemet/integrations/gitlab-ci-templates' ref: 'v1' file: '/purplemet-analyze.gitlab-ci.yml' stages: - test - notify purplemet: extends: .purplemet-analyze stage: test variables: PURPLEMET_TARGET_URL: "https://your-app.example.com" notify: stage: notify needs: - purplemet script: - echo "Analysis rating: $PURPLEMET_RATING" - echo "Issues found: $PURPLEMET_ISSUES" - | if [ "$PURPLEMET_EXIT_CODE" = "1" ]; then echo "Security issues detected — review purplemet-report.json" fi rules: - if: $CI_PIPELINE_SOURCE != "schedule" ``` ### Warning Mode (Non-blocking) The default `.purplemet-analyze` template already treats exit code 1 as a warning. For fully non-blocking: ```yaml purplemet: extends: .purplemet-analyze allow_failure: true variables: PURPLEMET_TARGET_URL: "https://your-app.example.com" ``` ### Full Pipeline with Strict Gates ```yaml include: - project: 'purplemet/integrations/gitlab-ci-templates' ref: 'v1' file: '/purplemet-analyze.gitlab-ci.yml' stages: - build - test - deploy build: stage: build script: - docker build -t my-app . purplemet: extends: .purplemet-analyze-blocking stage: test variables: PURPLEMET_TARGET_URL: "https://staging.example.com" PURPLEMET_FAIL_SEVERITY: "high" PURPLEMET_FAIL_ON_EOL: "true" PURPLEMET_FAIL_ON_KEV: "true" PURPLEMET_FAIL_ON_SSL: "true" PURPLEMET_FAIL_ON_CERT_EXPIRY: "30" PURPLEMET_REQUIRE_WAF: "true" needs: - build deploy: stage: deploy script: - ./deploy.sh production needs: - purplemet environment: name: production when: manual ``` ## Results and Exit Codes ### Exit Codes | Code | Meaning | Pipeline Behavior | |------|---------|-------------------| | **0** | No issues above threshold | Pipeline **passes** | | **1** | Issues found above threshold | **Warning** (`.purplemet-analyze`) or **Fail** (`.purplemet-analyze-blocking`) | | **2** | Analysis error on Purplemet | Pipeline **fails** | | **3** | Timeout exceeded | Pipeline **fails** | | **4** | Network or API error | Pipeline **fails** | | **5** | Usage error (bad config) | Pipeline **fails** | | **6** | API contract error | Pipeline **fails** | ### 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 Template Version ```yaml include: - project: 'purplemet/integrations/gitlab-ci-templates' ref: 'v1.0.0' # exact version file: '/purplemet-analyze.gitlab-ci.yml' ``` Or use a major version tag for automatic updates: ```yaml include: - project: 'purplemet/integrations/gitlab-ci-templates' ref: 'v1' # latest v1.x.x file: '/purplemet-analyze.gitlab-ci.yml' ``` ### Custom Stage By default, the template runs in the `test` stage. Override: ```yaml purplemet: extends: .purplemet-analyze stage: security variables: PURPLEMET_TARGET_URL: "https://your-app.example.com" ``` ### Save Report as Pages Artifact ```yaml pages: stage: deploy needs: - purplemet script: - mkdir -p public - cp purplemet-report.json public/ artifacts: paths: - public ``` ### Generate HTML Report ```yaml purplemet-html: image: ppmsupport/purplemet-cli:latest stage: test script: - purplemet-cli analyze "$PURPLEMET_TARGET_URL" --format html --output-file purplemet-report.html --fail-on-severity high || true artifacts: paths: - purplemet-report.html when: always ``` ## FAQ / Common Errors ### `PURPLEMET_API_TOKEN is not set` The CI/CD variable is missing or inaccessible. **Fix:** Add the token as a masked CI/CD variable: Settings → CI/CD → Variables → Add variable → `PURPLEMET_API_TOKEN`. For protected variables, ensure the pipeline runs on a protected branch or tag. --- ### `PURPLEMET_TARGET_URL is not set` The target URL variable is missing. **Fix:** Set `PURPLEMET_TARGET_URL` in the `variables:` block of your job definition. --- ### "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 CI/CD variable --- ### Analysis times out (exit code 3) **Fix:** Increase `PURPLEMET_WAIT_TIMEOUT` (default is `1800000` / 30 min): ```yaml variables: PURPLEMET_WAIT_TIMEOUT: "3600000" # 60 minutes ``` Or set to `0` for no limit. --- ### Pipeline fails with exit code 1 but I want it to continue Exit code 1 means vulnerabilities were found above the threshold. **Option 1:** Use `.purplemet-analyze` template (default behavior — exit code 1 is a warning). **Option 2:** Add `allow_failure`: ```yaml purplemet: extends: .purplemet-analyze-blocking allow_failure: exit_codes: [1] ``` **Option 3:** Full non-blocking: ```yaml purplemet: extends: .purplemet-analyze allow_failure: true ``` --- ### Protected variable not available in pipeline **Fix:** Either: - Run the pipeline on a protected branch/tag - Uncheck **Protect variable** on the CI/CD variable --- ### Network error (exit code 4) The runner cannot reach the Purplemet API. **Fix:** - Check that the runner has outbound HTTPS access to `api.purplemet.com` - If behind a proxy, configure `HTTP_PROXY` / `HTTPS_PROXY` in the runner config - If using a custom API endpoint, set `PURPLEMET_BASE_URL` --- ### "api http 429: Too Many Requests" Rate limit reached. The CLI automatically retries with the `Retry-After` delay. **Fix:** If persistent, reduce analysis frequency or contact Purplemet support. --- ### How do I use the template from a different GitLab instance? Use `include:remote` with the raw URL: ```yaml include: - remote: 'https://raw.githubusercontent.com/Purplemet/cli/v1.0.0/integrations/gitlab/purplemet-analyze.gitlab-ci.yml' ``` --- ### How do I analyze multiple sites? Create multiple jobs extending the template: ```yaml purplemet:app1: extends: .purplemet-analyze variables: PURPLEMET_TARGET_URL: "https://app1.example.com" purplemet:app2: extends: .purplemet-analyze variables: PURPLEMET_TARGET_URL: "https://app2.example.com" ``` --- ### Where can I see the results? 1. **Job log**: Human-readable summary in the CI job output 2. **Artifacts**: Download `purplemet-report.json` from the job page 3. **Downstream jobs**: Access `PURPLEMET_RATING`, `PURPLEMET_ISSUES` via dotenv variables 4. **Security Dashboard** (SARIF template + GitLab Ultimate): Findings in the security dashboard 5. **Purplemet dashboard**: [cloud.purplemet.com](https://cloud.purplemet.com) for detailed results