Back openDesk Edu for a sovereign, open-source education β every vote counts.
Vote nowPalantir Gotham is valued at over $60 billion. Its core product β an intelligence platform that fuses hundreds of data sources into a single knowledge graph β has become the de facto standard for Western intelligence agencies. The architecture works. Yet for the open-source intelligence (OSINT) community, the graph-driven approach remains vastly underutilised. Most OSINT practitioners still work in document silos: spreadsheets, PDFs, chat logs, and browser tabs.
This article makes the case that graphs are the natural data model for intelligence analysis, surveys the graph algorithms that directly map to analytical tradecraft, and profiles the open-source tooling now emerging to bring Palantir-grade graph intelligence to anyone.
OSINT analysts face a structural challenge that no amount of spreadsheet discipline can solve. A single investigation produces data from dozens of sources:
Each source contributes entities β people, organisations, domains, IP addresses, locations, events β and the relationships between them. The total number of entities grows roughly linearly with sources, but the number of potential relationships grows quadratically. An investigation tracking 500 entities across 20 sources generates over 120,000 possible pairwise connections.
A flat file or relational database cannot surface these connections efficiently. Every new relationship requires either a JOIN across disparate tables or a manual cross-reference that an analyst must remember to make. This is why Palantir, i2 Analyst's Notebook, and Maltego all converged on the same architectural answer: the knowledge graph.
Intelligence data is inherently graph-structured. Entities are nodes. Relationships are edges. Attributes are node and edge properties. This is not an analogy β it is a direct mapping:
| Intelligence Concept | Graph Representation |
|---|---|
| A person of interest | Node with label Person |
| A company they own | Node with label Organization |
| The ownership relationship | Directed edge OWNS |
| A phone call between two people | Edge with property timestamp |
| A meeting location | Node with label Location |
| Temporal confidence | Edge property confidence: 0.85 |
This model supports exactly the operations that intelligence analysts need:
A property graph database like Neo4j models this directly. The Cypher query for a two-hop intelligence trace is succinct:
MATCH (p:Person {name: "Target"})-[r:KNOWS|OWNS|CONTACTS*1..2]-(connected)
RETURN p, connected, r
Compare this to the equivalent SQL, which would require six JOINs across three junction tables and still fail to capture varying path lengths.
The real power of graph-powered OSINT lies not in storage but in computation. A knowledge graph is queryable by hand, but graph algorithms automate the analytical patterns that intelligence analysts apply manually.
The most fundamental intelligence question is: who is working with whom? Community detection algorithms partition a graph into clusters where internal connections are dense and external connections sparse. This directly maps to identifying operational cells within a larger network.
The Louvain algorithm β ubiquitous in graph analytics β optimises modularity to find community structure. A landmark study of the Brazilian Federal Police's criminal intelligence network (BFP2013) applied Louvain to a network of 9,887 individuals and found a modularity of Q = 0.96, meaning the network is extraordinarily well-structured into communities. Critically, the study found that Module-Based Attacks (MBA) β removing nodes that bridge communities β fragmented the network after removing only 2% of vertices. Bridge figures (lawyers, accountants, money launderers) were topologically more consequential than hierarchical leaders.
// Louvain community detection in Neo4j GDS
CALL gds.louvain.stream('intel-graph')
YIELD nodeId, communityId, intermediateCommunityIds
RETURN gds.util.asNode(nodeId).name AS entity, communityId
ORDER BY communityId
For OSINT practitioners, this is actionable: community detection can reveal operational boundaries that no document explicitly describes.
Once communities are identified, the next question is who matters most? Centrality measures provide a ranked answer:
| Algorithm | What It Measures | Intelligence Application |
|---|---|---|
| Degree Centrality | Number of direct connections | Hub identification β money mules, coordinators |
| Betweenness Centrality | Frequency of lying on shortest paths | Bridge figures β intermediaries connecting otherwise separate groups |
| Eigenvector Centrality | Quality of connections (connected to well-connected nodes) | Leadership identification β core network members |
| PageRank | Influence propagation through the graph | Ultimate beneficiaries in money laundering networks |
The 'Ndrangheta study analysed 254 mafia members and found that betweenness centrality was the single best predictor of "boss" status β 15 times higher for bosses than for non-bosses. The zP-score (a combined measure of within-community connectivity and inter-community bridging) matched betweenness in predictive power, confirming that criminal leaders are simultaneously central to their own group and bridges to others.
How is entity A connected to entity B? This is the canonical OSINT question. Shortest-path algorithms automate what analysts do manually with whiteboards and string.
// Find shortest path between two entities
MATCH (a:Person {name: "SuspectA"}), (b:Organization {name: "ShellCorp"})
CALL gds.shortestPath.dijkstra.stream('intel-graph', {
sourceNode: id(a),
targetNode: id(b),
relationshipWeightProperty: 'communication_frequency'
})
YIELD nodeIds, costs
RETURN [nodeId IN nodeIds | gds.util.asNode(nodeId).name] AS path, costs
In intelligence contexts, all-paths analysis is often more useful than a single shortest path, because criminals deliberately create multiple redundant communication channels. The K-shortest paths variant reveals these alternative routes, each representing a potential investigative lead.
Intelligence is never static. Organisations restructure. Personnel change. Communication patterns shift. Temporal graph analysis captures this:
The OSINT-NEXUS system uses a 30-day exponential decay on edge weights in its Neo4j temporal knowledge graph. Edges that are not reinforced by new evidence weaken over time, ensuring the graph reflects current operational reality rather than historical noise.
When a threat actor or compromised entity is identified, the immediate question is who else is affected? Risk propagation treats the graph as a diffusion network:
// BFS risk propagation with decay
MATCH (start:Entity {name: "CompromisedAsset"})
CALL gds.bfs.stream('intel-graph', {
sourceNode: id(start),
maxDepth: 4
})
YIELD path
RETURN path
The AEGIS system implements this with exponential decay across 18 weighted relationship types. A compromised endpoint connected to a server, which is administered by a person, who also manages other servers, produces a ranked blast radius list β directly actionable for incident response.
Let's walk through a concrete OSINT investigation from first sighting to network-level attribution. An analyst discovers a phishing URL impersonating a major bank: hxxps://secure-login[.]bankofmerica[.]com. The goal: map the entire criminal infrastructure behind this domain using the graph.
Step 1 β Create the core artefacts
Every piece of evidence becomes a graph node with a shared :Artefact label and a type-specific sub-label:
CREATE (d:Domain:Artefact {
domain: "bankofmerica.com",
firstSeen: "2026-06-15",
registrar: "Namecheap",
whoisPrivacy: true
})
CREATE (ip:IPAddress:Artefact {
address: "185.220.101.42",
asn: "AS47890",
hostingProvider: "Hetzner"
})
CREATE (cert:SSLCertificate:Artefact {
fingerprint: "A1:B2:C3:D4:E5:F6:...",
issuerOrg: "Let's Encrypt",
issuedDate: "2026-05-20",
sans: [
"bankofmerica.com",
"secure-paypal-verify.com",
"login-apple-update.com"
]
})
CREATE (url:URL:Artefact {
url: "https://secure-login.bankofmerica.com/login",
firstSeen: "2026-06-15"
})
CREATE (url)-[:BELONGS_TO_DOMAIN]->(d)
CREATE (d)-[:RESOLVES_TO]->(ip)
CREATE (d)-[:HAS_CERTIFICATE]->(cert)
Step 2 β Certificate transparency link analysis
The SSL certificate's Subject Alternative Names (SANs) reveal four additional phishing domains targeting different brands. One query links them all:
MATCH (known:Domain {domain: "bankofmerica.com"})
-[:HAS_CERTIFICATE]->(cert:SSLCertificate)
MATCH (cert)<-[:HAS_CERTIFICATE]-(suspected:Domain)
WHERE suspected.domain <> known.domain
RETURN suspected.domain, cert.fingerprint, cert.issuedDate
ORDER BY cert.issuedDate DESC
This single query β impossible in a flat-file workflow β surfaces the full phishing cluster in milliseconds.
Step 3 β Passive DNS expansion
Historical DNS lookups reveal that the IP address 185.220.101.42 has hosted six other domains in the past 90 days:
// Import passive DNS observations from a threat intel feed
LOAD CSV WITH HEADERS FROM 'file:///passive_dns_export.csv' AS row
MERGE (ip:IPAddress:Artefact {address: row.ip})
MERGE (d:Domain:Artefact {domain: row.domain})
MERGE (d)-[:RESOLVED_FROM {
firstSeen: row.firstSeen,
lastSeen: row.lastSeen,
source: "passive_dns"
}]->(ip)
// Find all domains sharing the suspicious IP
MATCH (ip:IPAddress {address: "185.220.101.42"})
<-[:RESOLVED_FROM]-(d:Domain)
RETURN d.domain, d.registrar, d.firstSeen
ORDER BY d.firstSeen DESC
Step 4 β Community detection on the infrastructure graph
With 40+ nodes now in the graph, run the Leiden algorithm to identify operational clusters:
CALL gds.graph.project(
'osint-investigation',
'Artefact',
{
RESOLVES_TO: {orientation: 'UNDIRECTED'},
HAS_CERTIFICATE: {orientation: 'UNDIRECTED'},
BELONGS_TO_DOMAIN: {orientation: 'UNDIRECTED'}
}
)
CALL gds.leiden.write('osint-investigation', {
writeProperty: 'community',
includeIntermediateCommunities: true
})
YIELD communityCount, modularity
Nodes in the same community likely belong to the same threat actor or campaign. When the phishing domains for Bank of America, PayPal, and Apple all fall into community 0, you have strong evidence of a single operator.
Step 5 β Temporal blast radius assessment
Once the infrastructure is mapped, determine which artefacts are still active and who else may be at risk:
MATCH (start:Domain {domain: "bankofmerica.com"})
CALL gds.bfs.stream('osint-investigation', {
sourceNode: id(start),
maxDepth: 3
})
YIELD path
RETURN [n IN nodes(path) | n.domain] AS attack_path
This five-step workflow β from a single URL to community-level actor attribution β completes in minutes inside a knowledge graph. With spreadsheets and flat files, the same analysis would require days of manual cross-referencing across WHOIS records, certificate logs, and DNS databases.
A remarkable shift has occurred in 2025β2026: multiple open-source projects now implement production-grade graph-based OSINT, directly inspired by Palantir Gotham's architecture.
OGI is a self-hosted visual link analysis framework combining a Python/FastAPI backend with a React frontend using Sigma.js for graph visualisation. It ships with 20+ built-in OSINT transforms (DNS, WHOIS, SSL, geolocation, email, hash lookups) and a transform hub for community extensions.
What sets OGI apart is its graph analysis engine: centrality, community detection, and shortest-path algorithms run directly on the investigation graph. An "AI Investigator" mode uses LLMs to autonomously plan transform runs, stream results, and summarise findings. OGI stores data locally via SQLite (zero-config) or PostgreSQL for team deployments, making it the most accessible entry point for graph-based OSINT.
OSINT-NEXUS is an autonomous all-source fusion system that ingests RSS news, Telegram channels, ADS-B flight tracking, AIS maritime signals, and civil defence alerts into a Neo4j temporal knowledge graph. At time of writing, its production deployment contains 760+ nodes with 6 relationship types.
The system applies ICD 203 confidence levels (HIGH / MODERATE / LOW / VERY LOW) to every analytic product and NATO 2Γ6 source reliability scoring (AβF source reliability Γ 1β6 claim credibility) to each event. Its LLM reasoning chain produces structured SITREPs with causal chain analysis, contradiction detection, and ranked priority actions. The architecture β PostgreSQL for raw events, Neo4j for graph fusion, and an LLM for reasoning β defines a replicable pattern for production OSINT.
Estorides is a pure open-source re-imagining of the Palantir toolchain, supporting 99+ free OSINT sources with an async fanout architecture. It stores its knowledge graph in KΓΉzu, an embedded columnar graph DBMS that supports Cypher queries, and exports to GraphML for analysis in Gephi.
Its ontology engine cross-references observations against the OFAC SDN sanctions list and auto-tags MITRE ATT&CK techniques. A fuzzy entity clustering layer (using Python's difflib.SequenceMatcher at a 0.85 threshold) merges aliases across sources. Estorides is the most architecturally ambitious of the open-source tools, implementing a five-surface plugin system for parsers, LLM backends, relationship inferers, real-time feeds, and encrypted exporters.
Inspired by BloodHound's Active Directory graph analysis but adapted for general OSINT, Basset Hound is an API-first Neo4j entity relationship engine. It supports 26 relationship types, path finding, cluster detection, centrality analysis, and an MCP (Model Context Protocol) server exposing 119 tools for AI agent integration.
Basset Hound introduces a novel concept: orphan data. Unlinked identifiers (emails, phone numbers, addresses) are stored in a holding pool until the system finds connections, at which point it suggests linking with a confidence score (50β100%). This reflects the reality that OSINT investigations often accumulate fragments before the full picture emerges.
NEXUS is a desktop OSINT application (Electron + React) backed by Neo4j with the GDS (Graph Data Science) library, giving it access to Louvain community detection, betweenness and PageRank centrality, and shortest-path analysis. It defines 24 entity types and 20+ relationship types in its POLE schema (Person, Organization, Location, Event).
NEXUS implements a two-tier data model: streaming data stays in Redis (with TTL), while high-value intelligence (risk score β₯ 6, tracked aircraft, significant earthquakes) is selectively persisted to Neo4j. This hot/warm/cold data architecture is directly relevant to any OSINT system dealing with high-velocity public data.
All these tools converge on an architectural pattern that Palantir formalised: the ontology-driven knowledge graph. The ontology defines:
A Palantir-inspired OSINT platform (open-source, Neo4j-based) defines this ontology:
Object Types:
Actor: state, non-state, individual
Event: conflict, sanction, strike, movement
Location: country, region, coordinates, facility
Asset: weapon system, vessel, aircraft, facility
Organization: military unit, NGO, company, network
Document: report, cable, intercept, publication
Link Types: Actor ββ[CONTROLS]βββΊ Asset
Actor ββ[LOCATED_IN]βββΊ Location
Event ββ[OCCURRED_AT]ββ Location
Actor ββ[SANCTIONED_BY]β Organization
Actor ββ[LINKED_TO]ββββ Actor
Asset ββ[USED_IN]ββββββ Event
The ontology is not merely a schema β it is an analytical constraint that prevents meaningless connections and guides analysts toward productive lines of inquiry. Without an ontology, a knowledge graph becomes a dense, noisy web where everything connects to everything and nothing is actionable.
The convergence of knowledge graphs and large language models has produced GraphRAG, which is particularly well-suited to OSINT. A 2025 paper introduced a multi-agent OSINT architecture with GraphRAG that replaces separate graph and vector stores with a unified hybrid retrieval layer. The system achieved 82% accuracy on people profiling and 95% accuracy on event summarisation across OSINT tasks.
The GraphRAG pattern for OSINT works as follows:
The following Python class implements a hybrid OSINT retriever that fuses graph neighbourhood traversal with vector similarity search, then passes the combined context to an LLM for analysis:
from neo4j import GraphDatabase
from openai import OpenAI
class OSINTGraphRAG:
"""Hybrid retriever combining knowledge graph traversal
with vector similarity for OSINT analysis."""
def __init__(self, neo4j_uri: str, neo4j_auth: tuple):
self.driver = GraphDatabase.driver(neo4j_uri, auth=neo4j_auth)
self.llm = OpenAI()
def retrieve_graph_context(self, entity_name: str) -> list[dict]:
"""Fetch the entity's immediate neighbourhood from the graph."""
with self.driver.session() as session:
result = session.run("""
MATCH (e:Entity {name: $name})-[r]-(neighbour)
RETURN e.name AS source,
type(r) AS relationship,
neighbour.name AS target,
neighbour.description AS description
LIMIT 50
""", name=entity_name)
return result.data()
def retrieve_vector_context(self, entity_name: str) -> list[dict]:
"""Find semantically similar entities via vector index."""
embedding = self._embed(entity_name)
with self.driver.session() as session:
result = session.run("""
CALL db.index.vector.queryNodes(
'entity-embeddings', 10, $embedding
)
YIELD node, score
RETURN node.name AS name,
node.description AS description,
score
""", embedding=embedding)
return result.data()
def _embed(self, text: str) -> list[float]:
response = self.llm.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
def fuse_contexts(
self,
graph_ctx: list[dict],
vector_ctx: list[dict]
) -> str:
"""Merge graph and vector contexts into a single prompt."""
lines = []
for row in graph_ctx:
lines.append(
f"{row['source']} --[{row['relationship']}]--> "
f"{row['target']}: {row['description']}"
)
for row in vector_ctx:
lines.append(
f"[similar: {row['score']:.2f}] {row['name']}: "
f"{row['description']}"
)
return "\n".join(lines[:30])
def analyse(self, query: str, target_entity: str) -> str:
"""Run full hybrid retrieval + LLM analysis."""
graph_ctx = self.retrieve_graph_context(target_entity)
vector_ctx = self.retrieve_vector_context(target_entity)
context = self.fuse_contexts(graph_ctx, vector_ctx)
prompt = (
"You are an OSINT analyst. Using the intelligence graph "
"context below, answer the query. Cite specific "
"relationships and entities.\n\n"
f"Query: {query}\n\n"
f"Intelligence Context:\n{context}\n\nAnalysis:"
)
response = self.llm.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=0.1
)
return response.choices[0].message.content
# Usage
rag = OSINTGraphRAG(
"neo4j://localhost:7687",
("neo4j", "password")
)
report = rag.analyse(
"What criminal networks is this entity connected to?",
"bankofmerica.com"
)
print(report)
This architecture eliminates the fundamental limitation of document-based RAG for OSINT: vector similarity finds text that means the same thing, but it cannot traverse entities that are connected. A knowledge graph does both. By fusing graph neighbourhoods with vector embeddings, the OSINT analyst gets context that is simultaneously structurally precise and semantically broad.
For practitioners who want to deploy graph-powered OSINT today, the architecture pattern that emerges from these projects is consistent:
flowchart LR
subgraph Sources["Data Sources"]
RSS["RSS/News"]
TG["Telegram"]
WEB["Web Scraping"]
API["Public APIs"]
CERT["SSL/Certificates"]
end
subgraph Ingest["Ingestion Layer"]
COLL["Collectors"]
PARSE["Parsers"]
NER["Entity Extraction<br/>(GLiNER/LLM)"]
end
subgraph Graph["Graph Layer"]
NEO["Neo4j / KΓΉzu<br/>Knowledge Graph"]
RES["Entity Resolution<br/>Fuzzy Matching"]
TEMP["Temporal Index<br/>Edge Decay"]
end
subgraph Analysis["Analysis Layer"]
COM["Community Detection<br/>(Louvain)"]
CENT["Centrality<br/>(PageRank/Betweenness)"]
PATH["Path Finding<br/>(Shortest Path)"]
end
subgraph Output["Presentation"]
VIS["Visualisation<br/>(Sigma.js/D3)"]
RAG["GraphRAG<br/>LLM Analysis"]
EXP["Export<br/>(GraphML/STIX)"]
end
Sources --> Ingest
Ingest --> Graph
Graph --> Analysis
Analysis --> Output
Graph --> Output
| Layer | Production Choice | Lightweight Alternative |
|---|---|---|
| Graph DB | Neo4j 5 + GDS | KΓΉzu (embedded, columnar) |
| Entity Resolution | GLiNER + difflib | Dedupe (Python) |
| Graph Algorithms | Neo4j GDS | NetworkX (in-memory) |
| Visualisation | Sigma.js (graphology) | D3.js force layout |
| LLM Integration | Graphiti + Neo4j | LangChain + NetworkX |
| Source Connectors | Custom async collectors | Estorides registry (99+ sources) |
For OSINT platforms that need to share intelligence with broader threat intelligence communities, STIX (Structured Threat Information Expression) and TAXII (Trusted Automated Exchange of Intelligence Reports) are the de facto standards. A Neo4j knowledge graph can both consume and produce STIX 2.1, creating a bridge between internal graph analysis and external intelligence sharing.
The mapping between STIX domain objects and graph nodes is natural:
| STIX Domain Object | Graph Representation |
|---|---|
indicator | :Artefact { type: "indicator", pattern: "[domain-name:value = 'evil.com']" } |
threat-actor | :Actor { name: "APT-42", motivation: "financial-crime" } |
campaign | :Campaign { name: "Operation PhishPhry", firstSeen, lastSeen } |
relationship | Directed edge with type matching the STIX relationship name |
Exporting a graph investigation to STIX is a single Cypher query:
// Export a threat actor and all linked artefacts as STIX 2.1 bundle
MATCH (a:Actor {name: "APT-42"})-[r]-(art:Artefact)
WITH a, collect(DISTINCT art) AS artefacts
UNWIND artefacts AS art
RETURN {
type: "bundle",
objects: [
{
id: "threat-actor--" + a.id,
type: "threat-actor",
name: a.name,
aliases: a.aliases,
first_seen: toString(a.firstSeen)
} +
[art IN artefacts | {
id: "indicator--" + art.id,
type: "indicator",
pattern: "[domain-name:value = '" + art.domain + "']",
valid_from: toString(art.firstSeen)
}]
]
} AS stix_bundle
For TAXII ingestion, adapters in OGI's transform hub poll TAXII 2.1 servers and convert incoming STIX objects into graph nodes using the ontology mapping. This means an OSINT knowledge graph can participate in organised intelligence sharing communities (ISACs, MISP) without abandoning the graph model. The MISP to Neo4j bridge, for example, converts MISP events into subgraphs where event attributes become :Artefact nodes and event-to-event correlations become LINKED_TO edges β preserving the analytical structure that MISP's flat event model loses.
Confidence scoring every assertion. An intelligence graph without confidence metadata is not analysis β it is gossip. Implement the ICD 203 four-level scale (HIGH / MODERATE / LOW / VERY LOW) or the NATO 2Γ6 system on every edge.
Temporal awareness as a first-class property. Every relationship needs valid_from and valid_to timestamps. Edges that expire should decay or be pruned. Without temporal metadata, the graph represents a timeless fiction that intelligence analysis cannot rely on.
Entity resolution before graph construction. The same person appears as "John Smith", "J. Smith", and "Johnny Smith" across different sources. Fuzzy matching (sequence matching at 0.85 threshold, as Estorides implements) and cross-referencing against Wikidata/OFAC prevent duplicate nodes that fragment the analytical picture.
Bidirectional source attribution. Every edge must be traceable to its originating source document. A graph without provenance is useless for intelligence: analysts must be able to verify and challenge any connection.
The OSINT community is undergoing a structural shift comparable to the move from paper files to digital databases in the 1990s. Knowledge graphs are not a nice-to-have enhancement to existing OSINT tooling β they are the architectural foundation that makes intelligence analysis tractable at scale.
The open-source ecosystem has matured past the proof-of-concept stage. OGI, OSINT-NEXUS, Estorides, Basset Hound, and NEXUS are production-grade systems that implement the same architectural patterns as Palantir Gotham, but at a fraction of the cost and with full source code transparency. The knowledge graph β with its native support for traversal, community detection, centrality analysis, and temporal reasoning β is the analytical engine that transforms OSINT from a document retrieval task into a true intelligence discipline.
The tools are ready. The algorithms are proven. The question is no longer whether to use graphs for OSINT, but how quickly the community can adopt them.