Skip to content

Passive OSINT enrichment

Use ZettelForge's OSINT executor to enrich seed entities — domains, IP addresses, ASNs, and IP blocks — with infrastructure intelligence gathered from passive sources. The executor runs registered collectors, validates each result against the ontology, canonicalizes entity values, and writes nodes and edges into the knowledge graph.

Prerequisites

  • ZettelForge installed and initialized.
  • For DNS, WHOIS, and certificate transparency collectors, install the OSINT extra:
pip install "zettelforge[osint]"

The [osint] extra adds dnspython, python-whois, and ipwhois. The base package already includes httpx, so the BGP collector works without it.

Supported seed types

The executor accepts five seed types: DomainName, IPv4Address, IPv6Address, ASNumber, and Netblock. Seed type must match exactly — the executor raises ValueError for any other value.

Collectors registered per seed type (Phase 1 / 1.5):

Seed type Collectors run
DomainName dns_collector, whois_collector, cert_collector, builtwith_collector, wappalyzer_collector
IPv4Address whois_collector, port_scanner (active-gated; see Safety controls)
IPv6Address whois_collector, port_scanner (active-gated)
ASNumber bgp_collector
Netblock None registered yet — valid seed type but no collectors return results today

Run a passive enrichment

from zettelforge.osint import run_osint_collection

result = run_osint_collection("DomainName", "Example.COM.")
print(result.canonical_input_value)  # example.com
print(result.collectors_run)         # ['dns_collector', 'cert_collector', ...]
print(result.persisted_count)
print(result.error_count)

Input values are canonicalized before any KG writes: "Example.COM." becomes "example.com", "AS15169" becomes "15169". Alternate spellings of the same entity collapse onto the same KG node.

The function also accepts a collect_osint() alias, which is identical — some integrations prefer the verb-first naming.

Result shape

run_osint_collection returns an OSINTCollectionResult dataclass:

Field Type Notes
input_entity_type str Seed type you passed
input_value str Seed value you passed
canonical_input_value str Canonicalized form
seed_node_id str \| None KG node ID for the seed (None when persist=False)
collectors_run list[str] Names of collectors that ran
tuples_collected int Total tuples emitted across all collectors
persisted_count int Tuples actually written to the KG
error_count int Collector or tuple errors (non-fatal)
errors list[OSINTExecutionError] Per-collector error detail
started_at str ISO-8601 timestamp
finished_at str \| None ISO-8601 timestamp

A failing collector does not abort the run. Its error is captured in result.errors and execution continues with the remaining collectors.

Dry-run without KG writes

Pass persist=False to run collectors and validate output without writing to the knowledge graph. Useful for testing new seeds or inspecting what a run will produce:

result = run_osint_collection("ASNumber", "AS15169", persist=False)
# result.seed_node_id is None
# result.persisted_count is 0
# result.error_count shows any validation failures

Run only specific collectors

Use collector_names to restrict which collectors run for a given seed:

result = run_osint_collection(
    "DomainName",
    "example.com",
    collector_names=["dns_collector", "cert_collector"],
)

Any name not in the registry is silently skipped — no error is raised for an unknown collector name.

Safety controls

Active port scanning is off by default. Without an explicit opt-in, the port scanner collector returns an empty result and sends no packets:

export ZETTELFORGE_OSINT_ACTIVE_SCAN=1

Set this variable only when you own the target network or have written authorization to scan it. Accepted values: 1, true, TRUE, yes. Anything else (including an empty string) keeps scanning off.

Even with the flag set, the port scanner requires python-nmap and the nmap binary. If either is missing, the collector returns an empty result and logs a warning — it never raises.

What gets persisted

The executor writes canonical entity values to the knowledge graph. Duplicate spellings collapse onto the same node. For example, AS15169 and 15169 resolve to the same canonical ASN node (15169), and Example.COM. and example.com write to the same domain node.

Each persisted tuple also writes a KG edge with an osint: true property and a source property set to the collector name, so you can later query which collector produced a given edge.

Phase 2-5 collectors (in progress)

Breach, social, and people enrichment collectors (HIBP, HaveIBeenPwned, Holehe, Hunter.io, and others) are registered in the transform registry but use seed types such as EmailAddress, Alias, and Hashtag that are not yet in SUPPORTED_SEED_TYPES. Passing those types raises ValueError today. Support for those seed types is in progress.