threat intelligence

Implementing TAXII Server with OpenTAXII

Deploy and configure an OpenTAXII server to share and consume STIX-formatted cyber threat intelligence using the TAXII 2.1 protocol for automated indicator exchange between organizations.

automationctiindicator-exchangeopentaxiistixtaxiitaxii-serverthreat-sharing
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Overview

TAXII (Trusted Automated eXchange of Intelligence Information) is an OASIS standard protocol for exchanging cyber threat intelligence over HTTPS. OpenTAXII is an open-source TAXII server implementation by EclecticIQ that supports TAXII 1.x, while the OASIS cti-taxii-server provides a TAXII 2.1 reference implementation. This skill covers deploying a TAXII server, configuring collections for threat intelligence feeds, publishing STIX 2.1 bundles, and integrating with SIEM/SOAR platforms for automated indicator ingestion.

When to Use

  • When deploying or configuring implementing taxii server with opentaxii capabilities in your environment
  • When establishing security controls aligned to compliance requirements
  • When building or improving security architecture for this domain
  • When conducting security assessments that require this implementation

Prerequisites

  • Python 3.9+ with medallion, stix2, taxii2-client, opentaxii, cabby libraries
  • Docker and Docker Compose for containerized deployment
  • Understanding of STIX 2.1 objects (Indicator, Malware, Attack Pattern, Relationship)
  • Familiarity with REST APIs and HTTPS configuration
  • TLS certificates for production deployment

Key Concepts

TAXII 2.1 Architecture

TAXII 2.1 defines three services: Discovery (find available API roots), API Root (entry point for collections), and Collections (repositories of CTI objects). Collections support two access models: the Collection endpoint allows consumers to poll for objects, and the Status endpoint tracks the result of add operations. TAXII uses HTTP content negotiation with application/taxii+json;version=2.1.

Sharing Models

TAXII supports hub-and-spoke (central server distributes to consumers), peer-to-peer (bidirectional sharing between partners), and source-subscriber (producer publishes, consumers subscribe) models. Each collection can have read-only, write-only, or read-write access controls.

STIX 2.1 Content

TAXII transports STIX 2.1 bundles containing Structured Threat Information objects: Indicators (detection patterns), Observed Data, Malware, Attack Patterns, Threat Actors, Intrusion Sets, Campaigns, Relationships, and Sightings. Each object has a unique STIX ID, creation/modification timestamps, and optional TLP marking definitions.

Workflow

Step 1: Deploy TAXII 2.1 Server with Medallion

# Install medallion (OASIS reference implementation)
# pip install medallion
 
# medallion_config.json
import json
 
config = {
    "backend": {
        "module_class": "MemoryBackend",
        "filename": "taxii_data.json"
    },
    "users": {
        "admin": "admin_password_change_me",
        "analyst": "analyst_password_change_me",
        "readonly": "readonly_password_change_me"
    },
    "taxii": {
        "max_content_length": 10485760
    }
}
 
# Create initial data store
taxii_data = {
    "discovery": {
        "title": "Threat Intelligence TAXII Server",
        "description": "TAXII 2.1 server for sharing CTI indicators",
        "contact": "soc@organization.com",
        "default": "https://taxii.organization.com/api/",
        "api_roots": ["https://taxii.organization.com/api/"]
    },
    "api_roots": {
        "api": {
            "title": "Threat Intelligence API Root",
            "description": "Primary API root for threat intelligence sharing",
            "versions": ["application/taxii+json;version=2.1"],
            "max_content_length": 10485760,
            "collections": {
                "malware-iocs": {
                    "id": "91a7b528-80eb-42ed-a74d-c6fbd5a26116",
                    "title": "Malware IOCs",
                    "description": "Indicators of compromise from malware analysis",
                    "can_read": True,
                    "can_write": True,
                    "media_types": ["application/stix+json;version=2.1"]
                },
                "apt-intelligence": {
                    "id": "52892447-4d7e-4f70-b94a-5460e242dd23",
                    "title": "APT Intelligence",
                    "description": "Advanced persistent threat group intelligence",
                    "can_read": True,
                    "can_write": True,
                    "media_types": ["application/stix+json;version=2.1"]
                },
                "phishing-indicators": {
                    "id": "64993447-4d7e-4f70-b94a-5460e242ee34",
                    "title": "Phishing Indicators",
                    "description": "Phishing URLs, domains, and email indicators",
                    "can_read": True,
                    "can_write": True,
                    "media_types": ["application/stix+json;version=2.1"]
                }
            }
        }
    }
}
 
