Security audit events answer:
who or what attempted which action, against which resource,
with what result, from which trusted context, and when?
GraphJSON can help search, aggregate, dashboard, and alert on a minimized copy of these events. It should not be the only authoritative security ledger when your audit, legal, or compliance requirements demand immutability, guaranteed retention, restricted evidence access, or cryptographic verification.
Recommended architecture:
security-sensitive operation
↓
authoritative append-only audit record
├── restricted evidence store
└── minimized analytical copy → GraphJSON
Use OWASP’s logging guidance as a threat-modeling reference, then adapt the vocabulary to your application.
Separate audit events from application logs#
Application logs explain system behavior:
database_timeout
request_completed
job_retry_scheduled
Security audit events record actions and decisions:
login_challenged
access_denied
role_changed
api_key_rotated
data_export_requested
Use a dedicated security_audit_events collection with deliberate access,
retention, and volume. Do not mix it with debug logs whose owners or lifecycle
differ.
Use one stable audit contract#
{
"event": "security_audit_event",
"event_id": "audit_01J...",
"occurred_at": 1785081600,
"action": "report.export",
"outcome": "denied",
"reason_code": "missing_permission",
"actor_type": "user",
"actor_id": "usr_42",
"subject_id": "usr_42",
"account_id": "acct_7",
"resource_type": "report",
"resource_id": "rpt_91",
"authentication_method": "session_mfa",
"authorization_policy": "report-export-v3",
"request_id": "req_01J...",
"source_service": "reporting-api",
"environment": "production",
"schema_version": 2
}
Send occurred_at as the GraphJSON request timestamp. Keep ingestion time
separate for delivery-lag monitoring.
Model actor, subject, and initiator#
These roles can differ:
| Role | Meaning |
|---|---|
| Actor | Identity whose authority was evaluated |
| Subject | Person or account affected by the action |
| Initiator | System, support agent, API client, or delegated process that began it |
| Resource | Object or capability acted upon |
For a support agent acting on behalf of a customer:
{
"actor_type": "support_agent",
"actor_id": "staff_12",
"subject_id": "usr_42",
"account_id": "acct_7",
"action": "account.session.revoked",
"reason_code": "verified_support_request"
}
Do not write the customer as the actor merely because their account was affected. Delegation and impersonation must remain visible.
For service actions, use a registered service identity rather than a human owner’s user ID.
Name actions as stable capabilities#
Use:
resource.action
Examples:
session.created
session.revoked
member.invited
member.role_changed
api_key.rotated
dashboard.shared
report.exported
customer_data.deleted
integration.connected
Do not use UI labels such as clicked_red_button. The same capability can be
invoked through a browser, API, support tool, or background service.
Keep the vocabulary bounded and reviewed. Put variable object IDs in resource fields, not in the action name.
Record outcomes and reasons#
Recommended outcomes:
allowed
denied
failed
challenged
cancelled
denied means policy rejected the action. failed means it was authorized but
could not complete. That distinction matters during an investigation.
Use controlled reason codes:
missing_permission
tenant_mismatch
mfa_required
invalid_credential
resource_not_found
rate_limited
policy_error
Do not store a raw exception, policy expression, or user-visible error message as the reason. It can leak sensitive detail and create unbounded categories.
Log the decision at the authoritative boundary#
Emit the audit fact after the authorization decision and, for allowed actions, after the intended state transition commits.
request received
→ authenticate
→ authorize
→ commit state change
→ append authoritative audit record
→ deliver analytical copy
For a denied request, record the denial after the policy engine returns. For a partially failed operation, record the actual outcome; do not log success when only the button click occurred.
High-value audit facts should use a transactional outbox or another durable mechanism. A best-effort request emitted after returning to the user can be lost during a crash.
Protect the event itself#
Never include:
- passwords or password hashes
- API keys, access tokens, cookies, or reset links
- one-time passcodes
- authorization headers
- raw request or response bodies
- full exported documents
- payment instruments
- private keys or signing material
IP addresses, device signals, emails, user-agent strings, and location can be personal data. Collect only what the investigation and policy require, apply access controls, and define retention.
Prefer opaque internal IDs. Resolve display names through an authorized application surface rather than copying them into every event.
Treat client context as untrusted#
The browser can provide a request ID or UI surface, but the server must derive:
- authenticated actor
- account or tenant
- authorization outcome
- policy version
- resource scope
- source service
Never accept actor_id, role, or outcome from a client payload and forward
it as a trusted audit fact.
Preserve evidence integrity#
For an authoritative ledger, consider:
- append-only write permissions
- separate operator and auditor roles
- immutable or retention-locked storage
- sequence numbers or hash chaining
- signed segment manifests
- independent backups
- monitored export and deletion access
GraphJSON’s analytical copy can carry:
{
"ledger_sequence": 982014,
"ledger_segment": "audit-2026-07-26-09",
"record_digest": "sha256:..."
}
Do not claim that these fields make GraphJSON itself tamper-proof. Verification must occur against the authoritative evidence store.
Query denied actions#
SELECT
JSONExtractString(json, 'action') AS action,
JSONExtractString(json, 'reason_code') AS reason,
JSONExtractString(json, 'source_service') AS service,
count() AS denials,
uniqExact(JSONExtractString(json, 'actor_id')) AS actors
FROM security_audit_events
WHERE
JSONExtractString(json, 'outcome') = 'denied'
AND timestamp >= toUnixTimestamp(now() - INTERVAL 24 HOUR)
GROUP BY action, reason, service
ORDER BY denials DESC
High denial volume can be an attack, a broken client, or a policy deployment. Investigate release and policy versions before assigning intent.
Detect suspicious patterns carefully#
Useful reviewed signals:
- many denied actions for one actor across resources
- one source attempting access across many accounts
- API-key rotation followed by unusual export activity
- disabled actor continuing to generate allowed actions
- role elevation followed by sensitive changes
- audit delivery lag or missing sequence ranges
Avoid a universal “risk score” built from unexplained weights. Preserve the facts and reason codes that allow an investigator to understand the signal.
For a native threshold alert, create a supported collection visualization over a bounded numeric signal. Keep urgent security paging, deduplication, acknowledgment, and escalation in the security monitoring system that owns the response.
Build an investigation view#
Include:
- UTC occurrence time
- actor, subject, account, and resource
- action, outcome, and reason
- authentication method and authorization policy version
- source service, environment, and release
- request or trace correlation
- ledger sequence or evidence reference
Authorize the view on the server. A shared dashboard or embed URL is not a substitute for investigator access control.
Record audit-log searches and exports as security-sensitive actions too.
Retention and deletion#
Audit requirements can conflict with privacy deletion or minimum-retention obligations. Define:
- legal basis and approved purpose
- fields retained
- analytical-copy retention
- evidence-ledger retention
- access and export policy
- deletion or pseudonymization behavior
- exception and hold process
Do not promise indefinite retention based on a dashboard setting alone. Review Export, delete, and move your data and coordinate every authoritative system.
Validate the trail#
Test:
| Scenario | Expected audit result |
|---|---|
| Allowed administrative change | One committed allowed event |
| Missing permission | One denied event with policy reason |
| State commit fails | Failed, not allowed |
| Support delegation | Staff actor and customer subject remain distinct |
| Delivery retries | Stable event ID; no false action count |
| Client spoofs actor | Server-derived actor wins |
| Evidence consumer stops | Independent lag or gap signal fires |
Reconcile event IDs and sequence ranges against the authoritative ledger on a regular schedule.
Launch checklist#
- Audit events use a dedicated collection and access policy.
- Actor, subject, initiator, and resource semantics are distinct.
- Actions and reasons use controlled vocabularies.
- Trusted identity and outcome are derived on the server.
- High-value facts have durable delivery.
- Secrets and raw payloads are prohibited.
- Personal context has an approved purpose and retention.
- The authoritative evidence store is named explicitly.
- Investigations and exports are themselves audited.
- Delivery gaps reconcile independently of GraphJSON.
For rate-limit, authentication challenge, abuse, and risk-decision quality, continue with Abuse and risk-signal analytics.
For product and policy questions about grants, revocations, denied actions, entitlement utilization, unused assignments, and policy-version impact, use Permissions, roles, and entitlement analytics.