threat intelligence

Processing STIX/TAXII Feeds

Processes STIX 2.1 threat intelligence bundles delivered via TAXII 2.1 servers, normalizing objects into platform-native schemas and routing them to appropriate consuming systems. Use when onboarding new TAXII collection endpoints, automating bi-directional intelligence sharing with ISACs, or building pipeline validation for malformed STIX bundles. Activates for requests involving OASIS STIX, TAXII server configuration, MISP TAXII, or Cortex XSOAR feed integrations.

ctiiocmispnist-sp-800-150oasisstix-2.1taxii-2.1threat-intelligence
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

Use this skill when:

  • Onboarding a new TAXII 2.1 collection from a government feed (CISA AIS, FS-ISAC) or commercial provider
  • Validating that ingested STIX bundles conform to the OASIS STIX 2.1 specification before import
  • Building automated pipelines that parse STIX relationship objects to reconstruct campaign context

Do not use this skill for proprietary vendor feed formats (Recorded Future JSON, CrowdStrike IOC lists) that require vendor-specific parsers rather than STIX processing.

Prerequisites

  • Python 3.9+ with stix2 library (pip install stix2) and taxii2-client library
  • Network access to TAXII 2.1 server endpoint with valid credentials
  • Target TIP or SIEM with import API (MISP, OpenCTI, or Splunk ES)

Workflow

Step 1: Discover TAXII Server Collections

from taxii2client.v21 import Server, as_pages
 
server = Server("https://cti.example.com/taxii/",
                user="apiuser", password="apikey")
api_root = server.api_roots[0]
for collection in api_root.collections:
    print(collection.id, collection.title, collection.can_read)

Select collections relevant to your threat profile. CISA AIS provides collections segmented by sector (financial, energy, healthcare).

Step 2: Fetch STIX Bundles with Pagination

from taxii2client.v21 import Collection
from datetime import datetime, timedelta, timezone
 
collection = Collection(
    "https://cti.example.com/taxii/api1/collections/<id>/objects/",
    user="apiuser", password="apikey")
 
# Fetch only objects added in the last 24 hours
added_after = datetime.now(timezone.utc) - timedelta(hours=24)
for bundle_page in as_pages(collection.get_objects,
                             added_after=added_after, per_request=100):
    process_bundle(bundle_page)

Step 3: Parse and Validate STIX Objects

import stix2
 
def process_bundle(bundle_dict):
    bundle = stix2.parse(bundle_dict, allow_custom=True)
    for obj in bundle.objects:
        if obj.type == "indicator":
            validate_indicator(obj)
        elif obj.type == "threat-actor":
            upsert_threat_actor(obj)
        elif obj.type == "relationship":
            link_objects(obj)
 
def validate_indicator(indicator):
    required = ["id", "type", "spec_version", "created",
                "modified", "pattern", "pattern_type", "valid_from"]
    for field in required:
        if not hasattr(indicator, field):
            raise ValueError(f"Missing required field: {field}")
    # Check confidence range
    if hasattr(indicator, "confidence"):
        assert 0 <= indicator.confidence <= 100

Step 4: Route Objects to Consuming Platforms

Map STIX object types to destination systems:

  • indicator objects → SIEM lookup tables and firewall blocklists
  • malware objects → EDR threat intelligence library
  • threat-actor / campaign objects → TIP for analyst context
  • course-of-action objects → Security team wiki or SOAR playbook triggers

Use TLP marking definitions to enforce sharing restrictions:

for marking in obj.get("object_marking_refs", []):
    if "tlp-red" in marking:
        route_to_restricted_platform_only(obj)

Step 5: Publish Back to TAXII (Bi-directional Sharing)

# Add validated local intelligence back to shared collection
new_indicator = stix2.Indicator(
    name="Malicious C2 Domain",
    pattern="[domain-name:value = 'evil-c2.example.com']",
    pattern_type="stix",
    valid_from="2025-01-15T00:00:00Z",
    confidence=80,
    labels=["malicious-activity"],
    object_marking_refs=["marking-definition--34098fce-860f-479c-ae..."]  # TLP:GREEN
)
collection.add_objects(stix2.Bundle(new_indicator))

