threat intelligence

Modeling Threats with OpenCTI

Model threat actors, intrusion sets, campaigns, and TTPs as a STIX 2.1 knowledge graph in OpenCTI (Filigran) using the pycti Python client, connectors, and import workers for structured cyber threat intelligence.

ctiknowledge-graphmitre-attackopenctipyctistix2threat-intelligencethreat-modeling
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Overview

OpenCTI (Open Cyber Threat Intelligence) is an open-source threat-intelligence platform developed by Filigran that lets analysts store, organize, visualize, and share structured cyber threat intelligence as a knowledge graph. Every object — Threat Actors, Intrusion Sets, Campaigns, Attack Patterns, Malware, Indicators, Observables, Vulnerabilities — is modeled on the STIX 2.1 standard, and the relationships between them (uses, attributed-to, targets, indicates) form a graph that reveals how adversaries operate end to end.

Architecturally, OpenCTI is built from a GraphQL API backed by Elasticsearch/OpenSearch and a graph database, a Redis stream, RabbitMQ message broker, import/export workers, and connectors. Connectors retrieve information from external sources (MITRE ATT&CK, MISP, AlienVault OTX, CISA, abuse.ch, etc.), convert it into STIX 2.1 bundles, and submit those bundles to the platform; workers then ingest the bundles into the graph. The official Python client, pycti (OpenCTIApiClient), is the programmatic interface analysts use to create entities, build relationships, and push STIX bundles.

This skill follows the official OpenCTI documentation (docs.opencti.io) and the OpenCTI-Platform/client-python (pycti) repository. It maps to MITRE ATT&CK T1589 (Gather Victim Identity Information) as part of the broader CTI analysis lifecycle — OpenCTI is where reconnaissance and adversary tradecraft observed across reporting is consolidated, deduplicated, and modeled so detection and response teams can act on it. The threat context is the volume and fragmentation of modern CTI: hundreds of vendor reports, IOC feeds, and ATT&CK updates that are useless until correlated into a single, queryable adversary picture.

When to Use

  • Building a centralized, STIX-native knowledge base of threat actors, campaigns, and TTPs
  • Correlating IOCs and reports from multiple feeds into a single adversary graph
  • Mapping observed activity to MITRE ATT&CK techniques for coverage and gap analysis
  • Producing structured intelligence (STIX bundles) for downstream detection engineering
  • Tracking attribution: which intrusion sets are attributed to which threat actors and campaigns
  • Automating CTI ingestion via connectors and the pycti API

Prerequisites

  • Docker and Docker Compose (OpenCTI is deployed as a container stack)
  • Python 3.8+ for the pycti client:
    pip install pycti stix2
  • An OpenCTI instance and an API token (Profile > API access in the UI)
  • Familiarity with the STIX 2.1 data model (SDOs, SROs, observables)
  • RabbitMQ, Redis, and Elasticsearch/OpenSearch reachable by the platform (handled by the reference compose)

Objectives

  • Deploy an OpenCTI platform with workers via Docker Compose
  • Authenticate to the GraphQL API with pycti using an API token
  • Create core STIX domain objects: Threat Actor, Intrusion Set, Campaign, Attack Pattern, Malware
  • Build relationships (uses, attributed-to, targets) to form the adversary graph
  • Enable connectors (MITRE ATT&CK, MISP) to auto-ingest intelligence
  • Submit STIX 2.1 bundles via send_stix2_bundle
  • Query the graph and export an actor's full TTP profile

MITRE ATT&CK Mapping

ID Name Relevance
T1589 Gather Victim Identity Information OpenCTI consolidates reconnaissance and victim/target intelligence observed across reporting into a structured, queryable knowledge graph that supports analysis of adversary targeting.

Workflow

Step 1: Deploy the OpenCTI platform

Use the official Docker Compose stack. Generate the required tokens/UUIDs and start the platform, workers, and dependencies.

git clone https://github.com/OpenCTI-Platform/docker.git opencti-docker
cd opencti-docker
 
