Skip to content

Configure OpenCTI integration

In progress

Native OpenCTI pull, push, and scheduled synchronization are not available in ZettelForge 2.8.0. ThreatRecall.ai supports manual STIX 2.1 exchange and a generic, manually triggered TAXII 2.1 pull. This audit did not connect the TAXII path to a live OpenCTI server, so OpenCTI-specific TAXII compatibility remains in progress.

Use this guide to choose a working handoff path today. You can exchange STIX bundles with ThreatRecall.ai, stage a compatible TAXII collection in ThreatRecall.ai, or write relationships from your own connector into ZettelForge.

Prerequisites

  • For the ZettelForge path, install ZettelForge 2.8.0 and complete the Quickstart.
  • For the ThreatRecall.ai paths, use a plan with direct ingestion and create an ingest-scoped API key. A read-capable key is sufficient for export.
  • Export a STIX 2.1 bundle from OpenCTI, or obtain a compatible TAXII 2.1 collection objects URL from your OpenCTI operator.
  • Keep OpenCTI and ThreatRecall credentials out of source-controlled files.

Choose a path

Goal Product and path State
Move a bundle from OpenCTI into managed memory ThreatRecall.ai STIX import Verified in the SaaS route tests
Move a recall session back to OpenCTI ThreatRecall.ai STIX export Implemented with read, TLP, lifecycle, and redistribution gates
Pull a TAXII collection into a review batch ThreatRecall.ai TAXII ingestion Runtime and safety controls verified; live OpenCTI compatibility unverified
Feed selected relationships into local memory ZettelForge ingest_relationship() Verified in ZettelForge 2.8.0
Run native or scheduled OpenCTI synchronization Neither product In progress

Exchange a STIX bundle through ThreatRecall.ai

  1. Export a STIX 2.1 bundle from OpenCTI.
  2. Submit the bundle to POST https://app.threatrecall.ai/api/ingest/stix. Send your ingest-scoped key in X-API-Key and wrap the STIX object in the JSON field bundle. A valid bundle returned HTTP 201 in the route test; a read-scoped key returned HTTP 403 before ingestion.
  3. Run recall in ThreatRecall.ai and retain the returned recall_id.
  4. Request GET https://app.threatrecall.ai/api/recall/sessions/{recall_id}/export.stix with a read-capable key.
  5. Import the returned STIX 2.1 bundle into OpenCTI through the import path your OpenCTI operator supports.

The export route returns application/stix+json;version=2.1 after its policy checks pass. It denies export when source redistribution rules block an object. It also requires an explicit workspace grant when the remaining export contains TLP:RED data. These gates prevent a technically valid STIX handoff from bypassing sharing restrictions.

Stage a TAXII collection in ThreatRecall.ai

ThreatRecall.ai contains a generic TAXII 2.1 pull runtime. It does not contain an OpenCTI-specific adapter or a scheduler.

  1. Ask your ThreatRecall operator to allowlist the TAXII host with TAXII_ALLOWED_HOSTS.
  2. Register the feed with POST /api/threat-intel/feeds. Store the collection objects URL in connector_config_ref and set an explicit default TLP and redistribution state.
  3. Trigger POST /api/threat-intel/batches with input_kind set to taxii_2_1_pull and the registered feed_source_id.
  4. Review the staged items, then commit the approved item identifiers through POST /api/threat-intel/batches/{batch_id}/commit.

The pull requires HTTPS outside local development, rejects hosts that are not allowlisted, revalidates redirect destinations, blocks private or reserved addresses, applies an eight-second default timeout, and caps a response at 512 KiB. The service stages returned STIX objects for review; it does not silently commit them.

OpenCTI compatibility boundary

This run verified the TAXII client and staging code with local fixtures. It did not authenticate to a live OpenCTI collection. The current client sends an Accept header but no API token or other authorization credential, so an authenticated OpenCTI TAXII collection is unsupported. Confirm the collection URL and returned STIX objects in your environment before you depend on this path.

Write relationships into ZettelForge

Use ingest_relationship() when your connector already reads OpenCTI data and you want to add selected edges to the local ZettelForge knowledge graph.

from zettelforge import MemoryManager

mm = MemoryManager()
mm.ingest_relationship(
    from_type="actor",
    from_value="apt28",
    to_type="tool",
    to_value="mimikatz",
    relationship="USES_TOOL",
    properties={"confidence": 0.85, "source": "opencti-export"},
)

relationships = mm.get_entity_relationships("actor", "apt28")
print(len(relationships))
print(relationships[0]["relationship"])
print(relationships[0]["node"]["entity_value"])

Observed output:

1
USES_TOOL
mimikatz

Use canonical lowercase entity values. The lookup path canonicalizes the query before it reads the graph; the audit found that display-case values such as APT28 did not round-trip through this lookup. Repeating the same source, target, and relationship updates the edge properties and keeps one edge.

This method writes a relationship only. Your connector remains responsible for reading OpenCTI, selecting and normalizing objects, handling checkpoints, and scheduling later runs.

Understand the ZettelForge configuration keys

ZettelForge 2.8.0 parses this block from config.yaml:

opencti:
  url: http://localhost:8080
  token: ""
  sync_interval: 0

The matching environment variables are OPENCTI_URL, OPENCTI_TOKEN, and OPENCTI_SYNC_INTERVAL. The defaults are http://localhost:8080, an empty token, and 0 seconds.

These keys do not activate a connector in ZettelForge 2.8.0. Setting a nonzero interval does not start a worker because the release contains no native OpenCTI connector module. Keep the token blank in the YAML file and provide it through the environment if your own connector reads this configuration.