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.

MISP attribute types mapped to ECS threat.indicator fields MISP attribute types on the left map to ECS threat.indicator fields on the right: ip-dst to threat.indicator.ip with type ipv4-addr, domain to threat.indicator.url.domain with type domain-name, md5 to threat.indicator.file.hash.md5 with type file, and url to threat.indicator.url.full with type url. Only attributes flagged to_ids become detection indicators. MISP type ECS threat.indicator ip-dst .ip (ipv4-addr) domain .url.domain (domain-name) md5 .file.hash.md5 (file) url .url.full (url) only to_ids = true attributes become detection indicators

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|md5 attribute 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.