# Generate required secrets (UUID v4 for tokens, base64 for app secret)
cat > .env <<EOF
OPENCTI_ADMIN_EMAIL=admin@opencti.local
OPENCTI_ADMIN_PASSWORD=$(openssl rand -hex 16)
OPENCTI_ADMIN_TOKEN=$(cat /proc/sys/kernel/random/uuid)
OPENCTI_BASE_URL=http://localhost:8080
MINIO_ROOT_USER=$(cat /proc/sys/kernel/random/uuid)
MINIO_ROOT_PASSWORD=$(cat /proc/sys/kernel/random/uuid)
RABBITMQ_DEFAULT_USER=guest
RABBITMQ_DEFAULT_PASS=guest
ELASTIC_MEMORY_SIZE=4G
CONNECTOR_HISTORY_ID=$(cat /proc/sys/kernel/random/uuid)
CONNECTOR_EXPORT_FILE_STIX_ID=$(cat /proc/sys/kernel/random/uuid)
EOF
 
# Increase vm.max_map_count for Elasticsearch, then start the stack
sudo sysctl -w vm.max_map_count=1048575
docker compose up -d

Access the UI at http://localhost:8080 and log in with the admin credentials from .env.

Step 2: Authenticate with pycti

Create an OpenCTIApiClient instance using your platform URL and API token.

from pycti import OpenCTIApiClient
 
opencti = OpenCTIApiClient(
    "http://localhost:8080",
    "YOUR_API_TOKEN",  # from Profile > API access, or OPENCTI_ADMIN_TOKEN
)

Step 3: Create core STIX domain objects

Create a Threat Actor, an Intrusion Set, a Campaign, and an Attack Pattern. pycti create() calls act as upserts when update=True.

# Threat Actor (group)
actor = opencti.threat_actor_group.create(
    name="APT-EXAMPLE",
    description="Financially motivated intrusion group tracked in this case.",
    threat_actor_types=["crime-syndicate"],
)
 
# Intrusion Set
intrusion_set = opencti.intrusion_set.create(
    name="EXAMPLE-SET",
    description="Cluster of activity sharing infrastructure and TTPs.",
)
 
# Campaign
campaign = opencti.campaign.create(
    name="Operation Example 2026",
    description="Spearphishing campaign targeting the finance sector.",
)
 
# Attack Pattern linked to MITRE ATT&CK (x_mitre_id maps to the technique)
technique = opencti.attack_pattern.create(
    name="Spearphishing Attachment",
    x_mitre_id="T1566.001",
)

Step 4: Build relationships to form the graph

Connect the objects with STIX relationships so the graph reflects how the adversary operates.

# Intrusion set attributed to the threat actor
opencti.stix_core_relationship.create(
    fromId=intrusion_set["id"],
    toId=actor["id"],
    relationship_type="attributed-to",
)
 
# Campaign attributed to the intrusion set
opencti.stix_core_relationship.create(
    fromId=campaign["id"],
    toId=intrusion_set["id"],
    relationship_type="attributed-to",
)
 
# Intrusion set uses the technique
opencti.stix_core_relationship.create(
    fromId=intrusion_set["id"],
    toId=technique["id"],
    relationship_type="uses",
)

Step 5: Add indicators and observables

Create an indicator with a STIX pattern and tie it to the intrusion set via an indicates relationship.

from dateutil.parser import parse
 
date = parse("2026-06-01").strftime("%Y-%m-%dT%H:%M:%SZ")
 
indicator = opencti.indicator.create(
    name="C2 domain for Operation Example",
    pattern_type="stix",
    pattern="[domain-name:value = 'malicious-c2.example']",
    x_opencti_main_observable_type="Domain-Name",
    valid_from=date,
)
 
opencti.stix_core_relationship.create(
    fromId=indicator["id"],
    toId=intrusion_set["id"],
    relationship_type="indicates",
)

Step 6: Submit a STIX 2.1 bundle directly

For bulk ingestion, build a STIX bundle and submit it with send_stix2_bundle — the recommended bulk-ingest path.

