STIX 2.1 Bundle Creation

SkillSecurity

STIX 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.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

STIX 2.1 Bundle CreationStart free

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 TypeSTIX 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[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

SourceRelationshipTarget
threat-actorusesmalware, attack-pattern, tool
threat-actortargetsidentity, vulnerability
threat-actorattributed-toidentity (sponsoring nation)
campaignusesmalware, attack-pattern, tool
campaigntargetsidentity, vulnerability
campaignattributed-tothreat-actor
indicatorindicatesmalware, threat-actor, campaign
malwaretargetsidentity, vulnerability
malwareusesattack-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 supportconfidence on export
established90
firm70
tentative50
disputed30
unverifiedomit the property

Unverified means the property is left out. It is never written as 0.

Which object carries it

ClaimObject 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, exploitsThe 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 of direct, limited, indirect, untraced, adversary.
  • x_liberty91_claim_support: the word, one of established, firm, tentative, disputed, unverified.
  • An external_references entry for the primary source (source_name, url, and description with 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 importClaim support
60 to 100firm
40 to 59tentative
1 to 39disputed
absent or 0unverified

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