STIX 2.1 Bundle Creation
SkillSecuritySTIX 2.1 bundle creation reference. Object types, relationships, and JSON templates for structured threat intelligence sharing. Includes the per-claim mapping of evidence grades (access level and claim support) from /quality-of-information-check to STIX confidence, and the reverse mapping on import.
Use STIX 2.1 Bundle Creation in Claude, ChatGPT or Ahel Desktop
Free. Sign in, add STIX 2.1 Bundle Creation and connect your AI. About a minute.
Also: Claude Code · Cursor · Codex
Then ask your AI: use the STIX 2.1 Bundle Creation skill
Details
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; ahel provides instructions and does not run this skill.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
What this skill tells your AI
The instructions your AI receives, as published by liberty91ltd/cti-skills in skills/stix-bundle/SKILL.md and read by ahel’s review.
STIX (Structured Threat Information Expression) 2.1 is the standard for representing and sharing cyber threat intelligence.
Bundle Structure
{
"type": "bundle",
"id": "bundle--<UUID>",
"objects": [
// Array of STIX Domain Objects (SDOs) and Relationship Objects (SROs)
]
}
STIX Domain Objects (SDOs)
Indicator
Represents a pattern that can be used to detect suspicious activity.
{
"type": "indicator",
"spec_version": "2.1",
"id": "indicator--<UUID>",
"created": "2026-01-15T00:00:00.000Z",
"modified": "2026-01-15T00:00:00.000Z",
"name": "Malicious IP used by APT28",
"description": "C2 server observed in campaign targeting European government entities",
"pattern": "[ipv4-addr:value = '203.0.113.42']",
"pattern_type": "stix",
"valid_from": "2026-01-15T00:00:00.000Z",
"valid_until": "2026-04-15T00:00:00.000Z",
"kill_chain_phases": [
{
"kill_chain_name": "mitre-attack",
"phase_name": "command-and-control"
}
],
"confidence": 70,
"labels": ["malicious-activity"],
"object_marking_refs": ["marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9"]
}
STIX Pattern Syntax
| IOC Type | STIX Pattern |
|---|---|
| IPv4 | [ipv4-addr:value = '1.2.3.4'] |
| IPv6 | [ipv6-addr:value = '2001:db8::1'] |
| Domain | [domain-name:value = 'evil.com'] |
| URL | [url:value = 'https://evil.com/payload'] |
| SHA-256 | [file:hashes.'SHA-256' = 'abc123...'] |
| MD5 | [file:hashes.MD5 = 'abc123...'] |
[email-addr:value = 'phish@evil.com'] | |
| File name | [file:name = 'malware.exe'] |
| Combined | [file:hashes.'SHA-256' = 'abc...' AND file:name = 'payload.dll'] |
Threat Actor
{
"type": "threat-actor",
"spec_version": "2.1",
"id": "threat-actor--<UUID>",
"created": "2026-01-15T00:00:00.000Z",
"modified": "2026-01-15T00:00:00.000Z",
"name": "APT28",
"description": "Russian state-sponsored threat actor also known as Fancy Bear, Sofacy",
"aliases": ["Fancy Bear", "Sofacy", "Pawn Storm", "Forest Blizzard"],
"threat_actor_types": ["nation-state"],
"roles": ["agent"],
"sophistication": "expert",
"resource_level": "government",
"primary_motivation": "political",
"goals": ["espionage", "disruption"]
}
Malware
{
"type": "malware",
"spec_version": "2.1",
"id": "malware--<UUID>",
"created": "2026-01-15T00:00:00.000Z",
"modified": "2026-01-15T00:00:00.000Z",
"name": "SUNBURST",
"description": "Backdoor trojanised into SolarWinds Orion software",
"malware_types": ["backdoor", "trojan"],
"is_family": true,
"kill_chain_phases": [
{"kill_chain_name": "mitre-attack", "phase_name": "initial-access"},
{"kill_chain_name": "mitre-attack", "phase_name": "command-and-control"}
]
}
Attack Pattern (MITRE ATT&CK Technique)
{
"type": "attack-pattern",
"spec_version": "2.1",
"id": "attack-pattern--<UUID>",
"created": "2026-01-15T00:00:00.000Z",
"modified": "2026-01-15T00:00:00.000Z",
"name": "Phishing: Spearphishing Attachment",
"description": "Adversaries send spearphishing emails with malicious attachments",
"external_references": [
{
"source_name": "mitre-attack",
"external_id": "T1566.001",
"url": "https://attack.mitre.org/techniques/T1566/001"
}
],
"kill_chain_phases": [
{"kill_chain_name": "mitre-attack", "phase_name": "initial-access"}
]
}
Campaign
{
"type": "campaign",
"spec_version": "2.1",
"id": "campaign--<UUID>",
"created": "2026-01-15T00:00:00.000Z",
"modified": "2026-01-15T00:00:00.000Z",
"name": "Operation Fancy Storm",
"description": "Targeted espionage campaign against European government entities",
"first_seen": "2025-11-01T00:00:00.000Z",
"last_seen": "2026-01-15T00:00:00.000Z"
}
Identity (Victim/Target)
{
"type": "identity",
"spec_version": "2.1",
"id": "identity--<UUID>",
"created": "2026-01-15T00:00:00.000Z",
"modified": "2026-01-15T00:00:00.000Z",
"name": "European Government Sector",
"identity_class": "class",
"sectors": ["government-national"]
}
Relationship Objects (SROs)
{
"type": "relationship",
"spec_version": "2.1",
"id": "relationship--<UUID>",
"created": "2026-01-15T00:00:00.000Z",
"modified": "2026-01-15T00:00:00.000Z",
"relationship_type": "uses",
"source_ref": "threat-actor--<UUID>",
"target_ref": "malware--<UUID>",
"confidence": 70
}
confidence on an SRO grades the relationship itself, the claim that the source uses, targets or is attributed to the target. See Confidence and source grading for how the value is derived.
Common Relationship Types
| Source | Relationship | Target |
|---|---|---|
| threat-actor | uses | malware, attack-pattern, tool |
| threat-actor | targets | identity, vulnerability |
| threat-actor | attributed-to | identity (sponsoring nation) |
| campaign | uses | malware, attack-pattern, tool |
| campaign | targets | identity, vulnerability |
| campaign | attributed-to | threat-actor |
| indicator | indicates | malware, threat-actor, campaign |
| malware | targets | identity, vulnerability |
| malware | uses | attack-pattern |
Confidence and source grading
Grades are exported per claim, never per event. Each evidence item from /quality-of-information-check (schema: skills/quality-of-information-check/references/evidence-item-schema.md) carries its own grading.access_level and grading.claim_support, and each lands on the one STIX object that expresses that claim. Do not stamp one value across every object in a bundle, and do not put confidence on the bundle itself. A bundle is a container and has no such property.
The evidence grade is not the Admiralty scale. It is two words, access level and claim support, and it is never written or exported as an Admiralty letter or number. Admiralty ratings apply to lookup results and single items; the evidence grade applies to claims from documents; neither is converted into the other.
Claim support to confidence
STIX confidence comes from claim support. The values are the pack's own mapping, chosen to sit inside the bands of /confidence-levels (High 80 to 100, Moderate 60 to 79, Low 40 to 59). Use these values and no others.
| Claim support | confidence on export |
|---|---|
| established | 90 |
| firm | 70 |
| tentative | 50 |
| disputed | 30 |
| unverified | omit the property |
Unverified means the property is left out. It is never written as 0.
Which object carries it
| Claim | Object that carries confidence |
|---|---|
Any observable on its own (domain-name, ipv4-addr, file, url, email-addr) | None. SCOs never carry confidence. An observable is a fact about the world, not a claim. |
| "This observable is malicious" or "this pattern detects X" | The Indicator. One Indicator per distinct assertion, so the same IP asserted as C2 by one primary and as a scanner by another is two Indicators with separate grades. |
Relational claims: indicates, based-on, uses, attributed-to, targets, exploits | The Relationship. For "X was seen at Y", the Sighting. |
Other SDOs (threat-actor, intrusion-set, malware, campaign) | Only when the claim is the entity's own existence as a distinct thing, for example a vendor asserting a new cluster. Otherwise leave it off and grade the relationships. |
An attribution is a Relationship (attributed-to), so its grade goes there and not on the Threat Actor. A well-known actor object with confidence: 50 wrongly says the actor's existence is in doubt.
Access level and the primary source
STIX has no property for how the source knows. On the same object that carries confidence:
x_liberty91_access_level: the word, one ofdirect,limited,indirect,untraced,adversary.x_liberty91_claim_support: the word, one ofestablished,firm,tentative,disputed,unverified.- An
external_referencesentry for the primary source (source_name,url, anddescriptionwith the report title and date). Cite the primary, not the outlet that relayed it. - When the grade was read from a confidence value rather than assessed,
x_liberty91_derived_from_confidence: true.
Both custom properties are still written when claim support is unverified and confidence is omitted, for example adversary and unverified on an uncorroborated actor claim.
OpenCTI. Do not write the evidence grade into an Organization's native reliability field. That field is a track-record rating of the source, and the evidence grade belongs to the claim. Put the two words in the labels or the description of the object that expresses the claim. The custom properties still travel in the bundle for other consumers.
Worked example
Two claims from one vendor report: an observation graded direct, firm and an attribution graded direct, tentative.
[
{
"type": "indicator",
"spec_version": "2.1",
"id": "indicator--<UUID>",
"created": "2026-09-27T00:00:00.000Z",
"modified": "2026-09-27T00:00:00.000Z",
"name": "C2 domain observed in incident response",
"pattern": "[domain-name:value = 'update-check.example.net']",
"pattern_type": "stix",
"valid_from": "2026-09-20T00:00:00.000Z",
"confidence": 70,
"x_liberty91_access_level": "direct",
"x_liberty91_claim_support": "firm",
"external_references": [
{
"source_name": "Example Vendor",
"description": "Primary source. Intrusion report, 2026-09-22",
"url": "https://vendor.example.com/blog/intrusion-report"
}
]
},
{
"type": "relationship",
"spec_version": "2.1",
"id": "relationship--<UUID>",
"created": "2026-09-27T00:00:00.000Z",
"modified": "2026-09-27T00:00:00.000Z",
"relationship_type": "attributed-to",
"source_ref": "campaign--<UUID>",
"target_ref": "threat-actor--<UUID>",
"confidence": 50,
"x_liberty91_access_level": "direct",
"x_liberty91_claim_support": "tentative",
"external_references": [
{
"source_name": "Example Vendor",
"description": "Primary source. Intrusion report, 2026-09-22",
"url": "https://vendor.example.com/blog/intrusion-report"
}
]
}
]
The domain-name SCO and the threat-actor SDO, if included, carry neither confidence nor the two grade properties.
Report objects
A Report groups many claims, so it has no grade of its own. Where a consumer needs a value, write the weakest link: the confidence of the lowest-graded load-bearing claim (event_qoi_summary.weakest_link), with x_liberty91_confidence_basis: "weakest_link". Never an average. If the weakest link is unverified, omit confidence and keep the basis property. This value exists for interoperability only. Analysis consumes the per-claim grades.
Import direction
Reading a bundle from elsewhere, turn confidence into claim support with this table.
confidence read on import | Claim support |
|---|---|
| 60 to 100 | firm |
| 40 to 59 | tentative |
| 1 to 39 | disputed |
| absent or 0 | unverified |
An imported value never gives established. That level needs counted independent primaries (rubric rule R5), and a number cannot show them. Access level for an imported item is indirect unless the primary is resolved. Do not infer it from the producer's name. Set the derived_from_confidence flag on every imported item. The number tells you what the producer thought, not how they knew, so /quality-of-information-check re-grades the item when a primary is available (rubric rule R11).
An Admiralty rating that arrives on imported data is kept as that party's Admiralty rating and shown as such. It is not converted into an evidence grade.
MISP tagging for the same grades is in /ioc-export.
TLP Marking Definitions (Standard UUIDs)
These are the canonical marking-definition object IDs. Reference them from object_marking_refs; do not include the marking-definition object itself in the bundle (see below).
"marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9" // TLP:CLEAR
"marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da" // TLP:GREEN
"marking-definition--f88d31f6-486f-44da-b317-01333bde0b82" // TLP:AMBER
"marking-definition--826578e1-40a3-4b46-a8d7-b76e4fd71d29" // TLP:AMBER+STRICT
"marking-definition--e828b379-4e03-4974-9ac4-e53a884c97c1" // TLP:RED
MISP-compatible bundles — reference TLPs, don't inline them
For bundles that will be imported into MISP via /lookup-misp upload-stix, only reference TLP marking-definition UUIDs in object_marking_refs. Don't include a {"type": "marking-definition", ...} object in the bundle's objects array.
The inline shape that older STIX 2.0 examples show — definition_type: "tlp" with definition: {"tlp": "clear"} — fails MISP's STIX 2 importer with "Does not match any TLP Marking definition!". MISP resolves the canonical UUIDs from its own catalog; supplying an inline copy triggers the spec-conformance check and the whole bundle is rejected.
✅ Correct (MISP-compatible) — reference only, marking resolved by consumer:
{
"type": "bundle",
"id": "bundle--...",
"objects": [
{
"type": "indicator",
"id": "indicator--...",
"object_marking_refs": ["marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9"],
"pattern": "[ipv4-addr:value = '203.0.113.42']",
"pattern_type": "stix",
"valid_from": "2026-04-26T00:00:00.000Z"
}
]
}
❌ Don't do this for MISP-bound bundles — inlined TLP definition trips the importer:
{
"type": "marking-definition",
"id": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9",
"definition_type": "tlp",
"definition": {"tlp": "clear"}
}
If you need a self-contained bundle for non-MISP consumers (offline ingest, archives, partners without a TLP catalog), the inline form is valid STIX 2.1 — keep a separate non-MISP variant rather than tweaking the MISP one.
If you need TLP applied inside MISP after import, do it via a tag-event call rather than a STIX marking — see tools/integrations/misp.md.
OpenCTI is not affected — it is STIX 2.1-native and imports bundles as-is via /lookup-opencti upload-stix, inline TLP marking-definitions included. Only MISP-bound bundles need the reference-only treatment above.
Output Location
Write STIX bundles to: data/stix-bundles/YYYY-MM-DD-<context>.json
Signals
- GitHub stars
- 27
- Forks
- 8
- Last commit
- Sep 2026
Advanced
- Item type
- skill
- Key
stix-bundle- Source
- github.com/liberty91ltd/cti-skills
github.com/liberty91ltd/cti-skills