MISP speaks in attributes and categories; ECS speaks in threat.indicator fields — and until you bridge them, a MISP feed cannot enrich a single normalized event. This guide maps MISP’s attribute model onto ECS indicator fields, as one precise technique within threat intel feed mapping and the broader SOC log architecture & taxonomy discipline.
Root-Cause Context
MISP structures intelligence as events containing attributes, where each attribute has a type (ip-dst, domain, md5, url, email-src) and a category (Network activity, Payload delivery). ECS instead models an indicator under threat.indicator.* with a type drawn from a controlled vocabulary (ipv4-addr, domain-name, file, url) and the value under a type-specific field (threat.indicator.ip, threat.indicator.file.hash.md5). The two taxonomies overlap in meaning but agree on almost no literal name, so a MISP attribute cannot be matched against ECS-normalized telemetry without translation.
Two subtleties make a naive mapping wrong. First, one MISP type can carry multiple ECS-relevant values — a filename|md5 composite attribute holds both a filename and a hash. Second, MISP’s to_ids flag distinguishes attributes meant for detection from context-only ones; blindly loading everything as an indicator floods correlation with non-actionable values. A correct mapping resolves each MISP type to the right ECS field, splits composites, and respects to_ids so only detection-grade indicators reach the matching path.
Prerequisites
The module targets Python 3.11+ and uses only the standard library plus pydantic. It consumes MISP attribute dicts (from the MISP REST API or a feed export) and emits ECS-shaped indicators consumed by the parent threat intel feed mapping guides.
pip install "pydantic>=2.6,<3.0"
Production-Ready Implementation
The mapper resolves each MISP type to an ECS field path and indicator type, splits composite attributes, and drops context-only attributes unless configured otherwise.
from __future__ import annotations
import logging
from typing import Optional
from pydantic import BaseModel, Field
logger = logging.getLogger("soc.intel.misp")
# MISP type -> (ECS indicator type, ECS value field path).
MISP_TO_ECS: dict[str, tuple[str, str]] = {
"ip-dst": ("ipv4-addr", "threat.indicator.ip"),
"ip-src": ("ipv4-addr", "threat.indicator.ip"),
"domain": ("domain-name", "threat.indicator.url.domain"),
"hostname": ("domain-name", "threat.indicator.url.domain"),
"url": ("url", "threat.indicator.url.full"),
"md5": ("file", "threat.indicator.file.hash.md5"),
"sha1": ("file", "threat.indicator.file.hash.sha1"),
"sha256": ("file", "threat.indicator.file.hash.sha256"),
"email-src": ("email-addr", "threat.indicator.email.address"),
}
# Composite MISP types that carry two values, split on '|'.
COMPOSITES: dict[str, tuple[str, str]] = {
"filename|md5": ("filename", "md5"),
"domain|ip": ("domain", "ip-dst"),
}
class MispAttribute(BaseModel):
type: str = Field(min_length=1)
value: str = Field(min_length=1)
to_ids: bool = False
category: str = ""
def map_attribute(attr: MispAttribute, require_ids: bool = True) -> list[dict]:
"""Return zero or more ECS indicators for one MISP attribute."""
if require_ids and not attr.to_ids:
return [] # context-only, skip
if attr.type in COMPOSITES:
return _map_composite(attr)
resolved = MISP_TO_ECS.get(attr.type)
if resolved is None:
logger.warning("ERR_INTEL_401 unmapped MISP type=%s", attr.type)
return []
ind_type, field = resolved
return [{"threat.indicator.type": ind_type, field: attr.value.strip()}]
def _map_composite(attr: MispAttribute) -> list[dict]:
left_type, right_type = COMPOSITES[attr.type]
try:
left_val, right_val = attr.value.split("|", 1)
except ValueError:
logger.warning("ERR_INTEL_402 malformed composite=%s", attr.value)
return []
out: list[dict] = []
for t, v in ((left_type, left_val), (right_type, right_val)):
sub = MispAttribute(type=t, value=v, to_ids=attr.to_ids)
out.extend(map_attribute(sub, require_ids=False))
return out
if __name__ == "__main__":
a = MispAttribute(type="ip-dst", value="203.0.113.7", to_ids=True)
assert map_attribute(a)[0]["threat.indicator.ip"] == "203.0.113.7"
c = MispAttribute(type="filename|md5", value="evil.exe|d41d8cd98f00b204e9800998ecf8427e",
to_ids=True)
mapped = map_attribute(c)
assert any("threat.indicator.file.hash.md5" in m for m in mapped)
Splitting composites recursively through map_attribute keeps one code path for the actual type resolution, and the require_ids gate means only detection-grade attributes become matchable indicators while the mapper still can load context on request. Unmapped types are logged as ERR_INTEL_401 rather than dropped silently, so feed coverage is measurable.
Error-Code Reference
| Code | Meaning | Action |
|---|---|---|
ERR_INTEL_401 |
MISP attribute type has no ECS mapping | Log and count; extend the map for detection-relevant types |
ERR_INTEL_402 |
Composite attribute value is malformed | DLQ the attribute; inspect the feed export |
ERR_INTEL_403 |
Indicator value fails ECS type validation | DLQ; the value is not a valid IP/hash/URL |
ERR_INTEL_404 |
Duplicate indicator across MISP events | Deduplicate on value+type; keep the highest-confidence source |
Operational Notes
- Respect
to_ids. Load only detection-flagged attributes into the matching path by default; context attributes belong in enrichment, not correlation, or they flood the pipeline with non-actionable values. - Split composites. A
filename|md5attribute is two indicators; mapping it as one loses a matchable value. - Deduplicate on value + type. The same IP appears across many MISP events; collapse duplicates and retain provenance so a match still cites its source.
- Validate values against ECS types. A malformed IP or truncated hash must fail closed (
ERR_INTEL_403), not enter the indicator set where it can never match anything.
Verification Checklist
FAQ
Should I load every MISP attribute as an indicator?
No. Load only attributes flagged to_ids into the detection-matching path; those are the values the feed author marked as suitable for automated detection. Context-only attributes — added for analyst reference — should enrich a case if needed but must not enter correlation, where they generate non-actionable matches and dilute the signal. Treating the to_ids flag as authoritative is the single most important guard against feed-driven false positives.
How do I handle MISP composite attribute types?
Split them into their constituent indicators. A filename|md5 attribute carries both a filename and a hash separated by a pipe; map each half through the same type-resolution path so you get two ECS indicators. Mapping the composite as a single opaque value loses the matchable hash and leaves you unable to correlate the indicator against file telemetry.