import json
 
with open("threat_report_bundle.json") as f:
    bundle = json.load(f)
 
opencti.stix2.import_bundle_from_json(
    json.dumps(bundle),
    update=True,
)

Step 7: Enable connectors for automated ingestion

Add connectors to the compose stack so external intelligence (MITRE ATT&CK, MISP) is ingested continuously. Each connector needs its own token.

# Append to docker-compose.yml under services:
  connector-mitre:
    image: opencti/connector-mitre:latest
    environment:
      - OPENCTI_URL=http://opencti:8080
      - OPENCTI_TOKEN=${CONNECTOR_MITRE_TOKEN}
      - CONNECTOR_ID=${CONNECTOR_MITRE_ID}
      - CONNECTOR_TYPE=EXTERNAL_IMPORT
      - CONNECTOR_NAME=MITRE ATT&CK
      - CONNECTOR_SCOPE=tool,report,malware,identity,attack-pattern,intrusion-set,campaign
      - MITRE_INTERVAL=7   # days
    restart: always
docker compose up -d connector-mitre

Step 8: Query the graph and export an actor profile

Read back the adversary's full picture for reporting and detection engineering.

# Resolve all techniques an intrusion set uses
iset = opencti.intrusion_set.read(filters={
    "mode": "and",
    "filters": [{"key": "name", "values": ["EXAMPLE-SET"]}],
    "filterGroups": [],
})
 
rels = opencti.stix_core_relationship.list(
    fromId=iset["id"],
    relationship_type="uses",
)
for r in rels:
    print(r["to"]["name"], r["to"].get("x_mitre_id"))

Tools and Resources

Tool Purpose Source
OpenCTI Platform STIX 2.1 threat-intel knowledge graph https://github.com/OpenCTI-Platform/opencti
OpenCTI Docker Reference compose stack https://github.com/OpenCTI-Platform/docker
pycti Official Python client for the GraphQL API https://github.com/OpenCTI-Platform/client-python
OpenCTI Connectors Importers (MITRE, MISP, OTX, CISA, abuse.ch) https://github.com/OpenCTI-Platform/connectors
OpenCTI docs Official documentation https://docs.opencti.io/latest/
STIX 2.1 spec Underlying data model https://oasis-open.github.io/cti-documentation/

STIX Object Cheat Sheet (pycti entities)

pycti entity STIX type Use
threat_actor_group threat-actor Named adversary group
intrusion_set intrusion-set Clustered activity / tracked set
campaign campaign Time-bounded operation
attack_pattern attack-pattern MITRE ATT&CK technique
malware malware Tooling/implant
indicator indicator Detection pattern (STIX/Sigma/YARA)
vulnerability vulnerability CVE
stix_core_relationship relationship uses, attributed-to, targets, indicates

Validation Criteria

  • OpenCTI platform and workers deployed and reachable at the base URL
  • pycti authenticates with a valid API token
  • Threat Actor, Intrusion Set, Campaign, and Attack Pattern objects created
  • Relationships (attributed-to, uses, indicates) built between objects
  • At least one indicator created and linked to an intrusion set
  • A STIX 2.1 bundle ingested via import_bundle_from_json
  • MITRE ATT&CK connector enabled and importing techniques
  • Intrusion-set TTP profile queryable and exportable
Source materials

References and resources

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

References 2

api-reference.md3.1 KB

API Reference — OpenCTI / pycti

Client initialization

from pycti import OpenCTIApiClient
opencti = OpenCTIApiClient("http://localhost:8080", "API_TOKEN")
Constructor arg Purpose
url Base URL of the OpenCTI platform
token API token (Profile > API access)
ssl_verify Verify TLS (default False for self-hosted dev)
log_level info, debug, etc.

Core entity creators (upsert with update=True)

