# PPS Quarantine Manager (PoC) A small web client for the Proofpoint Protection Server (PPS) quarantine. It lists quarantined mail by folder and runs bulk actions — **Release**, **Report & Release**, **Delete**, **Move** — as **asynchronous background jobs**, so the operator never waits on slow PPS API calls (deletes in particular). > Proof of concept: static login, plaintext secrets in a config file, no permission model. > SAML/OIDC and RBAC come later. ## How it works - **Frontend** (`templates/`, `static/`): one page. A folder switcher scopes the whole view to one quarantine folder; the list shows date / sender / recipient / subject with a checkbox per row. Clicking a row opens the message content in an overlay over the list (HTML parts render in a sandboxed iframe). Actions apply to all checked rows, which then disappear from the list immediately. - **Backend** (`app.py`, `pps_client.py`): Flask serves the UI and proxies the PPS Quarantine Search REST API. Actions are queued and return instantly (HTTP 202). - **Worker** (`worker.py`): a daemon thread drains a SQLite-backed job queue, batching message ids into chunked PPS POSTs. Jobs are persistent, so queued work survives a restart; a job left mid-flight is requeued on startup. ## Action semantics | UI action | PPS API call(s) | |------------------|---------------------------------------------------------------------------------| | Release | `release` without rescan; `deletedfolder` set so it leaves the folder | | Report & Release | `release` (in place, no rescan) **then** `move` a copy to `report_release_folder` | | Delete | `delete` with `deletedfolder` (moves to deleted items, not a hard delete) | | Move | `move` to the folder chosen in the dropdown | **Report & Release** keeps a copy in `report_release_folder` (default `"Debugging - Josef"`) for manual false-positive submission while the mail is delivered. If this PPS deployment's `release` relocates the message to the deleted folder (like the admin GUI does), the worker re-finds it there by its stable `guid` and moves it from there — no configuration needed. ## Operations log (audit trail) Every message the background worker acts on is recorded in a dedicated, rotating log file (`worker_log`, default `worker.log` — 5 MB × 5 files). One line per message, e.g.: ``` 2026-07-14 17:39:56 job=#12 action=delete result=ok src='Quarantine' dst='Deleted' localguid=6:6:1 guid=g-aaa from='sender@spam.example' rcpt='victim@corp.com' subject='You won a prize' ``` `result` is `ok` or `FAILED` (failures also carry `error=...`). Fields are `key=value` for easy `grep`. This is separate from the general application log (stdout, controlled by `log_level`), which covers requests, job lifecycle, and PPS API calls. ## Setup ```bash python3 -m venv venv venv/bin/pip install -r requirements.txt cp config.example.toml config.toml # edit config.toml: PPS host, API credentials, folders, login venv/bin/python app.py ``` Open http://127.0.0.1:8080 and sign in with the `[auth]` credentials from `config.toml`. ## Configuration notes - **Folders are maintained locally.** The PPS API has no endpoint to list folders, so the `folders` array in `config.toml` is the source of truth for the switcher, the Move dropdown, and the delete/report targets. Names must match PPS **exactly** (case- and space-sensitive, e.g. `"Debugging - Josef"`); a wrong name only errors at action time. - **`list_query` = `"from=*"`.** The search API requires a `from`/`rcpt`/`subject` filter — it can't list a folder by folder alone. A bare wildcard means "everything in the folder". If your PPS doesn't treat `from=*` as match-all, set it to something like `rcpt=@yourdomain.com`. - **`default_days_back`.** The search API alone returns only the last 24h; this widens the window (uses `startdate`). - **Limits.** Up to 1000 messages per search (API cap, no pagination). `default_limit` sets the UI default; the operator can raise it to 1000. - **`verify_tls`.** PPS admin certs are often self-signed; `false` skips verification (PoC). Point it at a CA bundle path to verify instead. - **Mutual TLS (client certificate).** If PPS is fronted by nginx requiring a client cert, requests without one fail with `400 No required SSL certificate was sent`. Set `client_cert` (a combined cert+key PEM, or a cert PEM plus `client_key` for the key). Note this is a **TLS client cert**, separate from the `[pps]` Basic-auth credentials — the endpoint may require both. ## API credentials The account in `[pps]` must be a PPS admin with an **API Role** that has the Quarantine module enabled (see the PPS management interface / a Proofpoint support ticket for PoD). Auth is HTTP Basic against the admin port (default 10000). ## Files ``` app.py Flask app: routes, login, config, worker startup pps_client.py PPS Quarantine REST client (search / get_raw / act) worker.py SQLite job queue + background worker thread config.example.toml Config template (copy to config.toml) templates/ login.html, index.html static/ app.js, style.css ```