NetStacksNetStacks

Audit Logs

Enterprise

Review the NetStacks Controller audit log: who did what, when, and from which IP. Filter by event type, resource, actor, and date range, and query the API.

Overview

The NetStacks Controller records security- and operations-relevant actions in an append-only audit_logs table in PostgreSQL. Every login (success and failure), logout, token refresh, user and role change, credential operation, credential-folder access grant, config deployment, device command execution, LLM provider change, session takeover, tunnel operation, and system backup/restore is written with full attribution — the event type, the acting user, the client IP address, a UTC timestamp, and a structured JSON payload describing what changed.

Audit logs serve three primary purposes:

  • Security monitoring — spot failed logins, credential password reveals, and session takeovers across the platform.
  • Compliance — provide a who/what/when record of administrative and operational actions for SOX, PCI-DSS, SOC 2, and HIPAA programs.
  • Troubleshooting — reconstruct the sequence of events that led to a problem, such as identifying who deployed a config to a device.
Controller-only feature

Audit logging lives in the NetStacks Controller (the self-hosted, multi-user backend). The audit log is reached at Audit Logs in the admin sidebar (route /audit). The endpoints sit under /api/admin/audit and require a valid Controller session (JWT). In the admin UI the Audit Logs item is shown to users with administrative access; there is no separate admin.audit permission key.

What this page does not cover

The Controller does not currently ship a built-in CSV/JSON export button, a SIEM/syslog/CEF forwarder, or a configurable retention policy. The audit data is exposed through the read API documented below; use that API to feed an external SIEM or archival job. Earlier drafts of this page described those features — they are not part of the shipped product.

How It Works

Event Schema

Each entry returned by the audit API has the following fields. Internally the row stores org_id, user_id, event_type, event_data, ip_address, user_agent, and created_at; the API reshapes that into the response below.

FieldTypeDescription
idUUIDUnique event identifier
event_typestringDotted action name, e.g. credential.password_revealed
actor_idUUID (nullable)User who performed the action; null for system/unauthenticated events
actor_emailstring (nullable)Email of the acting user, joined for display
resource_typestring (nullable)Derived from the part of event_type before the first dot (no dedicated column)
resource_idstring (nullable)Always null — not persisted as a separate column today
detailsJSON (nullable)Per-event payload (the stored event_data); shape varies by event type
ip_addressstring (nullable)Client IP of the actor, when available
created_attimestampWhen the event occurred (UTC)
resource_id is always null; resource_type is derived

The handler hard-codes resource_id to null and computes resource_type from the event_type prefix (the text before the first dot). The specific resource identifier (a device ID, credential ID, user ID, and so on) lives inside the details JSON, not in a top-level resource_id field.

Event Types

Event types follow a resource.action (sometimes resource.action.outcome) convention. The authoritative list for any given deployment is whatever has actually been written — query GET /api/admin/audit/event-types rather than relying on a hardcoded set. The event types emitted by the Controller include:

Event TypeDescription
auth.login.successSuccessful login; details.method is local, ldap, or oidc
auth.login.failedFailed login attempt (includes the attempted username and method)
auth.logoutUser logout
auth.refreshAccess token refreshed from a refresh token
auth.password_changeUser changed their password
auth.session_revokedA session/refresh token was revoked
user.createdNew user account created
user.updatedUser profile updated
user.deletedUser account deleted
user.role_assignedA role was assigned to a user
user.role_removedA role was removed from a user
user.roles_updatedA user's full role set was replaced
role.createdNew custom role created
role.updatedRole name or permissions changed
role.deletedCustom role deleted
credential.createdCredential added to the vault
credential.updatedCredential edited
credential.deletedCredential removed
credential.password_revealedA user revealed a credential secret (reason captured)
credential.internal_accessA credential was used internally by the platform
credential_folder.created / .updated / .deletedCredential folder lifecycle
credential_folder.access_granted / .access_revokedFolder access shared with or revoked from a user
device.exec_commandA command was run against a device over SSH
device.exec_command.failedA device command execution failed
session.share.created / .revokedA terminal session share was created or revoked
session.takeoverA shared session was taken over
session.terminatedA session was terminated
tunnel.created / .updated / .deletedTunnel definition lifecycle
tunnel.started / .stopped / .reconnectedTunnel runtime state changes
tunnel.start_all / .stop_allBulk tunnel start/stop
llm_provider.created / .updated / .deletedAI/LLM provider configuration changes
ai.ssh_execute / ai.ssh_execute.failedAI-assisted SSH execution
system.backup / system.restoreSystem backup created or restore performed
event-types is the source of truth

