Audit log
If you’re a security lead investigating a BLOCKED install — or an org admin asking “who invited that contractor?” — this is the page you open. It lives at /admin/ui/orgs/<slug>/audit and lists every row Upwarden writes to audit_log for your org: install decisions emitted by the proxy, membership and role changes, policy override edits, OAuth and passkey logins, domain-claim verifications, and CSV exports of the audit log itself.
The page is server-rendered. Filters submit as a GET form, so the URL is shareable — paste it into a ticket and your coworker lands on the same view. Pagination is cursor-based via ?cursor=<ISO>, so paging through a hot table doesn’t grind.
What you see when you land
Section titled “What you see when you land”A tier pill at the top names your tier (free, team, org, enterprise) and the window your tier permits — 7 days, 30 days, 1 year, or unbounded. If your tier doesn’t include CSV export, an inline banner reads: “Upgrade to team for 30-day retention, or org to export the audit log as CSV.” The banner disappears once you’re on Org or Enterprise.
Below that, a filter card. Below the filter card, the rows table. Below the table, a “Load more” link if there’s another cursor page.
The filter card
Section titled “The filter card”Every filter is a GET form field — the form submits to the same URL with the values in the query string. The filters are:
- Since / Until (
datetime-localinputs). Browsers visually clamp Since at the tier-allowedminso you can’t pick a date that would 400 the request. Server-side, the same check runs and is load-bearing — a direct URL hit with a too-oldsincereturns400 since predates the tier's allowed audit windowand a JSON body carrying{tier, maxWindowDays}. - Event (dropdown of known envelope events:
member_invited,role_changed,policy_override_loosened,passkey_registered,audit_exported, and the rest of theKNOWN_AUDIT_EVENTSlist). This is a substring match against the"event":"<name>"token inside the row’sdecisionReasonJSON. - Actor (free-text). Substring match against the envelope’s
by_user_idfield — paste either a full principal id or a fragment likestytch:to narrow. - Subject (free-text). Substring match against the row’s
package_namecolumn. Envelope rows use thepackage_nameslot to carry the subject id —member:<id>,policy:<slug>,blocklist:<slug>,audit:<slug>. Scan-decision rows carry the actual package name. - Decision (
SAFE/BLOCKED/QUARANTINED/ Any). - Exclude package decisions (checkbox). Hides every row with a non-null
client_family— i.e. all the proxy-traffic rows. Tick this when you’re asking “who edited which policy?” and don’t want the table buried in install decisions. - Actor identity, CI repository, CI ref, CI event name — multi-select dropdowns populated from the distinct values your org has actually emitted. Empty dropdowns render as a disabled “no data” summary; you’ll see them fill in once a row with that column shows up.
- Team — multi-select. Renders only when your org has at least one team. The
no-teamsentinel matches rows whose actor doesn’t bridge to any team via the member → team chain. - CI PR number — free-text, numeric. Must be a non-negative 32-bit integer;
42abcor1e5both reject withci_pr_number must be a non-negative integer.
A single Search button submits the form. A “Clear” link next to it strips every filter.
The rows table
Section titled “The rows table”Seven columns: When, Event, Subject, Actor, CI, Outcome, and a detail toggle. 50 rows per page.
The Actor cell does two things. Display: it resolves stytch:member-live-… strings to the bridged member email (via the tenant_members join) so you don’t read raw Stytch IDs in the common case. Click: it pivots the page to filter on the row’s raw actor_identity value — the JSON API filters on the column, not the resolved email, so the pivot lands on the row’s actual data. Team chips render next to the actor name when the actor bridges to one or more teams.
The CI cell is a <details> chip. Closed, it shows repository · ref · PR #N. Opened, it lists the row’s repo, ref, PR number, event name, commit (truncated to 12 chars), workflow, and run ID. Each value is a click-to-pivot link that filters the page to that value. Rows with no CI columns set show nothing in this cell — graceful degrade, no empty box.
The Outcome pill renders the row’s verdict: SAFE, BLOCKED, or QUARANTINED.
The row detail panel
Section titled “The row detail panel”Hitting Details on a row expands a panel that pretty-prints the envelope.
For known events, the panel renders labelled rows for each envelope field (member_invited shows email, role, invite_id; role_changed shows target_member_id, previous role, new role; policy_override_loosened shows policy_name, default_value, override_value, acknowledgement_text; and so on). Below that, a meta block always carries the row’s actor_tenant_id, client_ip, verdict_source, actor_identity, actor_teams, and any populated CI fields. If the envelope carries fields the dashboard doesn’t yet know about, they fall into a collapsible Extra fields section as raw JSON — so a pod parsing an envelope shape from a newer engine still surfaces the wire data.
For unknown events (free-text reasons that predate the envelope contract, or a brand-new event name the dashboard hasn’t been updated to label yet), the panel falls back to a JSON pretty-print of the full envelope, with the meta block above it.
Advisory subpanel
Section titled “Advisory subpanel”If the row is BLOCKED and the verdict came from an OSV or GHSA blocklist match, the detail panel includes an Advisory subpanel showing the advisory source (osv, ghsa), the advisory ID, the severity, and the human-readable summary. The engine attaches this via the join to blocklist; the dashboard doesn’t re-derive, so what you see is the same advisory shape the 403 returned to the install request. Cooldown blocks, scanner-driven blocks, and manual operator entries don’t carry an advisory and don’t render this subpanel.
Fingerprint subpanel
Section titled “Fingerprint subpanel”If the row is a BLOCKED install for a known transitive of a vulnerable build tool, the detail panel also includes a Fingerprint subpanel showing the tool name, the affected version range, the remediation text the engine attached to the 403, and any cross-referenced advisory IDs. Same data the install request received — so you can hand it to a developer asking “what do I do about this?” without rebuilding it.
CSV export
Section titled “CSV export”If your tier includes audit:export (Org or Enterprise), a second form below the filter card carries every active filter as hidden inputs and POSTs to /admin/ui/orgs/<slug>/audit/export.csv. The download mirrors what’s on screen — same filter narrowing, same window.
If you blow past the tier window on the export form (e.g. picked a since outside the cap), the export bounces back to the audit page with ?flash=window_exceeded rather than returning a partial CSV. Re-pick a valid window and re-submit.
On Free or Team tiers, the form doesn’t render. The upgrade banner names the threshold.
Quoting the tier-window error
Section titled “Quoting the tier-window error”If you script the JSON API directly and pick a since that predates your tier’s cap, you’ll see:
{ "error": "since predates the tier's allowed audit window", "tier": "team", "maxWindowDays": 30 }The dashboard’s date picker’s min attribute clamps since to the same boundary, but the server is the load-bearing check — a hand-crafted URL doesn’t escape it.
Per-actor filter banner
Section titled “Per-actor filter banner”When you set ?actor_identity=, the page renders a status banner above the table — “Showing audit for
When you’d touch this page
Section titled “When you’d touch this page”- A developer reports
[UPWARDEN] BLOCKED— filter bysubject(package name) anddecision=BLOCKED, open the detail panel for the row, read the advisory / fingerprint subpanels and the verdict source. - A new admin asks “did Alice accept her invite?” — filter
event=member_accepted, searchactorfor Alice’s email. - An incident review needs every policy override change in the last 30 days —
event=policy_override_loosened, set the Since to 30 days ago, export to CSV (Org+). - A CI pipeline is producing surprising decisions — paste the repo into CI repository, narrow with CI ref or CI event name, click any row’s CI chip to pivot further.
Related
Section titled “Related”- Querying the audit log — JSON-API query patterns that mirror this dashboard surface, including the
decisionReasonenvelope vocabulary. - Verdicts & the scanner — the model behind the
SAFE/BLOCKED/QUARANTINEDoutcomes the audit log records. - Troubleshooting — when an audit row points at a misconfiguration rather than a deliberate block.