The difference between a case an analyst has to research and one that already says “this IP is a known Cobalt Strike C2, last seen yesterday” is a single automated enrichment step. This guide builds that step — attaching threat-intel verdicts to a case and lifting its confidence — as one precise technique within case enrichment pipelines and the broader SOAR playbook & response automation discipline.

Root-Cause Context

A correlated case arrives with entity indicators — a source IP, a file hash, a domain — but no verdict on whether those indicators are known-bad. Threat-intel enrichment answers that, and the answer changes the case’s whole trajectory: a confirmed-malicious verdict can lift confidence past the threshold for automated containment, while a clean verdict keeps the case at analyst-review priority. Skipping enrichment forces the analyst to pivot into a separate intel console for every case, which is exactly the manual toil automation exists to remove.

The step has two hazards. First, it is a network call, so it must be cached and bounded — an un-cached lookup per case will exhaust the feed’s rate limit during a storm, and an un-bounded one will stall the pipeline. Second, its output is advisory: a missing or slow verdict must never block the case from progressing, because a delayed containment is worse than an un-enriched one. Enrichment lifts confidence when a verdict is present and leaves the base confidence untouched when it is not — it never lowers confidence or gates the case on its own success.

Threat-intel case enrichment with confidence lift A case's indicators are looked up against a cached threat-intel source under a deadline. A malicious verdict lifts the case confidence and attaches indicator context; a clean or missing verdict leaves the base confidence unchanged. The enriched case proceeds to the decision gate regardless of whether a verdict was found, because enrichment is advisory and fail-open. Cached intel lookup indicators · deadline Verdict? Lift + attach malicious Proceed unchanged malicious clean/none

Prerequisites

The step targets Python 3.11+ with an async intel client. It reuses the budgeted, cached lookup discipline from enrichment latency budgets and plugs into the parent case enrichment pipelines fan-out.

pip install "pydantic>=2.6,<3.0"

Production-Ready Implementation

The step extracts indicators from the case, looks each up under a deadline, and applies a bounded confidence lift when any resolves malicious — always fail-open.

from __future__ import annotations

import asyncio
import logging
from typing import Awaitable, Callable, Optional

from pydantic import BaseModel, Field

logger = logging.getLogger("soc.soar.enrich.intel")


class Case(BaseModel):
    case_id: str = Field(min_length=1)
    confidence: float = Field(ge=0.0, le=1.0)
    src_ip: Optional[str] = None
    file_hash: Optional[str] = None
    domain: Optional[str] = None


class IntelResult(BaseModel):
    verdict: str = "unknown"          # malicious | suspicious | clean | unknown
    source: str = ""
    last_seen: Optional[str] = None


IntelLookup = Callable[[str], Awaitable[Optional[IntelResult]]]

# Bounded confidence lift per verdict; never lowers confidence.
_LIFT = {"malicious": 0.3, "suspicious": 0.1}


async def enrich_case(
    case: Case, lookup: IntelLookup, deadline: float = 0.05,
) -> tuple[Case, dict]:
    """Return (possibly-lifted case, enrichment context). Fail open."""
    indicators = [v for v in (case.src_ip, case.file_hash, case.domain) if v]
    context: dict = {"intel": [], "coverage": "none"}
    if not indicators:
        return case, context

    async def _one(ind: str) -> Optional[IntelResult]:
        try:
            return await asyncio.wait_for(lookup(ind), deadline)
        except asyncio.TimeoutError:
            logger.info("ERR_ENRICH_004 intel timeout indicator=%s", ind)
            return None
        except Exception as exc:
            logger.warning("ERR_ENRICH_005 intel error indicator=%s: %s", ind, exc)
            return None

    results = await asyncio.gather(*(_one(i) for i in indicators))
    got = [r for r in results if r is not None]
    context["coverage"] = "full" if len(got) == len(indicators) else "partial" if got else "none"

    best_lift = 0.0
    for r in got:
        context["intel"].append(r.model_dump())
        best_lift = max(best_lift, _LIFT.get(r.verdict, 0.0))

    if best_lift > 0:
        lifted = min(1.0, case.confidence + best_lift)   # clamp, never exceed 1.0
        return case.model_copy(update={"confidence": lifted}), context
    return case, context                                 # unchanged, still proceeds


async def _demo() -> None:
    async def lookup(ind: str) -> Optional[IntelResult]:
        if ind == "203.0.113.9":
            return IntelResult(verdict="malicious", source="feed-x", last_seen="2026-05-18")
        return IntelResult(verdict="clean", source="feed-x")

    case = Case(case_id="C-1", confidence=0.6, src_ip="203.0.113.9")
    enriched, ctx = await enrich_case(case, lookup)
    assert enriched.confidence == 0.9          # 0.6 + 0.3 malicious lift
    assert ctx["intel"][0]["verdict"] == "malicious"


if __name__ == "__main__":
    asyncio.run(_demo())

The lift is bounded per verdict and clamped at 1.0, so a malicious verdict raises confidence toward the auto-act threshold without ever exceeding the ceiling, and a clean or absent verdict leaves the case exactly as it arrived. Every lookup is deadline-bounded and returns None on timeout or error, so one slow feed degrades this case’s coverage to partial rather than blocking it — the fail-open contract the enrichment layer depends on.

Error-Code Reference

Code Meaning Action
ERR_ENRICH_004 Intel lookup exceeded its deadline Record coverage gap; proceed fail-open
ERR_ENRICH_005 Intel source returned an error Record coverage gap; circuit-break on repeated failures
ERR_ENRICH_011 Case has no usable indicators Skip enrichment; the case proceeds unchanged
ERR_ENRICH_031 Intel source returned a malformed verdict Discard that verdict; keep other sources’ results

Operational Notes

  • Lift, never lower. Enrichment can raise confidence on a malicious verdict but must never reduce it; a clean verdict is context, not a reason to de-prioritize a scored case.
  • Bound every lookup. A per-indicator deadline plus caching keeps enrichment off the critical path and within the feed’s rate limit.
  • Fail open on coverage gaps. A timeout or error records a partial-coverage marker and the case still proceeds — enrichment refines the decision but never gates it.
  • Attach provenance. Carry the source and last-seen with each verdict so an analyst can judge freshness and the ticket cites its evidence.

Verification Checklist

FAQ

Should a threat-intel verdict be able to lower a case's confidence?

No. Enrichment is additive: a malicious or suspicious verdict lifts confidence toward the auto-act threshold, but a clean verdict is treated as context, not as a reason to de-prioritize. A case earned its base confidence from correlation and scoring, and a “clean” intel result on one indicator does not negate that — the activity may still be malicious in a way the feed has not catalogued. Lifting only keeps enrichment from ever suppressing a real signal.

What happens if the intel feed is down when a case arrives?

The case proceeds with unchanged confidence and a recorded coverage gap. Every lookup is deadline-bounded and fails open, so a down or slow feed simply means no verdict is attached and the coverage marker reads partial or none. The decision gate then acts on the case’s base confidence, and the missing verdict can be back-filled as a later case update once the feed recovers — enrichment never holds a case hostage to feed availability.