# Configuration ## Priority Order Configuration is resolved in this order (first wins): 1. **CLI flags** (`--token`, `--json`, etc.) 2. **Environment variables** (`PURPLEMET_API_TOKEN`, etc.) 3. **`.env` file** (auto-loaded from current directory) 4. **Project config** (`.purplemet.yml` — auto-discovered from CWD up to the project root) 5. **User config** (`~/.purplemet/config.yml`) 6. **Defaults** ## Environment Variables ### Authentication & Connection | Variable | Description | Default | |----------|-------------|---------| | `PURPLEMET_API_TOKEN` | API authentication token | — | | `PURPLEMET_BASE_URL` | API base URL | `https://api.purplemet.com` | | `PURPLEMET_TIMEOUT` | Per-request HTTP timeout (ms) | `30000` | | `PURPLEMET_WAIT_TIMEOUT` | Global polling timeout (ms) | `0` (unlimited) | | `PURPLEMET_POLL_INTERVAL` | Polling interval (ms) | `10000` | | `PURPLEMET_INSECURE` | Skip TLS verification (dev only) | `false` | | `PURPLEMET_CONFIG` | Path to a project config file (default: auto-discover `.purplemet.yml`) | — | ### Output | Variable | Description | Default | |----------|-------------|---------| | `PURPLEMET_JSON` | JSON output mode | `false` | | `PURPLEMET_VERBOSE` | Verbose logging to stderr | `false` | | `PURPLEMET_NO_ADMIN_WARNING` | Suppress the warning shown when invoking admin-only commands (`users`, `sessions`, `tokens`) | `false` | | `PURPLEMET_NO_PAGER` | Disable the interactive paginator on `list` commands (TTY only) | `false` | ### Security Gates | Variable | Description | Default | |----------|-------------|---------| | `PURPLEMET_FAIL_SEVERITY` | Severity gate: `critical`, `high`, `medium`, `low`, `info` | — (disabled) | | `PURPLEMET_FAIL_RATING` | Rating gate: `A`, `B`, `C`, `D`, `E`, `F` | — (disabled) | | `PURPLEMET_FAIL_CVSS` | Max CVSS score threshold (e.g. `9.0`) | `0` (disabled) | | `PURPLEMET_FAIL_ON_EOL` | Block on end-of-life components | `false` | | `PURPLEMET_FAIL_ON_SSL` | Block on SSL/TLS protocol issues | `false` | | `PURPLEMET_FAIL_ON_CERT` | Block on certificate issues | `false` | | `PURPLEMET_FAIL_ON_HEADERS` | Block on HTTP security header issues (CSP, HSTS, X-Frame-Options) | `false` | | `PURPLEMET_FAIL_ON_COOKIES` | Block on insecure cookies (HttpOnly, Secure, SameSite) | `false` | | `PURPLEMET_FAIL_ON_UNSAFE` | Block on unsafe components | `false` | | `PURPLEMET_FAIL_ON_KEV` | Block on CISA Known Exploited Vulnerabilities | `false` | | `PURPLEMET_FAIL_ON_EPSS` | Max EPSS score threshold (e.g. `0.75`, 0 = disabled) | `0` (disabled) | | `PURPLEMET_FAIL_ON_ACTIVE_EXPLOITS` | Block on actively exploited vulnerabilities | `false` | | `PURPLEMET_FAIL_ON_OSSF_SCORE` | Min OpenSSF Scorecard score (e.g. `5.0`, 0 = disabled) | `0` (disabled) | | `PURPLEMET_FAIL_ON_CERT_EXPIRY` | Block if certificate expires within N days | `0` (disabled) | | `PURPLEMET_FAIL_ON_ISSUE_COUNT` | Block if total issues >= threshold | `0` (disabled) | | `PURPLEMET_REQUIRE_WAF` | Block if no WAF detected | `false` | | `PURPLEMET_FAIL_ON_SENSITIVE_SERVICES` | Block if sensitive services are exposed | `false` | | `PURPLEMET_EXCLUDE_TECH` | Block if specified technologies detected (comma-separated) | — | ### Dev-only Variables | Variable | Description | |----------|-------------| | `PPM_CLOUD_UI_BASIC_USER` | Basic auth username (`.dev.` URLs only) | | `PPM_CLOUD_UI_BASIC_PASS` | Basic auth password (`.dev.` URLs only) | ## Pagination All `list` commands share the same set of pagination flags: | Flag | Description | Default | |------|-------------|---------| | `--limit` | Page size (1–1000) | `100` | | `--offset` | Skip the first N items (mutually exclusive with `--page`) | `0` | | `--page` | Page number, 1-indexed (mutually exclusive with `--offset`) | `0` (uses `--offset`) | | `--all` | Fetch every page automatically (capped by `--max`) | `false` | | `--max` | Hard cap on the number of items fetched when `--all` is set | `10000` | **Default behavior depends on where the output goes:** - **Interactive terminal (TTY) with the pager enabled** — `list` commands automatically fetch every page (capped by `--max`) so you can scroll through the entire result set with the pager's arrow keys. No need to pass `--all` manually. - **Pipe / redirect / `--no-pager` / `--json` / CI** — only the first page is fetched, exactly as you'd expect from a scriptable CLI. Use `--all`, `--page N`, or `--offset N` to access more. This split keeps both modes ergonomic: humans get effortless scrolling, scripts get predictable single-page output. ``` # In a terminal: auto-fetches everything, pager handles navigation $ purplemet-cli sites list … Showing 142 of 142 site(s) — all pages fetched # Piped: only the first page, with a hint to get more $ purplemet-cli sites list | head … Showing 1-100 of 142 site(s) — page 1 of 2 (next: --page 2) # Force the script behavior in a TTY $ purplemet-cli sites list --no-pager … Showing 1-100 of 142 site(s) — page 1 of 2 (next: --page 2) # Explicit page works the same in both modes $ purplemet-cli sites list --page 2 --limit 20 … Showing 21-40 of 142 site(s) — page 2 of 8 (next: --page 3) # Cap the auto-fetch when you know the workspace is huge $ purplemet-cli sites list --max 50 … Showing 50 of 5000 site(s) — capped by --max, raise it to fetch more ``` `--all` cannot be combined with `--page` or `--offset`. Raise `--max` when you legitimately need more than the default cap (every API call is counted by the server, so a full fetch on a large workspace can be slow). `--query` is evaluated across the **whole** result set, not just one page (see [Filtering with `--query`](#filtering-with---query) below), so you do not need `--all` for a filter to find matches on later pages. ## Interactive Pagination (TTY) When stdout is an interactive terminal, `list` commands open a built-in paginator that fetches **one page at a time** and lets you navigate between pages with the keyboard. Each arrow-right keystroke triggers a fresh API call for the next page — there is no pre-fetch, so a workspace with thousands of items stays cheap to browse. | Key | Action | |-----|--------| | `←` / `h` / `p` | Previous page (refetched) | | `→` / `l` / `n` | Next page (refetched) | | `↑` / `k` | Scroll one line up within the current page | | `↓` / `j` | Scroll one line down within the current page | | `PgUp` / `b` | Scroll one screen up within the current page | | `PgDn` / `Space` / `f` | Scroll one screen down within the current page | | `g` | Jump to the top of page 1 | | `G` | Jump to the bottom of the last page | | `r` | Refetch the current page | | `q` / `Esc` / `Ctrl+C` | Quit | The bottom status bar shows your position: `page 3/8 rows 1-30/100` and the available shortcuts. The terminal returns to its previous state on exit (alternate screen buffer, like `vim` or `less`). **Auto-disables when:** - `--no-pager` is set, or `PURPLEMET_NO_PAGER=1` - `--json` is set (machine output is never paginated) - stdout or stdin is not a TTY (pipe, redirect, CI logs) - `--all`, `--page N` or `--offset N` is set (those imply you want one specific result set, not interactive browsing) **Disable per-call or globally:** ```bash # One-off purplemet-cli sites list --no-pager # Globally (e.g. in CI) export PURPLEMET_NO_PAGER=1 ``` ## Filtering with `--query` `--query` is a global flag available on every `list` command (and any command that returns a collection). It filters items client-side on their JSON fields. **Syntax:** `keyvalue[,keyvalue,…]` — multiple predicates are AND-combined. **Operators:** | Op | Meaning | Example | |----|---------|---------| | `=` | Exact match (case-insensitive for strings) | `severity=high` | | `!=` | Not equal | `status!=IGNORED` | | `~=` | Contains (substring, case-insensitive) | `url~=staging` | | `!~` | Does not contain | `name!~deprecated` | **Keys** are the JSON field names you see in `--json` output (e.g. `severity`, `status`, `url`, `role`). Nested fields use the dotted form exposed by the API (e.g. `technology.name` on issues). > **`--query` filters across the whole account.** Every `list` command > evaluates `--query` over all items in the workspace, not just the page you > land on, and reports the accurate filtered total — e.g. > `issues list --query "severity=high,status=OPEN"` prints > `Showing 1-34 of 34 issue(s)` even when those 34 are scattered across many > pages. How it's resolved depends on what the API supports: > > - **Server-side push-down** when the endpoint can filter on the field: > `sites list --query "url~=staging"` becomes a single `/site?url=like(...)` > request, and `domains list --query "name~=corp"` a single > `/domain?name=like(...)` request. These are the only two server-side > filterable fields (`site.url`, `domain.name`). > - **Client-side full scan** for every other field/command: the CLI walks > the pages and keeps the matches. `issues list` also folds its dedicated > `--severity` / `--status` flags into this scan. > > The exact total requires reading every page (the API can't count a filtered > set it doesn't filter), so: > > - **One-shot output** (piped, CI, `--no-pager`) scans to completion and prints > the exact total — `Showing 1-100 of 1500 issue(s) — page 1 of 15`. A sparse > filter over a large account therefore costs more API calls; the scan is > capped by `--max` (the footer says so when it hits the cap). > - **The interactive pager** stays lazy — it fetches just enough to fill the > screen and the total refines as you page, with a hint while it's still > unknown. Press `→` to keep going, or use `--all` / `--no-pager` to force the > exact count up front. ### Examples ```bash # Sites whose URL contains "staging" purplemet-cli sites list --query "url~=staging" # Open high-severity issues only purplemet-cli issues list --query "severity=high,status=OPEN" # All non-low issues mentioning a CVE in their name purplemet-cli issues list --all --query "severity!=low,name~=CVE-2024" # Admin tokens only purplemet-cli tokens list --query "role=ADMIN" # Active user accounts purplemet-cli users list --query "status=ENABLED" # Discoveries that succeeded purplemet-cli discoveries list --query "status=DONE" # Internal domains purplemet-cli domains list --query "name~=internal" # Sensitive services purplemet-cli services list --query "sensitive=true" # Filtering already spans the whole account; --all just disables the # interactive pager and prints every match in one shot purplemet-cli issues list --all --query "severity=critical,status=OPEN" ``` **Tip:** unsure of the field names? Run the same command with `--json` once — the keys you see in the output are the keys you can filter on. The footer reports the filtered count, not the unfiltered total: ``` Showing 1-34 of 34 issue(s) — page 1 of 1 ``` ## Admin-only Commands The `users`, `sessions`, and `tokens` command groups require an ADMIN token. Calling them with an OPERATOR or READER token returns `403 API_METHOD_NOT_ALLOWED`. When invoked, these commands print a one-line warning to stderr. To suppress it (e.g. in CI pipelines that legitimately use an ADMIN token), pass `--no-admin-warning` or set `PURPLEMET_NO_ADMIN_WARNING=1`. ## .env File The CLI automatically loads a `.env` file from the current directory. Format: ```bash PURPLEMET_API_TOKEN=your-token-here PURPLEMET_BASE_URL=https://api.purplemet.com PURPLEMET_FAIL_SEVERITY=high PURPLEMET_FAIL_ON_EOL=true ``` Rules: - Lines starting with `#` are comments - Empty lines are ignored - Existing environment variables are **not** overwritten - The `.env` file should be in `.gitignore` > **Security:** The CLI warns if the `.env` file is readable by group or others. Set `chmod 600 .env`. ## Project Config File (`.purplemet.yml`) A project-scoped YAML config lives at the root of your repository. It is auto-discovered by walking up from the current directory until the CLI finds either the file or a `.git/` directory (the project boundary). Lookup order: 1. `--config ` flag (explicit; errors if missing) 2. `$PURPLEMET_CONFIG` env var 3. `.purplemet.yml` / `.purplemet.yaml`, searched upward from CWD up to the nearest `.git/` ```yaml # Default target for `analyze` — accepts a siteId (UUID) or URL. # `purplemet-cli analyze` can then run without any argument. site: https://example.com verbose: true # Per-project security gates fail_severity: high fail_rating: C fail_on_kev: true fail_on_headers: true exclude_tech: - php - java ``` **Security — no secrets in project config.** Because this file is meant to be committed alongside the code, secret fields (`token`, `basic_user`, `basic_pass`) are stripped at load time and a warning is emitted. Keep secrets in env vars or in the user-level config below. ## User Config File (`~/.purplemet/config.yml`) Optional YAML configuration file for persistent user-level settings. Unlike the project config, this file **can** contain the API token — it is meant to stay on the user's machine, outside version control. ```yaml # Authentication token: ppm_xxx base_url: https://api.purplemet.com # Timeouts poll_interval_ms: 10000 timeout_ms: 30000 wait_timeout_ms: 300000 verbose: true no_admin_warning: true # silence admin-only command warnings # Security gates fail_severity: high fail_rating: C fail_cvss: 9.0 fail_on_eol: true fail_on_ssl: true fail_on_cert: true fail_on_headers: true fail_on_cookies: true fail_on_unsafe: true fail_on_kev: true fail_on_epss: 0.75 fail_on_active_exploits: true fail_on_ossf_score: 5.0 fail_on_cert_expiry: 30 fail_on_issue_count: 50 require_waf: true fail_on_sensitive_services: true exclude_tech: - php - java ``` > Issues explicitly ignored via `purplemet-cli issues ignore` are always excluded from gate evaluation. > **Security:** The CLI warns if config file permissions are broader than `0600`. Set `chmod 600 ~/.purplemet/config.yml`. ## Severity Levels For `--fail-on-severity` / `PURPLEMET_FAIL_SEVERITY`: | Level | Weight | Blocks on | |-------|--------|-----------| | `critical` | 4 | Critical only | | `high` | 3 | High + Critical | | `medium` | 2 | Medium + High + Critical | | `low` | 1 | Low + Medium + High + Critical | | `info` | 0 | All issues | **Default in CI templates**: `high` (blocks on high and critical issues). ## Rating Scale For `--fail-on-rating` / `PURPLEMET_FAIL_RATING`, accepted values are `A`, `B`, `C`, `D`, `E`, `F` (ordered from best to worst — `A` is the safest, `F` the most critical). The rating is computed by the Purplemet platform; see the [official Purplemet documentation](https://cloud.purplemet.com/docs/#/web%20applications/security-rating) for the scoring rules. The gate triggers if the analysis rating is **at or worse** than the threshold. Example: `--fail-on-rating C` fails on C, D, E, and F.