Key Concepts

Term Definition
STIX Bundle Top-level STIX container object (type: "bundle") holding any number of STIX Domain Objects (SDOs) and STIX Relationship Objects (SROs)
SDO STIX Domain Object — core intelligence types: indicator, threat-actor, malware, campaign, attack-pattern, course-of-action
SRO STIX Relationship Object — links two SDOs with a labeled relationship (e.g., "uses", "attributed-to", "indicates")
Pattern Language STIX pattern syntax for indicator conditions: [network-traffic:dst_port = 443 AND ipv4-addr:value = '10.0.0.1']
Marking Definition STIX object encoding TLP or statement restrictions on intelligence sharing
added_after TAXII 2.1 filter parameter (RFC 3339 timestamp) for incremental polling of new objects

Tools & Systems

  • stix2 (Python): Official OASIS Python library for creating, parsing, and validating STIX 2.0/2.1 objects
  • taxii2-client (Python): Client library for TAXII 2.0/2.1 server discovery, collection enumeration, and object retrieval
  • MISP: Open-source TIP with native TAXII 2.1 server and client; MISP-TAXII-Server plugin for publishing MISP events
  • OpenCTI: CTI platform with built-in TAXII 2.1 connector; supports STIX 2.1 import/export natively
  • Cabby: Legacy Python TAXII 1.x client for older government feeds still on TAXII 1.1

Common Pitfalls

  • Ignoring spec_version field: STIX 2.0 and 2.1 have incompatible schemas (2.1 adds confidence, object_marking_refs at bundle level). Always check spec_version before parsing.
  • No pagination handling: TAXII servers cap responses at 100–1000 objects per request. Missing pagination (via next link header) causes silent data loss.
  • Clock skew on added_after: Server and client time misalignment causes missed objects at interval boundaries. Use UTC exclusively and add 5-minute overlap windows.
  • Storing raw STIX blobs without indexing: Storing bundles as opaque JSON prevents querying by indicator type or campaign. Parse into relational or graph database.
  • Sharing TLP:RED content inadvertently: Automated pipelines must filter marking definitions before routing to any shared platform or SIEM with broad analyst access.
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md2.2 KB

API Reference: STIX/TAXII Feed Processing Agent

Overview

Discovers TAXII 2.1 servers, fetches STIX 2.1 bundles with pagination, parses and validates objects by type, extracts IOCs from indicator patterns, and builds relationship graphs.

Dependencies

Package Version Purpose
taxii2-client >= 2.3 TAXII 2.1 server discovery and collection fetching
stix2 >= 3.0 STIX 2.1 object parsing and validation

Core Functions

discover_server(taxii_url, user, password)

Discovers TAXII server API roots and their collections.

  • Returns: dict with api_roots containing collection metadata

fetch_collection(taxii_url, collection_id, user, password, added_after, limit)

Fetches all STIX objects from a collection with pagination via as_pages.

  • Parameters: added_after (str) - ISO timestamp for incremental fetch
  • Returns: dict with total_objects and objects list

parse_stix_bundle(bundle_data)

Parses and categorizes STIX objects: indicators, malware, threat-actors, attack-patterns, campaigns, relationships, identities.

  • Returns: dict with categories and parse_errors

extract_iocs(parsed_bundle)

Extracts actionable IOCs from STIX indicator patterns using regex.

  • IOC types: IPv4, IPv6, domain, URL, MD5, SHA-1, SHA-256, email
  • Returns: dict[str, list[str]] - deduplicated IOC lists

build_relationship_graph(parsed_bundle)

Maps STIX relationship objects into a graph of source -> [{relationship, target}].

  • Returns: dict[str, list[dict]]

STIX Object Types Handled

Type Fields Extracted
indicator id, name, pattern, pattern_type, valid_from, labels
malware id, name, is_family, malware_types
threat-actor id, name, threat_actor_types, aliases
attack-pattern id, name, external_references (ATT&CK IDs)
campaign id, name, first_seen
relationship id, relationship_type, source_ref, target_ref

