技能创建器
skill-creator
创建、改进和评估自定义技能,把专业流程转化为可重复使用的 AI 能力。
maltego-transforms【数据与研究】Build custom Maltego transforms in Python with the maltego-transforms SDK, so any API or data source appears as connected nodes in Maltego's link-analysis graph. Use when mapping domains, IPs, emails, persons and organizations, writing OSINT or threat-intelligence transforms, or migrating from the archived maltego-trx and Canari libraries.
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Maltego is a link-analysis tool for OSINT and threat intelligence. A transform takes one entity from the graph (a domain, IP, email, person) and returns related entities. You host transforms in your own server and Maltego calls it over HTTP.
The current library is the Maltego Transforms SDK (pip install maltego-transforms, import package maltego, version 1.1.1 on 25 September 2026, MIT, Python 3.10 to 3.14). It replaces maltego-trx, whose repository is marked archived and unmaintained (last release 1.7.0, November 2025), and Canari (canari, last release 2019). Old tutorials using DiscoverableTransform, response.addEntity(...) or canari create-profile target those libraries.
python3 -m venv .venv && source .venv/bin/activate
pip install maltego-transforms maltego-transforms-std-entities
maltego-transforms start recon-transforms # runnable project with example transforms
Other commands: maltego-transforms init (turn the current directory into a project) and maltego-transforms install-skills (add agent skills for authoring transforms). start --with-skills scaffolds with them.
A transform is an async function registered with @register_transform. Type hints declare the input and output entity types. Return one entity, None, or yield from an AsyncGenerator to stream results. Typed standard entities (Domain, DNSName, IPv4Address, EmailAddress, AS, Netblock, Organization, Website, Person, Phrase ...) come from maltego.entities.
Input is wrapped in a MaltegoContext; use context.log.inform(...), .partial(...), .fatal(...) for messages shown in the client, and raise MaltegoException("text") for a user-visible error. Settings (API keys, limits) are declared with TransformSetting and arrive as a settings dict argument.
from maltego.server import MaltegoServerSettings, ServerHTTPSettings, run_server
settings = MaltegoServerSettings(
server_name="Recon Helpers", ns="terminalskills.recon", author="terminal-skills",
http_settings=ServerHTTPSettings(protocol="http"), # plain HTTP for local development only
)
run_server(settings=settings)
python project.py serves on 127.0.0.1:3000 and prints a seed URL (/seed). In Maltego, add the seed URL as a transform seed to discover and install the transforms. The Graph Browser client requires HTTPS with a trusted certificate (see Maltego's "HTTPS, Certificates, and Browser Trust" article); the SDK enables SSL by default beyond local development.
input_constraints limit which entities a transform is offered for.TransformSetting(..., auth=True, is_global=True) stores one credential for every transform in the namespace; other types include int, float, boolean, date, datetime, datetime_range and list variants.@register_entity defines custom entities (MaltegoEntity subclass with TYPE_NAME, Config, and MaltegoEntityProperty fields).Request: "Add a transform that expands a Domain into the hostnames found in CT logs."
from typing import AsyncGenerator
import httpx
from maltego.entities import DNSName, Domain
from maltego.server import MaltegoContext, MaltegoException, register_transform
@register_transform(
display_name="Domain to Subdomains [CT Logs]",
description="Lists hostnames seen in certificate transparency logs via crt.sh.",
transform_set="Recon Helpers",
)
async def domain_to_subdomains(
input_entity: Domain, context: MaltegoContext
) -> AsyncGenerator[DNSName, None]:
domain = input_entity.value
headers = {"User-Agent": "terminal-skills-check (https://terminalskills.io)"}
async with httpx.AsyncClient(timeout=30, headers=headers) as http:
r = await http.get("https://crt.sh/", params={"q": f"%.{domain}", "output": "json"})
if r.status_code != 200:
raise MaltegoException(f"crt.sh answered HTTP {r.status_code}")
seen: set[str] = set()
for row in r.json():
for name in row["name_value"].splitlines():
name = name.strip().lstrip("*.").lower()
if name.endswith(domain) and name not in seen:
seen.add(name)
yield DNSName(name)
Result: right-click python.org on the graph, run "Domain to Subdomains [CT Logs]", and one DNSName node per hostname appears (for example education.python.org). crt.sh is a free service that times out under load; keep the timeout and the error message.
Request: "Find emails for a domain with Hunter, and keep the API key out of the code."
from typing import Any, AsyncGenerator, Dict
import httpx
from maltego.entities import Domain, EmailAddress
from maltego.server import MaltegoContext, MaltegoException, TransformSetting, register_transform
HUNTER_KEY = TransformSetting(
name="hunter_api_key", display_name="Hunter API key",
type=TransformSetting.Types.str, optional=False, auth=True, is_global=True,
)
@register_transform(
display_name="Domain to Emails [Hunter]",
description="Email addresses Hunter has seen for a domain.",
transform_set="Recon Helpers",
settings=[HUNTER_KEY],
)
async def domain_to_emails(
input_entity: Domain, settings: Dict[str, Any], context: MaltegoContext
) -> AsyncGenerator[EmailAddress, None]:
headers = {"X-API-KEY": settings["hunter_api_key"]}
async with httpx.AsyncClient(timeout=20, headers=headers) as http:
r = await http.get("https://api.hunter.io/v2/domain-search", params={"domain": input_entity.value})
if r.status_code == 401:
raise MaltegoException("Hunter rejected the API key")
r.raise_for_status()
for item in r.json()["data"]["emails"]:
yield EmailAddress(item["value"])
Result: Maltego asks each user for the key once and stores it; one EmailAddress node appears per address. Hunter accepts the key as api_key query parameter, X-API-KEY header or Bearer token; the header keeps it out of logs.
maltego-trx or Canari; if you inherit a project that uses them, port one transform at a time to the SDK (a function plus decorator replaces the class plus create_entities).TransformSetting and read them from the settings dict.yield) for long result lists so the graph fills while the transform runs.