Skip to main content

05 - Threat Intelligence Ingestion: Building a STIX/TAXII Processor

Threat Intelligence Ingestion: Building a STIX/TAXII Processor. Threat intelligence is the fuel of modern security operations: without data...

What do you think about this article?

At a Glance

Threat Intelligence Ingestion: Building a STIX/TAXII Processor. Threat intelligence is the fuel of modern security operations: without data...

Threat Intelligence Ingestion: STIX/TAXII Feed Processor

Threat intelligence is the fuel of modern security operations: without up-to-date data on actors, campaigns, techniques, and Indicators of Compromise (IOCs), a SOC operates reactively rather than proactively. But the challenge is not the availability of intelligence - it is its systematic processing.

STIX/TAXII feeds (Structured Threat Information eXpression / Trusted Automated eXchange of Intelligence Information) are the international standard for automated sharing of threat intelligence in machine-readable format. The combination of STIX 2.1 for data structure and TAXII 2.1 for transport allows building fully automated ingestion pipelines that update SIEMs, EDRs, and firewalls with new IOCs in near real-time.

This article builds a complete threat intelligence processor: from connecting to TAXII servers, to parsing STIX 2.1 objects, to IOC enrichment, to correlation with SIEM alerts, and automatic routing to appropriate security controls.

What You Will Learn

  • STIX 2.1 object structure and TI modeling patterns
  • Connecting to and polling TAXII 2.1 servers with Python taxii2-client
  • Parsing and normalizing IOCs from STIX bundles
  • IOC scoring and deduplication
  • Automatic routing to SIEM, EDR, and firewall
  • Real-time IOC-alert correlation and automatic alert enrichment generation

STIX 2.1: The Data Model

STIX 2.1 defines a vocabulary of objects to represent practically every aspect of threat intelligence. Understanding the structure of the main objects is fundamental before building the processor.

STIX Object Purpose Key Fields
indicator IOC with detection pattern pattern, valid_from, valid_until, labels
malware Malware family details name, malware_types, aliases, capabilities
threat-actor Threat actor profile name, aliases, threat_actor_types, goals
attack-pattern Attack technique (ATT&CK) name, external_references (ATT&CK ID)
campaign Attack campaign name, aliases, first_seen, last_seen
relationship Connections between objects relationship_type, source_ref, target_ref
# STIX Pattern Language examples
# MD5 file hash
[file:hashes.'MD5' = 'd41d8cd98f00b204e9800998ecf8427e']

# Malicious URL
[url:value = 'http://malicious-c2.com/beacon']

# IP with port
[network-traffic:dst_ref.type = 'ipv4-addr'
 AND network-traffic:dst_ref.value = '192.168.1.1'
 AND network-traffic:dst_port = 443]

# Domain
[domain-name:value = 'evil-c2.com']

Connecting to TAXII 2.1 Servers

TAXII 2.1 defines a REST API for discovery and polling of threat intelligence collections. The Python taxii2-client library provides a complete client that handles authentication, pagination, and error management.

# TAXII 2.1 Client
from taxii2client.v21 import Server
from taxii2client.common import TokenAuth, BasicAuth
from datetime import datetime, timedelta
from typing import Iterator
import logging

class TaxiiIngestor:
    def __init__(self, configs: list):
        self.configs = configs
        self.logger = logging.getLogger(__name__)

    def poll_collection(
        self,
        config,
        collection_id: str,
        added_after: datetime = None,
        limit: int = 1000
    ) -> Iterator[dict]:
        """Polls a TAXII collection and yields STIX bundles."""
        if added_after is None:
            added_after = datetime.utcnow() - timedelta(hours=24)

        auth = TokenAuth(key=config.auth_params.get('token'))
        server = Server(config.url, auth=auth)

        for api_root in server.api_roots:
            for collection in api_root.collections:
                if collection.id != collection_id:
                    continue

                next_page = None
                total_fetched = 0

                while True:
                    try:
                        kwargs = {
                            'added_after': added_after.isoformat() + 'Z',
                            'limit': min(limit, 500)
                        }
                        if next_page:
                            kwargs['next'] = next_page

                        response = collection.get_objects(**kwargs)
                        objects = response.get('objects', [])
                        total_fetched += len(objects)

                        if objects:
                            yield response

                        next_page = response.get('next')
                        if not next_page or total_fetched >= limit:
                            break

                    except Exception as e:
                        self.logger.error(f"Polling error: {e}")
                        break

Parsing and Normalizing STIX IOCs

STIX objects must be transformed into normalized structures that security systems (SIEM, EDR, firewall) can consume directly. Parsing indicators requires interpreting the STIX Pattern Language.

# STIX IOC Parser
import re
from dataclasses import dataclass
from datetime import datetime
from typing import Optional