Method STIX type Key arguments
opencti.threat_actor_group.create(...) threat-actor name, description, threat_actor_types
opencti.intrusion_set.create(...) intrusion-set name, description, first_seen, last_seen
opencti.campaign.create(...) campaign name, description, objective
opencti.attack_pattern.create(...) attack-pattern name, x_mitre_id
opencti.malware.create(...) malware name, is_family, malware_types
opencti.indicator.create(...) indicator name, pattern, pattern_type, x_opencti_main_observable_type, valid_from
opencti.vulnerability.create(...) vulnerability name (CVE id), description
opencti.identity.create(...) identity name, type (organization/individual/sector)

Relationships

opencti.stix_core_relationship.create(
    fromId=..., toId=..., relationship_type="uses")
relationship_type Meaning
uses Actor/set/campaign uses a technique, tool, or malware
attributed-to Campaign -> intrusion-set -> threat-actor
targets Adversary targets an identity/sector/location
indicates Indicator indicates a malware/intrusion-set/campaign
based-on Indicator based-on an observable

Reading / listing

Method Purpose
opencti.intrusion_set.read(filters=...) Read a single object by filter
opencti.stix_core_relationship.list(fromId=..., relationship_type=...) List relationships from an object
opencti.stix_domain_object.list(types=[...]) List SDOs by type

Bundle ingestion

Method Purpose
opencti.stix2.import_bundle_from_json(json_str, update=True) Import a STIX 2.1 bundle (JSON string)
opencti.stix2.import_bundle_from_file(path, update=True) Import a bundle from a file
connector_helper.send_stix2_bundle(bundle) Connector path to send a bundle to workers

Connector environment variables (compose)

Variable Purpose
OPENCTI_URL Platform URL reachable by the connector
OPENCTI_TOKEN Connector-specific API token
CONNECTOR_ID UUID v4 unique per connector
CONNECTOR_TYPE EXTERNAL_IMPORT / INTERNAL_ENRICHMENT / STREAM
CONNECTOR_SCOPE STIX types the connector handles
CONNECTOR_NAME Display name

Deployment quick reference

Command Purpose
docker compose up -d Start platform, workers, dependencies
sysctl -w vm.max_map_count=1048575 Required for Elasticsearch
cat /proc/sys/kernel/random/uuid Generate UUID v4 tokens
standards.md1.1 KB

Standards Mapping — Modeling Threats with OpenCTI

MITRE ATT&CK

ID Name Rationale
T1589 Gather Victim Identity Information OpenCTI consolidates reconnaissance/targeting intelligence and adversary tradecraft from many sources into a single STIX 2.1 knowledge graph for analysis and attribution.

NIST Cybersecurity Framework (CSF 2.0)

ID Name Rationale
ID.RA-03 Internal and external threats to the organization are identified and recorded Modeling threat actors, intrusion sets, campaigns, and TTPs as a structured knowledge graph is precisely the identification and recording of threats this control requires.

Supporting References

Scripts 1

agent.py5.0 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""
opencti_modeler.py — Build a threat-actor knowledge graph in OpenCTI via pycti.

Reads a simple JSON model describing an actor, intrusion set, campaign, techniques
(by MITRE ATT&CK id), and indicators, then creates the STIX domain objects and
relationships in OpenCTI. Can also export an intrusion set's full TTP profile.

Real tooling: pycti OpenCTIApiClient (the official OpenCTI Python client).

Usage:
  export OPENCTI_URL=http://localhost:8080
  export OPENCTI_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  python3 opencti_modeler.py build --model actor_model.json
  python3 opencti_modeler.py export --intrusion-set EXAMPLE-SET

