Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Development]
<!-- Do Not Erase This Section - Used for tracking unreleased changes -->

### Added
- **Microsoft Sentinel Graph connector (#1088)**: New plugin for querying the Microsoft Sentinel Graph API (Microsoft Security Platform, public preview) and visualizing the response with Graphistry. Public API: `graphistry.configure_sentinel_graph(...)`, `graphistry.sentinel_graph(query, language='GQL', response_formats=['Graph'])`, `graphistry.sentinel_graph_list()` (graph-instance discovery via `GET /graphs/graph-instances?graphTypes=Custom`), `graphistry.sentinel_graph_from_credential(...)`, `graphistry.sentinel_graph_close()`. Authentication supports `InteractiveBrowserCredential` (default), `ClientSecretCredential` (service principal via `tenant_id`/`client_id`/`client_secret`), `DeviceCodeCredential`, and any custom `azure.core.credentials.TokenCredential`. Response parsing handles the public-preview envelope `result.graph.{nodes,edges}` and the `result.rawData.tables` fallback. Returned `Plottable` binds reserved graphistry columns (`g_NodeId` for node IDs; `g_src`, `g_dst`, `g_EdgeId` for edges; plus `g_label`, `g_labels`, `g_edge` for label metadata) so Sentinel response properties named `id`, `source`, `target`, `label`, `labels`, or `edge` cannot silently corrupt bindings. The interactive-browser auth path also falls back to `DefaultAzureCredential` when `get_token()` fails (e.g. headless/server environments), matching the previously-documented fallback contract. Security hardening includes HTTPS enforcement (HTTP endpoints rejected), SSL verification on by default, sanitized error messages, no token/query logging, token caching with 5-minute expiry buffer, and HTTP retry with exponential backoff. New optional install extra: `pip install graphistry[sentinel-graph]` (pulls `azure-identity`, `azure-core`). Test coverage in `graphistry/tests/plugins/test_sentinel_graph.py` with synthetic fixtures in `graphistry/tests/fixtures/sentinel_graph_responses.py`; auth tests skip cleanly when `azure.identity` is not installed so the minimal-Python CI lane stays green. Demo notebook at `demos/demos_databases_apis/microsoft/sentinel/sentinel_graph_examples.ipynb`.

## [0.55.1 - 2026-05-05]

### Tests
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Microsoft Sentinel Graph API with Graphistry\n",
"\n",
"This notebook demonstrates how to query Microsoft Sentinel Graph API and visualize threat intelligence data with Graphistry.\n",
"\n",
"## Requirements\n",
"\n",
"```bash\n",
"pip install graphistry[sentinel-graph]\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Install dependencies (uncomment if needed)\n",
"# !pip install graphistry[sentinel-graph]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"Import libraries and configure Graphistry."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import graphistry\n",
"from azure.identity import InteractiveBrowserCredential\n",
"\n",
"# Register with Graphistry\n",
"# IMPORTANT: Store credentials securely using environment variables\n",
"graphistry.register(\n",
" api=3,\n",
" protocol=\"https\",\n",
" server=\"hub.graphistry.com\"\n",
" # personal_key_id='YOUR_KEY_ID',\n",
" # personal_key_secret='YOUR_KEY_SECRET'\n",
")\n",
"\n",
"print(\"\u2713 Graphistry configured\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": "## Discover Available Graph Instances\n\nUse `sentinel_graph_list()` to see what graph instances are available in your tenant. You only need a placeholder `graph_instance` for this call \u2014 the value is not used by the list endpoint."
},
{
"cell_type": "code",
"source": "g = graphistry.configure_sentinel_graph(\n graph_instance=graph_instance_name,\n credential=credential,\n response_formats=[\"Graph\"] # default; use [\"Table\", \"Graph\"] to also get raw tabular data\n)\n\nprint(f\"\u2713 Sentinel Graph configured for instance: {graph_instance_name}\")",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Interactive browser authentication\n",
"credential = InteractiveBrowserCredential()\n",
"\n",
"# Replace 'YourGraphInstance' with your actual graph instance name\n",
"g = graphistry.configure_sentinel_graph(\n",
" graph_instance='YourGraphInstance',\n",
" credential=credential\n",
")\n",
"\n",
"print(\"\u2713 Sentinel Graph configured\")"
]
},
{
"cell_type": "code",
"metadata": {},
"source": "query = \"\"\"\nMATCH (n)-[e]->(m)\nRETURN *\nLIMIT 50\n\"\"\"\n\nviz = g.sentinel_graph(query)\nprint(f\"Query returned {len(viz._nodes)} nodes and {len(viz._edges)} edges\")\n\nviz.plot()",
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Basic query to get nodes and edges\n",
"query = \"\"\"\n",
"MATCH (n)-[e]->(m)\n",
"RETURN *\n",
"LIMIT 50\n",
"\"\"\"\n",
"\n",
"viz = g.sentinel_graph(query)\n",
"print(f\"Query returned {len(viz._node)} nodes and {len(viz._edge)} edges\")\n",
"\n",
"viz.plot()"
]
},
{
"cell_type": "code",
"metadata": {},
"source": "print(\"=\" * 80)\nprint(\"NODES\")\nprint(\"=\" * 80)\nprint(f\"Shape: {viz._nodes.shape}\")\nprint(f\"Columns: {list(viz._nodes.columns)}\")\nprint(\"\\nSample nodes:\")\ndisplay(viz._nodes.head(3))\n\nprint(\"\\n\" + \"=\" * 80)\nprint(\"EDGES\")\nprint(\"=\" * 80)\nprint(f\"Shape: {viz._edges.shape}\")\nprint(f\"Columns: {list(viz._edges.columns)}\")\nprint(\"\\nSample edges:\")\ndisplay(viz._edges.head(3))",
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Access node and edge DataFrames\n",
"print(\"=\" * 80)\n",
"print(\"NODES\")\n",
"print(\"=\" * 80)\n",
"print(f\"Shape: {viz._node.shape}\")\n",
"print(f\"Columns: {list(viz._node.columns)}\")\n",
"print(\"\\nSample nodes:\")\n",
"display(viz._node.head(3))\n",
"\n",
"print(\"\\n\" + \"=\" * 80)\n",
"print(\"EDGES\")\n",
"print(\"=\" * 80)\n",
"print(f\"Shape: {viz._edge.shape}\")\n",
"print(f\"Columns: {list(viz._edge.columns)}\")\n",
"print(\"\\nSample edges:\")\n",
"display(viz._edge.head(3))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Example 3: Enhanced Visualization\n",
"\n",
"Add visual encodings for better graph exploration."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"styled = (\n",
" viz\n",
" .encode_edge_color('edge', as_categorical=True)\n",
" .encode_point_color('label', as_categorical=True)\n",
" .encode_point_size('label', default_mapping=100)\n",
")\n",
"\n",
"styled.plot()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Example 4: Query with Filters\n",
"\n",
"Use WHERE clause to filter results."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Query with WHERE clause (adjust property name as needed for your graph)\n",
"filtered_query = \"\"\"\n",
"MATCH (a)-[e]->(b)\n",
"WHERE a.id IS NOT NULL\n",
"RETURN *\n",
"LIMIT 30\n",
"\"\"\"\n",
"\n",
"filtered_viz = g.sentinel_graph(filtered_query)\n",
"print(f\"Found {len(filtered_viz._edge)} edges\")\n",
"\n",
"filtered_viz.plot()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Example 5: Query Nodes Only\n",
"\n",
"Retrieve specific nodes from the graph."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Query nodes only\n",
"nodes_query = \"\"\"\n",
"MATCH (n)\n",
"RETURN n\n",
"LIMIT 20\n",
"\"\"\"\n",
"\n",
"nodes_viz = g.sentinel_graph(nodes_query)\n",
"nodes_viz.plot()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Example 6: Error Handling\n",
"\n",
"Demonstrate robust error handling."
]
},
{
"cell_type": "code",
"source": "# Request both Table and Graph formats in a single call\n# Graphistry automatically parses the Graph section for visualization\nboth_formats_viz = g.sentinel_graph(\n \"MATCH (n)-[e]->(m) RETURN * LIMIT 20\",\n response_formats=[\"Table\", \"Graph\"]\n)\n\nprint(f\"Nodes: {len(both_formats_viz._nodes)}, Edges: {len(both_formats_viz._edges)}\")\nboth_formats_viz.plot()",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": "## Requesting Both Graph and Table Formats\n\nPass `response_formats=[\"Table\", \"Graph\"]` to get both structured graph data and the raw tabular rows in a single API call. Graphistry will parse the `Graph` section; the `Table` section is available for additional inspection if needed.",
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"try:\n",
" # Invalid query syntax\n",
" bad_query = \"INVALID SYNTAX\"\n",
" result = g.sentinel_graph(bad_query)\n",
"except Exception as e:\n",
" print(f\"Query failed as expected: {type(e).__name__}\")\n",
" print(f\"Error message: {e}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Alternative Authentication: Service Principal\n",
"\n",
"For production environments, use service principal authentication with credentials stored securely."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Uncomment and configure for production use\n",
"#\n",
"# import os\n",
"# \n",
"# g_prod = graphistry.configure_sentinel_graph(\n",
"# graph_instance='YourGraphInstance', # Replace with your graph instance name\n",
"# tenant_id=os.environ.get('AZURE_TENANT_ID'),\n",
"# client_id=os.environ.get('AZURE_CLIENT_ID'),\n",
"# client_secret=os.environ.get('AZURE_CLIENT_SECRET')\n",
"# )\n",
"# \n",
"# result = g_prod.sentinel_graph('MATCH (n) RETURN n LIMIT 10')\n",
"# result.plot()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Cleanup\n",
"\n",
"Clear cached authentication token."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"g.sentinel_graph_close()\n",
"print(\"\u2713 Sentinel Graph connection closed\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.13"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
6 changes: 6 additions & 0 deletions graphistry/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
kusto_from_client,
kql,
kusto_graph,
configure_sentinel_graph,
sentinel_graph_from_credential,
sentinel_graph,
sentinel_graph_close,
sentinel_graph_list,
gsql,
gsql_endpoint,
cosmos,
Expand Down Expand Up @@ -131,6 +136,7 @@

from . import compute as compute # noqa: F401
from . import pygraphistry as pygraphistry # noqa: F401
from . import plugins as plugins # noqa: F401
from . import render as render # noqa: F401
from . import arrow_uploader as arrow_uploader # noqa: F401

Expand Down
2 changes: 1 addition & 1 deletion graphistry/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class NotThisMethod(Exception):


LONG_VERSION_PY : Any = {}
HANDLERS = {}
HANDLERS: Any = {}


def register_vcs_handler(vcs, method): # decorator
Expand Down
2 changes: 2 additions & 0 deletions graphistry/client_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from . import util
from .plugins_types.spanner_types import SpannerConfig
from .plugins_types.kusto_types import KustoConfig
from .plugins_types.sentinel_graph_types import SentinelGraphConfig



Expand Down Expand Up @@ -86,6 +87,7 @@ def __init__(self) -> None:
# NOTE: These are dataclasses, so we shallow copy them
self.kusto: Optional[KustoConfig] = None
self.spanner: Optional[SpannerConfig] = None
self.sentinel_graph: Optional[SentinelGraphConfig] = None

# TODO: Migrate to a pattern like Kusto or Spanner
self._bolt_driver: Optional[Any] = None
Expand Down
Loading
Loading