The audit log records every authentication event, finding-status change, secret operation, webhook delivery, admin action, and authenticated API call within a workspace.
The record is append-only from the application: no console route, API route or background job updates or deletes a row once written, and the API exposes reads only. Write statements against the table are additionally logged at the database layer by pgaudit.
It is not, today, tamper-evident in the cryptographic sense. There is no hash chain over the rows, and the database role the writer uses retains UPDATE and DELETE. An operator with database or host access can therefore alter history without leaving a signature that would prove it. Treat the log as a reliable record of what the platform did, not as evidence against someone who controls the platform.
GET /api/v2/audit
curl -sS "https://penaxtra.com/api/v2/audit?action=finding.status_changed&from=2026-04-01T00:00:00Z" \
-H "Authorization: Bearer $TOKEN"
Required scope: audit:read. Result set is tenant-scoped; newest first; capped per the limit parameter (default 50, max 200).
Query parameters
| Param | Type | Notes |
|---|---|---|
action | string | Action slug. Must match ^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*){1,2}$ (e.g. auth.login_success, finding.status_changed, api.call). |
target_kind | string | Short slug, e.g. finding, endpoint, scan, api_token. |
result | enum | success or failure. |
from | ISO 8601 | Inclusive lower bound on ts. Must include zone (Z or +HH:MM). |
to | ISO 8601 | Exclusive upper bound on ts. |
limit | integer | 1 to 200; default 50. |
Filtering by actor_user_id, target_id, and cursor pagination beyond limit are planned.
Response
{
"data": [
{
"id": 42891,
"ts": "2026-05-22T10:24:33Z",
"actor_user_id": "<user uuid>",
"actor_ip": "203.0.113.42",
"action": "finding.status_changed",
"target_kind": "finding",
"target_id": "<uuid>",
"before_jsonb": { "status": "open" },
"after_jsonb": { "status": "triaged" },
"result": "success",
"request_id": "<uuid>"
}
]
}
tenant_id is not echoed back in the row payload since every result is already scoped to the calling token's tenant by RLS; surfacing it again would be redundant noise.
Errors
| Code | Body | Reason |
|---|---|---|
400 | {"error":"invalid_action_filter"} | action does not match the slug regex or exceeds 64 chars. |
400 | {"error":"invalid_target_kind"} | target_kind is not a short lowercase slug. |
400 | {"error":"invalid_result"} | result is not success or failure. |
400 | {"error":"invalid_from"} / {"error":"invalid_to"} | Timestamp is not ISO 8601 with explicit zone. |
403 | {"error":"scope_missing"} | Token lacks the audit:read scope. |
Stream events (planned)
A server-sent-events endpoint for tail-following the audit log is planned. Until then, poll with from=<last-seen-ts> at a conservative cadence (no more than once every five seconds).
Security notes
- Append-only at the database level; the underlying table has no UPDATE or DELETE grant for any application role.
- A tamper-evident database channel mirrors every write at the storage layer, so even an actor with database-administrator access leaves a second-source trail.
- Per-tenant audit retention is configurable from one day to ten years; older events are pruned automatically.
- The
request_idfield correlates with the same id surfaced in webhook deliveries and in the platform request log; use it to stitch a single customer request across multiple subsystems. - Free-text filter parameters are regex-whitelisted at the API boundary in addition to PDO parameter binding, so the audit log itself cannot become an injection vector even with a leaked token.
Related
Last reviewed: 2026-08-05. Reviewed by: Engineering. Content type: Developer documentation. Reach the maintainers: [email protected] .