Skip to content
Open
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
3 changes: 3 additions & 0 deletions docs/spec/trace/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,13 @@ The full mapping lives in [`trace/ocsf-mapping.json`](https://genai-security-pro
| `steps/subagentStart`, `steps/subagentStop` | Authentication | 3002 (logon/logoff for the subagent's session) |
| `steps/turnStart`, `steps/turnEnd` | Application Lifecycle | 6002 |
| Decision (deny/modify/ask/defer) | Detection Finding | 2004 |
| Decision (allow) | Application Lifecycle | 6002 |
| `agbom/snapshot`, `agbom/changed` | Device Inventory Info | 5001 (`activity_id` 2, Collect) |
| `steps/skillRegister` | Device Inventory Info | 5001 (`activity_id` 2, Collect: skill enters the component set) |
| `steps/sessionEnd` | Authentication | 3002 (logoff) |

An allow goes to Application Lifecycle (6002) with `severity_id` 1. A deployment MAY record allows as Detection Findings instead, for audit symmetry, and then every decision event rule below applies to them. A 6002 allow carries the decision's `unmapped.acs` fields as a decision event does, so `unmapped.acs.decision` holds the disposition and `unmapped.acs.metadata` holds the evaluator. It sets `metadata.correlation_uid` to the gated step's pair. `allow_class` in the mapping lists its required fields.

OCSF `severity_id` for decision events is set from the disposition:

| Disposition | `severity_id` |
Expand Down
1 change: 1 addition & 0 deletions docs/spec/trace/extend_ocsf.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ ACS events map to OCSF 1.9.0 event classes. The normative class assignments and
| `steps/skillLoad`, `steps/skillUnload` | Module Activity | 1005 (`activity_id` 1 load, 2 unload) |
| `steps/knowledgeRetrieval`, `steps/memoryStore`, `steps/memoryContextRetrieval`, `steps/preCompact`, `steps/postCompact` | Datastore Activity | 6005 |
| Decision (deny/modify/ask/defer) | Detection Finding | 2004 |
| Decision (allow) | Application Lifecycle | 6002 |
| `agbom/snapshot`, `agbom/changed` | Device Inventory Info | 5001 (`activity_id` 2, Collect) |
| `steps/skillRegister` | Device Inventory Info | 5001 (`activity_id` 2, Collect) |

Expand Down
16 changes: 15 additions & 1 deletion specification/v0.1.0/trace/ocsf-mapping.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,24 @@
},
"decision_class": {
"type": "object",
"description": "Decisions (deny/modify/ask/defer) are emitted as Detection Finding events. 'allow' is normally emitted as Application Activity with informational severity rather than as a finding, unless the deployment elects to record allows as findings for full audit symmetry. action_id and disposition_id record the Guardian's verdict. Only the Observed Agent sees the enforced outcome (Specification 搂6.4), so an action_id of 2 (Denied) or a disposition_id of 2 (Blocked) never proves the action was blocked. When any acs.decision.failure event in either party's stream carries the same session_id and request_id, a reader MUST NOT count the decision event as an enforced outcome. A missing failure event does not prove the decision was enforced. A Guardian SHOULD NOT set verdict_id or the finding's status_id, because both belong to whoever triages the finding.",
"description": "Decisions (deny/modify/ask/defer) are emitted as Detection Finding events. allow_class says where an allow goes. action_id and disposition_id record the Guardian's verdict. Only the Observed Agent sees the enforced outcome (Specification 搂6.4), so an action_id of 2 (Denied) or a disposition_id of 2 (Blocked) never proves the action was blocked. When any acs.decision.failure event in either party's stream carries the same session_id and request_id, a reader MUST NOT count the decision event as an enforced outcome. A missing failure event does not prove the decision was enforced. A Guardian SHOULD NOT set verdict_id or the finding's status_id, because both belong to whoever triages the finding.",
"properties": {
"class_uid": { "type": "integer", "const": 2004 },
"class_name": { "type": "string", "const": "Detection Finding" },
"allow_class": {
"type": "object",
"description": "Where an allow decision goes. An allow is emitted as Application Lifecycle (6002) with severity_id 1, unless the deployment elects to record allows as Detection Findings for audit symmetry, in which case every rule for a decision event applies to it. A 6002 allow carries the decision's join_fields and acs_fields under unmapped.acs, as a decision event does, so unmapped.acs.decision holds the disposition and unmapped.acs.metadata the evaluator, and it sets metadata.correlation_uid to the gated step's pair. Its activity_id is open with the other 6002 rows.",
"properties": {
"class_uid": { "type": "integer", "const": 6002 },
"class_name": { "type": "string", "const": "Application Lifecycle" },
"required_fields": {
"type": "array",
"items": { "type": "string" },
"description": "Fields every allow event emitted as Application Lifecycle carries. The rest of acs_fields ride under unmapped.acs whenever the response carries them.",
"default": ["severity_id", "metadata.correlation_uid", "unmapped.acs.request_id", "unmapped.acs.session_id", "unmapped.acs.decision"]
}
}
},
"required_fields": {
"type": "array",
"items": { "type": "string" },
Expand Down
19 changes: 19 additions & 0 deletions tests/test_trace_mapping_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,25 @@ def test_ocsf_decision_codes_match_the_answered_proposal():
assert set(decision["late_decision"]["properties"]["failure_fields"]["default"]) <= failure_fields


def test_ocsf_allow_class_is_structured_and_its_fields_exist():
# Issue #198: the allow rule lived only in decision_class.description, so
# nothing held its class or said where the disposition goes. Its class
# name must match the step_to_class caption for 6002, and every
# unmapped.acs field it requires must be one a decision event carries.
ocsf = json.loads((TRACE / "ocsf-mapping.json").read_text(encoding="utf-8"))["properties"]
decision = ocsf["decision_class"]["properties"]
allow = decision["allow_class"]["properties"]
assert allow["class_uid"]["const"] == 6002
captions = {entry["class_name"] for entry in ocsf["step_to_class"]["default"].values() if entry["class_uid"] == 6002}
assert captions == {allow["class_name"]["const"]}
carried = set(decision["join_fields"]["default"]) | set(decision["acs_fields"]["default"])
required = allow["required_fields"]["default"]
assert "unmapped.acs.decision" in required
unknown = [field for field in required if field.startswith("unmapped.acs.") and field.removeprefix("unmapped.acs.") not in carried]
assert not unknown, f"allow_class requires unmapped.acs fields no decision event carries: {unknown}"
assert ocsf["severity_mapping"]["default"]["allow"] == 1


def test_padded_source_fields_reject_the_empty_string():
# Trace pads the policy and provenance arrays with "" where a value is
# absent, so "" must never be a legal wire value for these fields (#263).
Expand Down
Loading