Skip to content

STIX alignment in ZettelForge

ZettelForge does not implement a STIX 2.1 database. It implements a CTI memory system whose entity taxonomy, metadata schema, and access-control model align with STIX 2.1 concepts so that notes you write in, search, and export fit naturally into a STIX workflow. Understanding where the alignment is tight and where it loosens helps you use ZettelForge correctly and integrate it with STIX-native platforms.

What STIX is, briefly

STIX (Structured Threat Information Expression) organizes threat intelligence into three object categories:

  • SDOs (STIX Domain Objects): the analytical subjects — threat actors, malware, vulnerabilities, campaigns
  • SROs (STIX Relationship Objects): how subjects connect — uses, targets, attributed-to
  • SCOs (STIX Cyber Observables): technical indicators — IP addresses, file hashes, domain names

ZettelForge aligns with all three categories, but expresses them differently from a STIX JSON store.

Where ZettelForge aligns with STIX

Entity taxonomy

When ZettelForge extracts entities from a note, it produces typed strings that mirror STIX naming. There are 19 entity types in three groups (source: entity_indexer.py:73-96, live-verified):

Group Entity types Extraction method STIX alignment
CTI cve, intrusion_set, actor, tool, campaign, attack_pattern Regex SDO-aligned
IOC ipv4, domain, url, md5, sha1, sha256, email Regex SCO-aligned
Conversational person, location, organization, event, activity, temporal LLM NER Non-STIX

actor and intrusion_set are aliases for the STIX threat-actor SDO. When you call mm.recall_actor("APT28"), ZettelForge searches all three entity type strings (actor, threat_actor, intrusion_set) to handle notes regardless of which label was applied during extraction (memory_manager.py:1184).

Note metadata fields

Every MemoryNote carries three STIX-aligned fields in its Metadata object (source: note_schema.py:73-76, live-verified):

Field Type Default Meaning
metadata.stix_confidence int -1 STIX confidence 0–100; -1 means not set
metadata.tier str "B" Epistemic tier: A (authoritative), B (operational), C (support)
metadata.tlp str "" TLP marking; empty string means unclassified (treated as TLP:CLEAR)

The tier field is ZettelForge's own contribution. STIX uses an integer confidence field (0–100) without a categorical classification. ZettelForge adds tier because CTI analysts routinely classify sources by type, not just score: an authoritative CISA advisory is tier A regardless of the number you assign it; an unverified forum post is tier C.

The two fields coexist: stix_confidence carries the numeric value when you import from a STIX-native platform; tier carries the editorial classification applied by analysts or by ZettelForge's extraction pipeline.

TLP access control

ZettelForge enforces TLP markings on every read path. The six supported levels match the FIRST Traffic Light Protocol standard (source: tlp_policy.py:17-52, live-verified):

Level Rank Meaning
TLP:CLEAR 0 Unrestricted; default for notes with no marking
TLP:WHITE 0 Legacy alias for TLP:CLEAR
TLP:GREEN 1 Community-shareable
TLP:AMBER 2 Limited to organization and named recipients
TLP:AMBER+STRICT 3 Recipients only, no further sharing
TLP:RED 4 Named recipients only; not for sharing

Default behavior: Notes are created with TLP:CLEAR. Every recall() call applies a max_tlp ceiling; the default ceiling for sharing contexts is TLP:GREEN. Notes above the ceiling are silently filtered and logged. To retrieve TLP:AMBER or higher notes, you pass a max_tlp override with an audit reason string.

# Default recall — returns up to TLP:GREEN
results = mm.recall("APT28 lateral movement TTPs")

# Elevated recall — returns up to TLP:RED; requires an audit reason
results = mm.recall(
    "APT28 lateral movement TTPs",
    max_tlp="TLP:RED",
    tlp_override_reason="threat hunt authorized by SOC lead 2026-06-20"
)

This enforcement is in-process within ZettelForge. It does not replace your platform's access control; it adds a second layer that prevents TLP:AMBER material from leaking into a recall that feeds a TLP:GREEN-bounded workflow.

Vulnerability metadata (VulnerabilityMeta)

When a note represents a CVE, the metadata.vuln field holds structured scoring data (source: note_schema.py:47-60, live-verified):

