The gap between ECS and OSSEM is not conceptual — both describe the same security events — it is a hundred small naming and typing differences that break a query the moment it crosses from one to the other. This guide is the concrete field-by-field crosswalk and a bidirectional mapping module, as one precise technique within schema mapping comparisons and the broader SOC log architecture & taxonomy discipline.
Root-Cause Context
ECS and OSSEM were designed by different communities for different primary uses, so the same concept lands under different names and, occasionally, different structures. A user’s account name is user.name in ECS and SubjectUserName (or TargetUserName, context-dependent) in OSSEM; a source address is source.ip versus SrcIpAddr; a process command line happens to agree (process.command_line vs CommandLine in the raw event). Most differences are pure renames, but a few are genuine structural conflicts — where ECS nests a field that OSSEM keeps flat, or where an enumeration’s allowed values differ.
The practical failure is a hunt written against OSSEM field names silently returning nothing when run over ECS-normalized data, because SrcIpAddr simply does not exist there. A hard-coded, one-directional translation buried in a parser cannot serve both a SIEM and a hunt notebook. The fix is an explicit, versioned crosswalk that both directions consult, with the genuine conflicts handled by named policy rather than by whichever translation happened to run first.
Prerequisites
The crosswalk targets Python 3.11+ and uses only the standard library plus pydantic. The map is version-controlled data and plugs into the parent schema mapping comparisons layer.
pip install "pydantic>=2.6,<3.0"
Production-Ready Implementation
The crosswalk is one authoritative map; both directions derive from it, so ECS-to-OSSEM and OSSEM-to-ECS can never disagree. Conflicting enumerations are resolved by an explicit policy.
from __future__ import annotations
import logging
from typing import Optional
logger = logging.getLogger("soc.taxonomy.crosswalk")
# One authoritative ECS -> OSSEM crosswalk (version-controlled).
ECS_TO_OSSEM: dict[str, str] = {
"@timestamp": "TimeCreated",
"source.ip": "SrcIpAddr",
"destination.ip": "DstIpAddr",
"user.name": "SubjectUserName",
"process.command_line": "CommandLine",
"process.pid": "ProcessId",
"event.action": "EventID",
"network.direction": "Direction",
"host.name": "Hostname",
}
# Derived inverse — never hand-maintained, so the two cannot drift.
OSSEM_TO_ECS: dict[str, str] = {v: k for k, v in ECS_TO_OSSEM.items()}
# Enumeration conflict policy: ECS network.direction allowed values.
_ECS_DIRECTION = {"inbound", "outbound", "internal", "external", "unknown"}
def translate(fields: dict, to: str) -> tuple[dict, list[str]]:
"""Translate a field dict between 'ecs' and 'ossem'. Returns (out, unmapped)."""
table = ECS_TO_OSSEM if to == "ossem" else OSSEM_TO_ECS
out: dict = {}
unmapped: list[str] = []
for key, value in fields.items():
target = table.get(key)
if target is None:
unmapped.append(key) # ERR_MAP_201 candidate
out[key] = value # pass through, do not drop
continue
out[target] = value
return out, unmapped
def normalize_direction(value: str) -> str:
"""Resolve a direction value onto the ECS enumeration; never crash."""
v = value.strip().lower()
alias = {"in": "inbound", "ingress": "inbound",
"out": "outbound", "egress": "outbound"}
resolved = alias.get(v, v)
if resolved not in _ECS_DIRECTION:
logger.warning("ERR_MAP_203 unknown direction=%r -> unknown", value)
return "unknown"
return resolved
if __name__ == "__main__":
ecs_event = {"user.name": "jdoe", "source.ip": "10.0.0.9", "event.action": "4624"}
ossem, unmapped = translate(ecs_event, to="ossem")
assert ossem["SubjectUserName"] == "jdoe"
assert ossem["SrcIpAddr"] == "10.0.0.9"
back, _ = translate(ossem, to="ecs")
assert back["user.name"] == "jdoe" # round-trips exactly
Deriving OSSEM_TO_ECS as the inverse of one map is the crucial move: there is a single authoritative crosswalk, so the two translation directions cannot diverge the way two hand-maintained maps inevitably would. Unmapped fields pass through rather than being dropped, so a field the crosswalk does not yet know about is preserved for review, not lost.
Error-Code Reference
| Code | Meaning | Action |
|---|---|---|
ERR_MAP_201 |
Field absent from the crosswalk | Pass through and log; extend the crosswalk for required fields |
ERR_MAP_203 |
Enumeration value outside the ECS set | Default to unknown and count; extend the alias policy |
ERR_MAP_206 |
Round-trip translation lost a field | Investigate a rename collision in the crosswalk |
ERR_MAP_207 |
One ECS field maps to two OSSEM contexts | Apply the context rule (e.g. Subject vs Target user) explicitly |
Operational Notes
- Maintain one authoritative direction; derive the inverse. Never hand-maintain both ECS-to-OSSEM and OSSEM-to-ECS; invert one map so they cannot drift.
- Pass unmapped fields through, never drop them. A field the crosswalk lacks is a coverage gap to fix, not a value to discard — dropping it is a silent detection loss.
- Handle context-dependent OSSEM names explicitly.
SubjectUserNameversusTargetUserNamedepends on event semantics; encode that as a rule (ERR_MAP_207), not a guess. - Normalize enumerations, do not just rename them. Direction, action, and outcome values differ in allowed sets between the schemas; resolve them onto the canonical enumeration with a policy that defaults unknowns to a counted bucket.
Verification Checklist
FAQ
Are ECS and OSSEM differences mostly renames or real conflicts?
Overwhelmingly renames — the same concept under a different field name, which a crosswalk resolves cleanly. A minority are genuine conflicts: fields ECS nests that OSSEM keeps flat, context-dependent names like subject versus target user, and enumerations with different allowed values. Those few need explicit policy, but the bulk of the mapping is a mechanical, testable rename table that round-trips exactly.
Why derive the reverse map instead of writing both?
Because two independently maintained maps drift the instant someone updates one and forgets the other, and the drift is silent until a query returns wrong results. Deriving OSSEM_TO_ECS as the inverse of a single authoritative ECS_TO_OSSEM guarantees the two directions always agree, and reduces every future change to editing one map. It also makes round-trip tests trivially assert exactness.