with open("medallion_config.json", "w") as f:
    json.dump(config, f, indent=2)
with open("taxii_data.json", "w") as f:
    json.dump(taxii_data, f, indent=2)
print("[+] TAXII server configuration created")

Step 2: Docker Deployment

# docker-compose.yml
version: '3.8'
services:
  taxii-server:
    image: python:3.11-slim
    container_name: taxii-server
    working_dir: /app
    volumes:
      - ./medallion_config.json:/app/medallion_config.json
      - ./taxii_data.json:/app/taxii_data.json
      - ./certs:/app/certs
    ports:
      - "6100:6100"
    command: >
      bash -c "pip install medallion &&
      medallion --host 0.0.0.0 --port 6100
      --config /app/medallion_config.json"
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:6100/taxii2/"]
      interval: 30s
      timeout: 10s
      retries: 3

Step 3: Publish STIX 2.1 Objects to Collections

from stix2 import Indicator, Malware, Relationship, Bundle, TLP_WHITE
from taxii2client.v21 import Server, Collection, as_pages
import json
from datetime import datetime
 
class TAXIIPublisher:
    def __init__(self, server_url, username, password):
        self.server = Server(
            server_url,
            user=username,
            password=password,
        )
 
    def list_collections(self):
        """List all available collections."""
        api_root = self.server.api_roots[0]
        for collection in api_root.collections:
            print(f"  [{collection.id}] {collection.title} "
                  f"(read={collection.can_read}, write={collection.can_write})")
        return api_root.collections
 
    def publish_indicators(self, collection_id, indicators):
        """Publish STIX indicators to a TAXII collection."""
        api_root = self.server.api_roots[0]
        collection = Collection(
            f"{api_root.url}collections/{collection_id}/",
            user=self.server._user,
            password=self.server._password,
        )
        bundle = Bundle(objects=indicators)
        response = collection.add_objects(bundle.serialize())
        print(f"[+] Published {len(indicators)} objects to {collection_id}")
        print(f"    Status: {response.status}")
        return response
 
    def create_malware_indicators(self):
        """Create sample STIX malware indicators."""
        malware = Malware(
            name="SUNBURST",
            description="Backdoor used in SolarWinds supply chain attack (2020). "
                        "Trojanized SolarWinds.Orion.Core.BusinessLayer.dll module.",
            malware_types=["backdoor", "trojan"],
            is_family=True,
            object_marking_refs=[TLP_WHITE],
        )
 
        indicator_hash = Indicator(
            name="SUNBURST SHA-256 Hash",
            description="SHA-256 hash of trojanized SolarWinds Orion DLL",
            pattern="[file:hashes.'SHA-256' = "
                    "'32519b85c0b422e4656de6e6c41878e95fd95026267daab4215ee59c107d6c77']",
            pattern_type="stix",
            valid_from=datetime(2020, 12, 13),
            indicator_types=["malicious-activity"],
            object_marking_refs=[TLP_WHITE],
        )
 
        indicator_domain = Indicator(
            name="SUNBURST C2 Domain Pattern",
            description="DGA domain pattern used by SUNBURST for C2",
            pattern="[domain-name:value MATCHES "
                    "'^[a-z0-9]{4,}\\.appsync-api\\..*\\.avsvmcloud\\.com$']",
            pattern_type="stix",
            valid_from=datetime(2020, 12, 13),
            indicator_types=["malicious-activity"],
            object_marking_refs=[TLP_WHITE],
        )
 
        rel = Relationship(
            relationship_type="indicates",
            source_ref=indicator_hash.id,
            target_ref=malware.id,
        )
 
        return [malware, indicator_hash, indicator_domain, rel]
 