Environment Variables

Variable Required Description
TAXII_USER No TAXII server username
TAXII_PASSWORD No TAXII server password

Usage

python agent.py https://cti.example.com/taxii/

Scripts 1

agent.py8.2 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""STIX/TAXII threat intelligence feed processor using taxii2-client and stix2."""

import json
import sys

try:
    from taxii2client.v21 import Server, Collection, as_pages
    from stix2 import parse as stix_parse, Malware
    from stix2.exceptions import InvalidValueError
except ImportError:
    print("Install: pip install taxii2-client stix2")
    sys.exit(1)


def discover_server(taxii_url, user=None, password=None):
    """Discover TAXII server API roots and collections."""
    kwargs = {}
    if user and password:
        kwargs["user"] = user
        kwargs["password"] = password
    server = Server(taxii_url, **kwargs)
    roots = []
    for api_root in server.api_roots:
        collections = []
        for coll in api_root.collections:
            collections.append({
                "id": coll.id,
                "title": coll.title,
                "can_read": coll.can_read,
                "can_write": coll.can_write,
                "media_types": getattr(coll, "media_types", []),
            })
        roots.append({
            "title": api_root.title,
            "url": api_root.url,
            "collections": collections,
        })
    return {"server": taxii_url, "api_roots": roots}


def fetch_collection(taxii_url, collection_id, user=None, password=None,
                     added_after=None, limit=100):
    """Fetch STIX objects from a TAXII collection with pagination."""
    kwargs = {}
    if user and password:
        kwargs["user"] = user
        kwargs["password"] = password
    server = Server(taxii_url, **kwargs)
    collection = None
    for api_root in server.api_roots:
        for coll in api_root.collections:
            if coll.id == collection_id:
                collection = coll
                break
    if not collection:
        return {"error": f"Collection {collection_id} not found"}
    fetch_kwargs = {}
    if added_after:
        fetch_kwargs["added_after"] = added_after
    objects = []
    for bundle in as_pages(collection.get_objects, per_request=limit, **fetch_kwargs):
        if "objects" in bundle:
            objects.extend(bundle["objects"])
    return {
        "collection_id": collection_id,
        "total_objects": len(objects),
        "objects": objects,
    }


def parse_stix_bundle(bundle_data):
    """Parse and validate a STIX 2.1 bundle, categorizing objects by type."""
    if isinstance(bundle_data, str):
        bundle_data = json.loads(bundle_data)
    categories = {
        "indicators": [], "malware": [], "threat_actors": [],
        "attack_patterns": [], "campaigns": [], "relationships": [],
        "identities": [], "other": [],
    }
    errors = []
    for obj in bundle_data.get("objects", []):
        obj_type = obj.get("type", "unknown")
        try:
            parsed = stix_parse(obj, allow_custom=True)
            if obj_type == "indicator":
                categories["indicators"].append({
                    "id": parsed.id,
                    "name": getattr(parsed, "name", ""),
                    "pattern": parsed.pattern,
                    "pattern_type": parsed.pattern_type,
                    "valid_from": str(parsed.valid_from),
                    "labels": getattr(parsed, "labels", []),
                })
            elif obj_type == "malware":
                categories["malware"].append({
                    "id": parsed.id,
                    "name": parsed.name,
                    "is_family": parsed.is_family,
                    "malware_types": getattr(parsed, "malware_types", []),
                })
            elif obj_type == "threat-actor":
                categories["threat_actors"].append({
                    "id": parsed.id,
                    "name": parsed.name,
                    "threat_actor_types": getattr(parsed, "threat_actor_types", []),
                    "aliases": getattr(parsed, "aliases", []),
                })
            elif obj_type == "attack-pattern":
                categories["attack_patterns"].append({
                    "id": parsed.id,
                    "name": parsed.name,
                    "external_references": [
                        {"source": r.get("source_name"), "id": r.get("external_id")}
                        for r in getattr(parsed, "external_references", [])
                        if isinstance(r, dict)
                    ],
                })
            elif obj_type == "campaign":
                categories["campaigns"].append({
                    "id": parsed.id,
                    "name": parsed.name,
                    "first_seen": str(getattr(parsed, "first_seen", "")),
                })
            elif obj_type == "relationship":
                categories["relationships"].append({
                    "id": parsed.id,
                    "type": parsed.relationship_type,
                    "source": parsed.source_ref,
                    "target": parsed.target_ref,
                })
            elif obj_type == "identity":
                categories["identities"].append({
                    "id": parsed.id,
                    "name": parsed.name,
                })
            else:
                categories["other"].append({"id": obj.get("id"), "type": obj_type})
        except (InvalidValueError, Exception) as e:
            errors.append({"object_id": obj.get("id"), "error": str(e)})
    return {"categories": categories, "parse_errors": errors}


def extract_iocs(parsed_bundle):
    """Extract actionable IOCs from parsed STIX indicators."""
    iocs = {"ipv4": [], "ipv6": [], "domain": [], "url": [], "hash_md5": [],
            "hash_sha1": [], "hash_sha256": [], "email": []}
    for indicator in parsed_bundle["categories"]["indicators"]:
        pattern = indicator.get("pattern", "")
        import re
        ipv4 = re.findall(r"ipv4-addr:value\s*=\s*'([^']+)'", pattern)
        iocs["ipv4"].extend(ipv4)
        domains = re.findall(r"domain-name:value\s*=\s*'([^']+)'", pattern)
        iocs["domain"].extend(domains)
        urls = re.findall(r"url:value\s*=\s*'([^']+)'", pattern)
        iocs["url"].extend(urls)
        md5 = re.findall(r"MD5\s*=\s*'([a-fA-F0-9]{32})'", pattern)
        iocs["hash_md5"].extend(md5)
        sha256 = re.findall(r"SHA-256\s*=\s*'([a-fA-F0-9]{64})'", pattern)
        iocs["hash_sha256"].extend(sha256)
    for key in iocs:
        iocs[key] = list(set(iocs[key]))
    return iocs


def build_relationship_graph(parsed_bundle):
    """Map relationships between STIX objects."""
    graph = {}
    all_objects = {}
    for cat_name, objects in parsed_bundle["categories"].items():
        for obj in objects:
            if "id" in obj:
                all_objects[obj["id"]] = {"type": cat_name, "name": obj.get("name", obj["id"])}
    for rel in parsed_bundle["categories"]["relationships"]:
        src = rel["source"]
        tgt = rel["target"]
        src_name = all_objects.get(src, {}).get("name", src)
        tgt_name = all_objects.get(tgt, {}).get("name", tgt)
        graph.setdefault(src_name, []).append({
            "relationship": rel["type"], "target": tgt_name,
        })
    return graph


def print_report(parsed, iocs):
    print("STIX/TAXII Feed Processing Report")
    print("=" * 50)
    cats = parsed["categories"]
    print(f"Indicators:      {len(cats['indicators'])}")
    print(f"Malware:         {len(cats['malware'])}")
    print(f"Threat Actors:   {len(cats['threat_actors'])}")
    print(f"Attack Patterns: {len(cats['attack_patterns'])}")
    print(f"Campaigns:       {len(cats['campaigns'])}")
    print(f"Relationships:   {len(cats['relationships'])}")
    print(f"Parse Errors:    {len(parsed['parse_errors'])}")
    print(f"\nExtracted IOCs:")
    for ioc_type, values in iocs.items():
        if values:
            print(f"  {ioc_type}: {len(values)}")
            for v in values[:5]:
                print(f"    - {v}")


if __name__ == "__main__":
    taxii_url = sys.argv[1] if len(sys.argv) > 1 else "https://cti.example.com/taxii/"
    user = os.environ.get("TAXII_USER") if "os" in dir() else None
    import os
    user = os.environ.get("TAXII_USER")
    password = os.environ.get("TAXII_PASSWORD")
    print(f"Discovering TAXII server: {taxii_url}")
    discovery = discover_server(taxii_url, user, password)
    print(json.dumps(discovery, indent=2, default=str))
Keep exploring