Skip to content

Resolve threat actor aliases

ZettelForge separates entity detection from alias resolution. AliasResolver maps an entity string you already have, such as actor: Fancy Bear, to a canonical value such as apt28. MemoryManager calls the resolver after entity extraction, but the current regex extraction path does not detect every named alias.

For direct lookups, use canonical values. recall_actor() lowercases your input and searches the actor, threat_actor, and intrusion_set index buckets. It does not call AliasResolver at query time. If you have an alias string, resolve it first, then pass the canonical value to recall_actor() or recall_entity().

Prerequisites

  • ZettelForge 2.8.0 installed: pip install zettelforge
  • A local data directory. By default this is ~/.amem; set AMEM_DATA_DIR to use a different directory.

Steps

1. Understand what the extractor detects before alias resolution

Resolution happens only after extraction returns an entity value:

text -> entity_extractor -> extracted entity values
                         -> AliasResolver.resolve(entity_type, value)
                         -> entity index

The verified v2.8.0 regex path detects canonical APT labels, not every human-readable actor alias:

from zettelforge.entity_indexer import EntityIndexer

extractor = EntityIndexer().extractor

print(extractor.extract_all("Fancy Bear uses Cobalt Strike.", use_llm=False)["actor"])
print(extractor.extract_all("APT28 uses Cobalt Strike.", use_llm=False)["intrusion_set"])

Expected output:

[]
['apt28']

That means a note that only says Fancy Bear does not get an apt28 entity mapping through the regex-only store path. Write source text with the canonical identifier when you need deterministic graph lookup, for example APT28 (Fancy Bear). Use AliasResolver directly when you receive an alias from another tool or user input.

2. Use canonical values with direct lookups

At query time, recall_actor() does a direct index lookup across the actor-related entity buckets:

from zettelforge.memory_manager import MemoryManager

mm = MemoryManager()

# Direct index lookup: use the canonical name.
notes_apt28 = mm.recall_actor("APT28", k=5)

# This searches for "fancy bear" in the entity index.
notes_fancy = mm.recall_actor("Fancy Bear", k=5)

print(f"APT28 (canonical):  {len(notes_apt28)} notes")
print(f"Fancy Bear (alias): {len(notes_fancy)} notes")

recall() performs vector retrieval and graph augmentation. It resolves query entities only when the extractor emits them, so do not rely on it to turn every alias phrase into a graph lookup.

3. Use AliasResolver directly

Use AliasResolver for debugging, pre-processing, or any workflow where you already know the entity type:

from zettelforge.alias_resolver import AliasResolver

resolver = AliasResolver()

# Built-in actor aliases
print(resolver.resolve("actor", "Fancy Bear"))   # apt28
print(resolver.resolve("actor", "fancy-bear"))   # apt28
print(resolver.resolve("actor", "FANCY BEAR"))   # apt28
print(resolver.resolve("actor", "Pawn Storm"))   # apt28
print(resolver.resolve("actor", "Cozy Bear"))    # apt29

# Unknown entity: returns the input lowercased.
print(resolver.resolve("actor", "Volt Typhoon")) # volt typhoon
print(resolver.resolve("actor", "APT28"))        # apt28
print(resolver.resolve("actor", "NewGroup42"))   # newgroup42

Resolve before a direct lookup:

canonical = resolver.resolve("actor", "Fancy Bear")
notes = mm.recall_actor(canonical, k=5)

Built-in aliases are intentionally minimal

ZettelForge ships with six hardcoded actor entries: fancy bear, fancy-bear, pawn storm, and pawn-storm map to apt28; cozy bear and cozy-bear map to apt29. The tool type has no built-in aliases. All other mappings come from your local JSON file or TypeDB.

Input normalization rules:

  • The resolver lowercases lookup input before matching.
  • The resolver replaces hyphens with spaces before the local dictionary lookup.
  • Unknown values return as the original input lowercased.

4. Add custom aliases with entity_aliases.json

AliasResolver loads custom aliases from entity_aliases.json in the default data directory. Use AMEM_DATA_DIR to move this file; otherwise it lives at ~/.amem/entity_aliases.json.

import json
from zettelforge.memory_store import get_default_data_dir

