Store threat intelligence about an actor¶
Store threat actor intelligence with remember(). ZettelForge automatically extracts entities
(intrusion sets, tools, CVEs, campaigns), normalizes them, 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¶
from zettelforge.memory_manager import MemoryManager
mm = MemoryManager()
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}")
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 EntityExtractor
extractor = EntityExtractor()
entities = extractor.extract_all(content)
for entity_type, values in entities.items():
if values:
print(f" {entity_type}: {values}")
Expected output:
cve: ['cve-2024-3094']
intrusion_set: ['apt28']
tool: ['cobalt-strike', 'mimikatz']
Entity values are normalized to lowercase. Tool names are hyphenated (cobalt-strike).
CVEs are stored lowercase (cve-2024-3094). APT group designators like APT28 are classified
as intrusion_set, not actor. The actor type captures named actors matched by common
name (Lazarus, Sandworm, Volt Typhoon).
"Fancy Bear" does not appear in the extracted output above. Regex-based extraction matches structured designators like
APT28directly. Text aliases like "Fancy Bear" resolve to their canonical form only when TypeDB is running withalias-ofrelations populated, or when LLM NER is enabled withextract_all(content, use_llm=True).
4. Check the knowledge graph¶
Use the correct entity type (intrusion_set for APT groups) when querying. The return value
is a list of dicts, each containing a "node" sub-dict and a "relationship" key:
relationships = mm.get_entity_relationships("intrusion_set", "apt28")
for rel in relationships:
node = rel["node"]
print(f" {rel['relationship']}: {node['entity_type']}:{node['entity_value']}")
Expected output (after step 2 runs):
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¶
traverse_graph returns a list of paths. Each path is a list of step dicts with
from_type, from_value, relationship, to_type, and to_value:
paths = mm.traverse_graph(
start_type="intrusion_set",
start_value="apt28",
max_depth=2,
)
for path in paths:
for step in path:
print(
f" {step['from_type']}:{step['from_value']}"
f" --[{step['relationship']}]-->"
f" {step['to_type']}:{step['to_value']}"
)
If TypeDB is not running, graph traversal uses the JSONL fallback. Relationship data is identical, but query performance degrades above ~50,000 edges.
max_depthis capped at 2 in the default installation. Passing a higher value logs a warning and traversal proceeds at depth 2.
6. Store with memory evolution¶
Use evolve=True to deduplicate against existing notes. The LLM extracts facts, compares
them to existing memory, and returns the operation taken 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 (intrusion sets, actors, tools, CVEs, campaigns, assets)
are extracted automatically, values normalized, and graph edges created.
Entity extraction pipeline: Content passes through EntityExtractor().extract_all(), which
applies a regex fast-path for CTI types. Each extracted value is normalized (lowercased,
hyphenated) before indexing and graph storage.
Entity types for CTI content:
| Entity type | Matches | Example value |
|---|---|---|
intrusion_set |
APT\d+, UNC\d+, TA\d+, FIN\d+, TEMP\d+ | apt28 |
actor |
Lazarus, Sandworm, Volt Typhoon | lazarus |
tool |
Cobalt Strike, Mimikatz, Metasploit, … | cobalt-strike |
cve |
CVE-YYYY-NNNNN | cve-2024-3094 |
campaign |
Operation \<name> | operation aurora |
attack_pattern |
T1234, T1234.001 | T1566 |
Graph edges created automatically: USES_TOOL (intrusion-set/actor → tool),
EXPLOITS_CVE (intrusion-set/actor → cve, tool → cve), TARGETS_ASSET
(intrusion-set/actor/tool → asset), CONDUCTS_CAMPAIGN (intrusion-set/actor → campaign),
MENTIONED_IN (all entities → note).
get_entity_relationships return shape: list[dict] where each dict has "relationship"
(str), "node" (dict with entity_type, entity_value, properties),
"edge_properties" (dict), and "note_id" (str).
traverse_graph return shape: list[list[dict]] — a list of paths. Each path is a list
of step dicts with from_type, from_value, relationship, to_type, to_value.
Causal triples: For domain="cti" notes or content >200 chars, LLM-based causal triple
extraction runs in the background, 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 the operation taken. For programmatic batch
use, remember_with_extraction() returns a list of (MemoryNote, status) tuples.
Alias resolution: Structured designators like APT28 normalize to apt28 automatically.
Common name aliases ("Fancy Bear", "Pawn Storm") resolve to the canonical designator only when
TypeDB is running with alias-of relations populated, or when LLM NER is enabled. Without
these, use the canonical designator directly in your content.
Key parameters: domain="cti" activates CTI entity extraction. source_type accepts
"conversation", "report", "task_output". source_ref is a free-text provenance string.
tlp sets a TLP marking on the note ("WHITE", "GREEN", "AMBER", "RED").