Construct, validate, and emit OCSF Detection Finding (class_uid 2004) events with a consistent shape.
Project description
ocsf-emitter
Construct, validate, and emit OCSF Detection Finding events (class_uid
2004) with a consistent shape and mandatory runtime validation.
This is an internal library. Other services import it to turn their own
detection signals into valid OCSF findings; 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.
Install
uv pip install -e . # runtime: pydantic only
uv pip install -e ".[securitylake]" # + pyarrow, for the Parquet writer
uv pip install -e ".[dev]" # + mypy, pytest, ruff (and pyarrow)
uv pip install -e ".[codegen]" # + tools to regenerate the OCSF models
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 -- verifies
class_uid == 2004,category_uid == 2,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
detection_findingcoverage. - Self-contained and auditable. The generated module is committed and reviewable, and regeneration is a single script.
How the pinned version is generated
We must pin an older OCSF version (1.1.0 -- see Security Lake below), and
the OCSF JSON Schema HTTP endpoint only serves the latest deployed version. So
scripts/gen_models.py:
- Fetches the pinned version's metaschema with
ocsf-lib(OcsfApiClient().get_schema("1.1.0")) -- this works for any version. - Converts that metaschema (the
detection_findingclass plus its transitive object closure) into a self-contained draft JSON Schema. Attributes tagged with an OCSFprofile(e.g.cloud,osint) are dropped so we get the base class -- 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
Before bumping: if you ship to AWS Security Lake, confirm the target version is one Security Lake accepts (see below). The CI
aws-validationjob is a hard gate and will fail for unsupported versions.
-
Regenerate the models for the desired version:
uv run --extra codegen python scripts/gen_models.py 1.1.0
-
Update the one-line pin in
src/ocsf_emitter/defaults.pyto match:OCSF_SCHEMA_VERSION = "1.1.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.
AWS Security Lake compatibility
This package targets AWS Security Lake custom-source ingestion.
Version. Security Lake custom sources accept OCSF 1.1.0 / 1.0.0-rc.2, and
the AWS OCSF validation tool
maps detection_finding only under 1.1.0. We therefore pin 1.1.0
(not a newer version). To keep findings acceptable, the builder also stamps the
sibling label fields Security Lake expects: class_name ("Detection Finding")
and category_name ("Findings").
Validation is proven against AWS's own tool. CI runs that tool against an
emitted finding as a blocking job
(.github/workflows/ci.yml), pinned at a known
commit. Locally:
git clone https://github.com/aws-samples/amazon-security-lake-ocsf-validation.git /tmp/aws-ocsf
uv pip install -e ".[securitylake]" && uv pip install -r /tmp/aws-ocsf/requirements.txt pytest
OCSF_AWS_VALIDATION_DIR=/tmp/aws-ocsf uv run pytest tests/test_integ_aws_validation.py -v
Parquet packaging. Security Lake wants Parquet objects (not JSON),
zstd-compressed, partitioned in S3 and sorted by time. The optional
ocsf_emitter.securitylake module (install the securitylake extra) does this
without touching S3 -- you upload the returned bytes at the returned key:
from ocsf_emitter import securitylake as sl
obj = sl.build_parquet_object(
findings, # a batch of built findings
source_location="my_detector", # the prefix Security Lake assigned you
region="us-east-1",
account_id="123456789012",
object_name="batch-001",
)
# obj.key -> ext/my_detector/region=us-east-1/accountId=.../eventDay=YYYYMMDD/batch-001.parquet
# obj.data -> Parquet bytes (zstd); upload to your Security Lake S3 bucket at obj.key
The writer validates every finding (via emit), sorts records by time, uses
zstd compression, and bounds data-page (<=1 MB) and row-group sizes per the
Security Lake custom-source requirements.
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_detection_finding, build_from_signal, emit, ...
domain.py our domain shapes: DetectionSignal, Observable, MitreAttack, enums
builders.py domain signal -> OCSF DetectionFinding
defaults.py schema-version pin, metadata/product, severity/status/... mappings
validate.py runtime validation; raises InvalidFindingError
emit.py serialize to JSON dict/str (transport-agnostic)
securitylake.py OCSF -> Parquet for AWS Security Lake (needs [securitylake] extra)
errors.py OcsfEmitterError, InvalidFindingError
_models.py GENERATED OCSF Pydantic models (do not edit by hand)
scripts/gen_models.py regenerate _models.py (metaschema -> JSON Schema -> Pydantic)
tests/ mappings, builder/emit, validation-rejection, golden, parquet
tests/test_integ_aws_validation.py runs the AWS OCSF validation tool (CI-gated)
.github/workflows/ci.yml unit job + blocking AWS-validation job
Development
uv run pytest # tests
uv run mypy # strict type-checking
uv run ruff check . # lint
uv run ruff format . # format
Project details
Release history Release notifications | RSS feed
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.1.0.tar.gz.
File metadata
- Download URL: ocsf_emitter-0.1.0.tar.gz
- Upload date:
- Size: 103.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","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 |
d2c7696320f3f3e75e699ab04635fbe255c587b291726d6dcf22dde1178d7d3e
|
|
| MD5 |
750d8bbe200f9fc1d79eaacdf6ed37ec
|
|
| BLAKE2b-256 |
dde7c5b7d291a4eeef96be4c81160318ae974b85dcf851d2e3e69401c16b8b74
|
File details
Details for the file ocsf_emitter-0.1.0-py3-none-any.whl.
File metadata
- Download URL: ocsf_emitter-0.1.0-py3-none-any.whl
- Upload date:
- Size: 24.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","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 |
afc0b0039e94563b0138f97d121fb69769ac1307a034bc9a839df10ea71b2ffc
|
|
| MD5 |
7a64740d63a1182580d5aa3b8d747fb5
|
|
| BLAKE2b-256 |
51ba21fd41381b1c522d1888165aecf965018a024c734207bf0e1da0348e1827
|