Model file shape (actor_model.json):
{
  "actor": {"name": "APT-EXAMPLE", "description": "...", "types": ["crime-syndicate"]},
  "intrusion_set": {"name": "EXAMPLE-SET", "description": "..."},
  "campaign": {"name": "Operation Example 2026", "description": "..."},
  "techniques": [{"name": "Spearphishing Attachment", "mitre_id": "T1566.001"}],
  "indicators": [{"name": "C2 domain", "pattern": "[domain-name:value = 'bad.example']",
                  "observable_type": "Domain-Name"}]
}
"""
import argparse
import json
import os
import sys
from datetime import datetime, timezone

try:
    from pycti import OpenCTIApiClient
except ImportError:
    sys.exit("[!] pycti not installed. Run: pip install pycti stix2")


def client():
    url = os.environ.get("OPENCTI_URL")
    token = os.environ.get("OPENCTI_TOKEN")
    if not url or not token:
        sys.exit("[!] Set OPENCTI_URL and OPENCTI_TOKEN environment variables.")
    return OpenCTIApiClient(url, token, ssl_verify=False)


def now_iso():
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def relate(api, from_id, to_id, rtype):
    api.stix_core_relationship.create(
        fromId=from_id, toId=to_id, relationship_type=rtype)
    print(f"    + {rtype}: {from_id[:18]}... -> {to_id[:18]}...")


def build(api, model):
    # Threat actor
    a = model.get("actor", {})
    actor = api.threat_actor_group.create(
        name=a["name"], description=a.get("description", ""),
        threat_actor_types=a.get("types", []), update=True)
    print(f"[+] Threat actor: {a['name']} ({actor['id']})")

    # Intrusion set
    s = model.get("intrusion_set", {})
    iset = api.intrusion_set.create(
        name=s["name"], description=s.get("description", ""), update=True)
    print(f"[+] Intrusion set: {s['name']} ({iset['id']})")
    relate(api, iset["id"], actor["id"], "attributed-to")

    # Campaign
    c = model.get("campaign")
    campaign = None
    if c:
        campaign = api.campaign.create(
            name=c["name"], description=c.get("description", ""), update=True)
        print(f"[+] Campaign: {c['name']} ({campaign['id']})")
        relate(api, campaign["id"], iset["id"], "attributed-to")

    # Techniques (attack patterns by MITRE id)
    for t in model.get("techniques", []):
        ap = api.attack_pattern.create(
            name=t["name"], x_mitre_id=t.get("mitre_id"), update=True)
        print(f"[+] Technique: {t['name']} {t.get('mitre_id','')} ({ap['id']})")
        relate(api, iset["id"], ap["id"], "uses")

    # Indicators
    for ind in model.get("indicators", []):
        obj = api.indicator.create(
            name=ind["name"], pattern_type="stix", pattern=ind["pattern"],
            x_opencti_main_observable_type=ind.get("observable_type", "Unknown"),
            valid_from=now_iso(), update=True)
        print(f"[+] Indicator: {ind['name']} ({obj['id']})")
        relate(api, obj["id"], iset["id"], "indicates")

    print("[=] Knowledge graph build complete.")


def export(api, iset_name):
    iset = api.intrusion_set.read(filters={
        "mode": "and",
        "filters": [{"key": "name", "values": [iset_name]}],
        "filterGroups": [],
    })
    if not iset:
        sys.exit(f"[!] Intrusion set not found: {iset_name}")
    print(f"[+] Intrusion set: {iset['name']} ({iset['id']})")
    rels = api.stix_core_relationship.list(
        fromId=iset["id"], relationship_type="uses")
    print(f"[+] Techniques used ({len(rels)}):")
    for r in rels:
        to = r.get("to", {})
        print(f"    - {to.get('name')}  {to.get('x_mitre_id', '')}")


def main():
    ap = argparse.ArgumentParser(description="Model threats in OpenCTI via pycti.")
    sub = ap.add_subparsers(dest="cmd", required=True)

    b = sub.add_parser("build", help="Create the graph from a JSON model")
    b.add_argument("--model", required=True, help="Path to the model JSON file")

    e = sub.add_parser("export", help="Export an intrusion set's TTP profile")
    e.add_argument("--intrusion-set", required=True, help="Intrusion set name")

    args = ap.parse_args()
    api = client()

    if args.cmd == "build":
        try:
            with open(args.model) as f:
                model = json.load(f)
        except (OSError, json.JSONDecodeError) as ex:
            sys.exit(f"[!] Cannot read model: {ex}")
        build(api, model)
    elif args.cmd == "export":
        export(api, args.intrusion_set)


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