The exact set of event types present depends on what has happened in your deployment. Call GET /api/admin/audit/event-types to get the distinct list actually recorded — that is what populates the Event Type filter dropdown in the UI, so it never falls out of sync.

Pagination and Filtering

GET /api/admin/audit supports server-side pagination and filtering. The query parameters are:

  • page — 1-based page number (defaults to 1).
  • limit — page size, clamped to 1–100 (API default is 10; the admin UI requests 20).
  • event_type — exact event-type match, e.g. auth.login.failed.
  • resource_type — matches the prefix before the first dot (implemented as event_type LIKE '<resource_type>.%').
  • actor_id — the acting user's UUID.
  • from_date / to_date — RFC 3339 timestamps bounding created_at.

The response includes total (matching rows), page, limit, and total_pages for navigation.

Event Type Discovery

GET /api/admin/audit/event-types returns the distinct event types that have been recorded, so a client can build a filter dropdown without hardcoding the list.

Step-by-Step Guide

Viewing Recent Events

  1. Open the admin sidebar and click Audit Logs (route /audit).
  2. The default view lists the most recent events, newest first, 20 per page.
  3. Each row shows the time (relative, with the full timestamp on hover), the event type, the actor (email, or System when there is no actor), the derived resource type, and the IP address.
  4. Click View on a row to open the detail dialog, which shows the actor ID, IP address, resource type, and the full details JSON payload.

Filtering and Searching

  1. Click Filters to expand the filter bar.
  2. Event Type — select one event type (e.g. auth.login.failed to investigate failed logins). The dropdown is populated from /api/admin/audit/event-types.
  3. Resource Type — narrow to a resource family such as user, role, or credential (matches the event-type prefix).
  4. Actor — the field is labeled Actor Email, but it sends whatever you type as the actor_id query parameter, which matches on the actor's UUID. Paste a user's UUID (from Users in the admin area) for the filter to match.
  5. Date Range — set from/to dates to bound the time window.
  6. Use Clear all filters to reset.
Exporting and forwarding

There is no export button in the UI today. To export or forward audit data — for example into Splunk, Elastic, or a syslog/SIEM pipeline — pull it through GET /api/admin/audit on a schedule and transform it yourself. The Code Examples below show a paginated pull you can adapt into a cron job or collector.

Code Examples

Audit endpoints live under /api/admin/audit and require a valid Controller bearer token. Replace the host with your Controller URL.

Query Audit Logs with Filters

query-audit-logs.shbash
curl "https://netstacks.dc1.example.net/api/admin/audit?event_type=auth.login.failed&from_date=2026-03-01T00:00:00Z&to_date=2026-03-15T23:59:59Z&limit=50&page=1" \
  -H "Authorization: Bearer $TOKEN"

Response:

audit-response.jsonjson
{
  "data": [
    {
      "id": "e1f2a3b4-c5d6-7890-abcd-ef1234567890",
      "event_type": "auth.login.failed",
      "actor_id": null,
      "actor_email": null,
      "resource_type": "auth",
      "resource_id": null,
      "details": {
        "username": "admin",
        "method": "local"
      },
      "ip_address": "10.0.1.50",
      "created_at": "2026-03-15T14:30:22Z"
    },
    {
      "id": "f2a3b4c5-d6e7-8901-bcde-f23456789012",
      "event_type": "auth.login.failed",
      "actor_id": null,
      "actor_email": null,
      "resource_type": "auth",
      "resource_id": null,
      "details": {
        "username": "jsmith",
        "method": "ldap"
      },
      "ip_address": "192.168.10.25",
      "created_at": "2026-03-14T09:15:44Z"
    }
  ],
  "total": 23,
  "page": 1,
  "limit": 50,
  "total_pages": 1
}

Note that resource_type is just the auth prefix of the event type, and resource_id is always null — the attempted username lives in details.

