Skip to content

Store threat intelligence about an actor

Use MemoryManager.remember() to store a CTI note. In this exercise, you store synthetic actor data, confirm the normalized entities, and inspect the one-hop relationships that ZettelForge writes to its local knowledge graph.

Prerequisites

  • Install ZettelForge 2.8.0 and confirm it imports. Start with the Quickstart if you need a working installation.
  • Allow the first run to download the default fastembed model, or warm that model in your local cache before you work offline.
  • Set ZETTELFORGE_ENRICHMENT_ENABLED=false in the Python process environment for this fast-path exercise. This keeps the output deterministic and avoids LLM calls.

The default installation stores notes and graph data in SQLite and vectors in LanceDB. You do not need TypeDB for this task.

1. Create a memory manager

Start Python from a writable directory, then create the default manager:

from zettelforge.memory_manager import MemoryManager

mm = MemoryManager()

2. Store a synthetic CTI note

Keep test data visibly synthetic. This prevents an exercise from looking like a real-world attribution claim.

content = (
    "Synthetic exercise: APT28 deployed Cobalt Strike and Mimikatz. "
    "The exercise associates both tools with CVE-2024-3094."
)

note, status = mm.remember(
    content=content,
    source_type="report",
    source_ref="synthetic-actor-exercise",
    domain="cti",
)

print(f"Status: {status}")
print(f"Note ID starts with note_: {note.id.startswith('note_')}")

The direct write returns:

Status: created
Note ID starts with note_: True

domain="cti" scopes the note to CTI workflows. source_type and source_ref preserve where the note came from; they do not validate the content.

3. Verify the extracted entities

Run the same deterministic regex extractor used by the fast write path:

from zettelforge.entity_indexer import EntityExtractor

extractor = EntityExtractor()
entities = extractor.extract_all(content)

for entity_type in sorted(entities):
    values = sorted(entities[entity_type])
    if values:
        print(f"{entity_type}: {values}")

You get normalized, lowercase entity values:

cve: ['cve-2024-3094']
intrusion_set: ['apt28']
tool: ['cobalt-strike', 'mimikatz']

APT-style designators such as APT28 use the intrusion_set type. Tool names use hyphens, and CVE identifiers use lowercase.

4. Inspect direct relationships

Query with the normalized entity type and value:

relationships = mm.get_entity_relationships("intrusion_set", "apt28")

for rel in sorted(
    relationships,
    key=lambda item: (
        item["relationship"],
        item["node"]["entity_type"],
        item["node"]["entity_value"],
    ),
):
    node = rel["node"]
    value = "<stored-note>" if node["entity_value"] == note.id else node["entity_value"]
    print(f"{rel['relationship']}: {node['entity_type']}:{value}")

The stored note produces these relationships:

EXPLOITS_CVE: cve:cve-2024-3094
MENTIONED_IN: note:<stored-note>
USES_TOOL: tool:cobalt-strike
USES_TOOL: tool:mimikatz

5. Traverse one hop from the actor

Use a one-hop traversal to enumerate the same neighbors as paths:

paths = mm.traverse_graph(
    start_type="intrusion_set",
    start_value="apt28",
    max_depth=1,
)

one_hop_steps = sorted(
    (path[0] for path in paths if len(path) == 1),
    key=lambda step: (step["relationship"], step["to_type"], step["to_value"]),
)

for step in one_hop_steps:
    value = "<stored-note>" if step["to_value"] == note.id else step["to_value"]
    print(
        f"{step['from_type']}:{step['from_value']} "
        f"--[{step['relationship']}]--> "
        f"{step['to_type']}:{value}"
    )

The traversal returns:

intrusion_set:apt28 --[EXPLOITS_CVE]--> cve:cve-2024-3094
intrusion_set:apt28 --[MENTIONED_IN]--> note:<stored-note>
intrusion_set:apt28 --[USES_TOOL]--> tool:cobalt-strike
intrusion_set:apt28 --[USES_TOOL]--> tool:mimikatz

Interpret the graph correctly

The fast path infers these edges because the entities occur in the same note. The edges make the note traversable; they do not independently prove that the actor used a tool or exploited a CVE. Preserve trustworthy source metadata and review the underlying note before you treat an edge as attribution evidence.

For complete method signatures and return shapes, use the Memory Manager API reference. To retrieve stored actor knowledge as an analyst, continue with Query APT tool usage.