ocsf-emitter
Construct, validate, and emit OCSF 1.5.0 events with a consistent shape and mandatory runtime validation. Covers all 53 base OCSF classes across all eight categories (System Activity, Findings, Identity & Access Management, Network Activity, Discovery, Application Activity, Remediation, Unmanned Systems) — each with a typed builder.
This is an internal library. Other services import it to turn their own
detection signals into valid OCSF events; the library owns the OCSF field
names, the schema-version pin, the house defaults, and validation. Transport
is deliberately out of scope -- emit() returns a validated,
JSON-serializable payload and the caller ships it however it likes.
Each class has a builder (build_detection_finding, build_authentication,
build_file_hosting, …) sharing a common keyword shape; emit()/validate()
accept any supported class. See the usage guide for the full
class table and examples.
Install
uv pip install -e . # runtime: pydantic only
uv pip install -e ".[codegen]" # + tools to regenerate the OCSF models
uv sync # dev + docs groups (mypy, pytest, ruff, jsonschema, mkdocs)
Usage
import ocsf_emitter
from ocsf_emitter import (
build_detection_finding, emit,
Severity, Status, Activity, Confidence, RiskLevel,
Observable, ObservableType, MitreAttack,
)
# Configure the emitting product once at startup (see "Product identity").
ocsf_emitter.configure_product(name="Example Detector", vendor_name="Example, Inc.")
finding = build_detection_finding(
uid="det-2026-0715-001", # stable id -> finding_info.uid
title="Impossible-travel login",
severity=Severity.HIGH, # our enum -> OCSF severity_id
message="User alice logged in from two continents within 4 minutes.",
status=Status.NEW, # -> status_id
activity=Activity.CREATE, # -> activity_id + type_uid
observables=[
Observable(ObservableType.USER_NAME, "alice"),
Observable(ObservableType.IP_ADDRESS, "203.0.113.7"),
],
confidence=Confidence.HIGH, # -> confidence_id
risk_level=RiskLevel.HIGH, # -> risk_level_id
attacks=[MitreAttack("T1078", "Valid Accounts", "TA0001", "Initial Access")],
)
payload = emit(finding) # validates, returns dict; raises InvalidFindingError if invalid
build_detection_finding(...) returns a typed, already-valid
DetectionFinding model instance. emit(...) runs full validation and returns
a dict; emit_json(...) returns a JSON string. build_from_signal(signal)
takes a DetectionSignal dataclass if you'd rather build the domain object
yourself.
See tests/golden_detection_finding.json
for a full sample payload you can eyeball against the OCSF detection_finding
spec.
How validation behaves
Validation is mandatory and automatic inside emit()/emit_json(), and is
also callable standalone via validate(finding). It does two things:
- Schema validation -- re-runs Pydantic validation over the finding's current field values (catching any mutation after construction).
- OCSF invariant checks -- looks the event's class up in the class registry
by its
class_uidand verifiescategory_uid, theclass_name/category_namesiblings,type_uid == class_uid*100 + activity_id, and thatmetadata.versionmatches the pinned schema version.
On failure it raises InvalidFindingError, whose message and .field_errors
list name the offending field(s):
ocsf_emitter.errors.InvalidFindingError: Detection finding failed OCSF schema validation
- severity_id: Input should be 0, 1, 2, 3, 4, 5, 6 or 99
Model layer: chosen path and rationale
We generate a full Pydantic v2 model tree from OCSF's JSON Schema using
datamodel-code-generator,
and commit the result to
src/ocsf_emitter/_models.py. At runtime the
package depends only on pydantic -- no network access, no code generation.
Why this rather than py-ocsf-models? The task allowed either; we chose
schema-generation for two reasons:
- Exact fidelity to a pinned OCSF version. The models come straight from the OCSF schema for the exact version we pin -- no dependency on a third party's release cadence for class coverage.
- Self-contained and auditable. The generated module is committed and reviewable, and regeneration is a single script.
How the pinned version is generated
We pin an exact OCSF version and generate models from its metaschema (the
JSON Schema HTTP endpoint only serves the latest deployed version, so we don't
use it for codegen). scripts/gen_models.py:
- Fetches the pinned version's metaschema with
ocsf-lib(OcsfApiClient().get_schema("1.5.0")) -- this works for any version. - Converts that metaschema (every class in
ROOT_CLASSESplus the union of their transitive object closures) into a self-contained draft JSON Schema, where each root class and shared object is a$defso codegen emits one Pydantic model each. Attributes tagged with an OCSFprofile(e.g.cloud,osint) are dropped so we get the base classes -- mirroring the schema server's?profiles=selector, and keeping profile-only fields out of the required list. - Feeds that JSON Schema to
datamodel-code-generator-> Pydantic v2.
Bumping the OCSF schema version
-
Regenerate the models for the desired version:
uv run --extra codegen python scripts/gen_models.py 1.5.0
-
Update the one-line pin in
src/ocsf_emitter/defaults.pyto match (keepDEFAULT_VERSIONinscripts/gen_models.pyin sync):OCSF_SCHEMA_VERSION = "1.5.0" # <- change this
-
Run the suite and review the golden diff:
uv run pytest && uv run mypy && uv run ruff check .
If the emitted shape changed intentionally, regenerate
tests/golden_detection_finding.jsonand review the diff. Note that a newer version may reintroduceRootModel/union wrappers on some objects, which would require builder adjustments.
OCSF schema conformance
Beyond the library's own runtime validate() (Pydantic + registry invariants),
CI validates one emitted event per supported class against a JSON Schema
built from the OCSF metaschema (via ocsf-lib) for the pinned version -- the
same authoritative source the models are generated from. This is a blocking
job (.github/workflows/ci.yml) and is self-contained
-- no third-party validator. Run it locally:
OCSF_SCHEMA_VALIDATION=1 uv run pytest tests/test_integ_ocsf_schema.py -v
Without OCSF_SCHEMA_VALIDATION=1 the test is skipped (it fetches the metaschema
over the network).
Product identity
The emitting product is configurable, not hardcoded, so any service can use this library. Set it once at startup:
ocsf_emitter.configure_product(name="My Service", vendor_name="My Org")
or pass product=ocsf_emitter.make_product(...) per call. Building a finding
with no product configured raises OcsfEmitterError rather than emitting an
unattributed finding.
Package layout
src/ocsf_emitter/
__init__.py public API: build_* per class, emit, validate, OcsfClass, refs, ...
domain.py our domain input shapes: DetectionSignal, Observable, *Ref, enums
builders.py domain refs -> OCSF models; one typed build_* per class
defaults.py schema-version pin, metadata/product, severity/status/... mappings, registry
validate.py runtime validation; raises InvalidFindingError; SupportedEvent union
emit.py serialize to JSON dict/str (transport-agnostic)
errors.py OcsfEmitterError, InvalidFindingError
_models.py GENERATED OCSF Pydantic models (do not edit by hand)
_catalog.py GENERATED class catalog: OcsfClass, registry, per-class *Action enums
scripts/gen_models.py regenerate _models.py + _catalog.py (metaschema -> JSON Schema -> Pydantic)
tests/examples.py one example build per class, shared by the test suites
tests/ mappings, builder/emit, validation-rejection, golden, all-classes
tests/test_integ_ocsf_schema.py validates every class vs the OCSF metaschema (CI-gated)
.github/workflows/ci.yml unit job + blocking OCSF-schema-conformance job
Development
uv run pytest # tests
uv run mypy # strict type-checking
uv run ruff check . # lint
uv run ruff format . # format
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ocsf_emitter-0.2.0.tar.gz.
File metadata
- Download URL: ocsf_emitter-0.2.0.tar.gz
- Upload date:
- Size: 145.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.6 {"installer":{"name":"uv","version":"0.12.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec4d29f4895a3e28977c67d0d82882a49b0fc127534f774767839f4f74fd8e58
|
|
| MD5 |
fa3db16f50307f18fabb5ffe7b708831
|
|
| BLAKE2b-256 |
f4128599b3ad38884c197f5c324dacae43ebcde65754379f13f3de77b1e9a952
|
File details
Details for the file ocsf_emitter-0.2.0-py3-none-any.whl.
File metadata
- Download URL: ocsf_emitter-0.2.0-py3-none-any.whl
- Upload date:
- Size: 52.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.6 {"installer":{"name":"uv","version":"0.12.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
635eb70b3e1c6b8685ebd57c99a67a8f7f9a2727118ede87b0438313b2dd9653
|
|
| MD5 |
855f3b13a7bdafa17abe5381b2a93bdf
|
|
| BLAKE2b-256 |
7aab602b26fde7e3beacd9a0ede11b3280debee2b25e73bd65ed875f02e3611f
|