Store Threat Intelligence About an Actor¶
Store threat actor intelligence using remember(). ZettelForge automatically extracts entities (actors, tools, CVEs, campaigns), resolves aliases, and populates the knowledge graph with inferred relationships.
Prerequisites¶
- ZettelForge installed (
pip install zettelforge) - Embedding and LLM models available (download automatically on first use)
- TypeDB running (optional; falls back to JSONL graph)
Steps¶
1. Create a MemoryManager instance¶
To use custom storage paths:
mm = MemoryManager(
jsonl_path="/data/zettelforge/notes.jsonl",
lance_path="/data/zettelforge/lance"
)
2. Store threat actor intelligence with remember()¶
content = (
"APT28 (Fancy Bear) deployed Cobalt Strike beacons against NATO-aligned "
"government networks in Q1 2026. The campaign exploited CVE-2024-3094, "
"a critical backdoor in xz-utils, for initial access. Post-exploitation "
"relied on Mimikatz for credential harvesting."
)
note, status = mm.remember(
content=content,
source_type="report",
source_ref="mandiant-apt28-q1-2026",
domain="cti"
)
print(f"Note ID: {note.id}")
print(f"Status: {status}")
print(f"Created: {note.created_at}")
[!NOTE] The
domain="cti"parameter triggers CTI-specific entity extraction and causal triple extraction (MAGMA-style) for richer graph edges.
3. Verify extracted entities¶
from zettelforge.entity_indexer import EntityIndexer
indexer = EntityIndexer()
entities = indexer.extractor.extract_all(content)
for entity_type, values in entities.items():
print(f" {entity_type}: {values}")
Expected output:
[!TIP] Aliases resolve automatically. "Fancy Bear" in the content resolves to "apt28" before indexing. See Resolve Aliases for details.
4. Check the knowledge graph¶
relationships = mm.get_entity_relationships("actor", "apt28")
for rel in relationships:
print(f" {rel['relationship']}: {rel['to_type']}:{rel['to_value']}")
Expected output:
USES_TOOL: tool:cobalt strike
USES_TOOL: tool:mimikatz
EXPLOITS_CVE: cve:CVE-2024-3094
MENTIONED_IN: note:<note_id>
5. Traverse the graph from the actor¶
graph = mm.traverse_graph(
start_type="actor",
start_value="apt28",
max_depth=2
)
for entry in graph:
print(f" depth={entry.get('depth', 0)} "
f"{entry['entity_type']}:{entry['entity_value']} "
f"via {entry.get('relationship', 'root')}")
[!WARNING] If TypeDB is not running, graph traversal uses the JSONL fallback. Relationship data is identical, but query performance degrades above ~50,000 edges.
6. Store with memory evolution¶
Use evolve=True to deduplicate against existing notes — LLM extracts facts, compares to existing memory, and decides ADD/UPDATE/DELETE/NOOP per fact:
note, status = mm.remember(
content=(
"Lazarus Group used Cobalt Strike and a custom loader called "
"DTrack to target cryptocurrency exchanges in March 2026. "
"CISA advisory AA26-078A links the campaign to CVE-2024-3094."
),
domain="cti",
evolve=True,
)
print(f"[{status}] {note.id}: {note.content.raw[:80]}")
# status: "created", "updated", "corrected", or "noop"
LLM Quick Reference¶
Task: Store threat actor intelligence with automatic entity extraction and knowledge graph population.
Primary method: mm.remember(content, source_type="report", source_ref="...", domain="cti") returns (MemoryNote, str). Entities (actors, tools, CVEs, campaigns, assets) are extracted automatically, aliases resolved, and graph edges created.
Entity extraction pipeline: Content passes through EntityIndexer.extractor.extract_all() which identifies entity types. Each entity goes through AliasResolver.resolve() before indexing and graph storage.
Graph edges created automatically: USES_TOOL (actor-tool), EXPLOITS_CVE (actor-cve, tool-cve), TARGETS_ASSET (actor-asset, tool-asset), CONDUCTS_CAMPAIGN (actor-campaign), MENTIONED_IN (all entities-note).
Causal triples: For domain="cti" notes or content >200 chars, LLM-based causal triple extraction runs, adding richer semantic edges to the graph.
Memory evolution: mm.remember(content, domain="cti", evolve=True) extracts facts, compares each against existing notes, and returns ADD/UPDATE/DELETE/NOOP decisions. The MCP server and web API enable this by default. For programmatic batch use, remember_with_extraction() is also available.
Alias resolution: "Fancy Bear", "Pawn Storm", "Sofacy", "Forest Blizzard" all resolve to "apt28". Works via TypeDB alias-of relations with JSONL fallback.
Key config: domain="cti" activates CTI entity extraction. source_type accepts "conversation", "report", "task_output". source_ref is a free-text provenance string.