List Available Event Types

event-types.shbash
curl https://netstacks.dc1.example.net/api/admin/audit/event-types \
  -H "Authorization: Bearer $TOKEN"

Response (a flat array of the distinct types recorded in your deployment):

event-types-response.jsonjson
[
  "auth.login.success",
  "auth.login.failed",
  "auth.logout",
  "auth.refresh",
  "auth.password_change",
  "credential.created",
  "credential.password_revealed",
  "device.exec_command",
  "role.created",
  "user.created",
  "user.role_assigned",
  "user.roles_updated"
]

Filter by Actor (UUID)

filter-by-actor.shbash
# Find all actions by a specific user. Get the UUID from the admin Users page.
curl "https://netstacks.dc1.example.net/api/admin/audit?actor_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890&limit=25" \
  -H "Authorization: Bearer $TOKEN"

Audit Entry: Credential Password Revealed

Revealing a stored secret is audit-gated and records the reason the user supplied:

credential-reveal-event.jsonjson
{
  "id": "a3b4c5d6-e7f8-9012-cdef-345678901234",
  "event_type": "credential.password_revealed",
  "actor_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "actor_email": "[email protected]",
  "resource_type": "credential",
  "resource_id": null,
  "details": {
    "credential_id": "c9d8e7f6-1234-5678-9abc-def012345678",
    "credential_name": "core-rtr enable secret",
    "reason": "emergency console access during P1"
  },
  "ip_address": "10.0.1.50",
  "created_at": "2026-03-15T14:30:00Z"
}

Audit Entry: Device Command Execution

device-exec-event.jsonjson
{
  "id": "b4c5d6e7-f890-1234-defa-456789012345",
  "event_type": "device.exec_command",
  "actor_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "actor_email": "[email protected]",
  "resource_type": "device",
  "resource_id": null,
  "details": {
    "device_id": "d1e2f3a4-5678-90ab-cdef-1234567890ab",
    "device_name": "core-rtr-01",
    "host": "10.0.0.1",
    "command": "show running-config | include bgp",
    "execution_time_ms": 842
  },
  "ip_address": "10.0.1.50",
  "created_at": "2026-03-15T14:30:00Z"
}

Paginated Pull for Archival / SIEM

Because there is no built-in exporter, fetch all pages and emit newline-delimited JSON that an external collector can ingest:

export-audit-ndjson.shbash
#!/usr/bin/env bash
# Pull every audit event since a cutoff and write NDJSON to stdout.
set -euo pipefail

HOST="https://netstacks.dc1.example.net"
FROM="2026-03-01T00:00:00Z"
PAGE=1
LIMIT=100

while :; do
  RESP=$(curl -fsS "$HOST/api/admin/audit?from_date=$FROM&limit=$LIMIT&page=$PAGE" \
    -H "Authorization: Bearer $TOKEN")

  echo "$RESP" | jq -c '.data[]'

  TOTAL_PAGES=$(echo "$RESP" | jq -r '.total_pages')
  if [ "$PAGE" -ge "$TOTAL_PAGES" ]; then
    break
  fi
  PAGE=$((PAGE + 1))
done

Python: Investigate Failed Logins

failed_logins.pypython
import os
import requests

HOST = "https://netstacks.dc1.example.net"
TOKEN = os.environ["TOKEN"]

resp = requests.get(
    f"{HOST}/api/admin/audit",
    params={
        "event_type": "auth.login.failed",
        "from_date": "2026-03-01T00:00:00Z",
        "limit": 100,
        "page": 1,
    },
    headers={"Authorization": f"Bearer {TOKEN}"},
    timeout=30,
)
resp.raise_for_status()
body = resp.json()

print(f"{body['total']} failed logins")
for event in body["data"]:
    details = event.get("details") or {}
    print(
        event["created_at"],
        event.get("ip_address", "-"),
        details.get("username", "?"),
        details.get("method", "?"),
    )

Questions & Answers

