# GitHub Actions Integration This guide covers all aspects of integrating Purplemet security analyses into GitHub Actions workflows. ## Table of Contents - [Prerequisites](#prerequisites) - [Quick Start](#quick-start) - [Installation Methods](#installation-methods) - [Parameters](#parameters) - [Outputs](#outputs) - [Complete Pipeline Examples](#complete-pipeline-examples) - [SARIF and GitHub Code Scanning](#sarif-and-github-code-scanning) - [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. Repository Secret Add the token as a GitHub Actions secret: 1. Go to your repository on GitHub 2. Navigate to **Settings** → **Secrets and variables** → **Actions** 3. Click **New repository secret** 4. Name: `PURPLEMET_API_TOKEN` 5. Value: your API token 6. Click **Add secret** > **Organization-level secret:** For multi-repo usage, add the secret at the organization level (Settings → Secrets → Actions) and grant access to the required repositories. ### 3. Permissions (SARIF only) If using GitHub Code Scanning (SARIF upload), your workflow needs the `security-events: write` permission. GitHub Advanced Security must be enabled for the repository (free for public repos, requires license for private repos). ## Quick Start Create `.github/workflows/purplemet.yml`: ```yaml name: Security Analysis on: push: branches: [main] pull_request: jobs: purplemet: runs-on: ubuntu-latest steps: - uses: purplemet/purplemet-action@v1 with: api-token: ${{ secrets.PURPLEMET_API_TOKEN }} target-url: 'https://your-app.example.com' fail-severity: 'high' ``` This workflow: - Runs on every push to `main` and on pull requests - Fails the pipeline if **high** or **critical** vulnerabilities are found - Generates a visual summary in the Actions tab ## Installation Methods ### Method 1: Purplemet Action (recommended) Uses the official composite action that handles installation, execution, and reporting automatically. ```yaml - uses: purplemet/purplemet-action@v1 with: api-token: ${{ secrets.PURPLEMET_API_TOKEN }} target-url: 'https://your-app.example.com' ``` ### Method 2: Binary Installation Downloads and installs the CLI binary directly. Useful when you need more control over the execution. ```yaml - name: Install Purplemet CLI run: curl -sSL https://raw.githubusercontent.com/purplemet/cli/main/scripts/install.sh | sh - name: Run Security Analysis env: PURPLEMET_API_TOKEN: ${{ secrets.PURPLEMET_API_TOKEN }} run: purplemet-cli analyze https://your-app.com --json --fail-on-severity high ``` ### Method 3: Docker Image Uses the official Docker image (~15MB). No installation step needed. ```yaml - name: Run Security Analysis run: | docker run --rm \ -e PURPLEMET_API_TOKEN=${{ secrets.PURPLEMET_API_TOKEN }} \ ppmsupport/purplemet-cli analyze https://your-app.com --json --fail-on-severity high ``` Or as a Docker-based action: ```yaml - uses: docker://ppmsupport/purplemet-cli:latest with: args: analyze https://your-app.example.com --json --fail-on-severity high env: PURPLEMET_API_TOKEN: ${{ secrets.PURPLEMET_API_TOKEN }} ``` ## Parameters ### Action Inputs | Input | Required | Default | Description | |-------|----------|---------|-------------| | `api-token` | **Yes** | — | Purplemet API token (use `${{ secrets.PURPLEMET_API_TOKEN }}`) | | `target-url` | **Yes** | — | URL of the web application to analyze | | `fail-severity` | No | `high` | Fail threshold: `critical`, `high`, `medium`, `low`, `info` | | `timeout` | No | `1800000` | Wait timeout in milliseconds (30 min, 0 = unlimited) | | `version` | No | `latest` | CLI version to use (e.g. `v1.2.0`) | | `base-url` | No | — | API base URL override | | `sarif-upload` | No | `false` | Upload SARIF results to GitHub Code Scanning | ### Environment Variables When using the binary or Docker directly (methods 2 and 3), configure via environment variables: | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `PURPLEMET_API_TOKEN` | **Yes** | — | API authentication token | | `PURPLEMET_BASE_URL` | No | `https://api.purplemet.com` | API base URL | | `PURPLEMET_FAIL_SEVERITY` | No | — (disabled) | Severity gate threshold | | `PURPLEMET_WAIT_TIMEOUT` | No | `0` (unlimited) | Polling timeout in ms | ### CLI Flags When invoking `purplemet-cli` directly, the following flags are relevant: | Flag | Description | |------|-------------| | `--json` | Machine-readable JSON output (recommended for CI) | | `--fail-on-severity ` | Fail if issues at or above this severity | | `--wait-timeout ` | Bound total execution time | | `--format sarif` | Output in SARIF 2.1.0 format | | `--output-file ` | Write output to a file | | `--no-create` | Don't auto-create a site for unknown URLs | ## Outputs The Purplemet action exposes outputs for use in subsequent steps: | Output | Type | Description | Example | |--------|------|-------------|---------| | `exit-code` | int | Exit code of the analysis | `0` | | `rating` | string | Security rating (A–F) | `B` | | `issues` | int | Total number of issues found | `12` | | `result-json` | string | Full analysis result in JSON | `{"analysis": {...}}` | | `gate-results` | string | JSON with gate pass/fail results | `{"severity": {"passed": true}}` | ### Using Outputs ```yaml - uses: purplemet/purplemet-action@v1 id: analysis with: api-token: ${{ secrets.PURPLEMET_API_TOKEN }} target-url: 'https://your-app.example.com' continue-on-error: true - name: Check Results run: | echo "Rating: ${{ steps.analysis.outputs.rating }}" echo "Issues: ${{ steps.analysis.outputs.issues }}" echo "Exit code: ${{ steps.analysis.outputs.exit-code }}" - name: Fail on Critical if: steps.analysis.outputs.exit-code == '1' run: | echo "::error::Security issues found above threshold" exit 1 ``` ### Using JSON Output ```yaml - name: Parse Results if: always() run: | cat purplemet-report.json | jq '.analysis.issueCnts' ``` > **Note:** The `result-json` output contains the full JSON result, but for shell processing prefer reading the `purplemet-report.json` file directly (generated by the action) as it avoids shell escaping issues with multiline content. ## Complete Pipeline Examples ### Basic: Analysis on Push and PR ```yaml name: Security Analysis on: push: branches: [main] pull_request: jobs: purplemet: name: Purplemet Security Analysis runs-on: ubuntu-latest steps: - uses: purplemet/purplemet-action@v1 with: api-token: ${{ secrets.PURPLEMET_API_TOKEN }} target-url: 'https://your-app.example.com' fail-severity: 'high' ``` ### After Deployment: Analyze Staging Environment ```yaml name: Post-Deploy Security Analysis on: workflow_run: workflows: ['Deploy to Staging'] types: [completed] branches: [main] jobs: security: name: Security Analysis runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - uses: purplemet/purplemet-action@v1 with: api-token: ${{ secrets.PURPLEMET_API_TOKEN }} target-url: 'https://staging.your-app.example.com' fail-severity: 'high' timeout: '600000' ``` ### Scheduled Weekly Analysis ```yaml name: Weekly Security Analysis on: schedule: - cron: '0 6 * * 1' # Every Monday at 6:00 UTC jobs: security: name: Weekly Security Analysis runs-on: ubuntu-latest steps: - uses: purplemet/purplemet-action@v1 id: analysis with: api-token: ${{ secrets.PURPLEMET_API_TOKEN }} target-url: 'https://your-app.example.com' fail-severity: 'medium' timeout: '600000' continue-on-error: true - name: Notify on Issues if: steps.analysis.outputs.exit-code == '1' run: | echo "::warning::Weekly analysis found ${{ steps.analysis.outputs.issues }} issue(s) — rating: ${{ steps.analysis.outputs.rating }}" ``` ### Multi-Site Analysis with Matrix ```yaml name: Multi-Site Security Analysis on: schedule: - cron: '0 6 * * 1' jobs: security: name: Analyze ${{ matrix.site.name }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: site: - { name: 'Production', url: 'https://app.example.com' } - { name: 'Staging', url: 'https://staging.example.com' } - { name: 'API', url: 'https://api.example.com' } steps: - uses: purplemet/purplemet-action@v1 with: api-token: ${{ secrets.PURPLEMET_API_TOKEN }} target-url: ${{ matrix.site.url }} fail-severity: 'high' timeout: '600000' ``` ### Warning Mode (Non-blocking) The analysis runs and reports results but never fails the pipeline: ```yaml name: Security Analysis (Warning Mode) on: [push] jobs: security: runs-on: ubuntu-latest steps: - uses: purplemet/purplemet-action@v1 id: analysis with: api-token: ${{ secrets.PURPLEMET_API_TOKEN }} target-url: 'https://your-app.example.com' fail-severity: 'high' continue-on-error: true - name: Report if: always() run: | if [ "${{ steps.analysis.outputs.exit-code }}" = "1" ]; then echo "::warning::Security analysis found issues (rating: ${{ steps.analysis.outputs.rating }}, issues: ${{ steps.analysis.outputs.issues }})" fi ``` ### Full CI/CD Pipeline with Build, Deploy, and Analysis ```yaml name: CI/CD on: push: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Build run: make build - name: Test run: make test deploy: needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Deploy to Staging run: ./deploy.sh staging security-analysis: needs: deploy runs-on: ubuntu-latest steps: - uses: purplemet/purplemet-action@v1 with: api-token: ${{ secrets.PURPLEMET_API_TOKEN }} target-url: 'https://staging.your-app.example.com' fail-severity: 'high' timeout: '600000' ``` ### Binary Install with Custom Handling ```yaml name: Security Analysis on: [push] jobs: security: runs-on: ubuntu-latest steps: - name: Install Purplemet CLI run: curl -sSL https://raw.githubusercontent.com/purplemet/cli/main/scripts/install.sh | sh - name: Run Analysis id: analysis env: PURPLEMET_API_TOKEN: ${{ secrets.PURPLEMET_API_TOKEN }} run: | set +e RESULT=$(purplemet-cli analyze https://your-app.com \ --json --fail-on-severity high --wait-timeout 300000 2>/dev/null) EXIT_CODE=$? set -e echo "exit-code=${EXIT_CODE}" >> $GITHUB_OUTPUT # Multiline JSON requires a delimiter for GITHUB_OUTPUT EOF_DELIM=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) echo "result<<${EOF_DELIM}" >> $GITHUB_OUTPUT echo "${RESULT}" >> $GITHUB_OUTPUT echo "${EOF_DELIM}" >> $GITHUB_OUTPUT case $EXIT_CODE in 0) echo "Analysis passed" ;; 1) echo "::warning::Vulnerabilities found above threshold" ;; 2) echo "::error::Analysis error on Purplemet side" ;; 3) echo "::error::Analysis timed out" ;; 4) echo "::error::Network/API error — check token and connectivity" ;; *) echo "::error::Unexpected error (code ${EXIT_CODE})" ;; esac exit $EXIT_CODE ``` ## SARIF and GitHub Code Scanning ### What is SARIF? SARIF (Static Analysis Results Interchange Format) is a standard format for static analysis results. GitHub Code Scanning uses SARIF to display security findings directly in the **Security** tab of your repository. ### Prerequisites for SARIF 1. **GitHub Advanced Security** must be enabled (free for public repos) 2. Workflow must have `security-events: write` permission 3. The repository must be checked out (`actions/checkout`) for SARIF upload ### Using the Action with SARIF ```yaml name: Security Analysis with Code Scanning on: push: branches: [main] pull_request: jobs: purplemet: runs-on: ubuntu-latest permissions: security-events: write contents: read steps: - uses: actions/checkout@v5 - uses: purplemet/purplemet-action@v1 with: api-token: ${{ secrets.PURPLEMET_API_TOKEN }} target-url: 'https://your-app.example.com' sarif-upload: 'true' fail-severity: 'high' continue-on-error: true ``` Results appear in: - **Security** tab → **Code scanning alerts** - Pull request **Files changed** tab as inline annotations ### Manual SARIF Upload If you prefer manual control: ```yaml name: Security Analysis with SARIF on: [push] permissions: security-events: write contents: read jobs: security: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Install Purplemet CLI run: curl -sSL https://raw.githubusercontent.com/purplemet/cli/main/scripts/install.sh | sh - name: Run Analysis env: PURPLEMET_API_TOKEN: ${{ secrets.PURPLEMET_API_TOKEN }} run: | purplemet-cli analyze https://your-app.com \ --format sarif --output-file purplemet-results.sarif \ --fail-on-severity high || true - name: Upload SARIF if: always() uses: github/codeql-action/upload-sarif@v4 with: sarif_file: purplemet-results.sarif category: purplemet ``` ## Results and Exit Codes ### Exit Codes | Code | Meaning | Pipeline Behavior | |------|---------|-------------------| | **0** | No issues above threshold | Pipeline **passes** | | **1** | Issues found above severity threshold | Pipeline **fails** (use `continue-on-error: true` for warning) | | **2** | Analysis error on Purplemet platform | Pipeline **fails** | | **3** | Timeout exceeded | Pipeline **fails** | | **4** | Network or API error | Pipeline **fails** | | **5** | Usage error (bad arguments) | 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. ### Job Summary The Purplemet action automatically generates a visual summary in the **Actions** tab showing: - Security rating with color indicator - Issue count and severity breakdown - Pass/fail status against the configured threshold - Detailed issue breakdown (expandable) ## Advanced Usage ### Pin a Specific CLI Version ```yaml - uses: purplemet/purplemet-action@v1 with: api-token: ${{ secrets.PURPLEMET_API_TOKEN }} target-url: 'https://your-app.example.com' version: 'v1.2.0' ``` ### Save Report Artifact ```yaml - uses: purplemet/purplemet-action@v1 with: api-token: ${{ secrets.PURPLEMET_API_TOKEN }} target-url: 'https://your-app.example.com' - name: Upload Report if: always() uses: actions/upload-artifact@v4 with: name: purplemet-report path: purplemet-report.json retention-days: 30 ``` ### Generate HTML Report Artifact ```yaml - name: Install Purplemet CLI run: curl -sSL https://raw.githubusercontent.com/purplemet/cli/main/scripts/install.sh | sh - name: Run Analysis env: PURPLEMET_API_TOKEN: ${{ secrets.PURPLEMET_API_TOKEN }} run: | purplemet-cli analyze https://your-app.com \ --format html --output-file report.html \ --fail-on-severity high || true - name: Upload HTML Report if: always() uses: actions/upload-artifact@v4 with: name: security-report path: report.html ``` ### Conditional Analysis Based on Changed Files ```yaml name: Security Analysis on Infra Changes on: push: paths: - 'infrastructure/**' - 'nginx/**' - 'Dockerfile' - 'docker-compose.yml' jobs: security: runs-on: ubuntu-latest steps: - uses: purplemet/purplemet-action@v1 with: api-token: ${{ secrets.PURPLEMET_API_TOKEN }} target-url: 'https://your-app.example.com' fail-severity: 'high' ``` ### Using Environment-Specific URLs ```yaml name: Security Analysis on: push: branches: [main, staging] jobs: security: runs-on: ubuntu-latest steps: - name: Set Target URL id: target run: | if [ "${{ github.ref }}" = "refs/heads/main" ]; then echo "url=https://app.example.com" >> $GITHUB_OUTPUT else echo "url=https://staging.example.com" >> $GITHUB_OUTPUT fi - uses: purplemet/purplemet-action@v1 with: api-token: ${{ secrets.PURPLEMET_API_TOKEN }} target-url: ${{ steps.target.outputs.url }} fail-severity: 'high' ``` ## FAQ / Common Errors ### "PURPLEMET_API_TOKEN is not set" The secret is missing or not accessible. **Fix:** Add `PURPLEMET_API_TOKEN` as a repository secret: Settings → Secrets and variables → Actions → New repository secret. For organization secrets, verify the repository is in the allowed list. --- ### "Access is not authorized without a valid session" The API token is invalid or expired. **Fix:** 1. Verify the token works: `purplemet-cli auth check` 2. Create a new token at [cloud.purplemet.com](https://cloud.purplemet.com/#/tokens/create) 3. Update the repository secret --- ### The analysis takes too long and times out **Fix:** Increase the timeout. Default is 30 minutes (`1800000` ms). Set `0` for no limit. ```yaml - uses: purplemet/purplemet-action@v1 with: api-token: ${{ secrets.PURPLEMET_API_TOKEN }} target-url: 'https://your-app.example.com' timeout: '3600000' # 60 minutes ``` --- ### Pipeline fails with exit code 1 but I want it to continue Exit code 1 means vulnerabilities were found above the threshold. To make it a warning instead of a failure: ```yaml - uses: purplemet/purplemet-action@v1 id: analysis with: api-token: ${{ secrets.PURPLEMET_API_TOKEN }} target-url: 'https://your-app.example.com' fail-severity: 'high' continue-on-error: true - name: Warn on Issues if: steps.analysis.outputs.exit-code == '1' run: echo "::warning::Security issues found - rating: ${{ steps.analysis.outputs.rating }}" ``` --- ### "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. --- ### "Download failed" during CLI installation The specified version doesn't exist or the GitHub release is not accessible. **Fix:** - Check available versions at [github.com/purplemet/cli/releases](https://github.com/purplemet/cli/releases) - Use `version: 'latest'` (default) or a valid tag like `version: 'v1.0.0'` --- ### SARIF upload fails with "Resource not accessible by integration" The workflow doesn't have the required permissions. **Fix:** Add permissions to your workflow: ```yaml permissions: security-events: write contents: read ``` Also ensure GitHub Advanced Security is enabled for the repository (required for private repos). --- ### How do I analyze a site that doesn't exist yet? By default, `purplemet-cli analyze ` auto-creates the site if the URL is not found. No extra step needed. To disable this behavior, use `--no-create`. --- ### How do I analyze multiple sites? Use a matrix strategy: ```yaml strategy: fail-fast: false matrix: url: - 'https://app1.example.com' - 'https://app2.example.com' steps: - uses: purplemet/purplemet-action@v1 with: api-token: ${{ secrets.PURPLEMET_API_TOKEN }} target-url: ${{ matrix.url }} ``` --- ### How do I use a self-hosted runner? The action works on self-hosted runners. Ensure: - `curl` and `jq` are available - Outbound HTTPS access to `api.purplemet.com` and `github.com` - If behind a proxy, set `HTTP_PROXY` / `HTTPS_PROXY` environment variables --- ### Where can I see the analysis results? 1. **Actions tab**: Job summary with rating, issues, and severity breakdown 2. **Security tab** (if SARIF enabled): Code scanning alerts with detailed findings 3. **Artifacts**: Download `purplemet-report.json` from the workflow run 4. **Step outputs**: Access `rating`, `issues`, `result-json` in subsequent steps