@dataclass
class NormalizedIOC:
    raw_id: str
    ioc_type: str         # 'ip', 'domain', 'url', 'hash_md5', 'hash_sha256'
    value: str
    source: str
    confidence: int       # 0-100
    severity: str         # 'low', 'medium', 'high', 'critical'
    valid_from: datetime
    valid_until: Optional[datetime]
    tags: list[str]
    related_malware: list[str]
    related_threat_actors: list[str]
    mitre_techniques: list[str]

class STIXParser:
    PATTERN_EXTRACTORS = {
        'ip': re.compile(
            r"network-traffic:(?:dst|src)_ref\.value\s*=\s*'([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})'"
        ),
        'domain': re.compile(
            r"domain-name:value\s*=\s*'([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})'"
        ),
        'url': re.compile(r"url:value\s*=\s*'(https?://[^']+)'"),
        'hash_md5': re.compile(r"file:hashes\.'MD5'\s*=\s*'([a-fA-F0-9]{32})'"),
        'hash_sha256': re.compile(r"file:hashes\.'SHA-256'\s*=\s*'([a-fA-F0-9]{64})'"),
    }

    def parse_bundle(self, stix_bundle: dict) -> list[NormalizedIOC]:
        objects = stix_bundle.get('objects', [])
        obj_index = {obj['id']: obj for obj in objects}
        relationships = self._index_relationships(objects)

        iocs = []
        for obj in objects:
            if obj.get('type') == 'indicator':
                try:
                    ioc = self._parse_indicator(obj, obj_index, relationships)
                    if ioc:
                        iocs.append(ioc)
                except Exception as e:
                    pass  # Log warning in production

        return iocs

    def _extract_ioc_from_pattern(self, pattern: str) -> tuple[str, str]:
        clean = pattern.strip('[]').strip()
        for ioc_type, regex in self.PATTERN_EXTRACTORS.items():
            match = regex.search(clean)
            if match:
                return ioc_type, match.group(1)
        return None, None

    def _index_relationships(self, objects: list) -> dict:
        rels = {}
        for obj in objects:
            if obj.get('type') == 'relationship':
                src = obj.get('source_ref', '')
                rels.setdefault(src, []).append(obj)
        return rels

Deduplication and IOC Scoring

Multiple feeds may contain the same IOC with different confidence levels. The deduplication system must intelligently aggregate information, increasing confidence when multiple independent sources report the same indicator.

# IOC Deduplication
from collections import defaultdict

class IOCDeduplicator:
    def __init__(self, min_confidence: int = 30):
        self.min_confidence = min_confidence

    def deduplicate_and_score(self, iocs: list[NormalizedIOC]) -> list[NormalizedIOC]:
        """
        Deduplicates IOCs and increases confidence when
        multiple sources report the same value.
        """
        grouped = defaultdict(list)
        for ioc in iocs:
            grouped[(ioc.ioc_type, ioc.value.lower())].append(ioc)

        result = []
        for (ioc_type, value), group in grouped.items():
            ioc = group[0] if len(group) == 1 else self._merge_iocs(group)
            if ioc.confidence >= self.min_confidence:
                result.append(ioc)

        return sorted(result, key=lambda x: x.confidence, reverse=True)

    def _merge_iocs(self, iocs: list[NormalizedIOC]) -> NormalizedIOC:
        """Merges duplicate IOCs from different sources."""
        base = max(iocs, key=lambda x: x.valid_from)
        base_confidence = max(i.confidence for i in iocs)
        # Confidence increases with number of sources (diminishing returns)
        source_boost = min(len(iocs) - 1, 3) * 5  # Max +15
        merged_confidence = min(base_confidence + source_boost, 100)

        return NormalizedIOC(
            raw_id=base.raw_id,
            ioc_type=base.ioc_type,
            value=base.value,
            source=",".join(set(i.source for i in iocs)),
            confidence=merged_confidence,
            severity=base.severity,
            valid_from=base.valid_from,
            valid_until=base.valid_until,
            tags=list(set(t for i in iocs for t in i.tags)),
            related_malware=list(set(m for i in iocs for m in i.related_malware)),
            related_threat_actors=list(set(a for i in iocs for a in i.related_threat_actors)),
            mitre_techniques=list(set(t for i in iocs for t in i.mitre_techniques))
        )

Routing to Security Controls

A complete TI processor does not stop at parsing: it distributes normalized IOCs to the systems that can actually enact defense. Routing is based on IOC type and confidence.

# IOC Router to SIEM, EDR, Firewall
import httpx