alias_file = get_default_data_dir() / "entity_aliases.json"
alias_file.parent.mkdir(parents=True, exist_ok=True)

aliases = {}
if alias_file.exists():
    aliases = json.loads(alias_file.read_text())

aliases.setdefault("actor", {})
aliases["actor"]["charming kitten"] = "apt35"
aliases["actor"]["phosphorus"] = "apt35"
aliases["actor"]["bronze silhouette"] = "volt typhoon"

aliases.setdefault("tool", {})
aliases["tool"]["sliver c2"] = "sliver"
aliases["tool"]["cs beacon"] = "cobalt strike"

alias_file.write_text(json.dumps(aliases, indent=2))
print(f"Written to {alias_file}")

Reload and verify:

from zettelforge.alias_resolver import AliasResolver

resolver = AliasResolver()

print(resolver.resolve("actor", "Charming Kitten"))    # apt35
print(resolver.resolve("actor", "Phosphorus"))         # apt35
print(resolver.resolve("actor", "Bronze Silhouette"))  # volt typhoon
print(resolver.resolve("tool", "CS Beacon"))           # cobalt strike
print(resolver.resolve("actor", "Cozy Bear"))          # apt29

Reload after editing

A new AliasResolver instance loads changes from entity_aliases.json. MemoryManager creates its resolver at initialization, so create a new MemoryManager instance after editing aliases.

Use this JSON shape:

{
  "actor": {
    "alias phrase": "canonical name"
  },
  "tool": {
    "alias phrase": "canonical name"
  }
}

Write alias keys in lowercase, space-separated form. The resolver normalizes the lookup input before matching, but it returns canonical values exactly as you store them. Use lowercase canonical values when you want them to match ZettelForge's entity index values. JSON entries extend the hardcoded dictionary. If the same normalized key appears in both, the JSON file wins.

5. Check the TypeDB path

AliasResolver tries TypeDB alias-of relations before it checks the local dictionary for actor, tool, and malware entities. In the default OSS configuration, storage uses SQLite plus LanceDB, so TypeDB is not active and this path returns None.

from zettelforge.alias_resolver import AliasResolver

resolver = AliasResolver()

result = resolver._try_typedb_resolve("actor", "fancy bear")
print(f"TypeDB result: {result}")

print(f"TypeDB available: {resolver._typedb_available}")

canonical = resolver.resolve("actor", "fancy bear")
print(f"Final result: {canonical}")

Expected output when TypeDB is not configured:

TypeDB result: None
TypeDB available: False
Final result: apt28

If TypeDB is unreachable, _typedb_available becomes False for the lifetime of that resolver instance and later lookups go straight to the local dictionary. The resolver returns None instead of raising an exception to the caller.

ThreatRecall.ai keeps its hosted aliases in tenant-scoped knowledge graph records and seed packs. Your local entity_aliases.json configures ZettelForge only; it does not configure ThreatRecall.ai.

Troubleshooting

recall_actor() returns empty results for a group I know is in my store.

recall_actor() does not resolve aliases. It looks for the value you pass, lowercased, in the actor, threat_actor, and intrusion_set entity indexes. Use the canonical name:

notes = mm.recall_actor("APT28", k=5)

resolver = AliasResolver()
canonical = resolver.resolve("actor", "Fancy Bear")
notes = mm.recall_actor(canonical, k=5)

If the source text only contained an alias such as Fancy Bear, confirm that the note actually has an entity mapping. For deterministic graph lookup, include the canonical identifier in the stored text, for example APT28 (Fancy Bear).

An alias I added to entity_aliases.json is not resolving.

Confirm the alias key is in the correct entity type bucket, such as "actor" or "tool". Confirm the JSON is valid with python3 -m json.tool "$AMEM_DATA_DIR/entity_aliases.json". Create a new AliasResolver or MemoryManager instance after writing the file.

I need aliases that work for both actor and intrusion_set entity types.

The hardcoded dictionary contains only the "actor" key. If your store uses the STIX-aligned intrusion_set type for APT, UNC, or FIN groups, add the alias under both keys in your JSON file:

{
  "actor": {"fancy bear": "apt28"},
  "intrusion_set": {"fancy bear": "apt28"}
}