publisher = TAXIIPublisher(
    "https://taxii.organization.com/taxii2/",
    "admin", "admin_password_change_me"
)
collections = publisher.list_collections()
indicators = publisher.create_malware_indicators()
publisher.publish_indicators("91a7b528-80eb-42ed-a74d-c6fbd5a26116", indicators)

Step 4: Consume Intelligence from TAXII Collections

from taxii2client.v21 import Server, Collection, as_pages
import json
 
class TAXIIConsumer:
    def __init__(self, server_url, username, password):
        self.server = Server(server_url, user=username, password=password)
 
    def poll_collection(self, collection_id, added_after=None):
        """Poll a collection for new STIX objects."""
        api_root = self.server.api_roots[0]
        collection = Collection(
            f"{api_root.url}collections/{collection_id}/",
            user=self.server._user,
            password=self.server._password,
        )
 
        kwargs = {}
        if added_after:
            kwargs["added_after"] = added_after
 
        all_objects = []
        for bundle in as_pages(collection.get_objects, per_request=50, **kwargs):
            objects = json.loads(bundle).get("objects", [])
            all_objects.extend(objects)
 
        indicators = [o for o in all_objects if o.get("type") == "indicator"]
        malware = [o for o in all_objects if o.get("type") == "malware"]
        relationships = [o for o in all_objects if o.get("type") == "relationship"]
 
        print(f"[+] Polled {len(all_objects)} objects: "
              f"{len(indicators)} indicators, {len(malware)} malware, "
              f"{len(relationships)} relationships")
        return all_objects
 
    def extract_iocs_for_siem(self, stix_objects):
        """Extract IOCs from STIX objects for SIEM ingestion."""
        iocs = []
        for obj in stix_objects:
            if obj.get("type") == "indicator":
                pattern = obj.get("pattern", "")
                iocs.append({
                    "id": obj.get("id"),
                    "name": obj.get("name", ""),
                    "pattern": pattern,
                    "valid_from": obj.get("valid_from", ""),
                    "indicator_types": obj.get("indicator_types", []),
                    "confidence": obj.get("confidence", 0),
                })
        return iocs
 
consumer = TAXIIConsumer(
    "https://taxii.organization.com/taxii2/",
    "analyst", "analyst_password_change_me"
)
objects = consumer.poll_collection("91a7b528-80eb-42ed-a74d-c6fbd5a26116")
iocs = consumer.extract_iocs_for_siem(objects)

Step 5: Integrate with SIEM/SOAR

import requests
 
def push_to_splunk(iocs, splunk_url, hec_token):
    """Push extracted IOCs to Splunk via HEC."""
    headers = {"Authorization": f"Splunk {hec_token}"}
    for ioc in iocs:
        event = {
            "event": ioc,
            "sourcetype": "stix:indicator",
            "source": "taxii-server",
            "index": "threat_intel",
        }
        resp = requests.post(
            f"{splunk_url}/services/collector/event",
            headers=headers,
            json=event,
            verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true",  # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
        )
        if resp.status_code != 200:
            print(f"[-] Splunk HEC error: {resp.text}")
    print(f"[+] Pushed {len(iocs)} IOCs to Splunk")
 
def push_to_elasticsearch(iocs, es_url, index="threat-intel"):
    """Push IOCs to Elasticsearch."""
    for ioc in iocs:
        resp = requests.post(
            f"{es_url}/{index}/_doc",
            json=ioc,
            headers={"Content-Type": "application/json"},
        )
        if resp.status_code not in (200, 201):
            print(f"[-] ES error: {resp.text}")
    print(f"[+] Indexed {len(iocs)} IOCs in Elasticsearch")

Validation Criteria

  • TAXII 2.1 server deployed and accessible via HTTPS
  • Collections created with appropriate read/write permissions
  • STIX 2.1 bundles published successfully to collections
  • Consumer can poll and retrieve objects with filtering
  • IOCs extracted and forwarded to SIEM platform
  • Authentication and authorization enforced correctly

References

Source materials

References and resources

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

