# Jenkins Integration This guide covers all aspects of integrating Purplemet security analyses into Jenkins pipelines. ## Table of Contents - [Prerequisites](#prerequisites) - [Quick Start](#quick-start) - [Installation Methods](#installation-methods) - [Parameters](#parameters) - [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. Jenkins Credential Add the token as a **Secret text** credential: 1. Go to **Manage Jenkins** → **Credentials** 2. Select the appropriate scope (global or folder) 3. Click **Add Credentials** 4. Kind: **Secret text** 5. Secret: your API token 6. ID: `PURPLEMET_API_TOKEN` 7. Click **Create** ### 3. Agent Requirements The Jenkins agent must have: - `curl` (for CLI installation) - Outbound HTTPS access to `api.purplemet.com` - Docker (if using the Docker-based approach) ## Quick Start ```groovy @Library('purplemet') _ pipeline { agent any stages { stage('Security Analysis') { steps { purplemetAnalyze( url: 'https://your-app.example.com', failSeverity: 'high' ) } } } } ``` ## Installation Methods ### Method 1: Shared Library (recommended) Uses the official Jenkins shared library that provides `purplemetAnalyze` and `purplemetInstall` steps. **Setup:** 1. Make sure the **Pipeline: Shared Groovy Libraries** plugin is installed (**Manage Jenkins** → **Plugins** → **Available plugins**). It is included in the default suggested plugins. 2. Go to **Manage Jenkins** → **System** and scroll to **Global Trusted Pipeline Libraries** (not *Untrusted* — the Purplemet library runs trusted Groovy). 3. Click **Add** and configure the library: - **Name**: `purplemet` - **Default version**: `main` - **Retrieval method**: Modern SCM - **Source Code Management**: Git - **Project Repository**: `https://dev.purplemet.com/purplemet/cli.git` (add Git credentials if the repo is private) or the public mirror `https://github.com/Purplemet/cli.git` (no credentials needed) - **Library Path (optional)**: `integrations/jenkins` 4. Check **Load implicitly** for automatic availability (optional) 5. Click **Save**. > **Note:** if you see inline HTTP 403 "No valid crumb was included in the request" errors next to the Name / Default version fields, it is a cosmetic UI validation issue — the Save itself still works. If Save also fails with 403, set **Jenkins Location → Jenkins URL** to match the URL in your browser, hard-reload the page (Cmd/Ctrl+Shift+R) and try again. **Usage:** ```groovy @Library('purplemet') _ pipeline { agent any stages { stage('Security Analysis') { steps { purplemetAnalyze( url: 'https://your-app.example.com', failSeverity: 'high' ) } } } } ``` ### Method 2: Docker Image Uses the official Docker image. Requires Docker on the agent. ```groovy pipeline { agent { docker { image 'ppmsupport/purplemet-cli:latest' args '--entrypoint=' } } stages { stage('Security Analysis') { steps { withCredentials([string(credentialsId: 'PURPLEMET_API_TOKEN', variable: 'PURPLEMET_API_TOKEN')]) { sh 'purplemet-cli analyze https://your-app.com --format json --fail-on-severity high --output-file purplemet-report.json' } } } } post { always { archiveArtifacts artifacts: 'purplemet-report.json', allowEmptyArchive: true } } } ``` > **`args '--entrypoint='`** neutralises the image's built-in entrypoint so Jenkins can run its `cat` keep-alive command — without it the container exits immediately with `The container started but didn't run the expected command`. > > **Apple Silicon / ARM runners:** the `ppmsupport/purplemet-cli` image is currently published for `linux/amd64` and `linux/arm64`. If your Jenkins controller runs under QEMU emulation and Docker cannot resolve the right variant automatically, pin it with an `environment { DOCKER_DEFAULT_PLATFORM = 'linux/amd64' }` block at the pipeline level and add `--platform linux/amd64` to `args`. ### Method 3: Binary Installation Downloads and installs the CLI binary directly. On agents without `sudo` and without write access to `/usr/local/bin` (typical Jenkins containers), the installer falls back to `~/.local/bin` — hence the `PATH` override so subsequent `sh` steps find the binary. ```groovy pipeline { agent any environment { PATH = "${env.HOME}/.local/bin:${env.PATH}" } stages { stage('Security Analysis') { steps { sh 'curl -sSL https://raw.githubusercontent.com/purplemet/cli/main/scripts/install.sh | sh' withCredentials([string(credentialsId: 'PURPLEMET_API_TOKEN', variable: 'PURPLEMET_API_TOKEN')]) { sh 'purplemet-cli analyze https://your-app.com --format json --fail-on-severity high --output-file purplemet-report.json' } } } } post { always { archiveArtifacts artifacts: 'purplemet-report.json', allowEmptyArchive: true } } } ``` ## Parameters ### Shared Library Parameters (`purplemetAnalyze`) Core parameters below; see the [full list of gates](https://github.com/Purplemet/cli/blob/main/integrations/jenkins/README.md#parameters) (severity, CVE, SSL, HTTP, etc.) in the library README. | Parameter | Required | Default | Description | |-----------|----------|---------|-------------| | `url` | **Yes** | — | URL of the web application to analyze | | `token` | No | `PURPLEMET_API_TOKEN` | Jenkins credential ID for the API token | | `baseUrl` | No | — | API base URL override (e.g. `https://api.dev.purplemet.com`) | | `failSeverity` | No | `high` | Severity threshold: `critical`, `high`, `medium`, `low`, `info` | | `timeout` | No | `1800000` | Polling timeout in milliseconds (30 min, 0 = unlimited) | | `format` | No | `json` | Output format: `json`, `human`, `sarif`, `html` | | `version` | No | `latest` | CLI version to install | > **Important:** with the shared library, security gates must be passed as **named parameters** to `purplemetAnalyze()` (e.g. `failOnKev: true`, `failOnCertExpiry: '30'`). Setting `environment { PURPLEMET_* = ... }` has **no effect** — the library builds its own environment from the named arguments and overrides anything set outside. > **`token`** defaults to the credential ID `PURPLEMET_API_TOKEN`. Override only when your credential has a different ID: > ```groovy > purplemetAnalyze( > url: 'https://your-app.example.com', > token: 'MY_CUSTOM_CREDENTIAL_ID' > ) > ``` ### Environment Variables When using the Docker or binary methods (not the shared library), all standard Purplemet environment variables are supported: | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `PURPLEMET_API_TOKEN` | **Yes** | — | API authentication token | | `PURPLEMET_FAIL_SEVERITY` | No | — | Severity threshold | | `PURPLEMET_WAIT_TIMEOUT` | No | `0` | Polling timeout (ms) | | `PURPLEMET_BASE_URL` | No | — | API base URL override | ### Security Gate Variables All security gates are exposed as both env vars and CLI flags — use these with the Docker or binary methods. With the shared library, pass them as named parameters instead (see above). | Variable | Default | Description | |----------|---------|-------------| | `PURPLEMET_FAIL_SEVERITY` | — | 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 | | `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 | ## Security Gates Multiple gates can be combined — the analysis fails (exit code 1) if **any** gate triggers. ### Example: Strict Policy with Shared Library With the shared library, all gates are passed as named parameters to `purplemetAnalyze(...)`. Do **not** use `environment { PURPLEMET_* }` blocks — the library builds its own environment from the named parameters and will override anything set outside. ```groovy @Library('purplemet') _ pipeline { agent any stages { stage('Security Analysis') { steps { purplemetAnalyze( url: 'https://your-app.example.com', failSeverity: 'high', failOnEol: true, failOnKev: true, failOnSsl: true, requireWaf: true, failOnCertExpiry: '30' ) } } } } ``` ### Example: Advanced Gates with CLI Flags ```groovy pipeline { agent any environment { PATH = "${env.HOME}/.local/bin:${env.PATH}" } stages { stage('Security Analysis') { steps { sh 'curl -sSL https://raw.githubusercontent.com/purplemet/cli/main/scripts/install.sh | sh' withCredentials([string(credentialsId: 'PURPLEMET_API_TOKEN', variable: 'PURPLEMET_API_TOKEN')]) { sh ''' purplemet-cli analyze https://your-app.com \ --format json \ --fail-on-severity high \ --fail-on-cvss 9.0 \ --fail-on-eol \ --fail-on-kev \ --require-waf \ --fail-on-cert-expiry 30 \ --output-file purplemet-report.json ''' } } } } } ``` ## Complete Pipeline Examples ### Basic: Analysis on Every Build ```groovy @Library('purplemet') _ pipeline { agent any stages { stage('Security Analysis') { steps { purplemetAnalyze( url: 'https://your-app.example.com', failSeverity: 'high' ) } } } } ``` ### Build → Deploy → Analysis Pipeline ```groovy @Library('purplemet') _ pipeline { agent any stages { stage('Build') { steps { sh 'make build' } } stage('Test') { steps { sh 'make test' } } stage('Deploy to Staging') { steps { sh './deploy.sh staging' } } stage('Security Analysis') { steps { purplemetAnalyze( url: 'https://staging.example.com', failSeverity: 'high', timeout: '600000' ) } } stage('Deploy to Production') { when { branch 'main' } input { message 'Deploy to production?' } steps { sh './deploy.sh production' } } } } ``` ### Scripted Pipeline ```groovy @Library('purplemet') _ node { stage('Security Analysis') { purplemetAnalyze( url: 'https://your-app.example.com', failSeverity: 'medium', timeout: '600000' ) } } ``` ### Multi-Site Analysis (Parallel) ```groovy @Library('purplemet') _ pipeline { agent any stages { stage('Security Analyses') { parallel { stage('Analyze App 1') { steps { purplemetAnalyze( url: 'https://app1.example.com', failSeverity: 'high' ) } } stage('Analyze App 2') { steps { purplemetAnalyze( url: 'https://app2.example.com', failSeverity: 'high' ) } } } } } } ``` ### Scheduled Nightly Analysis ```groovy @Library('purplemet') _ pipeline { agent any triggers { cron('H 6 * * 1') // Every Monday at ~6:00 } stages { stage('Nightly Analysis') { steps { purplemetAnalyze( url: 'https://production.example.com', failSeverity: 'medium', timeout: '600000' ) } } } } ``` ### Custom Exit Code Handling ```groovy pipeline { agent any environment { PATH = "${env.HOME}/.local/bin:${env.PATH}" } stages { stage('Security Analysis') { steps { sh 'curl -sSL https://raw.githubusercontent.com/purplemet/cli/main/scripts/install.sh | sh' withCredentials([string(credentialsId: 'PURPLEMET_API_TOKEN', variable: 'PURPLEMET_API_TOKEN')]) { script { def exitCode = sh( script: 'purplemet-cli analyze https://your-app.com --format json --fail-on-severity high --output-file purplemet-report.json', returnStatus: true ) if (exitCode == 0) { echo 'Analysis passed — no issues above threshold' } else if (exitCode == 1) { currentBuild.result = 'UNSTABLE' echo 'WARNING: vulnerabilities found above threshold' } else { error "Analysis failed with exit code ${exitCode}" } } } } } } post { always { archiveArtifacts artifacts: 'purplemet-report.json', allowEmptyArchive: true } } } ``` ### Full Pipeline with Strict Gates ```groovy @Library('purplemet') _ pipeline { agent any stages { stage('Build') { steps { sh 'make build' } } stage('Deploy Staging') { steps { sh './deploy.sh staging' } } stage('Security Analysis') { steps { purplemetAnalyze( url: 'https://staging.example.com', failSeverity: 'high', timeout: '600000', failOnEol: true, failOnKev: true, failOnSsl: true, failOnCert: true, failOnCertExpiry: '30', requireWaf: true ) } } stage('Deploy Production') { when { branch 'main' } input { message 'Deploy to production?' } steps { sh './deploy.sh production' } } } post { always { archiveArtifacts artifacts: 'purplemet-report.json', allowEmptyArchive: true } } } ``` ## Results and Exit Codes ### Exit Codes | Code | Meaning | Jenkins Build Status | |------|---------|---------------------| | **0** | No issues above threshold | **SUCCESS** | | **1** | Issues found above threshold | **UNSTABLE** (shared library) or configurable | | **2** | Analysis error on Purplemet | **FAILURE** | | **3** | Timeout exceeded | **FAILURE** | | **4** | Network or API error | **FAILURE** | | **5** | Usage error (bad arguments) | **FAILURE** | | **6** | API contract error | **FAILURE** | ### 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. ### Report Artifact The analysis report is saved as `purplemet-report.json` and archived as a Jenkins build artifact. Download it from the build page. ## Advanced Usage ### Pin a Specific CLI Version ```groovy purplemetAnalyze( url: 'https://your-app.example.com', version: 'v1.2.0' ) ``` ### Use Docker Agent for Isolation ```groovy pipeline { agent { docker { image 'ppmsupport/purplemet-cli:latest' args '--entrypoint= -e PURPLEMET_API_TOKEN' } } stages { stage('Analysis') { steps { withCredentials([string(credentialsId: 'PURPLEMET_API_TOKEN', variable: 'PURPLEMET_API_TOKEN')]) { sh 'purplemet-cli analyze https://your-app.com --format json --output-file purplemet-report.json' } } } } } ``` ### Generate HTML Report The shared library auto-derives the report filename from `format` (`.html` here) and archives it — the report is available from the build's **Artifacts** section. ```groovy @Library('purplemet') _ pipeline { agent any stages { stage('Security Analysis') { steps { purplemetAnalyze( url: 'https://your-app.com', format: 'html', failSeverity: 'high' ) } } } } ``` #### Optional: render the report inline in the Jenkins UI To expose the report as a clickable link in the build sidebar (instead of a download), install the [HTML Publisher Plugin](https://plugins.jenkins.io/htmlpublisher/) and add a `post` block: ```groovy post { always { publishHTML(target: [ allowMissing: true, reportDir: '.', reportFiles: 'purplemet-report.html', reportName: 'Purplemet Security Report' ]) } } ``` #### Enabling CSS in the Jenkins-rendered report The Purplemet HTML report bundles all styling inline. Jenkins ships with a strict [Content Security Policy](https://www.jenkins.io/doc/book/security/configuring-content-security-policy/) that **strips inline CSS/JS from any user-served HTML** — including reports shown through HTML Publisher. The result: the report renders, but looks plain/unstyled. You have three ways to allow the report's styles through, ordered from least to most permissive: ##### 1. Relax the policy permanently (recommended) Add a Java system property on Jenkins startup. Edit your Jenkins service/launcher and add: ``` -Dhudson.model.DirectoryBrowserSupport.CSP="sandbox allow-scripts; default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:;" ``` Depending on how Jenkins is installed: - **systemd** (`/etc/systemd/system/jenkins.service.d/override.conf`): ``` [Service] Environment="JAVA_OPTS=-Dhudson.model.DirectoryBrowserSupport.CSP=sandbox allow-scripts; default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:;" ``` Then: `sudo systemctl daemon-reload && sudo systemctl restart jenkins` - **Docker** (`jenkins/jenkins:lts`): add the `JAVA_OPTS` env var when launching the container. - **`/etc/default/jenkins`** (debian-style installs): append to the `JAVA_ARGS` variable. ##### 2. Relax the policy at runtime (no restart, but lost on Jenkins restart) Go to **Manage Jenkins → Script Console** and run: ```groovy System.setProperty("hudson.model.DirectoryBrowserSupport.CSP", "sandbox allow-scripts; default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:;") ``` This is the right option for quick testing. For production use option 1 so the setting survives restarts. ##### 3. Disable the policy entirely (**not recommended**) ```groovy System.setProperty("hudson.model.DirectoryBrowserSupport.CSP", "") ``` This exposes Jenkins to [CVE-2017-2608](https://www.jenkins.io/security/advisory/2017-04-10/) — use only on isolated/dev Jenkins instances. ##### Verifying the change After reload, open the published report: colors, rating chart, and severity badges should now render. If not, check your browser console for blocked CSP sources and add the corresponding directive. The archived `.html` artifact downloaded from the build page always renders correctly because it's served from the user's filesystem, not through Jenkins. ## FAQ / Common Errors ### `PURPLEMET_API_TOKEN is not set` The credential is missing or inaccessible. **Fix:** Add a **Secret text** credential: Manage Jenkins → Credentials → Add Credentials → Secret text → ID: `PURPLEMET_API_TOKEN`. --- ### "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 Jenkins credential --- ### `curl: command not found` The Jenkins agent doesn't have `curl` installed. **Fix:** Either: - Install curl on the agent: `apt-get install -y curl` or `apk add curl` - Use a Docker agent with curl pre-installed - Use the Docker image method (no curl needed) --- ### Analysis times out (exit code 3) **Fix:** Increase the timeout parameter: ```groovy purplemetAnalyze( url: 'https://your-app.com', timeout: '600000' // 10 minutes ) ``` --- ### Build fails with exit code 1 but I want UNSTABLE The shared library already marks exit code 1 as **UNSTABLE** by default. If using the CLI directly: ```groovy script { def exitCode = sh(script: 'purplemet-cli analyze ... --format json', returnStatus: true) if (exitCode == 1) { currentBuild.result = 'UNSTABLE' } else if (exitCode > 1) { error "Analysis failed (exit code ${exitCode})" } } ``` --- ### "Library 'purplemet' not found" The shared library is not configured. **Fix:** Add the library in Manage Jenkins → System → Global Trusted Pipeline Libraries. See [Installation Methods](#method-1-shared-library-recommended). --- ### How do I analyze multiple sites? Use parallel stages (see [Multi-Site Analysis example](#multi-site-analysis-parallel)). --- ### Where can I see the results? 1. **Console output**: Summary in the build log 2. **Build artifacts**: Download `purplemet-report.json` 3. **Build status**: SUCCESS / UNSTABLE / FAILURE reflects analysis results 4. **Purplemet dashboard**: [cloud.purplemet.com](https://cloud.purplemet.com) for detailed results