Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,16 +297,25 @@ See [`sandbox_metrics.py`](examples/sandbox_metrics.py) · [reference](docs/api-

### Audit trail

Read what was run inside a sandbox, newest first: commands, terminal and SSH sessions,
and file and process operations. Each event says who made the call and how it ended.
Programs are recorded by name only, never with their arguments.
Read what was run inside a sandbox, newest first, as OCSF events: API calls (commands,
terminal and SSH sessions, file and process operations) with who made them, plus every
program the sandbox ran and every connection it opened, whether or not anything asked for
them. Programs are recorded by name only, never with their arguments.

```python
trail = sandbox.audit(operation="exec", status="failure")
trail = sandbox.audit(status="failure")
for e in trail.data:
print(e.time, e.api.operation, e.actor.user.uid, e.status)
if e.api is not None:
what = e.api.operation
else:
proc = e.process or e.actor.process
what = f"{e.class_name}: {proc.name if proc else '?'}"
print(e.time, what, e.status)
```

Program recording is on by default; turn it off with
`sandbox.update({"execution_trace": False})`.

Filter with `operation`, `status`, `actor_type`, `actor_uid` or `credential_uid`, and
page back with `cursor=trail.next_cursor`. Events take a few seconds to appear after a
call.
Expand Down
31 changes: 23 additions & 8 deletions docs/api-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -435,10 +435,11 @@ for series in metrics.series:

### `client.sandboxes.audit(id, *, from_=None, to=None, cursor=None, limit=None, operation=None, status=None, actor_type=None, actor_uid=None, credential_uid=None, org_id=None, project_id=None)`

Reads one page of the sandbox's audit trail, newest first: terminal commands, SSH,
process and file operations, each with who made the call and how it ended. Only the
program name is recorded, never its arguments, and input typed at a hidden password
prompt is not captured.
Reads one page of the sandbox's audit trail, newest first, as OCSF events: terminal
commands, SSH, process and file operations, each with who made the call and how it
ended, plus every program the sandbox ran and every connection it opened, unless its
`execution_trace` is off. Only the program name is recorded, never its arguments, and
input typed at a hidden password prompt is not captured.

`from_` / `to` (RFC3339) default to the last 24 hours. `limit` is clamped server-side.
The response's `window_truncated` is `True` when `from_` reaches past the
Expand All @@ -459,7 +460,8 @@ Filters, all optional:
trail = sandbox.audit(limit=50)
while True:
for e in trail.data:
print(e.time, e.api.operation, e.actor.user.name or e.actor.user.uid, e.status)
what = e.api.operation if e.api is not None else e.class_name
print(e.time, what, e.actor.user.name or e.actor.user.uid, e.status)
if not trail.next_cursor:
break
trail = sandbox.audit(limit=50, cursor=trail.next_cursor)
Expand Down Expand Up @@ -1911,15 +1913,28 @@ for `egress_add` / `egress_remove`.
| `next_cursor` | `str \| None` | no (pass as `cursor` for the next, older page) |
| `data` | `list[AuditEvent]` | yes (newest first) |

`AuditEvent` is one call, in the OCSF API Activity format. The fields most callers read:
`AuditEvent` is one OCSF 1.9.0 event. `class_uid` says which kind, and which fields
are present:

| `class_uid` | Kind | Carries |
| ----------- | ---- | ------- |
| 6003 | API Activity: a call to the sandbox | `api`, `src_endpoint`, `actor.user` |
| 1007 | Process Activity: a program started (`activity_id` 1, Launch), ended (2, Terminate) or the sandbox started (99) | `process`, `exit_code` on Terminate (or `status_detail` `signal N` if killed) |
| 4001 | Network Activity: a program opened a connection (`activity_id` 1, Open) | `dst_endpoint`, `actor.process` |

The fields most callers read:

| Field | Type | Required |
| ----- | ---- | -------- |
| `time` | `int` | yes (milliseconds since the Unix epoch) |
| `api.operation` | `str` | yes (e.g. `pty_command`, `ssh`, `exec`, `process.start`, `fs.read`) |
| `api.operation` | `str` | API Activity only (e.g. `pty_command`, `ssh`, `exec`, `process.start`, `fs.read`) |
| `process` | `AuditProcess \| None` | Process Activity only (`pid`, `name` without arguments, `cwd`, `file.path`, `parent_process.pid`) |
| `exit_code` | `int \| None` | no (what a program returned; on Terminate unless a signal stopped it) |
| `dst_endpoint` | `AuditDstEndpoint \| None` | Network Activity only (`ip` and `port`, or `name` as `unix:<path>`) |
| `actor.process` | `AuditProcess \| None` | Network Activity only (the program that connected) |
| `status` | `str` | yes (`Success` or `Failure`) |
| `status_code` | `str \| None` | no (`ok`, `permission_denied`, `internal`) |
| `actor.user.type` | `user` \| `api_key` \| `system` \| `unknown` | yes |
| `actor.user.type` | `user` \| `api_key` \| `system` \| `unknown` | yes (`unknown` on program and connection events) |
| `actor.user.uid` | `str` | yes (the user's or API key's id) |
| `actor.user.name` | `str \| None` | no |
| `actor.user.credential_uid` | `str \| None` | no (the API key used; absent for a user) |
Expand Down
8 changes: 6 additions & 2 deletions examples/agent_ports_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,12 @@ def main() -> None:
trail = agent.audit(limit=20)
for e in trail.data:
at = datetime.fromtimestamp(e.time / 1000, tz=timezone.utc)
who = e.actor.user.name or e.actor.user.uid
print(f"{at:%H:%M:%S} {e.api.operation:<14} {who} {e.status}")
if e.api is not None:
what = f"{e.api.operation} by {e.actor.user.name or e.actor.user.uid}"
else:
proc = e.process or e.actor.process
what = f"{e.class_name}: {proc.name if proc else '?'}"
print(f"{at:%H:%M:%S} {what} {e.status}")

agent.revoke_port(PORT)
except NeevAIError as e:
Expand Down
30 changes: 22 additions & 8 deletions examples/audit_trail.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""
Run a few commands in a sandbox, then read back its audit trail.

Each event is one call made inside the sandbox — terminal commands, SSH, process
and file operations — newest first, with who made it and how it ended. Only the
program name is recorded, never its arguments. Filter by ``operation``,
``status``, ``actor_type``, ``actor_uid`` or ``credential_uid``.
Events are OCSF, newest first: each call made to the sandbox — terminal commands,
SSH, process and file operations — with who made it, plus every program the
sandbox ran and every connection it opened, whether or not anything asked for
them. Only the program name is recorded, never its arguments. Filter by
``operation``, ``status``, ``actor_type``, ``actor_uid`` or ``credential_uid``.

Pages are read with ``cursor``: pass the previous page's ``next_cursor`` until it
is empty.
Expand All @@ -28,10 +29,23 @@
TEMPLATE = os.environ.get("NEEV_SANDBOX_TEMPLATE_ID", "sb-ubuntu-26-04-dev")


def describe(e: AuditEvent) -> str:
"""One line per event: the API call, or the program and what it did."""
if e.api is not None:
return f"{e.api.operation} by {e.actor.user.name or e.actor.user.uid}"
if e.dst_endpoint is not None:
d = e.dst_endpoint
dst = d.name or f"{d.ip}:{d.port}"
proc = e.actor.process.name if e.actor.process else "?"
return f"{proc} connect {dst}"
name = e.process.name if e.process else "?"
exit_code = f" exit {e.exit_code}" if e.exit_code is not None else ""
return f"{name} {e.activity_name.lower()}{exit_code}"


def show(e: AuditEvent) -> None:
at = datetime.fromtimestamp(e.time / 1000, tz=timezone.utc)
who = e.actor.user.name or e.actor.user.uid
print(f"{at:%H:%M:%S} {e.api.operation:<14} {who:<20} {e.status}")
print(f"{at:%H:%M:%S} {describe(e):<40} {e.status}")


def main() -> None:
Expand Down Expand Up @@ -63,8 +77,8 @@ def main() -> None:
break
trail = sandbox.audit(limit=20, cursor=trail.next_cursor)

# Only the commands that were run.
print("exec calls:")
# Exec calls and program launches.
print("exec:")
for e in sandbox.audit(operation="exec").data:
show(e)
except NeevAIError as e:
Expand Down
104 changes: 81 additions & 23 deletions specs/aiagent.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1904,7 +1904,7 @@ components:
in: query
required: false
description: >-
Only these calls (api.operation), one or several separated by commas, such as `exec,pty_command`.
Only these calls (api.operation), one or several separated by commas, such as `exec,pty_command`. A recorded program matches `exec` when it starts, `exit` when it ends and `connect` when it opens a connection.
schema: {type: string}
AuditStatus:
name: status
Expand Down Expand Up @@ -2075,6 +2075,12 @@ components:
type: object
required: [id, org_id, project_id, name, region, image, phase, created_at, updated_at]
properties:
execution_trace:
type: boolean
description: |
Whether this sandbox records every program that runs inside it, which
is on unless it was turned off. False means it records only the
operations you request.
id: {type: string, format: uuid}
org_id: {type: string}
project_id: {type: string}
Expand Down Expand Up @@ -2285,9 +2291,9 @@ components:
type: object
additionalProperties: false
description: |
Request to update a running sandbox's CPU/memory and/or egress rules.
At least one of `resources`, `egress`, `egress_add` or `egress_remove`
must be provided. `egress` replaces the policy in full; `egress_add` and
Request to update a running sandbox's CPU/memory, egress rules, or whether
it records what runs inside it. At least one of `resources`, `egress`,
`egress_add`, `egress_remove` or `execution_trace` must be provided. `egress` replaces the policy in full; `egress_add` and
`egress_remove` edit the allow-list in place and cannot be combined with it.
properties:
resources:
Expand All @@ -2304,6 +2310,14 @@ components:
New egress policy for the sandbox. Replaces the existing policy
in full and takes effect immediately for new connections — no sandbox
restart is required.
execution_trace:
type: boolean
description: |
Record every program that runs inside this sandbox, not only the
operations you request. On by default; false turns it off, and true
turns it back on. Takes effect on the running sandbox without a
restart. Recording costs a little processor time on each program
started.
egress_add:
allOf:
- $ref: '#/components/schemas/SandboxEgressRules'
Expand Down Expand Up @@ -2587,33 +2601,34 @@ components:
AuditEvent:
type: object
description: >-
One call to the sandbox, as an OCSF 1.9.0 API Activity event (class_uid 6003). Program names are recorded without their arguments, and input typed at a hidden password prompt is never recorded.
required: [class_uid, class_name, category_uid, category_name, activity_id, activity_name, type_uid, type_name, time, severity_id, severity, status_id, status, actor, api, cloud, src_endpoint, osint, metadata]
One thing that happened in the sandbox, as an OCSF 1.9.0 event: an API Activity event (class_uid 6003) for a call to the sandbox. Unless its `execution_trace` is turned off, a sandbox also records every program that runs inside it, whether or not anything requested it: a Process Activity event (1007) for a program starting or ending, and a Network Activity event (4001) for a connection a program opened. Program names are recorded without their arguments, and input typed at a hidden password prompt is never recorded.
required: [class_uid, class_name, category_uid, category_name, activity_id, activity_name, type_uid, type_name, time, severity_id, severity, status_id, status, actor, cloud, osint, metadata]
properties:
class_uid:
type: integer
enum: [6003]
x-enum-varnames: [AuditClassAPIActivity]
description: OCSF class, API Activity.
class_name: {type: string, description: Always API Activity.}
enum: [6003, 1007, 4001]
x-enum-varnames: [AuditClassAPIActivity, AuditClassProcessActivity, AuditClassNetworkActivity]
description: OCSF class, API Activity, Process Activity or Network Activity.
class_name: {type: string, description: "The name of class_uid, such as API Activity."}
category_uid:
type: integer
enum: [6]
x-enum-varnames: [AuditCategoryApplicationActivity]
description: OCSF category, Application Activity.
category_name: {type: string, description: Always Application Activity.}
enum: [6, 1, 4]
x-enum-varnames: [AuditCategoryApplicationActivity, AuditCategorySystemActivity, AuditCategoryNetworkActivity]
description: >-
OCSF category: Application Activity for an API call, System Activity for a program, Network Activity for a connection.
category_name: {type: string, description: "The name of category_uid, such as Application Activity."}
activity_id:
type: integer
enum: [1, 2, 3, 4, 99]
x-enum-varnames: [AuditActivityCreate, AuditActivityRead, AuditActivityUpdate, AuditActivityDelete, AuditActivityOther]
description: >-
1 Create (a write, a started process, a terminal), 2 Read, 3 Update (a move), 4 Delete (a removed file, a killed process), 99 Other.
activity_name: {type: string, description: "The name of activity_id, such as Create."}
Read with class_uid. For an API call: 1 Create (a write, a started process, a terminal), 2 Read, 3 Update (a move), 4 Delete (a removed file, a killed process), 99 Other. For a program: 1 Launch, 2 Terminate, 99 Other (the sandbox starting). For a connection: 1 Open.
activity_name: {type: string, description: "The name of activity_id within its class, such as Create or Launch."}
type_uid:
type: integer
format: int64
description: class_uid * 100 + activity_id, such as 600301.
type_name: {type: string, description: "Such as API Activity: Create."}
description: class_uid * 100 + activity_id, such as 600301 or 100701.
type_name: {type: string, description: "Such as API Activity: Create or Process Activity: Launch."}
time:
type: integer
format: int64
Expand Down Expand Up @@ -2641,9 +2656,15 @@ components:
status_detail:
type: string
description: >-
How a code-interpreter cell ended: `ok`, `error`, `deadline_exceeded`, `kernel_restarted`, `memory_exceeded`, `unavailable`, `invalid_response`, or `client_gone` when the caller disconnected first.
How a code-interpreter cell ended: `ok`, `error`, `deadline_exceeded`, `kernel_restarted`, `memory_exceeded`, `unavailable`, `invalid_response`, or `client_gone` when the caller disconnected first. On a program that was stopped rather than ending on its own, such as one out of memory or told to stop, `signal <n>` names the signal that stopped it.
actor: {$ref: '#/components/schemas/AuditActor'}
api: {$ref: '#/components/schemas/AuditAPI'}
process: {$ref: '#/components/schemas/AuditProcess'}
exit_code:
type: integer
description: >-
What a program returned when it ended. Zero means it succeeded. Present on a Process Activity Terminate event unless a signal stopped the program.
dst_endpoint: {$ref: '#/components/schemas/AuditDstEndpoint'}
resources:
type: array
description: >-
Expand All @@ -2653,12 +2674,48 @@ components:
src_endpoint: {$ref: '#/components/schemas/AuditEndpoint'}
osint:
type: array
description: Threat intelligence about the call. Always empty.
description: Threat intelligence about the event. Always empty.
items: {type: object}
metadata: {$ref: '#/components/schemas/AuditMetadata'}
AuditProcess:
type: object
description: >-
A program that ran inside the sandbox, as an OCSF process. On a Process Activity event it is the program that started or ended; on a Network Activity event it is actor.process, the program that opened the connection.
properties:
pid: {type: integer, description: Identifies the program within the sandbox.}
name:
type: string
description: >-
The program, without its arguments. A program can name itself freely, so file.path is the stronger field.
cwd: {type: string, description: The working directory the program ran in.}
file: {$ref: '#/components/schemas/AuditFile'}
parent_process: {$ref: '#/components/schemas/AuditParentProcess'}
AuditParentProcess:
type: object
description: >-
The process that started this one, so a run can be traced back to what caused it. Its own Launch event names it.
properties:
pid: {type: integer, description: Identifies the parent within the sandbox.}
AuditFile:
type: object
required: [path]
properties:
path:
type: string
description: The file that actually executed, after symlinks and interpreters were resolved.
AuditDstEndpoint:
type: object
description: >-
Where a program tried to connect, as OCSF dst_endpoint. It records the attempt and where it was aimed, not what was sent.
properties:
ip: {type: string, description: The address reached for.}
port: {type: integer, description: The port reached for.}
name:
type: string
description: A local socket, as `unix:<path>`, in place of ip and port.
AuditAPI:
type: object
description: The call made to the sandbox, as OCSF api.
description: The call made to the sandbox, as OCSF api. Present on API Activity only.
required: [operation]
properties:
operation:
Expand Down Expand Up @@ -2700,7 +2757,7 @@ components:
AuditEndpoint:
type: object
description: >-
Where the call came from. The caller's address is not recorded yet, so name is Platform for the platform's own calls and unknown otherwise.
Where the call came from. The caller's address is not recorded yet, so name is Platform for the platform's own calls and unknown otherwise. Present on API Activity only.
required: [name]
properties:
name: {type: string}
Expand All @@ -2724,11 +2781,12 @@ components:
AuditActor:
type: object
description: >-
Who made the call, as an OCSF actor. A console record from before actors were recorded names only its session, so its user is unknown.
Who made the call, as an OCSF actor. A console record from before actors were recorded names only its session, so its user is unknown. A program's event records no user, so its user is unknown too.
required: [user]
properties:
user: {$ref: '#/components/schemas/AuditActorUser'}
session: {$ref: '#/components/schemas/AuditActorSession'}
process: {$ref: '#/components/schemas/AuditProcess'}
AuditActorUser:
type: object
description: The principal that acted, as OCSF actor.user.
Expand Down
Loading
Loading