The platform YAML files in manifests/ (i.e., apple.yaml) provide instructions for the Python engine to map data exports into (a slightly modified version of) the Elastic Common Schema (ECS). The goal of this is to minimize how much of the Python engine in python_core we have to rewrite if we want to add support for a new platform or if the platforms change file formats.
A manifest has two main sections:
files: physical files to parse (inputs)views: specs for the logical events/states extracted from those files (outputs).
Define the physical file once, even if it contains multiple types of events.
files:
- id: "insta_devices" # Unique reference ID
path: "path/to/devices.json" # Relative path in the ZIP
parser:
format: "json"
json_root: "devices_devices[]"
drop_duplicates: # optional deduplication, follows pandas conventions
subset: ["Device ID"]
keep: "last"
- id: ....The attributes under parser: describe the shape of the file and how the engine should parse it.
-
format:json: standard JSON, ok for nested dictionaries within dictionaries, but not nested lists.json_label_values: Special Meta format where data is stored in lists of{label: "Key", value: "Val"}.jsonl: newline delimited JSON format, common in Discordcsvcsv_multi: a very odd file that contains multiple CSV sections separated by titles and newlines, common in Apple
-
json_root: (json/json_label_values/jsonl only; optional): The path to the list of objects you want to parse. Use [] to denote a list (e.g.,account_activity_v2[]). -
drop_duplicates: (optional) logic to clean data at the source level. Follows pandas conventions. Requires:subset: list of columns to checkkeep:first,last, orrow_completeness(keep the row with the most non-null row entries)
A view defines how to transform a source into a stream of events/states (vaguely using ECS conventions). You can have multiple views for a single source (e.g., one for "Logins" and one for "Logouts").
views:
- file:
id: "insta_devices" # Must match a file ID
where: {source: "status", op: "==", value: "active"} # Filtering (optional) only rows that match this condition
# hardcoded ECS values for every event in this view.
static:
event.kind: "state" # 'event' (action) or 'state' (snapshot)
event.category: ["authentication", "session"] # follow ECS conventions
entity.type: "application"
entity.sub_type: "app_registration"
# dynamic mappings
fields:
- {target: "entity.last_seen_timestamp", source: "'Last Login'.timestamp", type: "datetime"}
- {target: "user_agent.original", source: "'User Agent'.value", type: "string"}whereselects only rows that match this condition- Simple: i.e.,
{source: "event_type", op: "==", value: "login"} - Complex: Uses logic: any or all with a conditions list.
- Supports operators:
==,startswith,contains,endswith,!=. - Add more supported operators in
python_core/utils/filter_builder.py(OP_MAPPING), and mirror them intoKNOWN_WHERE_OPSinscripts/validate_manifests.py.
- Supports operators:
- Simple: i.e.,
JSON (and jsonl) source fields support path traversal for nested dictionaries via dot notation and bracket notation. Use single quotes for keys containing spaces, e.g.:
session.ip_address'Device ID details'.first_seen_timepush_tokens[0].id: Gets the id of the first item in the list.
hardcoded ECS (Elastic Common Schema) values for every event in this view.
event.kinddecides which table the row lands in. Anything else is logged asUnhandled event_kindand dropped.event: points in time (Logins, Messages, Clicks). Goes to theeventstable, keyed on@timestamp.state: inventory snapshots (Devices, Sessions, Contacts). Goes to thedevices_rawtable, keyed onentity.[first/last]_seen_timestamp.
entity.typeis required forstaterows. Only these three are kept; astaterow with any otherentity.typeis silently discarded bypython_core/semantic_map/worker.py.host: physical hardware (an iPhone, a laptop).application: an app or client install registered to the account.session: a logged-in session or cookie.
entity.sub_typeis an optional refinement used by the Devices view for grouping:app_registration,hardware_registration,platform_inferred_device,trusted_cookie,passkey_registration.event.category/event.type/event.outcomefollow ECS conventions. The combination that eachevent.action,entity.type, andentity.sub_typerequires is declared in__taxonomy.yamlunderrelationships:and enforced by the validator (see below).
The full accepted vocabulary for every field lives in manifests/__taxonomy.yaml, which is the source of
truth. This README summarizes it, but the taxonomy and the validator are what actually gate a manifest.
Every item must have target (the standardized ECS field name) and a source (the raw data key in the file), and optionally a type and transform. Path traversal for source fields follows conventions above.
If transform is set to "coalesce" (or if source is a list), the engine will pick the first non-null value.
- e.g.,
{ ... source: ["push_tokens[0].id", "family_id"], transform: "coalesce"}
ECS isn't really built for this use case, so we have a couple of custom fields that we're consistently using at the moment:
device.id.[platform]: internal device fingerprint (not user ID) for a specific platform --> also + .addevice.given_name: user-defined nickname for device (e.g., "Bob's iPhone"), common in Appledevice.imei: International Mobile Equipment Identitydevice.meid: Mobile Equipment Identifierentity.type/entity.sub_type: forstaterows. See the vocabulary under Static attributes aboveentity.[first/last]_seen_timestamp: forstaterows, instead of@timestampurl.title: for title of web page associated with a search/website (declared in the taxonomy, not currently mapped by any manifest)client.session.idandclient.session.typeuser.email.newanduser.email.olddevice.screen_resolution,device.locale,device.timezone
scripts/validate_manifests.py checks every manifests/*.yaml against manifests/__taxonomy.yaml. It is
read-only and never edits either file. Run it after any manifest change:
uv run python scripts/validate_manifests.py
# or against a single directory / alternate taxonomy
uv run python scripts/validate_manifests.py --manifests-dir manifests --taxonomy manifests/__taxonomy.yamlIt exits non-zero if there are any errors, and catches the failure modes that are otherwise silent at
runtime: a view whose file.id matches no files[].id, an unknown parser.format or where operator, a
transform that isn't implemented, a target that isn't in the taxonomy, and static values that violate
the relationships: shape (e.g. event.action: user_login without event.type: [start]).
Warnings don't fail the run: an unrecognized field type, a drop_duplicates.keep that will fall back
to first, or a taxonomy field with no description yet.
It also reports taxonomy entries that no manifest references anymore. That's informational, and usually means a field was renamed and the old entry can be deleted.
The validator rejects any target not present in the taxonomy, so new fields need an entry in
manifests/__taxonomy.yaml first:
fields:
device.locale:
description: not ECS, language/region the device is configured for (e.g. en_US)
url: https://www.elastic.co/guide/en/ecs/current/ecs-device.html # optional, if it is a real ECS fieldTwo flags matter:
allowed_values: if present, anystatic:value for this field outside the list is an error.parsed_field_only: true: the field is computed in Python (during normalization) and can never be set from a manifest. Setting it in a manifest is an error.