Query what tools an APT group uses¶
Use the local knowledge graph to list tools linked to a threat actor, then inspect the stored note that supports the result. This workflow uses canonical entity values and does not require a generative LLM or TypeDB.
Current feature state
Canonical APT lookups such as intrusion_set="apt28" are verified. Direct alias
shortcuts are in progress: Fancy Bear does not currently find an APT28 entity stored
as intrusion_set. Use the normalized value apt28 in this workflow.
Prerequisites¶
- Install ZettelForge 2.8.0 and confirm it imports. Start with the Quickstart if you need a working installation.
- Complete Store threat intelligence about an actor. The output below assumes you stored that page's synthetic APT28 exercise.
- Run every snippet below in the same Python session.
1. Create a memory manager¶
Open Python from the same environment and data directory you used to store the note:
from zettelforge.memory_manager import MemoryManager
mm = MemoryManager()
2. List the tools linked to APT28¶
Query the normalized entity type and value, then keep only USES_TOOL relationships whose
neighbor is a tool:
relationships = mm.get_entity_relationships("intrusion_set", "apt28")
tools = sorted(
{
rel["node"]["entity_value"]
for rel in relationships
if rel["relationship"] == "USES_TOOL"
and rel["node"]["entity_type"] == "tool"
}
)
print(f"Tools linked to APT28: {tools}")
if not tools:
raise RuntimeError(
"No tool relationships found. Complete the prerequisite exercise, "
"then query intrusion_set:apt28 from the same data directory."
)
The synthetic prerequisite produces:
Tools linked to APT28: ['cobalt-strike', 'mimikatz']
The set removes duplicate edges before sorting the values. An empty list means the current
store has no matching USES_TOOL neighbor for canonical intrusion_set:apt28; it does not
prove the actor uses no tools.
3. Inspect the supporting note¶
Cross-check the actor and the first returned tool through the entity index. Their shared note is the source context you review before treating the graph edge as attribution:
actor_notes = mm.recall_actor("apt28", k=10)
tool_notes = mm.recall_tool(tools[0], k=10)
matching_ids = {note.id for note in actor_notes} & {note.id for note in tool_notes}
matching_notes = [note for note in actor_notes if note.id in matching_ids]
print(f"Supporting notes: {len(matching_notes)}")
for note in matching_notes:
print(note.content.raw)
The exercise returns one shared note:
Supporting notes: 1
Synthetic exercise: APT28 deployed Cobalt Strike and Mimikatz. The exercise associates both tools with CVE-2024-3094.
get_entity_relationships() reads direct graph neighbors. recall_actor() and
recall_tool() read exact canonical values from the entity index. None of these three
calls performs vector retrieval or generates an answer with an LLM.
Interpret the result correctly¶
ZettelForge's fast write path creates these graph edges from entities that occur in the
same note. A USES_TOOL edge makes the relationship queryable; it does not independently
prove real-world tool use. Review the note's source metadata and underlying report before
you treat the result as attribution evidence.
For complete method signatures and return shapes, use the Memory Manager API reference. For multi-hop graph analysis, continue with Run temporal queries.