References 1

api-reference.md4.2 KB

API Reference: OpenTAXII Server

Libraries Used

Library Purpose
opentaxii TAXII 1.x and 2.x server implementation
taxii2-client TAXII 2.1 client for testing and integration
stix2 Create and parse STIX 2.1 objects
requests HTTP client for direct API testing

Installation

# Server
pip install opentaxii
 
# Client and testing
pip install taxii2-client stix2 requests

Server Configuration

opentaxii.yml

---
persistence_api:
  class: opentaxii.persistence.sqldb.SQLDatabaseAPI
  parameters:
    db_connection: sqlite:////tmp/opentaxii.db
    create_tables: true
 
auth_api:
  class: opentaxii.auth.sqldb.SQLDatabaseAuth
  parameters:
    db_connection: sqlite:////tmp/opentaxii.db
    create_tables: true
    secret: "change-this-secret-in-production"
 
taxii1:
  save_raw_inbox_messages: false
 
logging:
  opentaxii: info
  root: info

Start the Server

# Set config path
export OPENTAXII_CONFIG=/path/to/opentaxii.yml
 
# Run the server
opentaxii-run-dev --host 0.0.0.0 --port 9000
 
# Production (with gunicorn)
gunicorn opentaxii.http:app --bind 0.0.0.0:9000

TAXII 2.1 API Endpoints

Method Endpoint Description
GET /taxii2/ Server discovery
GET /{api-root}/ API root information
GET /{api-root}/collections/ List collections
GET /{api-root}/collections/{id}/ Get collection details
GET /{api-root}/collections/{id}/objects/ Get STIX objects
POST /{api-root}/collections/{id}/objects/ Add STIX objects
GET /{api-root}/collections/{id}/manifest/ Object manifest
GET /{api-root}/status/{id}/ Async operation status

Server Administration

Create Collections via CLI

opentaxii-create-services -c services.yml
opentaxii-create-collections -c collections.yml
opentaxii-create-account --username admin --password admin123

collections.yml

---
- name: "threat-indicators"
  id: "collection-001"
  description: "Threat intelligence indicators"
  type: "DATA_FEED"
  accept_all_content: true
  can_read: true
  can_write: true
 
- name: "malware-samples"
  id: "collection-002"
  description: "Malware sample hashes and metadata"
  type: "DATA_SET"
  can_read: true
  can_write: false

Client Operations

Discover Server and Collections

from taxii2client.v21 import Server
import os
 
server = Server(
    os.environ.get("OPENTAXII_URL", "http://localhost:9000/taxii2/"),
    user=os.environ.get("TAXII_USER", "admin"),
    password=os.environ.get("TAXII_PASS", "admin123"),
)
 
for api_root in server.api_roots:
    print(f"API Root: {api_root.title}")
    for coll in api_root.collections:
        print(f"  {coll.title} (ID: {coll.id})")
        print(f"  Read: {coll.can_read} | Write: {coll.can_write}")

Push STIX Objects to a Collection

import stix2
from taxii2client.v21 import Collection
 
collection = Collection(
    f"http://localhost:9000/collections/collection-001/",
    user="admin",
    password="admin123",
)
 
indicator = stix2.Indicator(
    name="Malicious C2 Domain",
    pattern="[domain-name:value = 'evil.example.com']",
    pattern_type="stix",
    valid_from="2025-01-15T00:00:00Z",
    labels=["malicious-activity"],
)
 
bundle = stix2.Bundle(objects=[indicator])
collection.add_objects(bundle.serialize())

Fetch Objects from a Collection

objects = collection.get_objects()
for obj in objects.get("objects", []):
    print(f"  {obj['type']}: {obj.get('name', obj['id'])}")

Health Check

import requests
 
resp = requests.get(
    "http://localhost:9000/taxii2/",
    auth=("admin", "admin123"),
    timeout=10,
)
if resp.status_code == 200:
    discovery = resp.json()
    print(f"Server title: {discovery.get('title')}")
    print(f"API roots: {discovery.get('api_roots', [])}")

Output Format

