You know those moments when a quick integration turns into a full-blown existential crisis?
That was me last week — trying to make Vulmatch play nicely with OpenCTI.
Let’s start with a game of Spot the Difference.
Image 1:

Image 2:

What are the differences between image 1 and image 2?
Even your partner who knows nothing about cybersecurity will notice the first image has EPSS, KEV, and CVSS details. The second? Crickets.
The first came from OpenCTI’s demo server.
The second — imported from Vulmatch — was missing all that juicy context (despite Vulmatch containing far more data: MITRE ATT&CK, CWE, and a buffet of other attributes).
“Fine,” I thought. “I’ll just copy the custom fields over. Easy.”
Cue the Benny Hill music.
The Naïve Plan
We will call this Plan A.
To figure out how those values were rendered, I downloaded one of the working Vulnerability objects from OpenCTI.
{
"id": "vulnerability--06246027-2025-5b73-b2af-08c56339f009",
"spec_version": "2.1",
"revoked": false,
"confidence": 80,
"created": "2022-02-28T15:15:07.983Z",
"modified": "2025-07-23T02:14:20.068Z",
"name": "CVE-2021-43086",
"description": "ARM astcenc 3.2.0 is vulnerable to Buffer Overflow. When the compression function of the astc-encoder project with -cl option was used, a stack-buffer-overflow occurred in function encode_ise() in function compress_symbolic_block_for_partition_2planes() in \"/Source/astcenc_compress_symbolic.cpp\".",
"x_opencti_cvss_vector_string": "CVSS:3.1/AV:N/C:H/I:H/A:H",
"x_opencti_cvss_base_score": 9.8,
"x_opencti_cvss_base_severity": "CRITICAL",
"x_opencti_cvss_attack_vector": "NETWORK",
"x_opencti_cvss_confidentiality_impact": "HIGH",
"x_opencti_cvss_integrity_impact": "HIGH",
"x_opencti_cvss_availability_impact": "HIGH",
"x_opencti_cisa_kev": false,
"x_opencti_epss_score": 0.00365,
"x_opencti_epss_percentile": 0.57706,
"external_references": [
{
"source_name": "[email protected]",
"url": "https://github.com/ARM-software/astc-encoder/issues/296."
},
{
"source_name": "NIST NVD",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-43086"
}
],
"x_opencti_id": "5340d041-4663-409f-b2f1-00107918f4f7",
"x_opencti_type": "Vulnerability",
"type": "vulnerability",
"created_by_ref": "identity--f11b0831-e7e6-5214-9431-ccf054e53e94"
}
Looks simple, right?
Just create matching custom properties (like x_opencti_epss_score) in Vulmatch and OpenCTI will render them.
Problem solved.
Famous. Last. Words.
How STIX Extensions Actually Work
Before I spiral into the thread that unwound, let’s rewind and talk about how STIX objects should be extended with new properties.
The STIX 2.1 Specification covers a lot of ground.
But sometimes, it’s just not broad enough — either the concept you need doesn’t exist, or the object doesn’t have the right properties.
Back in the early days of STIX (around 2019), adding new fields was easy: prefix them with x_ and call it innovation.
MITRE ATT&CK still does this today — take T1003.008 as an example:
{
"created": "2020-02-11T18:46:56.263Z",
"created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5",
"description": "Adversaries may attempt to dump the contents of <code>/etc/passwd</code> and <code>/etc/shadow</code> to enable offline password cracking. Most modern Linux operating systems use a combination of <code>/etc/passwd</code> and <code>/etc/shadow</code> to store user account information, including password hashes in <code>/etc/shadow</code>. By default, <code>/etc/shadow</code> is only readable by the root user.(Citation: Linux Password and Shadow File Formats)\n\nLinux stores user information such as user ID, group ID, home directory path, and login shell in <code>/etc/passwd</code>. A \"user\" on the system may belong to a person or a service. All password hashes are stored in <code>/etc/shadow</code> - including entries for users with no passwords and users with locked or disabled accounts.(Citation: Linux Password and Shadow File Formats)\n\nAdversaries may attempt to read or dump the <code>/etc/passwd</code> and <code>/etc/shadow</code> files on Linux systems via command line utilities such as the <code>cat</code> command.(Citation: Arctic Wolf) Additionally, the Linux utility <code>unshadow</code> can be used to combine the two files in a format suited for password cracking utilities such as John the Ripper - for example, via the command <code>/usr/bin/unshadow /etc/passwd /etc/shadow > /tmp/crack.password.db</code>(Citation: nixCraft - John the Ripper). Since the user information stored in <code>/etc/passwd</code> are linked to the password hashes in <code>/etc/shadow</code>, an adversary would need to have access to both.",
"external_references": [
{
"source_name": "mitre-attack",
"url": "https://attack.mitre.org/techniques/T1003/008",
"external_id": "T1003.008"
},
{
"source_name": "Arctic Wolf",
"description": "Julian Tuin, Stefan Hostetler, Jon Grimm, Aaron Diaz, and Trevor Daher. (2024, November 22). Arctic Wolf Observes Threat Campaign Targeting Palo Alto Networks Firewall Devices. Retrieved January 8, 2025.",
"url": "https://arcticwolf.com/resources/blog/arctic-wolf-observes-threat-campaign-targeting-palo-alto-networks-firewall-devices/"
},
{
"source_name": "Linux Password and Shadow File Formats",
"description": "The Linux Documentation Project. (n.d.). Linux Password and Shadow File Formats. Retrieved February 19, 2020.",
"url": "https://www.tldp.org/LDP/lame/LAME/linux-admin-made-easy/shadow-file-formats.html"
},
{
"source_name": "nixCraft - John the Ripper",
"description": "Vivek Gite. (2014, September 17). Linux Password Cracking: Explain unshadow and john Commands (John the Ripper Tool). Retrieved February 19, 2020.",
"url": "https://www.cyberciti.biz/faq/unix-linux-password-cracking-john-the-ripper/"
}
],
"id": "attack-pattern--d0b4fcdb-d67d-4ed2-99ce-788b12f8c0f4",
"kill_chain_phases": [
{
"kill_chain_name": "mitre-attack",
"phase_name": "credential-access"
}
],
"modified": "2025-04-15T19:59:09.955Z",
"name": "/etc/passwd and /etc/shadow",
"object_marking_refs": [
"marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"
],
"revoked": false,
"spec_version": "2.1",
"type": "attack-pattern",
"x_mitre_attack_spec_version": "3.2.0",
"x_mitre_data_sources": [
"File: File Access",
"Command: Command Execution"
],
"x_mitre_deprecated": false,
"x_mitre_detection": "The AuditD monitoring tool, which ships stock in many Linux distributions, can be used to watch for hostile processes attempting to access <code>/etc/passwd</code> and <code>/etc/shadow</code>, alerting on the pid, process name, and arguments of such programs.",
"x_mitre_domains": [
"enterprise-attack"
],
"x_mitre_is_subtechnique": true,
"x_mitre_modified_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5",
"x_mitre_platforms": [
"Linux"
],
"x_mitre_version": "1.2"
}
Looks neat. Clear naming, clear intent.
But over time, everyone started inventing their own custom fields, and suddenly the STIX world looked less like structured intelligence and more like a data swamp.
Poor naming conventions. Unclear definitions. Just… chaos.
The STIX team realised this pretty quickly and introduced STIX Extensions to the spec — a way to formally define your new properties and link them to a schema.
Example: Defining Software Extensions in Vulmatch
Here’s how we do it in Vulmatch.
We define an Extension Definition that references a schema.
{
"type": "extension-definition",
"spec_version": "2.1",
"id": "extension-definition--82cad0bb-0906-5885-95cc-cafe5ee0a500",
"created_by_ref": "identity--9779a2db-f98c-5f4b-8d08-8ee04e02dbb5",
"created": "2020-01-01T00:00:00.000Z",
"modified": "2025-10-03T00:00:00.000Z",
"name": "Software SCO CPE Properties",
"description": "This extension adds new properties to Software SCOs to capture CPE data.",
"schema": "https://raw.githubusercontent.com/muchdogesec/stix2extensions/main/schemas/properties/software-cpe-properties.json",
"version": "1.0",
"extension_types": [
"toplevel-property-extension"
],
"extension_properties": [
"x_cpe_struct",
"x_revoked",
"x_created",
"x_modified"
],
"object_marking_refs": [
"marking-definition--94868c89-83c2-464b-929b-a1a8aa3c8487",
"marking-definition--60c0f466-511a-5419-9f7e-4814e696da40"
]
}
The purpose of this object is to define the schema, not to store data itself.
Here’s an example of a software object making use of this extension;
{
"type": "software",
"spec_version": "2.1",
"id": "software--fda5adb5-23e5-5089-a256-36f298ba241f",
"name": "EGroupware 14.1.20140710 Community Edition",
"cpe": "cpe:2.3:a:egroupware:egroupware:14.1.20140710:*:*:*:community:*:*:*",
"swid": "A1F2EAFC-0523-4257-A9EA-94462CA2BDB8",
"languages": [
"en"
],
"vendor": "egroupware",
"version": "14.1.20140710",
"extensions": {
"extension-definition--82cad0bb-0906-5885-95cc-cafe5ee0a500": {
"extension_type": "toplevel-property-extension"
}
},
"x_revoked": false,
"x_created": "2020-01-01T00:00:00.000Z",
"x_modified": "2020-01-01T00:00:00.000Z",
"x_cpe_struct": {
"cpe_version": "2.3",
"edition": "*",
"language": "*",
"other": "*",
"part": "a",
"product": "egroupware",
"sw_edition": "community",
"vendor": "egroupware",
"target_hw": "*",
"version": "14.1.20140710",
"target_sw": "*",
"update": "*"
}
}
Notice the extensions property — it tells anyone reading this object which schema to use to interpret it.
It’s self-documenting, clean, and future-you friendly.
Strictly speaking you don’t need to use a x_ property when adding properties, but it does make it a lot easier for people reading them.
The Reality Check
Back to OpenCTI.
Looking at their Vulnerability object, the property naming looked clear enough — it totally baited me into thinking this would be a two-minute job.
My first issue: unversioned CVSS properties.
Is x_opencti_cvss_base_score referring to CVSS v2, v3, or v4?
No clue. The UI shows multiple versions.
So I did what every frustrated developer does: I opened the code.
And there it was.
Over fifty custom properties that could be added to Vulnerability objects.
At least I got my answer — each CVSS version had its own full set of fields:
x_opencti_cvss_v2_vector_stringx_opencti_cvss_vector_string(for 3.x)x_opencti_cvss_v4_vector_string
Then I hit the next question: If I provide a CVSS vector string, do I also need to provide each individual field like Access Complexity or Impact Score?
(Answer: sometimes yes, sometimes no.)
I’m not here to fire shots at OpenCTI — we’ve all been guilty of quick schema hacks. It’s also one of the easiest products in the threat intel space to debug these types of data model issues.
But it is a perfect example of why STIX Extensions matter.
Because without them, you’re left with schema spaghetti.
Building Proper Extension Definitions
So, my two-minute fix turned into:
- A morning defining a proper schema for [OpenCTI](https://filigran.io/platforms/opencti/) Vulnerability objects.
- Writing an Extension Definition to reference the schema
- Updating the way Vulnerability objects are generated in Vulmatch
Here’s an example of a final, extended object:
{
"type": "vulnerability",
"spec_version": "2.1",
"id": "vulnerability--c6577543-1589-5582-ab14-0e78ad12fabc",
"created_by_ref": "identity--37df4a90-2dcb-5a06-bc4f-104e2c080807",
"created": "2018-04-02T16:29:00.600Z",
"modified": "2024-11-21T04:10:23.120Z",
"name": "CVE-2018-6253",
"description": "NVIDIA GPU Display Driver contains a vulnerability in the DirectX and OpenGL Usermode drivers where a specially crafted pixel shader can cause infinite recursion leading to denial of service.",
"external_references": [
{
"source_name": "cve",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2018-6253",
"external_id": "CVE-2018-6253"
},
{
"source_name": "cwe",
"url": "https://cwe.mitre.org/data/definitions/CWE-835.html",
"external_id": "CWE-835"
},
{
"source_name": "[email protected]",
"description": "Vendor Advisory",
"url": "http://nvidia.custhelp.com/app/answers/detail/a_id/4649"
},
{
"source_name": "[email protected]",
"description": "",
"url": "https://usn.ubuntu.com/3662-1/"
},
{
"source_name": "[email protected]",
"description": "Third Party Advisory",
"url": "https://www.talosintelligence.com/vulnerability_reports/TALOS-2018-0522"
},
{
"source_name": "af854a3a-2127-422b-91ae-364da2661108",
"description": "Vendor Advisory",
"url": "http://nvidia.custhelp.com/app/answers/detail/a_id/4649"
},
{
"source_name": "af854a3a-2127-422b-91ae-364da2661108",
"description": "",
"url": "https://usn.ubuntu.com/3662-1/"
},
{
"source_name": "af854a3a-2127-422b-91ae-364da2661108",
"description": "Third Party Advisory",
"url": "https://www.talosintelligence.com/vulnerability_reports/TALOS-2018-0522"
},
{
"source_name": "vulnStatus",
"description": "Modified"
}
],
"object_marking_refs": [
"marking-definition--94868c89-83c2-464b-929b-a1a8aa3c8487",
"marking-definition--562918ee-d5da-5579-b6a1-fae50cc6bad3"
],
"extensions": {
"extension-definition--2c5c13af-ee92-5246-9ba7-0b958f8cd34a": {
"extension_type": "toplevel-property-extension"
},
"extension-definition--ec658473-1319-53b4-879f-488e47805554": {
"extension_type": "toplevel-property-extension"
}
},
"x_opencti_cvss_v2_base_score": 4.9,
"x_opencti_cvss_v2_vector_string": "AV:L/AC:L/Au:N/C:N/I:N/A:C",
"x_cvss": {
"v2_0": [
{
"base_score": 4.9,
"base_severity": "MEDIUM",
"exploitability_score": 3.9,
"impact_score": 6.9,
"source": "[email protected]",
"type": "Primary",
"vector_string": "AV:L/AC:L/Au:N/C:N/I:N/A:C"
}
],
"v3_0": [
{
"base_score": 5.5,
"base_severity": "MEDIUM",
"exploitability_score": 1.8,
"impact_score": 3.6,
"source": "[email protected]",
"type": "Primary",
"vector_string": "CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H"
}
]
},
"x_opencti_cvss_v2_base_severity": "MEDIUM"
}
TL;DR
This is about learning the hard way why STIX Extensions exist and why you should use them.
Because without them, your data model will betray you.
- Don’t just tack on
x_properties like it’s 2019. - Define proper STIX Extension Definitions with schemas.
- Save future-you from untyped chaos and missing CVSS scores.
STIX Extensions aren’t bureaucracy — they are the reason why developers will love building products with your tooling.
Vulmatch
The Vulnerability Search Engine.
Discuss this post
Head on over to the dogesec community to discuss this post.
Never miss an update
Sign up to receive new articles in your inbox as they published.