What events are captured in the audit log?
Authentication events (login success/failure, logout, token refresh, password change, session revoke), user and role management, credential operations (create/update/delete, password reveal, internal access), credential-folder lifecycle and access grants, device command execution, terminal session share/takeover/termination, tunnel lifecycle and runtime changes, LLM provider changes, AI-assisted SSH execution, and system backup/restore. Each event records the actor (when authenticated), the IP address, a UTC timestamp, and a structured details payload. The exact set in your deployment is discoverable via GET /api/admin/audit/event-types.
Is there an admin.audit permission?
No. The audit endpoints are protected by the standard authentication middleware (a valid Controller JWT) and are not gated by a dedicated admin.audit permission key. In the admin UI the Audit Logs sidebar item is presented to users with administrative access. The administrative permission keys that exist are users.manage, credentials.manage, and system.manage — see Roles & Permissions.
Can I export audit logs or forward them to a SIEM?
There is no built-in export button or SIEM/syslog/CEF forwarder in the current release. To get audit data out, page through GET /api/admin/audit and transform it yourself — the “Paginated Pull for Archival / SIEM” script above produces NDJSON suitable for a Splunk HEC uploader, an Elastic bulk import, or a custom syslog shipper.
How long are audit logs retained?
The Controller does not ship a configurable retention policy. Rows remain in the audit_logs table until you remove them. If you have a compliance retention requirement, manage it at the database layer (for example a scheduled delete or PostgreSQL partition drop) and/or archive events out through the API first.
Why is resource_id always null?
The audit response intentionally returns resource_id: null — the Controller does not store a dedicated resource-ID column. The specific identifier (device ID, credential ID, target user ID, role ID, and so on) is inside the details JSON. Likewise resource_type is derived from the event-type prefix, not stored separately.
How do I find who changed something on a device?
Filter by event_type=device.exec_command (or device.exec_command.failed) and read the details payload, which carries device_id, device_name, host, the command, and execution_time_ms. The actor_email field tells you who ran it. For configuration deployments, correlate with the deployment records described in Activity Monitor.
Can I see credential access history?
Yes. When a user reveals a stored secret, a credential.password_revealed event is written including the credential_id, credential_name, and the reason the user provided. Internal platform use of a credential is logged as credential.internal_access. Filter by these event types to audit who accessed which credentials and why.
How do I filter by user?
Use the actor_id query parameter with the user's UUID. The UI's actor field (labeled Actor Email) sends the same actor_idparameter, so paste the UUID there. Look up a user's UUID on the admin Users page.

Troubleshooting

Audit Logs Growing Too Large

  • There is no UI retention control, so manage growth at the PostgreSQL layer — for example a scheduled DELETE FROM audit_logs WHERE created_at < now() - interval '1 year' after you have archived what you need.
  • Use the paginated API pull (above) to offload events to external log storage (Splunk, Elastic, Loki) before deleting.
  • For very large tables, consider PostgreSQL range partitioning by created_at so old partitions can be dropped cheaply.

Missing Audit Events

  • Confirm you are authenticated — the endpoint returns 401 without a valid token, and no events are visible.
  • Check the date range filter; a narrow from_date/to_date window can exclude the events you want.
  • Check the event type filter and clear it to see the full log. Remember the modern names use dotted outcomes, e.g. auth.login.failed (not auth.login_failed).
  • Audit writes are non-blocking: a failed write logs a warning server-side rather than aborting the request, so in rare failure cases an event can be absent. Check the Controller logs for “audit log write failed” warnings.

Search Returns No Results

  • Widen the date range so you are not filtering out events by time.
  • Clear all filters and verify events exist at all.
  • Verify the event type spelling against GET /api/admin/audit/event-types — only types actually recorded appear there.
  • For actor searches, use the user's UUID (actor_id), not their email or username.

Resource Column Shows a Dash

  • The Resource column derives resource_type from the event-type prefix and shows the (always null) resource_id. A dash is expected — open the row detail and read the details JSON for the actual identifier.
  • User Management — user create/update/delete and role assignment actions are written as user.* audit events; look up actor UUIDs here.
  • Roles & Permissions — role changes generate role.* events; this is where the real permission keys (users.manage, system.manage) are defined.
  • Authentication (LDAP/OIDC) — login, logout, refresh, and failure events are core auth.* audit entries.
  • Activity Monitor — complements the audit log with live session and job activity.
  • Credential Vault — credential reveals and changes are recorded as credential.* events.
  • System Settings — backup/restore actions appear as system.backup / system.restore events.