class IOCRouter:
    CONFIDENCE_THRESHOLDS = {
        'firewall_block': 80,    # Block in firewall only at high confidence
        'edr_detection': 60,     # EDR alert from medium+ confidence
        'siem_enrichment': 30,   # SIEM enrichment for all IOCs
    }

    def route(self, ioc: NormalizedIOC) -> list[dict]:
        """Distributes an IOC to appropriate systems."""
        results = []

        # All IOCs go to SIEM for enrichment
        if ioc.confidence >= self.CONFIDENCE_THRESHOLDS['siem_enrichment']:
            results.append({'destination': 'siem', **self._push_to_siem(ioc)})

        # High confidence network IOCs go to firewall
        if (ioc.ioc_type in ('ip', 'domain', 'url') and
                ioc.confidence >= self.CONFIDENCE_THRESHOLDS['firewall_block']):
            results.append({'destination': 'firewall', **self._push_to_firewall(ioc)})

        # Hashes and IPs go to EDR
        if (ioc.ioc_type in ('hash_md5', 'hash_sha256', 'ip') and
                ioc.confidence >= self.CONFIDENCE_THRESHOLDS['edr_detection']):
            results.append({'destination': 'edr', **self._push_to_edr(ioc)})

        return results

Real-Time Alert Correlation

The true value of an operational TI pipeline is real-time correlation: when a new IOC is ingested, the system immediately checks if recent alerts contain that value, automatically enriching them with threat context.

# Real-time IOC-Alert correlation
async def _correlate_with_recent_alerts(
    self,
    iocs: list[NormalizedIOC],
    lookback_hours: int = 48
) -> list[dict]:
    """Searches recent alerts matching new IOCs."""
    correlations = []

    recent_alerts = await self.siem.search_recent_alerts(
        hours_back=lookback_hours,
        limit=10000
    )

    # Build IOC index for efficient lookup
    ioc_index = {(ioc.ioc_type, ioc.value.lower()): ioc for ioc in iocs}

    for alert in recent_alerts:
        fields_to_check = [
            ('ip', alert.get('src_ip', '')),
            ('ip', alert.get('dst_ip', '')),
            ('domain', alert.get('dns_query', '')),
            ('hash_md5', alert.get('file_hash_md5', '')),
            ('hash_sha256', alert.get('file_hash_sha256', '')),
        ]

        for ioc_type, value in fields_to_check:
            if not value:
                continue
            ioc = ioc_index.get((ioc_type, value.lower()))
            if ioc:
                correlation = {
                    'alert_id': alert.get('id'),
                    'ioc_type': ioc_type,
                    'ioc_value': value,
                    'ioc_confidence': ioc.confidence,
                    'related_malware': ioc.related_malware,
                    'related_actors': ioc.related_threat_actors
                }
                correlations.append(correlation)
                await self.siem.enrich_alert(alert.get('id'), correlation)

    return correlations

Recommended Free TAXII Feeds

  • MITRE ATT&CK TAXII: https://cti-taxii.mitre.org/taxii/ - Groups and software
  • Anomali LIMO: Free tier with community-curated IOCs
  • AlienVault OTX: Community-driven threat intelligence feed
  • MISP: Self-hosted with community feeds
  • AbuseIPDB: Malicious IP feeds (REST API)

Feed Quality Warning

Not all TAXII feeds have the same quality. Feeds with low confidence and high false positive rate (e.g., generic IPs, CDNs, Tor exit nodes) can block legitimate traffic if inserted into the firewall without validation. Always implement a whitelisting system (e.g., known cloud provider CIDRs, organization IPs) before blocking.

Conclusions and Key Takeaways

A mature STIX/TAXII processor transforms threat intelligence from a human resource (analysts reading reports) to an automated operational resource (IOCs that automatically propagate to security controls within minutes of publication).

Key Takeaways

  • STIX 2.1 is the standard language for modeling TI; TAXII 2.1 is the transport protocol
  • The STIX parser must handle relationships between objects to enrich IOCs with malware/actor context
  • Multi-source deduplication increases confidence proportionally to concordant sources
  • Routing must be based on IOC type and confidence (do not block everything in the firewall)
  • Real-time IOC-alert correlation is the true added value: turns passive IOCs into active alerts
  • Always implement whitelisting before automatic blocking to prevent self-inflicted outages

Related Articles

  • AI-Assisted Detection: LLMs for Sigma Rule Generation
  • SOAR Playbook in Python: Incident Response Automation
  • MITRE ATT&CK Integration: Mapping Coverage Gaps

Frequently asked questions

What is the purpose of using STIX/TAXII feeds for threat intelligence?

The purpose of using STIX/TAXII feeds is to enable automated sharing and processing of threat intelligence in machine-readable format, allowing security systems to stay up-to-date with new Indicators of Compromise (IOCs) in near real-time.

What is the difference between STIX 2.1 and TAXII 2.1?

STIX 2.1 defines a vocabulary of objects for representing threat intelligence data, while TAXII 2.1 provides a REST API for discovery and polling of threat intelligence collections.

How does the deduplication system handle multiple feeds with the same IOC but different confidence levels?

The deduplication system intelligently aggregates information from multiple independent sources, increasing confidence when multiple sources report the same indicator.

Why is it necessary to implement a whitelisting system before blocking IOCs from unknown TAXII feeds?

It is necessary to implement a whitelisting system to prevent legitimate traffic from being blocked due to low-confidence or high-false-positive-rate indicators, such as generic IPs or CDNs.

Sources and further reading

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects