In this post

We have built many OpenCTI connectors for our products.

Over that time, we’ve learned how OpenCTI actually ingests, transforms, stores, and exports STIX 2.1 — including where it diverges from the specification in ways that matter to producers.

This post is a practical guide for:

  • developers building OpenCTI connectors
  • teams exporting STIX for OpenCTI ingestion
  • architects designing STIX-native pipelines that include OpenCTI

It assumes you already understand STIX 2.1 and want to know how OpenCTI behaves in production.


The most important mental model

OpenCTI is often described as “STIX-native”.

In practice, it is more accurate to think of it as:

  • a graph platform with its own internal data model
  • that accepts STIX as an ingestion format
  • and exports STIX as an interoperability format

It is not a STIX object store.

When you import a STIX bundle:

  1. OpenCTI parses the bundle
  2. objects are validated against OpenCTI rules (not pure STIX spec)
  3. objects are recreated in OpenCTI’s internal schema
  4. unsupported properties, objects, and relationships are dropped
  5. OpenCTI-specific metadata is added
  6. a new STIX representation is generated on export

This process is not lossless.

Nearly every integration surprise comes from misunderstanding this pipeline.


How ingestion actually works

Think of ingestion as a transformation, not storage:

STIX bundle → validation → OpenCTI graph model → OpenCTI metadata → STIX export

Consequences:

  • IDs may change
  • properties may disappear
  • relationships may be rewritten or rejected
  • markings may be remapped
  • custom objects may be ignored

If you treat OpenCTI as a canonical STIX store, you will eventually hit sync and fidelity problems.


Custom STIX objects: mostly unsupported

OpenCTI does not support arbitrary custom SDOs/SCOs, even when defined correctly with Extension Definitions.

There are a few hardcoded exceptions (e.g. some MITRE ATT&CK extensions), but in general:

  • custom object definitions are ignored
  • custom SDO instances are dropped
  • dependent relationships may partially import but break

Take this bundle from CTI Butler for CWE-520. Note the use of a custom Weakness SDO (with associated Extension Definition).

Result:

  • Weakness objects did not import
  • Extension Definition did not import
  • relationships were created but invalid (as objects missing)

OpenCTI Weakness Import Entities

OpenCTI Weakness Import Relationships

This leaves producers with two choices:

  1. accept data loss
  2. transform custom SDOs into core STIX objects before ingestion

In practice, most connectors implement transformation layers.

This is why some OpenCTI connectors cannot expose full knowledge models (e.g. CWE ingestion in our CTI Butler connector).


Custom properties: sometimes survive, usually don’t

Custom properties behave differently from custom objects.

What happens

  • unsupported custom properties are stripped
  • Extension Definitions are ignored
  • object still imports if the core type is supported

Take this bundle from Vulmatch for CVE-2022-27948. Note the use of a custom x_cpe_struct list property inside Software objects (and accompanying Extension Definition).

Before import:

  • full structured CPE representation exists
    {
      "cpe": "cpe:2.3:o:tesla:model_x_firmware:2022-03-26:*:*:*:*:*:*:*",
      "extensions": {
        "extension-definition--82cad0bb-0906-5885-95cc-cafe5ee0a500": {
          "extension_type": "toplevel-property-extension"
        }
      },
      "id": "software--5ce82760-eec9-5c21-bdf6-aa2e3defcafd",
      "name": "Tesla Model X Firmware 2022-03-26",
      "object_marking_refs": [
        "marking-definition--94868c89-83c2-464b-929b-a1a8aa3c8487",
        "marking-definition--152ecfe1-5015-522b-97e4-86b60c57036d"
      ],
      "spec_version": "2.1",
      "swid": "2DDAA838-E91C-49FC-A3CC-721D5A7A2339",
      "type": "software",
      "vendor": "tesla",
      "version": "2022-03-26",
      "x_cpe_struct": {
        "cpe_version": "2.3",
        "part": "o",
        "vendor": "tesla",
        "product": "model_x_firmware",
        "version": "2022-03-26",
        "update": "*",
        "edition": "*",
        "language": "*",
        "sw_edition": "*",
        "target_sw": "*",
        "target_hw": "*",
        "other": "*"
      },
      "x_created": "2022-04-04T12:38:20.46Z",
      "x_modified": "2022-10-05T14:00:34.4Z",
      "x_revoked": false
    },

After import:

  • object exists
  • custom fields gone
  • OpenCTI metadata added
        {
            "id": "software--372ab49c-b326-5f12-9922-ffb003d050ea",
            "spec_version": "2.1",
            "name": "Tesla Model X Firmware 2022-03-26",
            "cpe": "cpe:2.3:o:tesla:model_x_firmware:2022-03-26:*:*:*:*:*:*:*",
            "swid": "2DDAA838-E91C-49FC-A3CC-721D5A7A2339",
            "vendor": "tesla",
            "version": "2022-03-26",
            "x_opencti_id": "1d7e4f16-ad15-470b-843c-393bf0dec931",
            "x_opencti_type": "Software",
            "type": "software",
            "object_marking_refs": [
                "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9",
                "marking-definition--d328e87d-1265-57ed-b0ef-ec222236e896"
            ]
        },

OpenCTI effectively performs:

  • retain: core STIX properties
  • drop: unknown custom properties
  • add: x_opencti_*

The exception: if you use OpenCTI-supported custom properties (e.g. x_opencti_cvss_vector_string which I talked about in this post), they are preserved. The best authoratitive source we could find that lists all the supported custom properties is here (ctrl+f > “x_”).

Lesson:

If the data must survive ingestion:

  • prefer core STIX properties
  • or use OpenCTI-recognized custom fields

Custom relationship types: restricted

STIX allows user-defined relationship types.

OpenCTI does not.

Only a defined set of relationship types are accepted, and only between allowed object pairs. This part of the OpenCTI codebase lists all available relationship types you can use to link objects.

You must also ensure the objects your joining are supported by the relationship type selected, or, you will again, get errrosrs.

If you attempt to import:

  • unsupported relationship type
  • or supported type between unsupported entities

You will get ingestion errors or silently broken relationships.

OpenCTI Custom Relationships

This is a major limitation for platforms that encode logic via relationships.

Example: In Vulmatch we use not-vulnerable relationships to indicate when a CPE exists but is not affected. These relationship types must be rewritten inside connectors before ingestion. Whilst possible, this causes fidelity issues between the two products.

The lesson: do not get creative with relationship types. In some of our connectors, we rewrite relationship types inside the connector to solve this issue.


OpenCTI modifies your STIX objects

Every imported object is changed.

Understanding these modifications is critical for sync, deduplication, and exports.


ID rewriting

Even valid STIX IDs may be replaced.

OpenCTI generates deterministic IDs based on object properties to prevent duplicates.

Example:

  • Attack Pattern IDs depend on (name OR alias) AND x_mitre_id (if exists)

Result:

  • IDs change unless generated using OpenCTI logic
  • mapping back to original producers becomes difficult

Exception:

  • SCOs retain IDs if generated per STIX UUIDv5 rules

So even if you followed the STIX specification, or used a helper like the stix2 library, only the IDs of SCOs imported will match those you originally produced in OpenCTI.

Adding x_opencti_type

OpenCTI adds an internal classification layer.

Example:

  • STIX identity → OpenCTI may classify as type; organization, sector, individual, etc.

This enables UI navigation and filtering.

OpenCTI Type

Adding x_opencti_id

Every entity receives:

  • an internal database identifier
  • non-portable
  • instance-specific
  • included in exports

Think of it as a primary key, not an intelligence identifier.

To illustrate;

In OpenCTI instance 1:

  • idindicator--1234
  • x_opencti_idabc-111

In OpenCTI instance B

  • idindicator--1234
  • x_opencti_idxyz-999

It’s not an issue on ingest, however, when exporting objects from OpenCTI this property will be included.

Modifying TLP Markings

STIX objects can contain an object_marking_refs property where TLP levels are defined using Marking Definition objects.

We use official STIX 2.1 TLP v2 markings in our tools, but we quickly noticed OpenCTI was always modifying them into a mix of different IDs.

Let me explain the problem by showing you the Marking Definitions between TLPv1/v2:

  • The official STIX 2.1 TLPv2:CLEAR object id: marking-definition--94868c89-83c2-464b-929b-a1a8aa3c8487
  • The OpenCTI TLPv2:CLEAR object id: marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9
  • The official STIX 2.1 v1 TLPv1:WHITE object id: marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9

Notice how OpenCTI essentially write their own TLP:CLEAR object which inherits the ID of the official STIX 2.1 TLPv1:WHITE object.

If you upload an object with the official STIX 2.1 TLPv2:CLEAR marking, OpenCTI will transform it into their TLPv2:CLEAR marking.

For TLPv2 IDs that don’t exist in v1 (e.g. TLP:AMBER+STRICT), they invent their own.

For reference, here is how OpenCTI define all supported TLPs in the code.


The practical rules

After building multiple production connectors, these are the patterns that consistently work.

  • Treat OpenCTI as a transformation engine
    • Not a STIX database.
  • Avoid custom SDOs/SCOs
    • Translate them into STIX 2.1 core objects before ingestion.
  • Assume custom properties will be dropped
    • are an OpenCTI-supported custom field
  • Rewrite relationships inside connectors. Use only
    • supported types
    • supported object pairs
  • Expect ID drift. Plan for:
    • mapping layers
    • reconciliation logic
    • external identifiers
  • Never rely on markings remaining unchanged. Treat markings as:
    • OpenCTI-managed
    • not source-of-truth

In Summary

OpenCTI is extremely powerful, but it is not a passive STIX repository.

It is a graph intelligence platform that:

  • ingests STIX
  • normalizes it
  • enriches it
  • restructures it
  • and exports a new representation

Once you design connectors with that assumption, most ingestion “bugs” disappear, because they are actually transformations.


Vulmatch

Know when software you use is being exploited.

Obstracts

Turn any blog into structured threat intelligence.

Stixify

Your automated threat intelligence analyst.

SIEM Rules

Your detection engineering AI assistant. Turn cyber threat intelligence research into highly-tuned detection rules.

SIEM Rules. Your detection engineering database.

CTI Butler

The most important cyber threat intelligence knowledgebases.

Cyber Threat Exchange

The Market Place for Cyber Threat Intelligence.

Cyber Threat Exchange. The Market Place for Cyber Threat Intelligence.

Discuss this post

Head on over to the dogesec community to discuss this post.

dogesec community

Open-Source Projects

All dogesec commercial products are built in-part from code-bases we have made available under permissive licenses.

dogesec Github

Posted by:

David Greenwood

David Greenwood, Do Only Good Everyday



Never miss an update


Sign up to receive new articles in your inbox as they published.

Your subscription could not be saved. Please try again.
Your subscription has been successful.