{
  "title": "OpenTAXII TAXII 2.1 Server",
  "description": "Threat intelligence sharing server",
  "api_roots": ["http://localhost:9000/api/"],
  "collections": [
    {
      "id": "collection-001",
      "title": "threat-indicators",
      "can_read": true,
      "can_write": true,
      "media_types": ["application/stix+json;version=2.1"]
    }
  ]
}

Scripts 1

agent.py8.5 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""OpenTAXII server configuration and health audit agent.

Audits an OpenTAXII server instance by checking service discovery,
collection availability, content block statistics, and API health.
Supports both TAXII 1.1 and 2.0/2.1 endpoints.
"""
import argparse
import json
import os
import sys
from datetime import datetime, timezone

try:
    import requests
except ImportError:
    print("[!] 'requests' required: pip install requests", file=sys.stderr)
    sys.exit(1)

try:
    from taxii2client.v20 import Server as Server20
    from taxii2client.v21 import Server as Server21
    HAS_TAXII_CLIENT = True
except ImportError:
    HAS_TAXII_CLIENT = False


def check_taxii1_discovery(base_url, username=None, password=None):
    """Check TAXII 1.1 discovery service."""
    findings = []
    discovery_url = f"{base_url}/services/discovery"
    print(f"[*] Checking TAXII 1.1 discovery: {discovery_url}")

    headers = {"Content-Type": "application/xml",
               "X-TAXII-Content-Type": "urn:taxii.mitre.org:message:xml:1.1",
               "X-TAXII-Protocol": "urn:taxii.mitre.org:protocol:http:1.0"}
    discovery_xml = (
        '<Discovery_Request xmlns="http://taxii.mitre.org/messages/taxii_xml_binding-1.1" '
        'message_id="1"/>'
    )

    auth = (username, password) if username else None
    try:
        resp = requests.post(discovery_url, data=discovery_xml, headers=headers,
                             auth=auth, timeout=15)
        if resp.status_code == 200:
            findings.append({
                "check": "TAXII 1.1 Discovery",
                "status": "PASS",
                "severity": "INFO",
                "detail": f"Discovery service responding ({len(resp.content)} bytes)",
            })
        else:
            findings.append({
                "check": "TAXII 1.1 Discovery",
                "status": "FAIL",
                "severity": "HIGH",
                "detail": f"HTTP {resp.status_code}",
            })
    except requests.RequestException as e:
        findings.append({
            "check": "TAXII 1.1 Discovery",
            "status": "FAIL",
            "severity": "HIGH",
            "detail": str(e)[:100],
        })

    return findings


def check_taxii2_discovery(base_url, username=None, password=None, version="2.1"):
    """Check TAXII 2.0/2.1 discovery and collections."""
    findings = []
    print(f"[*] Checking TAXII {version} discovery: {base_url}")

    if HAS_TAXII_CLIENT:
        try:
            kwargs = {}
            if username and password:
                kwargs["user"] = username
                kwargs["password"] = password
            if version == "2.0":
                server = Server20(base_url, **kwargs)
            else:
                server = Server21(base_url, **kwargs)

            findings.append({
                "check": f"TAXII {version} Discovery",
                "status": "PASS",
                "severity": "INFO",
                "detail": f"Server: {server.title or 'Untitled'}",
            })

            for api_root in server.api_roots:
                collections = list(api_root.collections)
                findings.append({
                    "check": f"API Root: {api_root.title or api_root.url}",
                    "status": "PASS",
                    "severity": "INFO",
                    "detail": f"{len(collections)} collections",
                    "collections": [{
                        "id": c.id,
                        "title": c.title,
                        "can_read": getattr(c, "can_read", True),
                        "can_write": getattr(c, "can_write", False),
                    } for c in collections],
                })

        except Exception as e:
            findings.append({
                "check": f"TAXII {version} Discovery",
                "status": "FAIL",
                "severity": "HIGH",
                "detail": str(e)[:100],
            })
    else:
        # Fallback to raw HTTP
        auth = (username, password) if username else None
        headers = {"Accept": "application/taxii+json;version=2.1"}
        try:
            resp = requests.get(base_url, headers=headers, auth=auth, timeout=15)
            if resp.status_code == 200:
                data = resp.json()
                findings.append({
                    "check": f"TAXII {version} Discovery",
                    "status": "PASS",
                    "severity": "INFO",
                    "detail": f"Title: {data.get('title', 'N/A')}, "
                              f"API Roots: {len(data.get('api_roots', []))}",
                })
            else:
                findings.append({
                    "check": f"TAXII {version} Discovery",
                    "status": "FAIL",
                    "severity": "HIGH",
                    "detail": f"HTTP {resp.status_code}",
                })
        except requests.RequestException as e:
            findings.append({
                "check": f"TAXII {version} Discovery",
                "status": "FAIL",
                "severity": "HIGH",
                "detail": str(e)[:100],
            })

    return findings


def check_server_health(base_url):
    """Check basic server health."""
    findings = []
    print(f"[*] Checking server health: {base_url}")

    # Check TLS
    if base_url.startswith("https://"):
        findings.append({
            "check": "TLS enabled",
            "status": "PASS",
            "severity": "INFO",
        })
    else:
        findings.append({
            "check": "TLS enabled",
            "status": "FAIL",
            "severity": "HIGH",
            "detail": "TAXII server not using HTTPS",
        })

    # Check response time
    try:
        resp = requests.get(base_url, timeout=10)
        response_time = resp.elapsed.total_seconds()
        if response_time > 5:
            findings.append({
                "check": "Response time",
                "status": "WARN",
                "severity": "MEDIUM",
                "detail": f"{response_time:.2f}s (slow)",
            })
        else:
            findings.append({
                "check": "Response time",
                "status": "PASS",
                "severity": "INFO",
                "detail": f"{response_time:.2f}s",
            })
    except requests.RequestException:
        pass

    return findings


def format_summary(all_findings, base_url):
    """Print audit summary."""
    print(f"\n{'='*60}")
    print(f"  OpenTAXII Server Audit Report")
    print(f"{'='*60}")
    print(f"  Server       : {base_url}")
    print(f"  Findings     : {len(all_findings)}")

    pass_count = sum(1 for f in all_findings if f["status"] == "PASS")
    fail_count = sum(1 for f in all_findings if f["status"] == "FAIL")
    print(f"  Passed       : {pass_count}")
    print(f"  Failed       : {fail_count}")

    for f in all_findings:
        icon = "OK" if f["status"] == "PASS" else "!!" if f["status"] == "FAIL" else "~~"
        print(f"    [{icon}] {f['check']}: {f.get('detail', '')[:50]}")

    severity_counts = {}
    for f in all_findings:
        sev = f.get("severity", "INFO")
        severity_counts[sev] = severity_counts.get(sev, 0) + 1
    return severity_counts


def main():
    parser = argparse.ArgumentParser(description="OpenTAXII server audit agent")
    parser.add_argument("--url", required=True, help="TAXII server base URL")
    parser.add_argument("--username", help="Authentication username")
    parser.add_argument("--password", help="Authentication password")
    parser.add_argument("--version", choices=["1.1", "2.0", "2.1"], default="2.1")
    parser.add_argument("--output", "-o", help="Output JSON report")
    parser.add_argument("--verbose", "-v", action="store_true")
    args = parser.parse_args()

    all_findings = []
    all_findings.extend(check_server_health(args.url))

    if args.version == "1.1":
        all_findings.extend(check_taxii1_discovery(args.url, args.username, args.password))
    else:
        all_findings.extend(check_taxii2_discovery(args.url, args.username, args.password, args.version))

    severity_counts = format_summary(all_findings, args.url)

    report = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "tool": "OpenTAXII Audit",
        "server": args.url,
        "version": args.version,
        "findings": all_findings,
        "severity_counts": severity_counts,
    }

    if args.output:
        with open(args.output, "w") as f:
            json.dump(report, f, indent=2)
        print(f"\n[+] Report saved to {args.output}")
    elif args.verbose:
        print(json.dumps(report, indent=2))


if __name__ == "__main__":
    main()
Keep exploring