class VulnerabilityMeta(BaseModel):
    cvss_v3_score: float | None = None       # 0.0–10.0
    cvss_v3_vector: str | None = None        # e.g. "CVSS:3.1/AV:N/AC:L/..."
    epss_score: float | None = None          # 0.0–1.0, exploitation probability
    epss_percentile: float | None = None     # 0.0–1.0, relative to all scored CVEs
    cisa_kev: bool = False                   # True if in CISA KEV catalog

This data is not extracted from note text. It is populated programmatically during an OpenCTI sync, which maps OpenCTI's CVSS fields to VulnerabilityMeta. If you are using ZettelForge without OpenCTI, vuln remains None on CVE notes.

Graph relationships

The in-memory knowledge graph stores entities and relationships that are STIX-inspired. You write relationships with ingest_relationship() (source: memory_manager.py:1737-1773, live-verified):

mm.ingest_relationship(
    from_type="actor",
    from_value="apt28",
    to_type="tool",
    to_value="mimikatz",
    relationship="USES_TOOL",
    properties={"confidence": 0.9, "note_id": "note_20260620_001"},
)

The edge is stored in SQLite (kg_edges table) and deduplicated by (from_type, from_value, to_type, to_value, relationship). Relationship label strings are not constrained to a fixed schema — use the labels that match your workflow. The KG edge schema reference documents the labels ZettelForge itself produces during extraction.

Alias resolution

recall_actor() resolves common aliases before searching. In OSS, the resolver checks a local JSON file and hardcoded fallbacks (source: alias_resolver.py:30-37, live-verified):

"fancy bear"  → "apt28"
"fancy-bear"  → "apt28"
"pawn storm"  → "apt28"
"cozy bear"   → "apt29"
"cozy-bear"   → "apt29"

You can extend the alias list by placing a JSON file at ~/.amem/entity_aliases.json with the structure {"actor": {"alias": "canonical"}}. The resolver loads that file at startup and merges it with the hardcoded list.

When TypeDB is configured (ThreatRecall.ai SaaS or an optional TypeDB extension), the resolver queries the alias-of relation in TypeDB first and falls back to local aliases only if TypeDB is unavailable.

What ZettelForge does not implement

A STIX JSON store. ZettelForge does not parse STIX bundles or store STIX JSON objects. MemoryNotes are the internal representation; STIX fields in Metadata are a mapping layer, not a native STIX schema.

STIX IDs. ZettelForge does not generate or validate threat-actor--uuid identifiers. Notes have a note_YYYYMMDD_HHMMSS_xxxx ID scheme. If you need STIX IDs on entities for platform interoperability, generate them from your import pipeline and store them as properties on ingest_relationship() calls or as content in notes.

STIX Sighting objects. The sighting SRO (who observed what, when) is not a first-class type. Sighting context lives in the text content of notes, in temporal attributes on KG edges, and in VulnerabilityMeta for KEV membership.

STIX Grouping and Opinion objects. ZettelForge replaces these with the tier classification and the two-phase extraction pipeline's importance score.

TypeDB STIX ontology. A full TypeDB schema with typed STIX entities (threat-actor, vulnerability, etc.), abstract base types, and STIX SRO relations is available in ThreatRecall.ai (the hosted SaaS). It is not in the OSS package.

The design choice

ZettelForge chose a STIX-aligned taxonomy over a full STIX implementation for two reasons:

Operational speed. A regex over "CVE-" is faster than deserializing a STIX bundle and querying a TypeDB constraint graph. ZettelForge's p50 recall latency on the keyword-judge path is around 79 ms (from BENCHMARK_REPORT.md session 2026-06-09). A full STIX database that enforces schema constraints at write time would increase write latency significantly for a system whose primary value is fast ingestion from conversational sources.

Source flexibility. CTI analysts ingest intelligence from incident notes, Slack threads, draft reports, and chat logs — none of which arrive as STIX JSON. ZettelForge's entity extraction runs against raw text. Aligning the extracted entity names with STIX types lets you connect ZettelForge output to a STIX platform without requiring that every input source be STIX-formatted.

The result: ZettelForge works as a structured cache for CTI reasoning. You write raw text in; you get STIX-aligned entities and relationships out; you pass those to OpenCTI, MISP, or a SIEM.


ThreatRecall.ai SaaS

ThreatRecall.ai adds a TypeDB graph layer with a full STIX 2.1 schema, typed entity hierarchies, the alias-of relation for inference-driven alias resolution, and STIX 2.1 bundle export. The OSS package described on this page uses SQLite for the KG and does not include TypeDB or STIX bundle I/O.